diff --git a/.github/contributing.md b/.github/contributing.md
index 2554582b887..f0ec46ee709 100644
--- a/.github/contributing.md
+++ b/.github/contributing.md
@@ -38,7 +38,6 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before
 ### Pull Request Checklist
 
 - Vue core has two primary work branches: `main` and `minor`.
-
   - If your pull request is a feature that adds new API surface, it should be submitted against the `minor` branch.
 
   - Otherwise, it should be submitted against the `main` branch.
@@ -46,12 +45,10 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before
 - [Make sure to tick the "Allow edits from maintainers" box](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork). This allows us to directly make minor edits / refactors and saves a lot of time.
 
 - If adding a new feature:
-
   - Add accompanying test case.
   - Provide a convincing reason to add this feature. Ideally, you should open a suggestion issue first and have it approved before working on it.
 
 - If fixing a bug:
-
   - If you are resolving a special issue, add `(fix #xxxx[,#xxxx])` (#xxxx is the issue id) in your PR title for a better release log, e.g. `update entities encoding/decoding (fix #3899)`.
   - Provide a detailed description of the bug in the PR. Live demo preferred.
   - Add appropriate test coverage if applicable. You can check the coverage of your code addition by running `nr test-coverage`.
@@ -69,9 +66,7 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before
 - The PR should fix the intended bug **only** and not introduce unrelated changes. This includes unnecessary refactors - a PR should focus on the fix and not code style, this makes it easier to trace changes in the future.
 
 - Consider the performance / size impact of the changes, and whether the bug being fixes justifies the cost. If the bug being fixed is a very niche edge case, we should try to minimize the size / perf cost to make it worthwhile.
-
   - Is the code perf-sensitive (e.g. in "hot paths" like component updates or the vdom patch function?)
-
     - If the branch is dev-only, performance is less of a concern.
 
   - Check how much extra bundle size the change introduces.
@@ -265,7 +260,6 @@ This repository employs a [monorepo](https://en.wikipedia.org/wiki/Monorepo) set
 - `vue`: The public facing "full build" which includes both the runtime AND the compiler.
 
 - Private utility packages:
-
   - `dts-test`: Contains type-only tests against generated dts files.
 
   - `sfc-playground`: The playground continuously deployed at https://play.vuejs.org. To run the playground locally, use [`nr dev-sfc`](#nr-dev-sfc).
@@ -290,27 +284,39 @@ This is made possible via several configurations:
 
 ```mermaid
   flowchart LR
+    vue["vue"]
     compiler-sfc["@vue/compiler-sfc"]
     compiler-dom["@vue/compiler-dom"]
+    compiler-vapor["@vue/compiler-vapor"]
     compiler-core["@vue/compiler-core"]
-    vue["vue"]
     runtime-dom["@vue/runtime-dom"]
+    runtime-vapor["@vue/runtime-vapor"]
     runtime-core["@vue/runtime-core"]
     reactivity["@vue/reactivity"]
 
     subgraph "Runtime Packages"
       runtime-dom --> runtime-core
+      runtime-vapor --> runtime-core
       runtime-core --> reactivity
     end
 
     subgraph "Compiler Packages"
       compiler-sfc --> compiler-core
       compiler-sfc --> compiler-dom
+      compiler-sfc --> compiler-vapor
       compiler-dom --> compiler-core
+      compiler-vapor --> compiler-core
     end
 
+    vue --> compiler-sfc
     vue ---> compiler-dom
     vue --> runtime-dom
+    vue --> compiler-vapor
+    vue --> runtime-vapor
+
+    %% Highlight class
+    classDef highlight stroke:#35eb9a,stroke-width:3px;
+    class compiler-vapor,runtime-vapor highlight;
 ```
 
 There are some rules to follow when importing across package boundaries:
diff --git a/.github/maintenance.md b/.github/maintenance.md
index b1fb550dd7a..7b0c2a33626 100644
--- a/.github/maintenance.md
+++ b/.github/maintenance.md
@@ -48,7 +48,6 @@ Depending on the type of the PR, different considerations need to be taken into
 - Performance: if a refactor PR claims to improve performance, there should be benchmarks showcasing said performance unless the improvement is self-explanatory.
 
 - Code quality / stylistic PRs: we should be conservative on merging this type PRs because (1) they can be subjective in many cases, and (2) they often come with large git diffs, causing merge conflicts with other pending PRs, and leading to unwanted noise when tracing changes through git history. Use your best judgement on this type of PRs on whether they are worth it.
-
   - For PRs in this category that are approved, do not merge immediately. Group them before releasing a new minor, after all feature-oriented PRs are merged.
 
 ### Reviewing a Feature
@@ -56,7 +55,6 @@ Depending on the type of the PR, different considerations need to be taken into
 - Feature PRs should always have clear context and explanation on why the feature should be added, ideally in the form of an RFC. If the PR doesn't explain what real-world problem it is solving, ask the contributor to clarify.
 
 - Decide if the feature should require an RFC process. The line isn't always clear, but a rough criteria is whether it is augmenting an existing API vs. adding a new API. Some examples:
-
   - Adding a new built-in component or directive is "significant" and definitely requires an RFC.
   - Template syntax additions like adding a new `v-on` modifier or a new `v-bind` syntax sugar are "substantial". It would be nice to have an RFC for it, but a detailed explanation on the use case and reasoning behind the design directly in the PR itself can be acceptable.
   - Small, low-impact additions like exposing a new utility type or adding a new app config option can be self-explanatory, but should still provide enough context in the PR.
@@ -70,7 +68,6 @@ Depending on the type of the PR, different considerations need to be taken into
 - Implementation: code style should be consistent with the rest of the codebase, follow common best practices. Prefer code that is boring but easy to understand over "clever" code.
 
 - Size: bundle size matters. We have a GitHub action that compares the size change for every PR. We should always aim to realize the desired changes with the smallest amount of code size increase.
-
   - Sometimes we need to compare the size increase vs. perceived benefits to decide whether a change is justifiable. Also take extra care to make sure added code can be tree-shaken if not needed.
 
   - Make sure to put dev-only code in `__DEV__` branches so they are tree-shakable.
@@ -80,7 +77,6 @@ Depending on the type of the PR, different considerations need to be taken into
   - Make sure it doesn't accidentally cause dev-only or compiler-only code branches to be included in the runtime build. Notable case is that some functions in @vue/shared are compiler-only and should not be used in runtime code, e.g. `isHTMLTag` and `isSVGTag`.
 
 - Performance
-
   - Be careful about code changes in "hot paths", in particular the Virtual DOM renderer (`runtime-core/src/renderer.ts`) and component instantiation code.
 
 - Potential Breakage
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c8c217f62c4..6b69e4727e4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,6 +9,7 @@ on:
     branches:
       - main
       - minor
+      - vapor
 
 jobs:
   test:
@@ -16,7 +17,7 @@ jobs:
     uses: ./.github/workflows/test.yml
 
   continuous-release:
-    if: github.repository == 'vuejs/core'
+    if: github.repository == 'vuejs/core' && github.ref_name != 'vapor'
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 1122eb35573..1202ef9c8b4 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -80,6 +80,32 @@ jobs:
       - name: verify treeshaking
         run: node scripts/verify-treeshaking.js
 
+  e2e-vapor:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Setup cache for Chromium binary
+        uses: actions/cache@v4
+        with:
+          path: ~/.cache/puppeteer
+          key: chromium-${{ hashFiles('pnpm-lock.yaml') }}
+
+      - name: Install pnpm
+        uses: pnpm/action-setup@v4.0.0
+
+      - name: Install Node.js
+        uses: actions/setup-node@v4
+        with:
+          node-version-file: '.node-version'
+          cache: 'pnpm'
+
+      - run: pnpm install
+      - run: node node_modules/puppeteer/install.mjs
+
+      - name: Run e2e tests
+        run: pnpm run test-e2e-vapor
+
   lint-and-test-dts:
     runs-on: ubuntu-latest
     env:
diff --git a/.gitignore b/.gitignore
index 9dd21f59bf6..973c062daf7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,4 @@ TODOs.md
 dts-build/packages
 *.tsbuildinfo
 *.tgz
+packages-private/benchmark/reference
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 302428290b9..7907859bb86 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -14,5 +14,6 @@
   "[json]": {
     "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
-  "editor.formatOnSave": true
+  "editor.formatOnSave": true,
+  "vitest.disableWorkspaceWarning": true
 }
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d490aa19d95..6e787f1a982 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,599 +1,144 @@
-## [3.5.17](https://github.com/vuejs/core/compare/v3.5.16...v3.5.17) (2025-06-18)
+# [3.6.0-alpha.2](https://github.com/vuejs/core/compare/v3.6.0-alpha.1...v3.6.0-alpha.2) (2025-07-18)
 
 
 ### Bug Fixes
 
-* **compat:** allow v-model built in modifiers on component ([#12654](https://github.com/vuejs/core/issues/12654)) ([cb14b86](https://github.com/vuejs/core/commit/cb14b860f150c4a83bcd52cd26096b7a5aa3a2bf)), closes [#12652](https://github.com/vuejs/core/issues/12652)
-* **compile-sfc:** handle mapped types work with omit and pick ([#12648](https://github.com/vuejs/core/issues/12648)) ([4eb46e4](https://github.com/vuejs/core/commit/4eb46e443f1878199755cb73d481d318a9714392)), closes [#12647](https://github.com/vuejs/core/issues/12647)
-* **compiler-core:** do not increase newlines in `InEntity` state ([#13362](https://github.com/vuejs/core/issues/13362)) ([f05a8d6](https://github.com/vuejs/core/commit/f05a8d613bd873b811cfdb9979ccac8382dba322))
-* **compiler-core:** ignore whitespace when matching adjacent v-if ([#12321](https://github.com/vuejs/core/issues/12321)) ([10ebcef](https://github.com/vuejs/core/commit/10ebcef8c870dbc042b0ea49b1424b2e8f692145)), closes [#9173](https://github.com/vuejs/core/issues/9173)
-* **compiler-core:** prevent comments from blocking static node hoisting  ([#13345](https://github.com/vuejs/core/issues/13345)) ([55dad62](https://github.com/vuejs/core/commit/55dad625acd9e9ddd5a933d5e323ecfdec1a612f)), closes [#13344](https://github.com/vuejs/core/issues/13344)
-* **compiler-sfc:** improved type resolution for function type aliases ([#13452](https://github.com/vuejs/core/issues/13452)) ([f3479aa](https://github.com/vuejs/core/commit/f3479aac9625f4459e650d1c0a70e73863147903)), closes [#13444](https://github.com/vuejs/core/issues/13444)
-* **custom-element:** ensure configureApp is applied to async component ([#12607](https://github.com/vuejs/core/issues/12607)) ([5ba1afb](https://github.com/vuejs/core/commit/5ba1afba09c3ea56c1c17484f5d8aeae210ce52a)), closes [#12448](https://github.com/vuejs/core/issues/12448)
-* **custom-element:** prevent injecting child styles if shadowRoot is false ([#12769](https://github.com/vuejs/core/issues/12769)) ([73055d8](https://github.com/vuejs/core/commit/73055d8d9578d485e3fe846726b50666e1aa56f5)), closes [#12630](https://github.com/vuejs/core/issues/12630)
-* **reactivity:** add `__v_skip` flag to `Dep` to prevent reactive conversion ([#12804](https://github.com/vuejs/core/issues/12804)) ([e8d8f5f](https://github.com/vuejs/core/commit/e8d8f5f604e821acc46b4200d5b06979c05af1c2)), closes [#12803](https://github.com/vuejs/core/issues/12803)
-* **runtime-core:** unset old ref during patching when new ref is absent ([#12900](https://github.com/vuejs/core/issues/12900)) ([47ddf98](https://github.com/vuejs/core/commit/47ddf986021dff8de68b0da72787e53a6c19de4c)), closes [#12898](https://github.com/vuejs/core/issues/12898)
-* **slots:** make cache indexes marker non-enumerable ([#13469](https://github.com/vuejs/core/issues/13469)) ([919c447](https://github.com/vuejs/core/commit/919c44744bba1f0c661c87d2059c3b429611aa7e)), closes [#13468](https://github.com/vuejs/core/issues/13468)
-* **ssr:** handle initial selected state for select with v-model + v-for/v-if option ([#13487](https://github.com/vuejs/core/issues/13487)) ([1552095](https://github.com/vuejs/core/commit/15520954f9f1c7f834175938a50dba5d4be0e6c4)), closes [#13486](https://github.com/vuejs/core/issues/13486)
-* **types:** typo of `vOnce` and `vSlot` ([#13343](https://github.com/vuejs/core/issues/13343)) ([762fae4](https://github.com/vuejs/core/commit/762fae4b57ad60602e5c84465a3bff562785b314))
+* **compiler-vapor:** handle empty interpolation ([#13592](https://github.com/vuejs/core/issues/13592)) ([d1f2915](https://github.com/vuejs/core/commit/d1f2915cfe7915fa73624485ff3dd443176a31a9))
+* **compiler-vapor:** handle special characters in cached variable names ([#13626](https://github.com/vuejs/core/issues/13626)) ([a5e106d](https://github.com/vuejs/core/commit/a5e106d96eb17d73c8673e826393c910d5594a2f))
+* **compiler-vapor:** selectors was not initialized in time when the initial value of createFor source was not empty ([#13642](https://github.com/vuejs/core/issues/13642)) ([f04c9c3](https://github.com/vuejs/core/commit/f04c9c342d398c11111c873143dc437f588578ee))
+* **reactivity:** allow collect effects in EffectScope ([#13657](https://github.com/vuejs/core/issues/13657)) ([b9fb79a](https://github.com/vuejs/core/commit/b9fb79a1fd099b67e01c5fe5941551c0da3a0cae)), closes [#13656](https://github.com/vuejs/core/issues/13656)
+* **reactivity:** remove link check to align with 3.5 ([#13654](https://github.com/vuejs/core/issues/13654)) ([3cb27d1](https://github.com/vuejs/core/commit/3cb27d156f6a30e8f950616a53a3726519eaf216)), closes [#13620](https://github.com/vuejs/core/issues/13620)
+* **runtime-core:** use __vapor instead of vapor to identify Vapor components ([#13652](https://github.com/vuejs/core/issues/13652)) ([ad21b1b](https://github.com/vuejs/core/commit/ad21b1b7e96bc894f5df0d95fbd77c9ba6b15c2e))
+* **runtime-vapor:** component emits vdom interop ([#13498](https://github.com/vuejs/core/issues/13498)) ([d95fc18](https://github.com/vuejs/core/commit/d95fc186c26e81345cd75037c3c1304b0eae13b4))
+* **runtime-vapor:** handle v-model vdom interop error ([#13643](https://github.com/vuejs/core/issues/13643)) ([2be828a](https://github.com/vuejs/core/commit/2be828a0c165c7f1533ace0bd81fba43a2af16d6))
+* **runtime-vapor:** remove access globalProperties warning ([#13609](https://github.com/vuejs/core/issues/13609)) ([fca74b0](https://github.com/vuejs/core/commit/fca74b00a86c6039aa05591618539a77aaa72daf))
 
 
 
-## [3.5.16](https://github.com/vuejs/core/compare/v3.5.15...v3.5.16) (2025-05-29)
-
-
-### Reverts
-
-* Revert "fix(compiler-sfc): add scoping tag to trailing universal selector" (#13406) ([19f23b1](https://github.com/vuejs/core/commit/19f23b180bb679e38db95d6a10a420abeedc8e1c)), closes [#13406](https://github.com/vuejs/core/issues/13406)
-* Revert "fix(compiler-sfc): add error handling for defineModel() without variable" (#13390) ([42f879f](https://github.com/vuejs/core/commit/42f879fcab48e0e1011967a771b4ad9e8838d760)), closes [#13390](https://github.com/vuejs/core/issues/13390)
-
-
-
-## [3.5.15](https://github.com/vuejs/core/compare/v3.5.14...v3.5.15) (2025-05-26)
-
-
-### Bug Fixes
-
-* **compat:** ensure false value on input retains value attribute ([#13216](https://github.com/vuejs/core/issues/13216)) ([1a66474](https://github.com/vuejs/core/commit/1a664749d4d65a345589a6d78106ede7574cb2e1)), closes [#13205](https://github.com/vuejs/core/issues/13205)
-* **compat:** should not warn COMPILER_V_BIND_OBJECT_ORDER when using v-bind together with v-for ([#12993](https://github.com/vuejs/core/issues/12993)) ([93949e6](https://github.com/vuejs/core/commit/93949e6587ee019bccd5b8b9d76f0e1ed6ea16fc)), closes [#12992](https://github.com/vuejs/core/issues/12992)
-* **compile-sfc:** handle inline template source map in prod build ([#12701](https://github.com/vuejs/core/issues/12701)) ([89edc6c](https://github.com/vuejs/core/commit/89edc6cdcbd34ea6394927ecbfaa61dc4f871de7)), closes [#12682](https://github.com/vuejs/core/issues/12682) [vitejs/vite-plugin-vue#500](https://github.com/vitejs/vite-plugin-vue/issues/500)
-* **compiler-core:** ensure mapping is added only if node source is available ([#13285](https://github.com/vuejs/core/issues/13285)) ([d37a2ac](https://github.com/vuejs/core/commit/d37a2ac59d904ac0e3257ba552b6c04920a363f0)), closes [#13261](https://github.com/vuejs/core/issues/13261) [vitejs/vite-plugin-vue#368](https://github.com/vitejs/vite-plugin-vue/issues/368)
-* **compiler-dom:** improve HTML nesting validation to allow any child element within template tag ([#13320](https://github.com/vuejs/core/issues/13320)) ([163b365](https://github.com/vuejs/core/commit/163b3651d174321911648a164052effa9249a2aa)), closes [#13318](https://github.com/vuejs/core/issues/13318)
-* **compiler-sfc:** add error handling for defineModel() without variable assignment ([#13352](https://github.com/vuejs/core/issues/13352)) ([00734af](https://github.com/vuejs/core/commit/00734afef5f7bddbdaee52aa5359a6ef989f32d3)), closes [#13280](https://github.com/vuejs/core/issues/13280)
-* **compiler-sfc:** add scoping tag to trailing universal selector ([#12918](https://github.com/vuejs/core/issues/12918)) ([949df80](https://github.com/vuejs/core/commit/949df808809fd7cccf7718797beab0654aa68302)), closes [#12906](https://github.com/vuejs/core/issues/12906)
-* **compiler-sfc:** improve type inference for TSTypeAliasDeclaration with better runtime type detection ([#13245](https://github.com/vuejs/core/issues/13245)) ([cf5a5e0](https://github.com/vuejs/core/commit/cf5a5e0edf0efcab25c27aa2d13eba91f7372d39)), closes [#13240](https://github.com/vuejs/core/issues/13240)
-* **compiler-sfc:** simulate `allowArbitraryExtensions` on resolving type ([#13301](https://github.com/vuejs/core/issues/13301)) ([f7ce5ae](https://github.com/vuejs/core/commit/f7ce5ae666129339c006b339437c2dff6bceffe0)), closes [#13295](https://github.com/vuejs/core/issues/13295)
-* **custom-element:** allow injecting values ​​from app context in nested elements ([#13219](https://github.com/vuejs/core/issues/13219)) ([b991075](https://github.com/vuejs/core/commit/b9910755a50c7d6c52b28c3aef20cf97810295c9)), closes [#13212](https://github.com/vuejs/core/issues/13212)
-* **custom-element:** ensure proper remount and prevent redundant slot parsing with shadowRoot false ([#13201](https://github.com/vuejs/core/issues/13201)) ([1d41d4d](https://github.com/vuejs/core/commit/1d41d4de7f64a37160c8171d0137fd8d35c346c9)), closes [#13199](https://github.com/vuejs/core/issues/13199)
-* **custom-element:** preserve appContext during update ([#12455](https://github.com/vuejs/core/issues/12455)) ([013749e](https://github.com/vuejs/core/commit/013749e75ef3b51762a86da379ea4ba4501b54ae)), closes [#12453](https://github.com/vuejs/core/issues/12453)
-* **custom-element:** properly resolve props for sync component defs ([#12855](https://github.com/vuejs/core/issues/12855)) ([a683c80](https://github.com/vuejs/core/commit/a683c80cf44ecc482f8ac9c76bf2381443c1b0bb)), closes [#12854](https://github.com/vuejs/core/issues/12854)
-* **hydration:** handle transition appear hydration edge case ([#13339](https://github.com/vuejs/core/issues/13339)) ([35aeae7](https://github.com/vuejs/core/commit/35aeae7fa3168adcf9ed95fd35495d17c8b93eeb)), closes [#13335](https://github.com/vuejs/core/issues/13335)
-* **hydration:** skip lazy hydration for patched components ([#13283](https://github.com/vuejs/core/issues/13283)) ([80055fd](https://github.com/vuejs/core/commit/80055fddfb3ca1e2a44f19c7f0ffaeba00de5140)), closes [#13255](https://github.com/vuejs/core/issues/13255)
-* **suspense:** handle edge case in patching list nodes within Suspense ([#13306](https://github.com/vuejs/core/issues/13306)) ([772b008](https://github.com/vuejs/core/commit/772b0087cb7be151c514a1d30365fb0f61a652ba)), closes [#13305](https://github.com/vuejs/core/issues/13305)
-* **teleport:** handle deferred teleport updates before and after mount ([#13350](https://github.com/vuejs/core/issues/13350)) ([d15dce3](https://github.com/vuejs/core/commit/d15dce3142474f2ef9fffed38383acdadcb26c4c)), closes [#13349](https://github.com/vuejs/core/issues/13349)
-* **types:** avoid merging component instance into `$props` in `ComponentInstance` ([#12870](https://github.com/vuejs/core/issues/12870)) ([f44feed](https://github.com/vuejs/core/commit/f44feed6fa461a9c4c724e9631c19e9e214c0a20)), closes [#12751](https://github.com/vuejs/core/issues/12751)
-* **types:** exclude `undefined` from inferred prop types with default values ([#13007](https://github.com/vuejs/core/issues/13007)) ([5179d32](https://github.com/vuejs/core/commit/5179d328d950015e7fb2a74fe1a8518fd8d2c94e)), closes [#13006](https://github.com/vuejs/core/issues/13006)
-* **watch:** update `oldValue` before running `cb` to prevent stale value ([#12296](https://github.com/vuejs/core/issues/12296)) ([c69c4bb](https://github.com/vuejs/core/commit/c69c4bb59c114f2b5e03733b55ef9ace3087b5c3)), closes [#12294](https://github.com/vuejs/core/issues/12294)
-
-
-
-## [3.5.14](https://github.com/vuejs/core/compare/v3.5.13...v3.5.14) (2025-05-15)
-
-
-### Bug Fixes
-
-* **compat:** correct deprecation message for v-bind.sync usage ([#13137](https://github.com/vuejs/core/issues/13137)) ([466b30f](https://github.com/vuejs/core/commit/466b30f4049ec89fb282624ec17d1a93472ab93f)), closes [#13133](https://github.com/vuejs/core/issues/13133)
-* **compiler-core:** remove slot cache from parent renderCache during unmounting ([#13215](https://github.com/vuejs/core/issues/13215)) ([5d166f3](https://github.com/vuejs/core/commit/5d166f3796a03a497435fc079c6a83a4e9c6cf52))
-* **compiler-sfc:** fix scope handling for props destructure in function parameters and catch clauses ([8e34357](https://github.com/vuejs/core/commit/8e3435779a667de485cf9efd78667d0ca14c5f84)), closes [#12790](https://github.com/vuejs/core/issues/12790)
-* **compiler-sfc:** treat the return value of `useTemplateRef` as a definite ref ([#13197](https://github.com/vuejs/core/issues/13197)) ([8ae1122](https://github.com/vuejs/core/commit/8ae11226e8ee938615e17c7b81dc38ae3f7cefb9))
-* **compiler:** fix spelling error in domTagConfig ([#13043](https://github.com/vuejs/core/issues/13043)) ([388295b](https://github.com/vuejs/core/commit/388295b27f3cc69eba25d325bbe60a36a3df831a))
-* **customFormatter:** properly accessing ref value during debugger ([#12948](https://github.com/vuejs/core/issues/12948)) ([fdbd026](https://github.com/vuejs/core/commit/fdbd02658301dd794fe0c84f0018d080a07fca9f))
-* **hmr/teleport:** adjust static children traversal for HMR in dev mode ([#12819](https://github.com/vuejs/core/issues/12819)) ([5e37dd0](https://github.com/vuejs/core/commit/5e37dd009562bcd8080a200c32abde2d6e4f0305)), closes [#12816](https://github.com/vuejs/core/issues/12816)
-* **hmr:** avoid hydration for hmr root reload ([#12450](https://github.com/vuejs/core/issues/12450)) ([1f98a9c](https://github.com/vuejs/core/commit/1f98a9c493d01c21befa90107f0593bc92a58932)), closes [vitejs/vite-plugin-vue#146](https://github.com/vitejs/vite-plugin-vue/issues/146) [vitejs/vite-plugin-vue#477](https://github.com/vitejs/vite-plugin-vue/issues/477)
-* **hmr:** avoid hydration for hmr updating ([#12262](https://github.com/vuejs/core/issues/12262)) ([9c4dbbc](https://github.com/vuejs/core/commit/9c4dbbc5185125835ad3e49baba303bd54676111)), closes [#7706](https://github.com/vuejs/core/issues/7706) [#8170](https://github.com/vuejs/core/issues/8170)
-* **reactivity:** ensure markRaw objects are not reactive ([#12824](https://github.com/vuejs/core/issues/12824)) ([295b5ec](https://github.com/vuejs/core/commit/295b5ec19b6a52c4a56652cc4d6e93a4ea7c14ed)), closes [#12807](https://github.com/vuejs/core/issues/12807)
-* **reactivity:** ensure multiple effectScope on() and off() calls maintains correct active scope ([22dcbf3](https://github.com/vuejs/core/commit/22dcbf3e20eb84f69c8952f6f70d9990136a4a68)), closes [#12631](https://github.com/vuejs/core/issues/12631) [#12632](https://github.com/vuejs/core/issues/12632) [#12641](https://github.com/vuejs/core/issues/12641)
-* **reactivity:** should not recompute if computed does not track reactive data ([#12341](https://github.com/vuejs/core/issues/12341)) ([0b23fd2](https://github.com/vuejs/core/commit/0b23fd23833cf085e7e112bf4435cfc9b360d072)), closes [#12337](https://github.com/vuejs/core/issues/12337)
-* **runtime-core:**  stop tracking deps in setRef during unmount ([#13210](https://github.com/vuejs/core/issues/13210)) ([016c472](https://github.com/vuejs/core/commit/016c472bd2e7604b21c69dee1da8545ce26e4d2f))
-* **runtime-core:**  update __vnode of static nodes when patching along the optimized path ([#13223](https://github.com/vuejs/core/issues/13223)) ([b3ecee3](https://github.com/vuejs/core/commit/b3ecee3da8ed5c55dea89ce6b4b376b2b722b018))
-* **runtime-core:** inherit comment nodes during block patch in production build  ([#10748](https://github.com/vuejs/core/issues/10748)) ([6264505](https://github.com/vuejs/core/commit/626450590d81f79117b34d2a73073b1dc8f551bd)), closes [#10747](https://github.com/vuejs/core/issues/10747) [#12650](https://github.com/vuejs/core/issues/12650)
-* **runtime-core:** prevent unmounted vnode from being inserted during transition leave ([#12862](https://github.com/vuejs/core/issues/12862)) ([d6a6ec1](https://github.com/vuejs/core/commit/d6a6ec13ce521683bfb2a22932778ef7b51f8600)), closes [#12860](https://github.com/vuejs/core/issues/12860)
-* **runtime-core:** respect immutability for readonly reactive arrays in `v-for` ([#13091](https://github.com/vuejs/core/issues/13091)) ([3f27c58](https://github.com/vuejs/core/commit/3f27c58ffbd4309df369bc89493fdc284dc540bb)), closes [#13087](https://github.com/vuejs/core/issues/13087)
-* **runtime-dom:** always treat autocorrect as attribute ([#13001](https://github.com/vuejs/core/issues/13001)) ([1499135](https://github.com/vuejs/core/commit/1499135c227236e037bb746beeb777941b0b58ff)), closes [#5705](https://github.com/vuejs/core/issues/5705)
-* **slots:** properly warn if slot invoked in setup ([#12195](https://github.com/vuejs/core/issues/12195)) ([9196222](https://github.com/vuejs/core/commit/9196222ae1d63b52b35ac5fbf5e71494587ccf05)), closes [#12194](https://github.com/vuejs/core/issues/12194)
-* **ssr:** properly init slots during ssr rendering ([#12441](https://github.com/vuejs/core/issues/12441)) ([2206cd2](https://github.com/vuejs/core/commit/2206cd235a1627c540e795e378b7564a55b47313)), closes [#12438](https://github.com/vuejs/core/issues/12438)
-* **transition:** fix KeepAlive with transition out-in mode behavior in production ([#12468](https://github.com/vuejs/core/issues/12468)) ([343c891](https://github.com/vuejs/core/commit/343c89122448719bd6ed6bd9de986dfb2721d6bf)), closes [#12465](https://github.com/vuejs/core/issues/12465)
-* **TransitionGroup:** reset prevChildren to prevent memory leak ([#13183](https://github.com/vuejs/core/issues/13183)) ([8b848cb](https://github.com/vuejs/core/commit/8b848cbbd2af337d23e19e202f9ab433f8580855)), closes [#13181](https://github.com/vuejs/core/issues/13181)
-* **types:** allow return any for Options API lifecycle hooks ([#5914](https://github.com/vuejs/core/issues/5914)) ([06310e8](https://github.com/vuejs/core/commit/06310e82f5bed62d1b9733dcb18cd8d6edc988de))
-* **types:** the directive's modifiers should be optional ([#12605](https://github.com/vuejs/core/issues/12605)) ([10e54dc](https://github.com/vuejs/core/commit/10e54dcc86a7967f3196d96200bcbd1d3d42082f))
-* **typos:** fix comments referencing transformElement.ts ([#12551](https://github.com/vuejs/core/issues/12551))[ci-skip] ([11c053a](https://github.com/vuejs/core/commit/11c053a5429ad0d27a0e2c78b6b026ea00ace116))
-
+# [3.6.0-alpha.1](https://github.com/vuejs/core/compare/v3.5.17...v3.6.0-alpha.1) (2025-07-12)
 
 ### Features
 
-* **types:** add type TemplateRef ([#12645](https://github.com/vuejs/core/issues/12645)) ([636a861](https://github.com/vuejs/core/commit/636a8619f06c71dfd79f7f6412fd130c4f84226f))
-
-
-
-## [3.5.13](https://github.com/vuejs/core/compare/v3.5.12...v3.5.13) (2024-11-15)
-
-
-### Bug Fixes
-
-* **compiler-core:** handle v-memo + v-for with functional key ([#12014](https://github.com/vuejs/core/issues/12014)) ([99009ee](https://github.com/vuejs/core/commit/99009eee0efc238392daba93792d478525b21afa)), closes [#12013](https://github.com/vuejs/core/issues/12013)
-* **compiler-dom:** properly stringify template string style ([#12392](https://github.com/vuejs/core/issues/12392)) ([2d78539](https://github.com/vuejs/core/commit/2d78539da35322aea5f821b3cf9b02d006abac72)), closes [#12391](https://github.com/vuejs/core/issues/12391)
-* **custom-element:** avoid triggering mutationObserver when relecting props ([352bc88](https://github.com/vuejs/core/commit/352bc88c1bd2fda09c61ab17ea1a5967ffcd7bc0)), closes [#12214](https://github.com/vuejs/core/issues/12214) [#12215](https://github.com/vuejs/core/issues/12215)
-* **deps:** update dependency postcss to ^8.4.48 ([#12356](https://github.com/vuejs/core/issues/12356)) ([b5ff930](https://github.com/vuejs/core/commit/b5ff930089985a58c3553977ef999cec2a6708a4))
-* **hydration:** the component vnode's el should be updated when a mismatch occurs. ([#12255](https://github.com/vuejs/core/issues/12255)) ([a20a4cb](https://github.com/vuejs/core/commit/a20a4cb36a3e717d1f8f259d0d59f133f508ff0a)), closes [#12253](https://github.com/vuejs/core/issues/12253)
-* **reactivity:** avoid unnecessary watcher effect removal from inactive scope ([2193284](https://github.com/vuejs/core/commit/21932840eae72ffcd357a62ec596aaecc7ec224a)), closes [#5783](https://github.com/vuejs/core/issues/5783) [#5806](https://github.com/vuejs/core/issues/5806)
-* **reactivity:** release nested effects/scopes on effect scope stop ([#12373](https://github.com/vuejs/core/issues/12373)) ([bee2f5e](https://github.com/vuejs/core/commit/bee2f5ee62dc0cd04123b737779550726374dd0a)), closes [#12370](https://github.com/vuejs/core/issues/12370)
-* **runtime-dom:** set css vars before user onMounted hooks ([2d5c5e2](https://github.com/vuejs/core/commit/2d5c5e25e9b7a56e883674fb434135ac514429b5)), closes [#11533](https://github.com/vuejs/core/issues/11533)
-* **runtime-dom:** set css vars on update to handle child forcing reflow in onMount ([#11561](https://github.com/vuejs/core/issues/11561)) ([c4312f9](https://github.com/vuejs/core/commit/c4312f9c715c131a09e552ba46e9beb4b36d55e6))
-* **ssr:** avoid updating subtree of async component if it is resolved ([#12363](https://github.com/vuejs/core/issues/12363)) ([da7ad5e](https://github.com/vuejs/core/commit/da7ad5e3d24f3e108401188d909d27a4910da095)), closes [#12362](https://github.com/vuejs/core/issues/12362)
-* **ssr:** ensure v-text updates correctly with custom directives in SSR output ([#12311](https://github.com/vuejs/core/issues/12311)) ([1f75d4e](https://github.com/vuejs/core/commit/1f75d4e6dfe18121ebe443cd3e8105d54f727893)), closes [#12309](https://github.com/vuejs/core/issues/12309)
-* **ssr:** handle initial selected state for select with v-model + v-for option ([#12399](https://github.com/vuejs/core/issues/12399)) ([4f8d807](https://github.com/vuejs/core/commit/4f8d8078221ee52deed266677a227ad2a6d8dd22)), closes [#12395](https://github.com/vuejs/core/issues/12395)
-* **teleport:** handle deferred teleport update before mounted ([#12168](https://github.com/vuejs/core/issues/12168)) ([8bff142](https://github.com/vuejs/core/commit/8bff142f99b646e9dd15897ec75368fbf34f1534)), closes [#12161](https://github.com/vuejs/core/issues/12161)
-* **templateRef:** set ref on cached async component which wrapped in KeepAlive ([#12290](https://github.com/vuejs/core/issues/12290)) ([983eb50](https://github.com/vuejs/core/commit/983eb50a17eac76f1bba4394ad0316c62b72191d)), closes [#4999](https://github.com/vuejs/core/issues/4999) [#5004](https://github.com/vuejs/core/issues/5004)
-* **test:** update snapshot ([#12169](https://github.com/vuejs/core/issues/12169)) ([828d4a4](https://github.com/vuejs/core/commit/828d4a443919fa2aa4e2e92fbd03a5f04b258eea))
-* **Transition:** fix transition memory leak edge case ([#12182](https://github.com/vuejs/core/issues/12182)) ([660132d](https://github.com/vuejs/core/commit/660132df6c6a8c14bf75e593dc47d2fdada30322)), closes [#12181](https://github.com/vuejs/core/issues/12181)
-* **transition:** reflow before leave-active class after leave-from ([#12288](https://github.com/vuejs/core/issues/12288)) ([4b479db](https://github.com/vuejs/core/commit/4b479db61d233b054561402ae94ef08550073ea1)), closes [#2593](https://github.com/vuejs/core/issues/2593)
-* **types:** defineEmits w/ interface declaration ([#12343](https://github.com/vuejs/core/issues/12343)) ([1022eab](https://github.com/vuejs/core/commit/1022eabaa1aaf8436876f5ec5573cb1e4b3959a6)), closes [#8457](https://github.com/vuejs/core/issues/8457)
-* **v-once:** setting hasOnce to current block only when in v-once ([#12374](https://github.com/vuejs/core/issues/12374)) ([37300fc](https://github.com/vuejs/core/commit/37300fc26190a7299efddbf98800ffd96d5cad96)), closes [#12371](https://github.com/vuejs/core/issues/12371)
-
-
-### Performance Improvements
-
-* **reactivity:** do not track inner key `__v_skip`` ([#11690](https://github.com/vuejs/core/issues/11690)) ([d637bd6](https://github.com/vuejs/core/commit/d637bd6c0164c2883e6eabd3c2f1f8c258dedfb1))
-* **runtime-core:** use feature flag for call to resolveMergedOptions ([#12163](https://github.com/vuejs/core/issues/12163)) ([1755ac0](https://github.com/vuejs/core/commit/1755ac0a108ba3486bd8397e56d3bdcd69196594))
-
-
-
-## [3.5.12](https://github.com/vuejs/core/compare/v3.5.11...v3.5.12) (2024-10-11)
-
-
-### Bug Fixes
-
-* **compiler-dom:** avoid stringify option with null value ([#12096](https://github.com/vuejs/core/issues/12096)) ([f6d9926](https://github.com/vuejs/core/commit/f6d99262364b7444ebab8742158599e8cdd79eaa)), closes [#12093](https://github.com/vuejs/core/issues/12093)
-* **compiler-sfc:**  do not skip TSInstantiationExpression when transforming props destructure ([#12064](https://github.com/vuejs/core/issues/12064)) ([d3ecde8](https://github.com/vuejs/core/commit/d3ecde8a696ff62c8d0ab067fd1d7ee0565b63c5))
-* **compiler-sfc:** use sass modern api if available and avoid deprecation warning ([#11992](https://github.com/vuejs/core/issues/11992)) ([4474c11](https://github.com/vuejs/core/commit/4474c113d1fb1c26298dd6794275d5b5c7cc4d93))
-* **compiler:** clone loc to `ifNode` ([#12131](https://github.com/vuejs/core/issues/12131)) ([cde2c06](https://github.com/vuejs/core/commit/cde2c0671b00d4f6111fcbd7aa76e45872f20b0c)), closes [vuejs/language-tools#4911](https://github.com/vuejs/language-tools/issues/4911)
-* **custom-element:** properly remove hyphenated attribute ([#12143](https://github.com/vuejs/core/issues/12143)) ([e16e9a7](https://github.com/vuejs/core/commit/e16e9a7341e7cfb3c443da4e5e5b06e8158712c3)), closes [#12139](https://github.com/vuejs/core/issues/12139)
-* **defineModel:** handle kebab-case model correctly ([#12063](https://github.com/vuejs/core/issues/12063)) ([c0418a3](https://github.com/vuejs/core/commit/c0418a3b8fa96a0b108ab71b7aab5d3388f90557)), closes [#12060](https://github.com/vuejs/core/issues/12060)
-* **deps:** update dependency monaco-editor to ^0.52.0 ([#12119](https://github.com/vuejs/core/issues/12119)) ([f7cbea2](https://github.com/vuejs/core/commit/f7cbea2111c7770a180b640f36f6a5d4d6abc698))
-* **hydration:** provide compat fallback for idle callback hydration strategy ([#11935](https://github.com/vuejs/core/issues/11935)) ([1ae545a](https://github.com/vuejs/core/commit/1ae545a3786abef983be1c969726489685569c92))
-* **reactivity:** trigger reactivity for Map key `undefined` ([#12055](https://github.com/vuejs/core/issues/12055)) ([7ad289e](https://github.com/vuejs/core/commit/7ad289e1e7fea654524008ff91e43a8b8a55ef22)), closes [#12054](https://github.com/vuejs/core/issues/12054)
-* **runtime-core:** allow symbol values for slot prop key ([#12069](https://github.com/vuejs/core/issues/12069)) ([d9d4d4e](https://github.com/vuejs/core/commit/d9d4d4e158cd51a9ddda249f29de8467f60b2792)), closes [#12068](https://github.com/vuejs/core/issues/12068)
-* **runtime-core:** fix required prop check false positive for kebab-case edge cases  ([#12034](https://github.com/vuejs/core/issues/12034)) ([9da1ac1](https://github.com/vuejs/core/commit/9da1ac156552ac449754e1373aac7e349841becb)), closes [#12011](https://github.com/vuejs/core/issues/12011)
-* **runtime-dom:** prevent unnecessary updates in v-model checkbox when value is unchanged ([#12146](https://github.com/vuejs/core/issues/12146)) ([ea943af](https://github.com/vuejs/core/commit/ea943afe404c4ca4b729906c5e8daf7aa2ccde9b)), closes [#12144](https://github.com/vuejs/core/issues/12144)
-* **teleport:** handle disabled teleport with updateCssVars ([#12113](https://github.com/vuejs/core/issues/12113)) ([76a8223](https://github.com/vuejs/core/commit/76a8223199c148b79a5c0ea19e235164809760cd)), closes [#12112](https://github.com/vuejs/core/issues/12112)
-* **transition/ssr:** make transition appear work with Suspense in SSR ([#12047](https://github.com/vuejs/core/issues/12047)) ([f1a4f67](https://github.com/vuejs/core/commit/f1a4f67aedfe83e440c54222213f070774faa421)), closes [#12046](https://github.com/vuejs/core/issues/12046)
-* **types:** ensure `this.$props` type does not include `string` ([#12123](https://github.com/vuejs/core/issues/12123)) ([704173e](https://github.com/vuejs/core/commit/704173e24276706de672cca6c9507e4dd9651197)), closes [#12122](https://github.com/vuejs/core/issues/12122)
-* **types:** retain union type narrowing with defaults applied ([#12108](https://github.com/vuejs/core/issues/12108)) ([05685a9](https://github.com/vuejs/core/commit/05685a9d7c42d4cd37169b867833776b91154fed)), closes [#12106](https://github.com/vuejs/core/issues/12106)
-* **useId:** ensure useId consistency when using serverPrefetch ([#12128](https://github.com/vuejs/core/issues/12128)) ([b4d3534](https://github.com/vuejs/core/commit/b4d35349d8bc39aa15bd3f1094d230e5928b177c)), closes [#12102](https://github.com/vuejs/core/issues/12102)
-* **watch:** watchEffect clean-up with SSR ([#12097](https://github.com/vuejs/core/issues/12097)) ([b094c72](https://github.com/vuejs/core/commit/b094c72b3d40c52c7124f145a9db028509a11202)), closes [#11956](https://github.com/vuejs/core/issues/11956)
+- **vapor mode** ([#12359](https://github.com/vuejs/core/issues/12359)) ([bfe5ce3](https://github.com/vuejs/core/commit/bfe5ce309c6fc16bb49cca78e141862bc12708ac))
 
+  Please see [About Vapor Mode](#about-vapor-mode) section below for details.
 
 ### Performance Improvements
 
-* **reactivity:** avoid unnecessary recursion in removeSub ([#12135](https://github.com/vuejs/core/issues/12135)) ([ec917cf](https://github.com/vuejs/core/commit/ec917cfdb9d0169cd0835d3a0e28244242657dc9))
-
-
-
-## [3.5.11](https://github.com/vuejs/core/compare/v3.5.10...v3.5.11) (2024-10-03)
-
+- **reactivity:** refactor reactivity core by porting [alien-signals](https://github.com/stackblitz/alien-signals) ([#12349](https://github.com/vuejs/core/issues/12349)) ([313dc61](https://github.com/vuejs/core/commit/313dc61bef59e6869aaec9b5ea47c0bf9044a3fc))
 
 ### Bug Fixes
 
-* **compiler-sfc:** do not skip `TSSatisfiesExpression` when transforming props destructure ([#12062](https://github.com/vuejs/core/issues/12062)) ([2328b05](https://github.com/vuejs/core/commit/2328b051f4efa1f1394b7d4e73b7c3f76e430e7c)), closes [#12061](https://github.com/vuejs/core/issues/12061)
-* **reactivity:** prevent overwriting `next` property during batch processing ([#12075](https://github.com/vuejs/core/issues/12075)) ([d3f5e6e](https://github.com/vuejs/core/commit/d3f5e6e5319b4ffaa55ca9a2ea3d95d78e76fa58)), closes [#12072](https://github.com/vuejs/core/issues/12072)
-* **scheduler:** job ordering when the post queue is flushing ([#12090](https://github.com/vuejs/core/issues/12090)) ([577edca](https://github.com/vuejs/core/commit/577edca8e7795436efd710d1c289ea8ea2642b0e))
-* **types:** correctly infer `TypeProps` when it is `any` ([#12073](https://github.com/vuejs/core/issues/12073)) ([57315ab](https://github.com/vuejs/core/commit/57315ab9688c9741a271d1075bbd28cbe5f71e2f)), closes [#12058](https://github.com/vuejs/core/issues/12058)
-* **types:** should not intersect `PublicProps` with `Props` ([#12077](https://github.com/vuejs/core/issues/12077)) ([6f85894](https://github.com/vuejs/core/commit/6f8589437635706f825ccec51800effba1d2bf5f))
-* **types:** infer the first generic type of `Ref` correctly ([#12094](https://github.com/vuejs/core/issues/12094)) ([c97bb84](https://github.com/vuejs/core/commit/c97bb84d0b0a16b012f886b6498e924415ed63e5))
-
+- **css-vars:** nullish v-bind in style should not lead to unexpected inheritance ([#12461](https://github.com/vuejs/core/issues/12461)) ([c85f1b5](https://github.com/vuejs/core/commit/c85f1b5a132eb8ec25f71b250e25e65a5c20964f)), closes [#12434](https://github.com/vuejs/core/issues/12434) [#12439](https://github.com/vuejs/core/issues/12439) [#7474](https://github.com/vuejs/core/issues/7474) [#7475](https://github.com/vuejs/core/issues/7475)
+- **reactivity:** ensure multiple effectScope `on()` and `off()` calls maintains correct active scope ([#12641](https://github.com/vuejs/core/issues/12641)) ([679cbdf](https://github.com/vuejs/core/commit/679cbdf4806cf8c325098f2b579abab60fffb1bb))
+- **reactivity:** queuing effects in an array ([#13078](https://github.com/vuejs/core/issues/13078)) ([826550c](https://github.com/vuejs/core/commit/826550cd629c59dd91aeb5abdbe101a483497358))
+- **reactivity:** toRefs should be allowed on plain objects ([ac43b11](https://github.com/vuejs/core/commit/ac43b118975b17d7ce7d9e6886f8806af11bee55))
+- **scheduler:** improve error handling in job flushing ([#13587](https://github.com/vuejs/core/issues/13587)) ([94b2ddc](https://github.com/vuejs/core/commit/94b2ddc6f97170f4169d9d81b963c6bcaab08be2))
+- **scheduler:** recover nextTick from error in post flush cb ([2bbb6d2](https://github.com/vuejs/core/commit/2bbb6d2fc56896e64a32b4421822d12bde2bb6e8))
 
+### About Vapor Mode
 
-## [3.5.10](https://github.com/vuejs/core/compare/v3.5.9...v3.5.10) (2024-09-27)
+Vapor Mode is a new compilation mode for Vue Single-File Components (SFC) with the goal of reducing baseline bundle size and improved performance. It is 100% opt-in, and supports a subset of existing Vue APIs with mostly identical behavior.
 
+Vapor Mode has demonstrated the same level of performance with Solid and Svelte 5 in [3rd party benchmarks](https://github.com/krausest/js-framework-benchmark).
 
-### Bug Fixes
+#### General Stability Notes
 
-* **custom-element:** properly set kebab-case props on Vue custom elements ([ea3efa0](https://github.com/vuejs/core/commit/ea3efa09e008918c1d9ba7226833a8b1a7a57244)), closes [#12030](https://github.com/vuejs/core/issues/12030) [#12032](https://github.com/vuejs/core/issues/12032)
-* **reactivity:** fix nested batch edge case ([93c95dd](https://github.com/vuejs/core/commit/93c95dd4cd416503f43a98a1455f62658d22b0b2))
-* **reactivity:** only clear notified flags for computed in first batch iteration ([aa9ef23](https://github.com/vuejs/core/commit/aa9ef2386a0cd39a174e5a887ec2b1a3525034fc)), closes [#12045](https://github.com/vuejs/core/issues/12045)
-* **types/ref:** handle nested refs in UnwrapRef ([#12049](https://github.com/vuejs/core/issues/12049)) ([e2c19c2](https://github.com/vuejs/core/commit/e2c19c20cfee9788519a80c0e53e216b78505994)), closes [#12044](https://github.com/vuejs/core/issues/12044)
+Vapor Mode is available starting in Vue 3.6 alpha. Please note it is still incomplete and unstable during the alpha phase. The current focus is making it available for wider stability and compatibility testing. For now, we recommend using it for the following cases:
 
+- Partial usage in existing apps, e.g. implementing a perf-sensitive sub page in Vapor Mode.
+- Build small new apps entirely in Vapor Mode.
 
+We do not recommend migrating existing components to Vapor Mode yet.
 
-## [3.5.9](https://github.com/vuejs/core/compare/v3.5.8...v3.5.9) (2024-09-26)
+#### Pending Features
 
+Things that do not work in this version yet:
 
-### Bug Fixes
+- SSR hydration\* (which means it does not work with Nuxt yet)
+- Async Component\*
+- Transition\*
+- KeepAlive\*
+- Suspense
 
-* **reactivity:** fix property dep removal regression ([6001e5c](https://github.com/vuejs/core/commit/6001e5c81a05c894586f9287fbd991677bdd0455)), closes [#12020](https://github.com/vuejs/core/issues/12020) [#12021](https://github.com/vuejs/core/issues/12021)
-* **reactivity:** fix recursive sync watcher on computed edge case ([10ff159](https://github.com/vuejs/core/commit/10ff15924053d9bd95ad706f78ce09e288213fcf)), closes [#12033](https://github.com/vuejs/core/issues/12033) [#12037](https://github.com/vuejs/core/issues/12037)
-* **runtime-core:** avoid rendering plain object as VNode ([#12038](https://github.com/vuejs/core/issues/12038)) ([cb34b28](https://github.com/vuejs/core/commit/cb34b28a4a9bf868be4785b001c526163eda342e)), closes [#12035](https://github.com/vuejs/core/issues/12035) [vitejs/vite-plugin-vue#353](https://github.com/vitejs/vite-plugin-vue/issues/353)
-* **runtime-core:** make useId() always return a string ([a177092](https://github.com/vuejs/core/commit/a177092754642af2f98c33a4feffe8f198c3c950))
-* **types:** correct type inference of union event names ([#12022](https://github.com/vuejs/core/issues/12022)) ([4da6881](https://github.com/vuejs/core/commit/4da688141d9e7c15b622c289deaa81b11845b2c7))
-* **vue:** properly cache runtime compilation ([#12019](https://github.com/vuejs/core/issues/12019)) ([fa0ba24](https://github.com/vuejs/core/commit/fa0ba24b3ace02d7ecab65e57c2bea89a2550dcb))
+Features marked with \* have pending PRs which will be merged during the alpha phase.
 
+#### Opting in to Vapor Mode
 
+Vapor Mode only works for Single File Components using `<script setup>`. To opt-in, add the `vapor` attribute to `<script setup>`:
 
-## [3.5.8](https://github.com/vuejs/core/compare/v3.5.7...v3.5.8) (2024-09-22)
-
-
-### Bug Fixes
+```vue
+<script setup vapor>
+// ...
+</script>
+```
 
-* **reactivity:** do not remove dep from depsMap when cleaning up deps of computed ([#11995](https://github.com/vuejs/core/issues/11995)) ([0267a58](https://github.com/vuejs/core/commit/0267a588017eee4951ac2a877fe1ccae84cad905))
-
-
-
-## [3.5.7](https://github.com/vuejs/core/compare/v3.5.6...v3.5.7) (2024-09-20)
-
-
-### Bug Fixes
+Vapor Mode components are usable in two scenarios:
 
-* **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)
+1. Inside a Vapor app instance create via `createVaporApp`. Apps created this way avoids pulling in the Virtual DOM runtime code and allows bundle baseline size to be drastically reduced.
 
+2. To use Vapor components in a VDOM app instance created via `createApp`, the `vaporInteropPlugin` must be installed:
 
-### 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)
-
-
-### Bug Fixes
-
-* **compile-dom:** should be able to stringify mathML ([#11891](https://github.com/vuejs/core/issues/11891)) ([85c138c](https://github.com/vuejs/core/commit/85c138ced108268f7656b568dfd3036a1e0aae34))
-* **compiler-sfc:** preserve old behavior when using withDefaults with desutructure ([8492c3c](https://github.com/vuejs/core/commit/8492c3c49a922363d6c77ef192c133a8fbce6514)), closes [#11930](https://github.com/vuejs/core/issues/11930)
-* **reactivity:** avoid exponential perf cost and reduce call stack depth for deeply chained computeds ([#11944](https://github.com/vuejs/core/issues/11944)) ([c74bb8c](https://github.com/vuejs/core/commit/c74bb8c2dd9e82aaabb0a2a2b368e900929b513b)), closes [#11928](https://github.com/vuejs/core/issues/11928)
-* **reactivity:** rely on dirty check only when computed has deps ([#11931](https://github.com/vuejs/core/issues/11931)) ([aa5dafd](https://github.com/vuejs/core/commit/aa5dafd2b55d42d6a29316a3bc91aea85c676a0b)), closes [#11929](https://github.com/vuejs/core/issues/11929)
-* **watch:** `once` option should be ignored by watchEffect ([#11884](https://github.com/vuejs/core/issues/11884)) ([49fa673](https://github.com/vuejs/core/commit/49fa673493d93b77ddba2165ab6545bae84fd1ae))
-* **watch:** unwatch should be callable during SSR ([#11925](https://github.com/vuejs/core/issues/11925)) ([2d6adf7](https://github.com/vuejs/core/commit/2d6adf78a047eed091db277ffbd9df0822fb0bdd)), closes [#11924](https://github.com/vuejs/core/issues/11924)
-
-
-
-## [3.5.5](https://github.com/vuejs/core/compare/v3.5.4...v3.5.5) (2024-09-13)
-
-
-### Bug Fixes
-
-* **compiler-core:** fix handling of delimiterOpen in VPre ([#11915](https://github.com/vuejs/core/issues/11915)) ([706d4ac](https://github.com/vuejs/core/commit/706d4ac1d0210b2d9134b3228280187fe02fc971)), closes [#11913](https://github.com/vuejs/core/issues/11913)
-* **compiler-dom:** fix stringify static edge for partially eligible chunks in cached parent ([1d99d61](https://github.com/vuejs/core/commit/1d99d61c1bd77f9ea6743f6214a82add8346a121)), closes [#11879](https://github.com/vuejs/core/issues/11879) [#11890](https://github.com/vuejs/core/issues/11890)
-* **compiler-dom:** should ignore leading newline in <textarea> per spec ([3c4bf76](https://github.com/vuejs/core/commit/3c4bf7627649ec1e3220f8c4e4163c20d2afb367))
-* **compiler-sfc:** nested css supports atrule and comment ([#11899](https://github.com/vuejs/core/issues/11899)) ([0e7bc71](https://github.com/vuejs/core/commit/0e7bc717e6640644f062957ec5031506f0dab215)), closes [#11896](https://github.com/vuejs/core/issues/11896)
-* **custom-element:** handle nested customElement mount w/ shadowRoot false ([#11861](https://github.com/vuejs/core/issues/11861)) ([f2d8019](https://github.com/vuejs/core/commit/f2d801918841e7673ff3f048d0d895592a2f7e23)), closes [#11851](https://github.com/vuejs/core/issues/11851) [#11871](https://github.com/vuejs/core/issues/11871)
-* **hmr:** reload async child wrapped in Suspense + KeepAlive ([#11907](https://github.com/vuejs/core/issues/11907)) ([10a2c60](https://github.com/vuejs/core/commit/10a2c6053bd30d160d0214bb3566f540187e6874)), closes [#11868](https://github.com/vuejs/core/issues/11868)
-* **hydration:** fix mismatch of leading newline in `<textarea>` and `<pre>` ([a5f3c2e](https://github.com/vuejs/core/commit/a5f3c2eb4d2e7fae93ff93ce865b269f01cc825e)), closes [#11873](https://github.com/vuejs/core/issues/11873) [#11874](https://github.com/vuejs/core/issues/11874)
-* **reactivity:** properly clean up deps, fix memory leak ([8ea5d6d](https://github.com/vuejs/core/commit/8ea5d6d6981ab7febda0be43c3c92b18869c3a2a)), closes [#11901](https://github.com/vuejs/core/issues/11901)
-* **runtime-core:** properly update async component nested in KeepAlive ([#11917](https://github.com/vuejs/core/issues/11917)) ([7fe6c79](https://github.com/vuejs/core/commit/7fe6c795a1fc7ddcea5ad91a56141561192373ac)), closes [#11916](https://github.com/vuejs/core/issues/11916)
-* **TransitionGroup:** not warn unkeyed text children with whitespece preserve ([#11888](https://github.com/vuejs/core/issues/11888)) ([7571f20](https://github.com/vuejs/core/commit/7571f20bc3d1854377a146f41d211e05bb68cd47)), closes [#11885](https://github.com/vuejs/core/issues/11885)
-
-
-
-## [3.5.4](https://github.com/vuejs/core/compare/v3.5.3...v3.5.4) (2024-09-10)
-
-
-### Bug Fixes
-
-* **compiler-sfc:** correct scoped injection for nesting selector ([#11854](https://github.com/vuejs/core/issues/11854)) ([b1de75e](https://github.com/vuejs/core/commit/b1de75ed04626b6423085dfde91fb0cb481a25e8)), closes [#10567](https://github.com/vuejs/core/issues/10567)
-* **reactivity:** fix markRaw error on already marked object ([#11864](https://github.com/vuejs/core/issues/11864)) ([67d6596](https://github.com/vuejs/core/commit/67d6596d40b1807b9cd8eb0d9282932ea77be3c0)), closes [#11862](https://github.com/vuejs/core/issues/11862)
-* Revert "fix: Revert "fix(reactivity): self-referencing computed should refresh"" ([e596378](https://github.com/vuejs/core/commit/e596378e0be728dad7d60938449f3fa557ca2ec9))
-* **runtime-core:** handle shallow reactive arrays in renderList correctly ([#11870](https://github.com/vuejs/core/issues/11870)) ([ced59ab](https://github.com/vuejs/core/commit/ced59ab8f2f2e89c13119bab3a0c25a1a1f1c3d6)), closes [#11869](https://github.com/vuejs/core/issues/11869)
-* **types:** correctly infer `TypeEmits` with both tuple and function syntax ([#11840](https://github.com/vuejs/core/issues/11840)) ([dad6738](https://github.com/vuejs/core/commit/dad673809929c084dcb8e42640eb7daa675d4ea4)), closes [#11836](https://github.com/vuejs/core/issues/11836)
-
-
-### Performance Improvements
-
-* **reactivity:** trigger deps directly instead of storing in an array first ([#11695](https://github.com/vuejs/core/issues/11695)) ([f80d447](https://github.com/vuejs/core/commit/f80d447c17662556e9e3f99f6d199967f4c8cf3d))
-
-
-
-## [3.5.3](https://github.com/vuejs/core/compare/v3.5.2...v3.5.3) (2024-09-06)
-
-
-### Bug Fixes
-
-* **hydration:** check __asyncHydrate presence for vue3-lazy-hydration compat ([#11825](https://github.com/vuejs/core/issues/11825)) ([8e6c337](https://github.com/vuejs/core/commit/8e6c3378676be05cea7f53664442acdfb86784f9)), closes [#11793](https://github.com/vuejs/core/issues/11793)
-* Revert "fix(reactivity): self-referencing computed should refresh" ([35c760f](https://github.com/vuejs/core/commit/35c760f82f749f7c6e3f9bfead8221ce498e892f))
-* **ssr:** respect app.config.warnHandler during ssr ([bf3d9a2](https://github.com/vuejs/core/commit/bf3d9a2af41659a743706306fc798b3d215df5af)), closes [#11830](https://github.com/vuejs/core/issues/11830)
-* **Transition:** handle KeepAlive child unmount in Transition out-in mode ([#11833](https://github.com/vuejs/core/issues/11833)) ([6b7901d](https://github.com/vuejs/core/commit/6b7901d28ed3a6a9242c666cc1b8e3c0b0b0fe62)), closes [#11775](https://github.com/vuejs/core/issues/11775)
-* **useId:** make generated IDs selector compatible ([babfb4c](https://github.com/vuejs/core/commit/babfb4cbcbf98601d76c1d7653eae8d250ce2710)), closes [#11828](https://github.com/vuejs/core/issues/11828)
-
-
-
-## [3.5.2](https://github.com/vuejs/core/compare/v3.5.1...v3.5.2) (2024-09-05)
-
-
-### Bug Fixes
-
-* **reactivity:** make toRaw work on proxies created by proxyRef ([46c3ab1](https://github.com/vuejs/core/commit/46c3ab1d714024894fa1d33e495d5d35c7817d4d))
-* **reactivity:** pass oldValue to computed getter ([#11813](https://github.com/vuejs/core/issues/11813)) ([98864a7](https://github.com/vuejs/core/commit/98864a7ef5c8080c407166c8221488a4eacbbc81)), closes [#11812](https://github.com/vuejs/core/issues/11812)
-* **reactivity:** prevent endless recursion in computed getters ([#11797](https://github.com/vuejs/core/issues/11797)) ([716275d](https://github.com/vuejs/core/commit/716275d1b1d2383d8ef0306fcd94558d4d9170f2))
-* **reactivity:** self-referencing computed should refresh ([e84c4a6](https://github.com/vuejs/core/commit/e84c4a608e9dc96fb2a4a29d538bcc64f26103a2)), closes [/github.com/vuejs/core/pull/11797#issuecomment-2330738633](https://github.com//github.com/vuejs/core/pull/11797/issues/issuecomment-2330738633)
-* **scheduler:** prevent duplicate jobs being queued ([#11826](https://github.com/vuejs/core/issues/11826)) ([df56cc5](https://github.com/vuejs/core/commit/df56cc528793b1d6131a1e64095dd5cb95c56bee)), closes [#11712](https://github.com/vuejs/core/issues/11712) [#11807](https://github.com/vuejs/core/issues/11807)
-* **suspense:** avoid updating anchor if activeBranch has not been rendered to the actual container ([#11818](https://github.com/vuejs/core/issues/11818)) ([3c0d531](https://github.com/vuejs/core/commit/3c0d531fa7fe762bfe46fbe63f318adc95221795)), closes [#11806](https://github.com/vuejs/core/issues/11806)
-* **Transition:** handle KeepAlive child unmount in Transition out-in mode ([#11778](https://github.com/vuejs/core/issues/11778)) ([3116553](https://github.com/vuejs/core/commit/311655352931863dfcf520b8cf29cebc5b7e1e00)), closes [#11775](https://github.com/vuejs/core/issues/11775)
-* **types:** add HTMLDialogElement missing close event ([#11811](https://github.com/vuejs/core/issues/11811)) ([3634f7a](https://github.com/vuejs/core/commit/3634f7a4c1649ad2e7e969eb4512512868c61d01))
-* **types:** added name attribute support to details tag ([#11823](https://github.com/vuejs/core/issues/11823)) ([c74176e](https://github.com/vuejs/core/commit/c74176ec7b4d1d34159ce21d600c04b157ac5549)), closes [#11821](https://github.com/vuejs/core/issues/11821)
-* **types:** fix defineComponent props inference when setup() has explicit annotation ([fca20a3](https://github.com/vuejs/core/commit/fca20a39aa4a6f98c8f972bd435ebb7dc535648a)), closes [#11803](https://github.com/vuejs/core/issues/11803)
-* **useTemplateRef:** properly fix readonly warning in dev and ensure prod behavior consistency ([9b7797d](https://github.com/vuejs/core/commit/9b7797d0d1fc773e979e042673d5b9b3151c40fc)), closes [#11808](https://github.com/vuejs/core/issues/11808) [#11816](https://github.com/vuejs/core/issues/11816) [#11810](https://github.com/vuejs/core/issues/11810)
-
-
-### Features
-
-* **compiler-core:** parse modifiers as expression to provide location data ([#11819](https://github.com/vuejs/core/issues/11819)) ([3f13203](https://github.com/vuejs/core/commit/3f13203564164eeb2945bdc0b9ef755c37477d75))
-
-
-
-## [3.5.1](https://github.com/vuejs/core/compare/v3.5.0...v3.5.1) (2024-09-04)
-
-
-### Bug Fixes
+   ```js
+   import { createApp, vaporInteropPlugin } from 'vue'
+   import App from './App.vue'
 
-* **build:** improve built-in components treeshakability ([4eee630](https://github.com/vuejs/core/commit/4eee630b3122a10d0baf9b91358cfffa92d6fd81))
-* **reactivity:** handle non-array arguments in reactive `concat` method ([#11794](https://github.com/vuejs/core/issues/11794)) ([475977a](https://github.com/vuejs/core/commit/475977a6f76b77392610e0a3ec2b0e076d1e1d59)), closes [#11792](https://github.com/vuejs/core/issues/11792)
-* **Transition:** avoid applying transition hooks on comment vnode ([#11788](https://github.com/vuejs/core/issues/11788)) ([51912f8](https://github.com/vuejs/core/commit/51912f8a02e35f172f6d30ed7a2f3a92c1407cf9)), closes [#11782](https://github.com/vuejs/core/issues/11782)
-* **types:** avoid using intersection type in `Readonly<...>` to fix JSDoc emit ([#11799](https://github.com/vuejs/core/issues/11799)) ([7518bc1](https://github.com/vuejs/core/commit/7518bc19dc73ba46dcf1eef6e23f9e6e75552675))
-* **useTemplateRef:** fix readonly warning when useTemplateRef has same variable name as template ref ([bc63df0](https://github.com/vuejs/core/commit/bc63df01992fdbf0b6749ad234153725697ed896)), closes [#11795](https://github.com/vuejs/core/issues/11795) [#11802](https://github.com/vuejs/core/issues/11802) [#11804](https://github.com/vuejs/core/issues/11804)
+   createApp(App)
+     .use(vaporInteropPlugin) // enable vapor interop
+     .mount('#app')
+   ```
 
+   A Vapor app instance can also install `vaporInteropPlugin` to allow vdom components to be used inside, but it will pull in the vdom runtime and offset the benefits of a smaller bundle.
 
+#### VDOM Interop Limitations
 
-# [3.5.0](https://github.com/vuejs/core/compare/v3.5.0-rc.1...v3.5.0) (2024-09-03)
+When the interop plugin is installed, Vapor and non-Vapor components can be nested inside each other. This currently covers standard props, events, and slots usage, but does not yet account for all possible edge cases. For example, there will most likely still be rough edges when using a VDOM-based component library in Vapor Mode.
 
-## Aggregated Features List for 3.5 (alpha to stable)
+This is expected to improve over time, but in general, we recommend having distinct "regions" in your app where it's one mode or another, and avoid mixed nesting as much as possible.
 
-### Reactivity
+In the future, we may provide support tooling to enforce Vapor usage boundaries in codebases.
 
-- **reactivity**: Refactor reactivity system to use version counting and doubly-linked list tracking ([#10397](https://github.com/vuejs/core/pull/10397)) ([05eb4e0](https://github.com/vuejs/core/commit/05eb4e0fefd585125dd60b7f8fe9c36928d921aa))
-- **reactivity**: Optimize array tracking ([#9511](https://github.com/vuejs/core/pull/9511)) ([70196a4](https://github.com/vuejs/core/commit/70196a40cc078f50fcc1110c38c06fbcc70b205e))
-- **compiler-sfc:** enable reactive props destructure by default ([d2dac0e](https://github.com/vuejs/core/commit/d2dac0e359c47d1ed0aa77eda488e76fd6466d2d))
-- **reactivity:** `onEffectCleanup` API ([2cc5615](https://github.com/vuejs/core/commit/2cc5615590de77126e8df46136de0240dbde5004)), closes [#10173](https://github.com/vuejs/core/issues/10173)
-- **reactivity:** add `failSilently` argument for `onScopeDispose` ([9a936aa](https://github.com/vuejs/core/commit/9a936aaec489c79433a32791ecf5ddb1739a62bd))
-- **reactivity/watch:** base `watch`, `getCurrentWatcher`, and `onWatcherCleanup` ([#9927](https://github.com/vuejs/core/issues/9927)) ([205e5b5](https://github.com/vuejs/core/commit/205e5b5e277243c3af2c937d9bd46cf671296b72))
-- **reactivity/watch:** add pause/resume for ReactiveEffect, EffectScope, and WatchHandle ([#9651](https://github.com/vuejs/core/issues/9651)) ([267093c](https://github.com/vuejs/core/commit/267093c31490050bfcf3ff2b30a2aefee2dad582))
-- **watch:** support passing number to `deep` option to control the watch depth ([#9572](https://github.com/vuejs/core/issues/9572)) ([22f7d96](https://github.com/vuejs/core/commit/22f7d96757956ebe0baafe52256aa327908cc51c))
-- **types:** export `MultiWatchSources` type ([#9563](https://github.com/vuejs/core/issues/9563)) ([998dca5](https://github.com/vuejs/core/commit/998dca59f140420280803233f41707580688562c))
-- **types:** allow computed getter and setter types to be unrelated ([#11472](https://github.com/vuejs/core/issues/11472)) ([a01675e](https://github.com/vuejs/core/commit/a01675ef8f99b5acd6832c53051f4415b18609f2)), closes [#7271](https://github.com/vuejs/core/issues/7271)
+#### Feature Compatibility
 
-### SSR
+By design, Vapor Mode supports a **subset** of existing Vue features. For the supported subset, we aim to deliver the exact same behavior per API specifications. At the same time, this means there are some features that are explicitly not supported in Vapor Mode:
 
-- **runtime-core:** `useId()` and `app.config.idPrefix` ([#11404](https://github.com/vuejs/core/issues/11404)) ([73ef156](https://github.com/vuejs/core/commit/73ef1561f6905d69f968c094d0180c61824f1247))
-- **hydration:** lazy hydration strategies for async components ([#11458](https://github.com/vuejs/core/issues/11458)) ([d14a11c](https://github.com/vuejs/core/commit/d14a11c1cdcee88452f17ce97758743c863958f4))
-- **hydration:** support suppressing hydration mismatch via data-allow-mismatch ([94fb2b8](https://github.com/vuejs/core/commit/94fb2b8106a66bcca1a3f922a246a29fdd1274b1))
+- Options API
+- `app.config.globalProperties`
+- `getCurrentInstance()` returns `null` in Vapor components
+- Implicit instance properties like `$slots` and `$props` are not available in Vapor template expressions
+- `@vue:xxx` per-element lifecycle events
 
-### Custom Element
+Custom directives in Vapor also have a different interface:
 
-- **custom-element:** `useHost()` helper ([775103a](https://github.com/vuejs/core/commit/775103af37df69d34c79f12c4c1776c47d07f0a0))
-- **custom-element:** `useShadowRoot()` helper ([5a1a89b](https://github.com/vuejs/core/commit/5a1a89bd6178cc2f84ba91da7d72aee4c6ec1282)), closes [#6113](https://github.com/vuejs/core/issues/6113) [#8195](https://github.com/vuejs/core/issues/8195)
-- **custom-element:** expose `this.$host` in Options API ([1ef8f46](https://github.com/vuejs/core/commit/1ef8f46af0cfdec2fed66376772409e0aa25ad50))
-- **custom-element:** inject child components styles to custom element shadow root ([#11517](https://github.com/vuejs/core/issues/11517)) ([56c76a8](https://github.com/vuejs/core/commit/56c76a8b05c45f782ed3a16ec77c6292b71a17f1)), closes [#4662](https://github.com/vuejs/core/issues/4662) [#7941](https://github.com/vuejs/core/issues/7941) [#7942](https://github.com/vuejs/core/issues/7942)
-- **custom-element:** support configurable app instance in defineCustomElement ([6758c3c](https://github.com/vuejs/core/commit/6758c3cd0427f97394d95168c655dae3b7fa62cd)), closes [#4356](https://github.com/vuejs/core/issues/4356) [#4635](https://github.com/vuejs/core/issues/4635)
-- **custom-element:** support css `:host` selector by applying css vars on host element ([#8830](https://github.com/vuejs/core/issues/8830)) ([03a9ea2](https://github.com/vuejs/core/commit/03a9ea2b88df0842a820e09f7445c4b9189e3fcb)), closes [#8826](https://github.com/vuejs/core/issues/8826)
-- **custom-element:** support emit with options ([e181bff](https://github.com/vuejs/core/commit/e181bff6dc39d5cef92000c10291243c7d6e4d08)), closes [#7605](https://github.com/vuejs/core/issues/7605)
-- **custom-element:** support expose on customElement ([#6256](https://github.com/vuejs/core/issues/6256)) ([af838c1](https://github.com/vuejs/core/commit/af838c1b5ec23552e52e64ffa7db0eb0246c3624)), closes [#5540](https://github.com/vuejs/core/issues/5540)
-- **custom-element:** support `nonce` option for injected style tags ([bb4a02a](https://github.com/vuejs/core/commit/bb4a02a70c30e739a3c705b3d96d09258d7d7ded)), closes [#6530](https://github.com/vuejs/core/issues/6530)
-- **custom-element:** support passing custom-element-specific options via 2nd argument of defineCustomElement ([60a88a2](https://github.com/vuejs/core/commit/60a88a2b129714186cf6ba66f30f31d733d0311e))
-- **custom-element:** support `shadowRoot: false` in `defineCustomElement()` ([37d2ce5](https://github.com/vuejs/core/commit/37d2ce5d8e0fac4a00064f02b05f91f69b2d5d5e)), closes [#4314](https://github.com/vuejs/core/issues/4314) [#4404](https://github.com/vuejs/core/issues/4404)
+```ts
+type VaporDirective = (
+  node: Element | VaporComponentInstance,
+  value?: () => any,
+  argument?: string,
+  modifiers?: DirectiveModifiers,
+) => (() => void) | void
+```
 
-### Teleport
+`value` is a reactive getter that returns the binding value. The user can set up reactive effects using `watchEffect` (auto released when component unmounts), and can optionally return a cleanup function. Example:
 
-- **teleport:** support deferred Teleport ([#11387](https://github.com/vuejs/core/issues/11387)) ([59a3e88](https://github.com/vuejs/core/commit/59a3e88903b10ac2278170a44d5a03f24fef23ef)), closes [#2015](https://github.com/vuejs/core/issues/2015) [#11386](https://github.com/vuejs/core/issues/11386)
-- **teleport/transition:** support directly nesting Teleport inside Transition ([#6548](https://github.com/vuejs/core/issues/6548)) ([0e6e3c7](https://github.com/vuejs/core/commit/0e6e3c7eb0e5320b7c1818e025cb4a490fede9c0)), closes [#5836](https://github.com/vuejs/core/issues/5836)
+```ts
+const MyDirective = (el, source) => {
+  watchEffect(() => {
+    el.textContent = source()
+  })
+  return () => console.log('cleanup')
+}
+```
 
-### Misc
+#### Behavior Consistency
 
-- **runtime-core:** `useTemplateRef()` ([3ba70e4](https://github.com/vuejs/core/commit/3ba70e49b5856c53611c314d4855d679a546a7df))
-- **runtime-core:** add `app.onUnmount()` for registering cleanup functions ([#4619](https://github.com/vuejs/core/issues/4619)) ([582a3a3](https://github.com/vuejs/core/commit/582a3a382b1adda565bac576b913a88d9e8d7a9e)), closes [#4516](https://github.com/vuejs/core/issues/4516)
-- **runtime-core:** add `app.config.throwUnhandledErrorInProduction` ([f476b7f](https://github.com/vuejs/core/commit/f476b7f030f2dd427ca655fcea36f4933a4b4da0)), closes [#7876](https://github.com/vuejs/core/issues/7876)
-- **runtime-dom:** Trusted Types compatibility ([#10844](https://github.com/vuejs/core/issues/10844)) ([6d4eb94](https://github.com/vuejs/core/commit/6d4eb94853ed1b2b1675bdd7d5ba9c75cc6daed5))
-- **compiler-core:** support `Symbol` global in template expressions ([#9069](https://github.com/vuejs/core/issues/9069)) ([a501a85](https://github.com/vuejs/core/commit/a501a85a7c910868e01a5c70a2abea4e9d9e87f3))
-- **types:** export more emit related types ([#11017](https://github.com/vuejs/core/issues/11017)) ([189573d](https://github.com/vuejs/core/commit/189573dcee2a16bd3ed36ff5589d43f535e5e733))
-* **types:** add loading prop to iframe ([#11767](https://github.com/vuejs/core/issues/11767)) ([d86fe0e](https://github.com/vuejs/core/commit/d86fe0ec002901dc359a0e85f3a421b4a8538d68))
-
-### Internals
-
-- **reactivity:** store value cache on CustomRefs impls ([#11539](https://github.com/vuejs/core/issues/11539)) ([e044b6e](https://github.com/vuejs/core/commit/e044b6e737efc9433d1d84590036b82280da6292))
-- **types:** provide internal options for directly using user types in language tools ([#10801](https://github.com/vuejs/core/issues/10801)) ([75c8cf6](https://github.com/vuejs/core/commit/75c8cf63a1ef30ac84f91282d66ad3f57c6612e9))
-- **types:** provide internal options for using refs type in language tools ([#11492](https://github.com/vuejs/core/issues/11492)) ([5ffd1a8](https://github.com/vuejs/core/commit/5ffd1a89455807d5069eb2c28eba0379641dca76))
-
-
-
-## Bug Fixes
-
-* **compiler-sfc:** fix import usage check for kebab-case same name shorthand binding ([0f7c0e5](https://github.com/vuejs/core/commit/0f7c0e5dc0eedada7a5194db87fd0a7dbd1d3354)), closes [#11745](https://github.com/vuejs/core/issues/11745) [#11754](https://github.com/vuejs/core/issues/11754)
-* **cssVars:** correctly escape double quotes in SSR ([#11784](https://github.com/vuejs/core/issues/11784)) ([7b5b6e0](https://github.com/vuejs/core/commit/7b5b6e0275f35748dca6d7eb842f8ab2364c6b9a)), closes [#11779](https://github.com/vuejs/core/issues/11779)
-* **deps:** update dependency postcss to ^8.4.44 ([#11774](https://github.com/vuejs/core/issues/11774)) ([cb843e0](https://github.com/vuejs/core/commit/cb843e0be31f9e563ccfc30eca0c06f2a224b505))
-* **hydration:** escape css var name to avoid mismatch ([#11739](https://github.com/vuejs/core/issues/11739)) ([ca12e77](https://github.com/vuejs/core/commit/ca12e776bc53aaa31f2df6bb6edc6be1b2f10c37)), closes [#11735](https://github.com/vuejs/core/issues/11735)
-* **hydration:** handle text nodes with 0 during hydration ([#11772](https://github.com/vuejs/core/issues/11772)) ([c756da2](https://github.com/vuejs/core/commit/c756da24b2d8635cf52b4c7d3abf5bf938852cc5)), closes [#11771](https://github.com/vuejs/core/issues/11771)
-* **reactivity:** correctly handle method calls on user-extended arrays ([#11760](https://github.com/vuejs/core/issues/11760)) ([9817c80](https://github.com/vuejs/core/commit/9817c80187bec6a3344c74d65fac92262de0fcdd)), closes [#11759](https://github.com/vuejs/core/issues/11759)
-* **runtime-dom:** avoid unnecessary prop patch for checkbox ([#11657](https://github.com/vuejs/core/issues/11657)) ([c3ce9fe](https://github.com/vuejs/core/commit/c3ce9fe3d8fc27d864ce7148cd36da882cfc21ab)), closes [#11647](https://github.com/vuejs/core/issues/11647)
-* **runtime-dom:** prevent unnecessary DOM update from v-model ([#11656](https://github.com/vuejs/core/issues/11656)) ([b1be9bd](https://github.com/vuejs/core/commit/b1be9bd64f2c7c4286fecb25bad5d5edd49efce9)), closes [#11647](https://github.com/vuejs/core/issues/11647)
-* **server-renderer:** Fix call to serverPrefetch in server renderer with an async setup ([#10893](https://github.com/vuejs/core/issues/10893)) ([6039e25](https://github.com/vuejs/core/commit/6039e25e04a8c1db5821955f011d57f1615807ab))
-* **server-renderer:** render `className` during SSR ([#11722](https://github.com/vuejs/core/issues/11722)) ([52cdb0f](https://github.com/vuejs/core/commit/52cdb0f991dc154ae32a2900874d5dbc4e078565))
-* **types/defineModel:** allow getter and setter types to be unrelated ([#11699](https://github.com/vuejs/core/issues/11699)) ([fe07f70](https://github.com/vuejs/core/commit/fe07f7073617df358c2f8cbc3de433359e873c96)), closes [#11697](https://github.com/vuejs/core/issues/11697)
-
-
-
-# [3.5.0-rc.1](https://github.com/vuejs/core/compare/v3.5.0-beta.3...v3.5.0-rc.1) (2024-08-29)
-
-
-### Bug Fixes
-
-* **compiler-sfc:** skip circular tsconfig project reference ([#11680](https://github.com/vuejs/core/issues/11680)) ([9c4c2e5](https://github.com/vuejs/core/commit/9c4c2e51b045218d0c5ca64b4fb58b17d5d580cc)), closes [#11382](https://github.com/vuejs/core/issues/11382)
-* **custom-element:** handle keys set on custom elements ([#11655](https://github.com/vuejs/core/issues/11655)) ([f1d1831](https://github.com/vuejs/core/commit/f1d1831f07fe52d5681a5ec9ec310572463abf26)), closes [#11641](https://github.com/vuejs/core/issues/11641)
-* **deps:** update dependency monaco-editor to ^0.51.0 ([#11713](https://github.com/vuejs/core/issues/11713)) ([434f8a9](https://github.com/vuejs/core/commit/434f8a97c77f68aeae050e9e4e1f54f63bc4bd26))
-* **keep-alive:**  reset keep alive flag when the component is removed from include ([#11718](https://github.com/vuejs/core/issues/11718)) ([29c321b](https://github.com/vuejs/core/commit/29c321bfd33f9197244dec3d027077e63b2cdf2f)), closes [#11717](https://github.com/vuejs/core/issues/11717)
-* **reactivity:** avoid infinite recursion when mutating ref wrapped in reactive ([313e4bf](https://github.com/vuejs/core/commit/313e4bf55214ac1e334a99c329a3ba5daca4f156)), closes [#11696](https://github.com/vuejs/core/issues/11696)
-* **reactivity:** ensure watcher with once: true are properly removed from effect scope  ([#11665](https://github.com/vuejs/core/issues/11665)) ([fbc0c42](https://github.com/vuejs/core/commit/fbc0c42bcf6dea5a6ae664223fa19d4375ca39f0))
-* **runtime-dom:** setting innerHTML when patching props should go through trusted types ([d875de5](https://github.com/vuejs/core/commit/d875de54e9e03e0768fe550aa4c4886a4baf3bd7))
-* **types:** GlobalDirective / GlobalComponents should not be records ([42e8df6](https://github.com/vuejs/core/commit/42e8df62030e7f2c287d9103f045e67b34a63e3b))
-
-
-
-# [3.5.0-beta.3](https://github.com/vuejs/core/compare/v3.5.0-beta.2...v3.5.0-beta.3) (2024-08-20)
-
-
-### Bug Fixes
-
-* **reactivity:** extended methods respect reactive ([#11629](https://github.com/vuejs/core/issues/11629)) ([9de1d10](https://github.com/vuejs/core/commit/9de1d101f98bf6081f41038f6974826f190330a0)), closes [#11628](https://github.com/vuejs/core/issues/11628)
-* **runtime-core:** correct type inference for PascalCase emits ([#11579](https://github.com/vuejs/core/issues/11579)) ([d7d0371](https://github.com/vuejs/core/commit/d7d0371e74707ee601020f67de88e091cdae2673)), closes [vuejs/language-tools#4269](https://github.com/vuejs/language-tools/issues/4269)
-* **runtime-core:** ensure suspense content inherit scopeId ([#10652](https://github.com/vuejs/core/issues/10652)) ([ac2a410](https://github.com/vuejs/core/commit/ac2a410e46392db63ca4ed2db3c0fa71ebe1e855)), closes [#5148](https://github.com/vuejs/core/issues/5148)
-* **runtime-core:** pre jobs without an id should run first ([#7746](https://github.com/vuejs/core/issues/7746)) ([b332f80](https://github.com/vuejs/core/commit/b332f80f0edb018229a23b43b93bb402b6368a3c))
-* **ssr:** apply ssr props to the the fallback vnode-based branch in ssr ([#7247](https://github.com/vuejs/core/issues/7247)) ([98b83e8](https://github.com/vuejs/core/commit/98b83e86d16c635547a1e735e5fb675aea2f0f1b)), closes [#6123](https://github.com/vuejs/core/issues/6123)
-* **types/custom-element:** `defineCustomElement` with required props ([#11578](https://github.com/vuejs/core/issues/11578)) ([5e0f6d5](https://github.com/vuejs/core/commit/5e0f6d5f8fe7c4eb8f247357c3e2e281726f36db))
-* **types:** strip non-prop default values from return type of withDefaults ([#9998](https://github.com/vuejs/core/issues/9998)) ([44973bb](https://github.com/vuejs/core/commit/44973bb3e790db7d8aa7af4eda21c80cac73a8de)), closes [#9899](https://github.com/vuejs/core/issues/9899)
-* **watch:** handle errors in computed used as watch source ([#11626](https://github.com/vuejs/core/issues/11626)) ([8bcaad4](https://github.com/vuejs/core/commit/8bcaad4a32cf0f1f89e0259f6a53036620b7fe9f)), closes [#11624](https://github.com/vuejs/core/issues/11624)
-
-
-### Features
-
-* **reactivity:** base `watch`, `getCurrentWatcher`, and `onWatcherCleanup` ([#9927](https://github.com/vuejs/core/issues/9927)) ([205e5b5](https://github.com/vuejs/core/commit/205e5b5e277243c3af2c937d9bd46cf671296b72))
-
-
-### Performance Improvements
-
-* **runtime-core:** use `apply` to avoid spreading. ([#5985](https://github.com/vuejs/core/issues/5985)) ([bb6babc](https://github.com/vuejs/core/commit/bb6babca8f206615d4e246457cd54d21bb3bc5a4))
-
-
-
-# [3.5.0-beta.2](https://github.com/vuejs/core/compare/v3.5.0-beta.1...v3.5.0-beta.2) (2024-08-15)
-
-
-### Bug Fixes
-
-* **build:** revert entities to 4.5 to avoid runtime resolution errors ([e9e0815](https://github.com/vuejs/core/commit/e9e08155bf8d00c3327ed7371330eb2ae467e560)), closes [#11603](https://github.com/vuejs/core/issues/11603)
-* **compiler-core:** use ast-based check for function expressions when possible ([5861229](https://github.com/vuejs/core/commit/58612294757480974e667652ede5bbcf72b1089d)), closes [#11615](https://github.com/vuejs/core/issues/11615)
-* **compiler-sfc:** fix prefixIdentifier default value ([3d6f015](https://github.com/vuejs/core/commit/3d6f01571b3fb61b32da599d0419eff4e3ebb231))
-* **compiler-sfc:** handle keyof operator with index object ([#11581](https://github.com/vuejs/core/issues/11581)) ([fe00815](https://github.com/vuejs/core/commit/fe008152c0612ff3ecc7ad88e7e66a06b1b2bc3f))
-* **custom-element:** keep instance.isCE for backwards compat ([e19fc27](https://github.com/vuejs/core/commit/e19fc270428b59456fee43224990138c4d6ccb2d))
-* **deps:** update dependency postcss to ^8.4.41 ([#11585](https://github.com/vuejs/core/issues/11585)) ([4c4e12a](https://github.com/vuejs/core/commit/4c4e12ae28d67d616924b0601e68adc551959971))
-* **keep-alive:** ensure include/exclude regexp work with global flag ([#11595](https://github.com/vuejs/core/issues/11595)) ([3653bc0](https://github.com/vuejs/core/commit/3653bc0f45d6fedf84e29b64ca52584359c383c0))
-* **reactivity:** ensure extended method arguments are not lost ([#11574](https://github.com/vuejs/core/issues/11574)) ([4085def](https://github.com/vuejs/core/commit/4085def1bae42d01ee3c22c731cc4a02096464ee)), closes [#11570](https://github.com/vuejs/core/issues/11570)
-* **reactivity:** sync watch should be executed correctly ([#11589](https://github.com/vuejs/core/issues/11589)) ([3bda3e8](https://github.com/vuejs/core/commit/3bda3e83fd9e2fbe451a1c79dae82ff6a7467683)), closes [#11577](https://github.com/vuejs/core/issues/11577)
-* **types/computed:** ensure type safety for `WritableComputedRef` ([#11608](https://github.com/vuejs/core/issues/11608)) ([5cf5a16](https://github.com/vuejs/core/commit/5cf5a1620d9a97382d386c277265d9dd051fe484))
-* **types:** add fallback stub for DOM types when DOM lib is absent ([#11598](https://github.com/vuejs/core/issues/11598)) ([fee6697](https://github.com/vuejs/core/commit/fee669764fbf475adce9e47a7a73b4937ab31ffc))
-
-
-### Features
-
-* **deprecated:** remove deprecated parseExpressions option ([#11597](https://github.com/vuejs/core/issues/11597)) ([4e7d5db](https://github.com/vuejs/core/commit/4e7d5db4d276a5d4aaf3af7d43cfd28c171db307))
-
-
-
-# [3.5.0-beta.1](https://github.com/vuejs/core/compare/v3.4.37...v3.5.0-beta.1) (2024-08-08)
-
-
-### Bug Fixes
-
-* **custom-element:** delay mounting of custom elements with async parent ([37ccb9b](https://github.com/vuejs/core/commit/37ccb9b9a0e4381f9465e0fc6459609003030da4)), closes [#8127](https://github.com/vuejs/core/issues/8127) [#9341](https://github.com/vuejs/core/issues/9341) [#9351](https://github.com/vuejs/core/issues/9351) [#9351](https://github.com/vuejs/core/issues/9351)
-* **custom-element:** delete prop on attribute removal ([506c4c5](https://github.com/vuejs/core/commit/506c4c53fdf9766c2ce9517ad58d501ef6b1b9de)), closes [#11276](https://github.com/vuejs/core/issues/11276)
-* **custom-element:** ignore scoped id ([7f2c505](https://github.com/vuejs/core/commit/7f2c505f92026408a8262ba9b5104a465be19446))
-* **custom-element:** reflect prop default value on custom element ([63689ed](https://github.com/vuejs/core/commit/63689ed77601d5f9b78540f810612806c3a5de15)), closes [#9006](https://github.com/vuejs/core/issues/9006) [#10537](https://github.com/vuejs/core/issues/10537)
-* **custom-element:** support early-set domProps for async custom elements ([a07e7bf](https://github.com/vuejs/core/commit/a07e7bf5536a6b3db70ba9bb1c3f366dac1bf5a0)), closes [#11081](https://github.com/vuejs/core/issues/11081) [#11082](https://github.com/vuejs/core/issues/11082)
-* **types/custome-element:** `defineCustomElement` props inference with array emits ([#11384](https://github.com/vuejs/core/issues/11384)) ([e94b01b](https://github.com/vuejs/core/commit/e94b01bd8a1ec740eddc823839ab2627b307c1b0)), closes [#11353](https://github.com/vuejs/core/issues/11353)
-* **types:** allow using InjectionKey as valid property key ([321d807](https://github.com/vuejs/core/commit/321d80758c42fccbd39ecbb63f1a4f6632a1580a)), closes [#5089](https://github.com/vuejs/core/issues/5089)
-
-
-### Features
-
-* **custom-element:** expose this.$host in Options API ([1ef8f46](https://github.com/vuejs/core/commit/1ef8f46af0cfdec2fed66376772409e0aa25ad50))
-* **custom-element:** inject child components styles to custom element shadow root ([#11517](https://github.com/vuejs/core/issues/11517)) ([56c76a8](https://github.com/vuejs/core/commit/56c76a8b05c45f782ed3a16ec77c6292b71a17f1)), closes [#4662](https://github.com/vuejs/core/issues/4662) [#7941](https://github.com/vuejs/core/issues/7941) [#7942](https://github.com/vuejs/core/issues/7942)
-* **custom-element:** support configurable app instance in defineCustomElement ([6758c3c](https://github.com/vuejs/core/commit/6758c3cd0427f97394d95168c655dae3b7fa62cd)), closes [#4356](https://github.com/vuejs/core/issues/4356) [#4635](https://github.com/vuejs/core/issues/4635)
-* **custom-element:** support css `:host` selector by applying css vars on host element ([#8830](https://github.com/vuejs/core/issues/8830)) ([03a9ea2](https://github.com/vuejs/core/commit/03a9ea2b88df0842a820e09f7445c4b9189e3fcb)), closes [#8826](https://github.com/vuejs/core/issues/8826)
-* **custom-element:** support emit with options ([e181bff](https://github.com/vuejs/core/commit/e181bff6dc39d5cef92000c10291243c7d6e4d08)), closes [#7605](https://github.com/vuejs/core/issues/7605)
-* **custom-element:** support for expose on customElement ([#6256](https://github.com/vuejs/core/issues/6256)) ([af838c1](https://github.com/vuejs/core/commit/af838c1b5ec23552e52e64ffa7db0eb0246c3624)), closes [#5540](https://github.com/vuejs/core/issues/5540)
-* **custom-element:** support nonce option for injected style tags ([bb4a02a](https://github.com/vuejs/core/commit/bb4a02a70c30e739a3c705b3d96d09258d7d7ded)), closes [#6530](https://github.com/vuejs/core/issues/6530)
-* **custom-element:** support passing custom-element-specific options via 2nd argument of defineCustomElement ([60a88a2](https://github.com/vuejs/core/commit/60a88a2b129714186cf6ba66f30f31d733d0311e))
-* **custom-element:** support shadowRoot: false in defineCustomElement() ([37d2ce5](https://github.com/vuejs/core/commit/37d2ce5d8e0fac4a00064f02b05f91f69b2d5d5e)), closes [#4314](https://github.com/vuejs/core/issues/4314) [#4404](https://github.com/vuejs/core/issues/4404)
-* **custom-element:** useHost() helper ([775103a](https://github.com/vuejs/core/commit/775103af37df69d34c79f12c4c1776c47d07f0a0))
-* **custom-element:** useShadowRoot() helper ([5a1a89b](https://github.com/vuejs/core/commit/5a1a89bd6178cc2f84ba91da7d72aee4c6ec1282)), closes [#6113](https://github.com/vuejs/core/issues/6113) [#8195](https://github.com/vuejs/core/issues/8195)
-* **hydration:** allow fine tuning of lazy hydration strategy triggers ([#11530](https://github.com/vuejs/core/issues/11530)) ([261c8b1](https://github.com/vuejs/core/commit/261c8b111d046204bd22392a8b920e3c3d4def48))
-* **reactivity/watch:** add pause/resume for ReactiveEffect, EffectScope, and WatchHandle ([#9651](https://github.com/vuejs/core/issues/9651)) ([267093c](https://github.com/vuejs/core/commit/267093c31490050bfcf3ff2b30a2aefee2dad582))
-* **reactivity:** store value cache on CustomRefs impls ([#11539](https://github.com/vuejs/core/issues/11539)) ([e044b6e](https://github.com/vuejs/core/commit/e044b6e737efc9433d1d84590036b82280da6292))
-* **runtime-dom:** Trusted Types compatibility ([#10844](https://github.com/vuejs/core/issues/10844)) ([6d4eb94](https://github.com/vuejs/core/commit/6d4eb94853ed1b2b1675bdd7d5ba9c75cc6daed5))
-* support specifying allowed keys via generic argument in useTemplateRef() ([1fbfa69](https://github.com/vuejs/core/commit/1fbfa6962b48634ff60837084b82dd57f215c109))
-* **types:** allow computed getter and setter types to be unrelated ([#11472](https://github.com/vuejs/core/issues/11472)) ([a01675e](https://github.com/vuejs/core/commit/a01675ef8f99b5acd6832c53051f4415b18609f2)), closes [#7271](https://github.com/vuejs/core/issues/7271)
-* **types:** export `MultiWatchSources` type ([#9563](https://github.com/vuejs/core/issues/9563)) ([998dca5](https://github.com/vuejs/core/commit/998dca59f140420280803233f41707580688562c))
-* **types:** provide internal options for using refs type in language tools ([#11492](https://github.com/vuejs/core/issues/11492)) ([5ffd1a8](https://github.com/vuejs/core/commit/5ffd1a89455807d5069eb2c28eba0379641dca76))
-* **watch:** support passing number to `deep` option to control the watch depth ([#9572](https://github.com/vuejs/core/issues/9572)) ([22f7d96](https://github.com/vuejs/core/commit/22f7d96757956ebe0baafe52256aa327908cc51c))
-
-
-
-# [3.5.0-alpha.5](https://github.com/vuejs/core/compare/v3.4.35...v3.5.0-alpha.5) (2024-07-31)
-
-
-### Features
-
-* **hydration:** support suppressing hydration mismatch via data-allow-mismatch ([94fb2b8](https://github.com/vuejs/core/commit/94fb2b8106a66bcca1a3f922a246a29fdd1274b1))
-* lazy hydration strategies for async components ([#11458](https://github.com/vuejs/core/issues/11458)) ([d14a11c](https://github.com/vuejs/core/commit/d14a11c1cdcee88452f17ce97758743c863958f4))
-
-
-
-# [3.5.0-alpha.4](https://github.com/vuejs/core/compare/v3.4.34...v3.5.0-alpha.4) (2024-07-24)
-
-### Bug Fixes
-
-* **suspense/hydration:** fix hydration timing of async component inside suspense ([1b8e197](https://github.com/vuejs/core/commit/1b8e197a5b65d67a9703b8511786fb81df9aa7cc)), closes [#6638](https://github.com/vuejs/core/issues/6638)
-* **useId:** properly mark async boundary for already resolved async component ([cd28172](https://github.com/vuejs/core/commit/cd281725781ada2ab279e919031ae307e146a9d9))
-
-
-
-# [3.5.0-alpha.3](https://github.com/vuejs/core/compare/v3.4.33...v3.5.0-alpha.3) (2024-07-19)
-
-
-### Bug Fixes
-
-* **build:** enable SSR branches in esm-browser builds ([b14cd9a](https://github.com/vuejs/core/commit/b14cd9a68bab082332b0169be075be357be076ca))
-* **compiler-core:** change node hoisting to caching per instance ([#11067](https://github.com/vuejs/core/issues/11067)) ([cd0ea0d](https://github.com/vuejs/core/commit/cd0ea0d479a276583fa181d8ecbc97fb0e4a9dce)), closes [#5256](https://github.com/vuejs/core/issues/5256) [#9219](https://github.com/vuejs/core/issues/9219) [#10959](https://github.com/vuejs/core/issues/10959)
-* **compiler-sfc:** should properly walk desutructured props when reactive destructure is not enabled ([0fd6193](https://github.com/vuejs/core/commit/0fd6193def2380916eb51a118f37f2d9ec2ace23)), closes [#11325](https://github.com/vuejs/core/issues/11325)
-* **types:** respect props with default on instance type when using __typeProps ([96e4738](https://github.com/vuejs/core/commit/96e473833422342c5ca371ae1aeb186dec9a55e3))
-
-
-### Features
-
-* **runtime-core:** useTemplateRef() ([3ba70e4](https://github.com/vuejs/core/commit/3ba70e49b5856c53611c314d4855d679a546a7df))
-* **runtime-core:** useId() and app.config.idPrefix ([#11404](https://github.com/vuejs/core/issues/11404)) ([73ef156](https://github.com/vuejs/core/commit/73ef1561f6905d69f968c094d0180c61824f1247))
-* **runtime-core:** add app.config.throwUnhandledErrorInProduction ([f476b7f](https://github.com/vuejs/core/commit/f476b7f030f2dd427ca655fcea36f4933a4b4da0)), closes [#7876](https://github.com/vuejs/core/issues/7876)
-* **teleport:** support deferred Teleport ([#11387](https://github.com/vuejs/core/issues/11387)) ([59a3e88](https://github.com/vuejs/core/commit/59a3e88903b10ac2278170a44d5a03f24fef23ef)), closes [#2015](https://github.com/vuejs/core/issues/2015) [#11386](https://github.com/vuejs/core/issues/11386)
-* **compiler-core:** support `Symbol` global in template expressions ([#9069](https://github.com/vuejs/core/issues/9069)) ([a501a85](https://github.com/vuejs/core/commit/a501a85a7c910868e01a5c70a2abea4e9d9e87f3))
-* **types:** export more emit related types ([#11017](https://github.com/vuejs/core/issues/11017)) ([189573d](https://github.com/vuejs/core/commit/189573dcee2a16bd3ed36ff5589d43f535e5e733))
-
-
-
-# [3.5.0-alpha.2](https://github.com/vuejs/core/compare/v3.4.26...v3.5.0-alpha.2) (2024-05-04)
-
-
-### Bug Fixes
-
-* **types:** fix app.component() typing with inline defineComponent ([908f70a](https://github.com/vuejs/core/commit/908f70adc06038d1ea253d96f4024367f4a7545d)), closes [#10843](https://github.com/vuejs/core/issues/10843)
-* **types:** fix compat with generated types that rely on CreateComponentPublicInstance ([c146186](https://github.com/vuejs/core/commit/c146186396d0c1a65423b8c9a21251c5a6467336)), closes [#10842](https://github.com/vuejs/core/issues/10842)
-* **types:** props in defineOptions type should be optional ([124c4ca](https://github.com/vuejs/core/commit/124c4cac833a28ae9bc8edc576c1d0c7c41f5985)), closes [#10841](https://github.com/vuejs/core/issues/10841)
-
-
-### Features
-
-* **runtime-core:** add app.onUnmount() for registering cleanup functions ([#4619](https://github.com/vuejs/core/issues/4619)) ([582a3a3](https://github.com/vuejs/core/commit/582a3a382b1adda565bac576b913a88d9e8d7a9e)), closes [#4516](https://github.com/vuejs/core/issues/4516)
-
-
-
-# [3.5.0-alpha.1](https://github.com/vuejs/core/compare/v3.4.25...v3.5.0-alpha.1) (2024-04-29)
-
-
-### Bug Fixes
-
-* **reactivity:** fix call sequence of ontrigger in effect ([#10501](https://github.com/vuejs/core/issues/10501)) ([28841fe](https://github.com/vuejs/core/commit/28841fee43a45c37905c2c1ed9ace23067539045))
-
-
-### Features
-
-* **compiler-sfc:** enable reactive props destructure by default ([d2dac0e](https://github.com/vuejs/core/commit/d2dac0e359c47d1ed0aa77eda488e76fd6466d2d))
-* **reactivity:** `onEffectCleanup` API ([2cc5615](https://github.com/vuejs/core/commit/2cc5615590de77126e8df46136de0240dbde5004)), closes [#10173](https://github.com/vuejs/core/issues/10173)
-* **reactivity:** add failSilently argument for onScopeDispose ([9a936aa](https://github.com/vuejs/core/commit/9a936aaec489c79433a32791ecf5ddb1739a62bd))
-* **transition:** support directly nesting Teleport inside Transition ([#6548](https://github.com/vuejs/core/issues/6548)) ([0e6e3c7](https://github.com/vuejs/core/commit/0e6e3c7eb0e5320b7c1818e025cb4a490fede9c0)), closes [#5836](https://github.com/vuejs/core/issues/5836)
-* **types:** provide internal options for directly using user types in language tools ([#10801](https://github.com/vuejs/core/issues/10801)) ([75c8cf6](https://github.com/vuejs/core/commit/75c8cf63a1ef30ac84f91282d66ad3f57c6612e9))
-
-
-### Performance Improvements
-
-* **reactivity:** optimize array tracking ([#9511](https://github.com/vuejs/core/issues/9511)) ([70196a4](https://github.com/vuejs/core/commit/70196a40cc078f50fcc1110c38c06fbcc70b205e)), closes [#4318](https://github.com/vuejs/core/issues/4318)
+Vapor Mode attempts to match VDOM Mode behavior as much as possible, but there could still be minor behavior inconsistencies in edge cases due to how fundamentally different the two rendering modes are. In general, we do not consider a minor inconsistency to be breaking change unless the behavior has previously been documented.
 
+## Previous Changelogs
 
+### 3.5.x (2024-04-29 - 2025-06-18)
 
-## Previous Changelogs
+See [3.5 changelog](./changelogs/CHANGELOG-3.5.md)
 
 ### 3.4.x (2023-10-28 - 2024-08-15)
 
diff --git a/changelogs/CHANGELOG-3.5.md b/changelogs/CHANGELOG-3.5.md
new file mode 100644
index 00000000000..7be776f325f
--- /dev/null
+++ b/changelogs/CHANGELOG-3.5.md
@@ -0,0 +1,592 @@
+## [3.5.17](https://github.com/vuejs/core/compare/v3.5.16...v3.5.17) (2025-06-18)
+
+
+### Bug Fixes
+
+* **compat:** allow v-model built in modifiers on component ([#12654](https://github.com/vuejs/core/issues/12654)) ([cb14b86](https://github.com/vuejs/core/commit/cb14b860f150c4a83bcd52cd26096b7a5aa3a2bf)), closes [#12652](https://github.com/vuejs/core/issues/12652)
+* **compile-sfc:** handle mapped types work with omit and pick ([#12648](https://github.com/vuejs/core/issues/12648)) ([4eb46e4](https://github.com/vuejs/core/commit/4eb46e443f1878199755cb73d481d318a9714392)), closes [#12647](https://github.com/vuejs/core/issues/12647)
+* **compiler-core:** do not increase newlines in `InEntity` state ([#13362](https://github.com/vuejs/core/issues/13362)) ([f05a8d6](https://github.com/vuejs/core/commit/f05a8d613bd873b811cfdb9979ccac8382dba322))
+* **compiler-core:** ignore whitespace when matching adjacent v-if ([#12321](https://github.com/vuejs/core/issues/12321)) ([10ebcef](https://github.com/vuejs/core/commit/10ebcef8c870dbc042b0ea49b1424b2e8f692145)), closes [#9173](https://github.com/vuejs/core/issues/9173)
+* **compiler-core:** prevent comments from blocking static node hoisting  ([#13345](https://github.com/vuejs/core/issues/13345)) ([55dad62](https://github.com/vuejs/core/commit/55dad625acd9e9ddd5a933d5e323ecfdec1a612f)), closes [#13344](https://github.com/vuejs/core/issues/13344)
+* **compiler-sfc:** improved type resolution for function type aliases ([#13452](https://github.com/vuejs/core/issues/13452)) ([f3479aa](https://github.com/vuejs/core/commit/f3479aac9625f4459e650d1c0a70e73863147903)), closes [#13444](https://github.com/vuejs/core/issues/13444)
+* **custom-element:** ensure configureApp is applied to async component ([#12607](https://github.com/vuejs/core/issues/12607)) ([5ba1afb](https://github.com/vuejs/core/commit/5ba1afba09c3ea56c1c17484f5d8aeae210ce52a)), closes [#12448](https://github.com/vuejs/core/issues/12448)
+* **custom-element:** prevent injecting child styles if shadowRoot is false ([#12769](https://github.com/vuejs/core/issues/12769)) ([73055d8](https://github.com/vuejs/core/commit/73055d8d9578d485e3fe846726b50666e1aa56f5)), closes [#12630](https://github.com/vuejs/core/issues/12630)
+* **reactivity:** add `__v_skip` flag to `Dep` to prevent reactive conversion ([#12804](https://github.com/vuejs/core/issues/12804)) ([e8d8f5f](https://github.com/vuejs/core/commit/e8d8f5f604e821acc46b4200d5b06979c05af1c2)), closes [#12803](https://github.com/vuejs/core/issues/12803)
+* **runtime-core:** unset old ref during patching when new ref is absent ([#12900](https://github.com/vuejs/core/issues/12900)) ([47ddf98](https://github.com/vuejs/core/commit/47ddf986021dff8de68b0da72787e53a6c19de4c)), closes [#12898](https://github.com/vuejs/core/issues/12898)
+* **slots:** make cache indexes marker non-enumerable ([#13469](https://github.com/vuejs/core/issues/13469)) ([919c447](https://github.com/vuejs/core/commit/919c44744bba1f0c661c87d2059c3b429611aa7e)), closes [#13468](https://github.com/vuejs/core/issues/13468)
+* **ssr:** handle initial selected state for select with v-model + v-for/v-if option ([#13487](https://github.com/vuejs/core/issues/13487)) ([1552095](https://github.com/vuejs/core/commit/15520954f9f1c7f834175938a50dba5d4be0e6c4)), closes [#13486](https://github.com/vuejs/core/issues/13486)
+* **types:** typo of `vOnce` and `vSlot` ([#13343](https://github.com/vuejs/core/issues/13343)) ([762fae4](https://github.com/vuejs/core/commit/762fae4b57ad60602e5c84465a3bff562785b314))
+
+
+
+## [3.5.16](https://github.com/vuejs/core/compare/v3.5.15...v3.5.16) (2025-05-29)
+
+
+### Reverts
+
+* Revert "fix(compiler-sfc): add scoping tag to trailing universal selector" (#13406) ([19f23b1](https://github.com/vuejs/core/commit/19f23b180bb679e38db95d6a10a420abeedc8e1c)), closes [#13406](https://github.com/vuejs/core/issues/13406)
+* Revert "fix(compiler-sfc): add error handling for defineModel() without variable" (#13390) ([42f879f](https://github.com/vuejs/core/commit/42f879fcab48e0e1011967a771b4ad9e8838d760)), closes [#13390](https://github.com/vuejs/core/issues/13390)
+
+
+
+## [3.5.15](https://github.com/vuejs/core/compare/v3.5.14...v3.5.15) (2025-05-26)
+
+
+### Bug Fixes
+
+* **compat:** ensure false value on input retains value attribute ([#13216](https://github.com/vuejs/core/issues/13216)) ([1a66474](https://github.com/vuejs/core/commit/1a664749d4d65a345589a6d78106ede7574cb2e1)), closes [#13205](https://github.com/vuejs/core/issues/13205)
+* **compat:** should not warn COMPILER_V_BIND_OBJECT_ORDER when using v-bind together with v-for ([#12993](https://github.com/vuejs/core/issues/12993)) ([93949e6](https://github.com/vuejs/core/commit/93949e6587ee019bccd5b8b9d76f0e1ed6ea16fc)), closes [#12992](https://github.com/vuejs/core/issues/12992)
+* **compile-sfc:** handle inline template source map in prod build ([#12701](https://github.com/vuejs/core/issues/12701)) ([89edc6c](https://github.com/vuejs/core/commit/89edc6cdcbd34ea6394927ecbfaa61dc4f871de7)), closes [#12682](https://github.com/vuejs/core/issues/12682) [vitejs/vite-plugin-vue#500](https://github.com/vitejs/vite-plugin-vue/issues/500)
+* **compiler-core:** ensure mapping is added only if node source is available ([#13285](https://github.com/vuejs/core/issues/13285)) ([d37a2ac](https://github.com/vuejs/core/commit/d37a2ac59d904ac0e3257ba552b6c04920a363f0)), closes [#13261](https://github.com/vuejs/core/issues/13261) [vitejs/vite-plugin-vue#368](https://github.com/vitejs/vite-plugin-vue/issues/368)
+* **compiler-dom:** improve HTML nesting validation to allow any child element within template tag ([#13320](https://github.com/vuejs/core/issues/13320)) ([163b365](https://github.com/vuejs/core/commit/163b3651d174321911648a164052effa9249a2aa)), closes [#13318](https://github.com/vuejs/core/issues/13318)
+* **compiler-sfc:** add error handling for defineModel() without variable assignment ([#13352](https://github.com/vuejs/core/issues/13352)) ([00734af](https://github.com/vuejs/core/commit/00734afef5f7bddbdaee52aa5359a6ef989f32d3)), closes [#13280](https://github.com/vuejs/core/issues/13280)
+* **compiler-sfc:** add scoping tag to trailing universal selector ([#12918](https://github.com/vuejs/core/issues/12918)) ([949df80](https://github.com/vuejs/core/commit/949df808809fd7cccf7718797beab0654aa68302)), closes [#12906](https://github.com/vuejs/core/issues/12906)
+* **compiler-sfc:** improve type inference for TSTypeAliasDeclaration with better runtime type detection ([#13245](https://github.com/vuejs/core/issues/13245)) ([cf5a5e0](https://github.com/vuejs/core/commit/cf5a5e0edf0efcab25c27aa2d13eba91f7372d39)), closes [#13240](https://github.com/vuejs/core/issues/13240)
+* **compiler-sfc:** simulate `allowArbitraryExtensions` on resolving type ([#13301](https://github.com/vuejs/core/issues/13301)) ([f7ce5ae](https://github.com/vuejs/core/commit/f7ce5ae666129339c006b339437c2dff6bceffe0)), closes [#13295](https://github.com/vuejs/core/issues/13295)
+* **custom-element:** allow injecting values ​​from app context in nested elements ([#13219](https://github.com/vuejs/core/issues/13219)) ([b991075](https://github.com/vuejs/core/commit/b9910755a50c7d6c52b28c3aef20cf97810295c9)), closes [#13212](https://github.com/vuejs/core/issues/13212)
+* **custom-element:** ensure proper remount and prevent redundant slot parsing with shadowRoot false ([#13201](https://github.com/vuejs/core/issues/13201)) ([1d41d4d](https://github.com/vuejs/core/commit/1d41d4de7f64a37160c8171d0137fd8d35c346c9)), closes [#13199](https://github.com/vuejs/core/issues/13199)
+* **custom-element:** preserve appContext during update ([#12455](https://github.com/vuejs/core/issues/12455)) ([013749e](https://github.com/vuejs/core/commit/013749e75ef3b51762a86da379ea4ba4501b54ae)), closes [#12453](https://github.com/vuejs/core/issues/12453)
+* **custom-element:** properly resolve props for sync component defs ([#12855](https://github.com/vuejs/core/issues/12855)) ([a683c80](https://github.com/vuejs/core/commit/a683c80cf44ecc482f8ac9c76bf2381443c1b0bb)), closes [#12854](https://github.com/vuejs/core/issues/12854)
+* **hydration:** handle transition appear hydration edge case ([#13339](https://github.com/vuejs/core/issues/13339)) ([35aeae7](https://github.com/vuejs/core/commit/35aeae7fa3168adcf9ed95fd35495d17c8b93eeb)), closes [#13335](https://github.com/vuejs/core/issues/13335)
+* **hydration:** skip lazy hydration for patched components ([#13283](https://github.com/vuejs/core/issues/13283)) ([80055fd](https://github.com/vuejs/core/commit/80055fddfb3ca1e2a44f19c7f0ffaeba00de5140)), closes [#13255](https://github.com/vuejs/core/issues/13255)
+* **suspense:** handle edge case in patching list nodes within Suspense ([#13306](https://github.com/vuejs/core/issues/13306)) ([772b008](https://github.com/vuejs/core/commit/772b0087cb7be151c514a1d30365fb0f61a652ba)), closes [#13305](https://github.com/vuejs/core/issues/13305)
+* **teleport:** handle deferred teleport updates before and after mount ([#13350](https://github.com/vuejs/core/issues/13350)) ([d15dce3](https://github.com/vuejs/core/commit/d15dce3142474f2ef9fffed38383acdadcb26c4c)), closes [#13349](https://github.com/vuejs/core/issues/13349)
+* **types:** avoid merging component instance into `$props` in `ComponentInstance` ([#12870](https://github.com/vuejs/core/issues/12870)) ([f44feed](https://github.com/vuejs/core/commit/f44feed6fa461a9c4c724e9631c19e9e214c0a20)), closes [#12751](https://github.com/vuejs/core/issues/12751)
+* **types:** exclude `undefined` from inferred prop types with default values ([#13007](https://github.com/vuejs/core/issues/13007)) ([5179d32](https://github.com/vuejs/core/commit/5179d328d950015e7fb2a74fe1a8518fd8d2c94e)), closes [#13006](https://github.com/vuejs/core/issues/13006)
+* **watch:** update `oldValue` before running `cb` to prevent stale value ([#12296](https://github.com/vuejs/core/issues/12296)) ([c69c4bb](https://github.com/vuejs/core/commit/c69c4bb59c114f2b5e03733b55ef9ace3087b5c3)), closes [#12294](https://github.com/vuejs/core/issues/12294)
+
+
+
+## [3.5.14](https://github.com/vuejs/core/compare/v3.5.13...v3.5.14) (2025-05-15)
+
+
+### Bug Fixes
+
+* **compat:** correct deprecation message for v-bind.sync usage ([#13137](https://github.com/vuejs/core/issues/13137)) ([466b30f](https://github.com/vuejs/core/commit/466b30f4049ec89fb282624ec17d1a93472ab93f)), closes [#13133](https://github.com/vuejs/core/issues/13133)
+* **compiler-core:** remove slot cache from parent renderCache during unmounting ([#13215](https://github.com/vuejs/core/issues/13215)) ([5d166f3](https://github.com/vuejs/core/commit/5d166f3796a03a497435fc079c6a83a4e9c6cf52))
+* **compiler-sfc:** fix scope handling for props destructure in function parameters and catch clauses ([8e34357](https://github.com/vuejs/core/commit/8e3435779a667de485cf9efd78667d0ca14c5f84)), closes [#12790](https://github.com/vuejs/core/issues/12790)
+* **compiler-sfc:** treat the return value of `useTemplateRef` as a definite ref ([#13197](https://github.com/vuejs/core/issues/13197)) ([8ae1122](https://github.com/vuejs/core/commit/8ae11226e8ee938615e17c7b81dc38ae3f7cefb9))
+* **compiler:** fix spelling error in domTagConfig ([#13043](https://github.com/vuejs/core/issues/13043)) ([388295b](https://github.com/vuejs/core/commit/388295b27f3cc69eba25d325bbe60a36a3df831a))
+* **customFormatter:** properly accessing ref value during debugger ([#12948](https://github.com/vuejs/core/issues/12948)) ([fdbd026](https://github.com/vuejs/core/commit/fdbd02658301dd794fe0c84f0018d080a07fca9f))
+* **hmr/teleport:** adjust static children traversal for HMR in dev mode ([#12819](https://github.com/vuejs/core/issues/12819)) ([5e37dd0](https://github.com/vuejs/core/commit/5e37dd009562bcd8080a200c32abde2d6e4f0305)), closes [#12816](https://github.com/vuejs/core/issues/12816)
+* **hmr:** avoid hydration for hmr root reload ([#12450](https://github.com/vuejs/core/issues/12450)) ([1f98a9c](https://github.com/vuejs/core/commit/1f98a9c493d01c21befa90107f0593bc92a58932)), closes [vitejs/vite-plugin-vue#146](https://github.com/vitejs/vite-plugin-vue/issues/146) [vitejs/vite-plugin-vue#477](https://github.com/vitejs/vite-plugin-vue/issues/477)
+* **hmr:** avoid hydration for hmr updating ([#12262](https://github.com/vuejs/core/issues/12262)) ([9c4dbbc](https://github.com/vuejs/core/commit/9c4dbbc5185125835ad3e49baba303bd54676111)), closes [#7706](https://github.com/vuejs/core/issues/7706) [#8170](https://github.com/vuejs/core/issues/8170)
+* **reactivity:** ensure markRaw objects are not reactive ([#12824](https://github.com/vuejs/core/issues/12824)) ([295b5ec](https://github.com/vuejs/core/commit/295b5ec19b6a52c4a56652cc4d6e93a4ea7c14ed)), closes [#12807](https://github.com/vuejs/core/issues/12807)
+* **reactivity:** ensure multiple effectScope on() and off() calls maintains correct active scope ([22dcbf3](https://github.com/vuejs/core/commit/22dcbf3e20eb84f69c8952f6f70d9990136a4a68)), closes [#12631](https://github.com/vuejs/core/issues/12631) [#12632](https://github.com/vuejs/core/issues/12632) [#12641](https://github.com/vuejs/core/issues/12641)
+* **reactivity:** should not recompute if computed does not track reactive data ([#12341](https://github.com/vuejs/core/issues/12341)) ([0b23fd2](https://github.com/vuejs/core/commit/0b23fd23833cf085e7e112bf4435cfc9b360d072)), closes [#12337](https://github.com/vuejs/core/issues/12337)
+* **runtime-core:**  stop tracking deps in setRef during unmount ([#13210](https://github.com/vuejs/core/issues/13210)) ([016c472](https://github.com/vuejs/core/commit/016c472bd2e7604b21c69dee1da8545ce26e4d2f))
+* **runtime-core:**  update __vnode of static nodes when patching along the optimized path ([#13223](https://github.com/vuejs/core/issues/13223)) ([b3ecee3](https://github.com/vuejs/core/commit/b3ecee3da8ed5c55dea89ce6b4b376b2b722b018))
+* **runtime-core:** inherit comment nodes during block patch in production build  ([#10748](https://github.com/vuejs/core/issues/10748)) ([6264505](https://github.com/vuejs/core/commit/626450590d81f79117b34d2a73073b1dc8f551bd)), closes [#10747](https://github.com/vuejs/core/issues/10747) [#12650](https://github.com/vuejs/core/issues/12650)
+* **runtime-core:** prevent unmounted vnode from being inserted during transition leave ([#12862](https://github.com/vuejs/core/issues/12862)) ([d6a6ec1](https://github.com/vuejs/core/commit/d6a6ec13ce521683bfb2a22932778ef7b51f8600)), closes [#12860](https://github.com/vuejs/core/issues/12860)
+* **runtime-core:** respect immutability for readonly reactive arrays in `v-for` ([#13091](https://github.com/vuejs/core/issues/13091)) ([3f27c58](https://github.com/vuejs/core/commit/3f27c58ffbd4309df369bc89493fdc284dc540bb)), closes [#13087](https://github.com/vuejs/core/issues/13087)
+* **runtime-dom:** always treat autocorrect as attribute ([#13001](https://github.com/vuejs/core/issues/13001)) ([1499135](https://github.com/vuejs/core/commit/1499135c227236e037bb746beeb777941b0b58ff)), closes [#5705](https://github.com/vuejs/core/issues/5705)
+* **slots:** properly warn if slot invoked in setup ([#12195](https://github.com/vuejs/core/issues/12195)) ([9196222](https://github.com/vuejs/core/commit/9196222ae1d63b52b35ac5fbf5e71494587ccf05)), closes [#12194](https://github.com/vuejs/core/issues/12194)
+* **ssr:** properly init slots during ssr rendering ([#12441](https://github.com/vuejs/core/issues/12441)) ([2206cd2](https://github.com/vuejs/core/commit/2206cd235a1627c540e795e378b7564a55b47313)), closes [#12438](https://github.com/vuejs/core/issues/12438)
+* **transition:** fix KeepAlive with transition out-in mode behavior in production ([#12468](https://github.com/vuejs/core/issues/12468)) ([343c891](https://github.com/vuejs/core/commit/343c89122448719bd6ed6bd9de986dfb2721d6bf)), closes [#12465](https://github.com/vuejs/core/issues/12465)
+* **TransitionGroup:** reset prevChildren to prevent memory leak ([#13183](https://github.com/vuejs/core/issues/13183)) ([8b848cb](https://github.com/vuejs/core/commit/8b848cbbd2af337d23e19e202f9ab433f8580855)), closes [#13181](https://github.com/vuejs/core/issues/13181)
+* **types:** allow return any for Options API lifecycle hooks ([#5914](https://github.com/vuejs/core/issues/5914)) ([06310e8](https://github.com/vuejs/core/commit/06310e82f5bed62d1b9733dcb18cd8d6edc988de))
+* **types:** the directive's modifiers should be optional ([#12605](https://github.com/vuejs/core/issues/12605)) ([10e54dc](https://github.com/vuejs/core/commit/10e54dcc86a7967f3196d96200bcbd1d3d42082f))
+* **typos:** fix comments referencing transformElement.ts ([#12551](https://github.com/vuejs/core/issues/12551))[ci-skip] ([11c053a](https://github.com/vuejs/core/commit/11c053a5429ad0d27a0e2c78b6b026ea00ace116))
+
+
+### Features
+
+* **types:** add type TemplateRef ([#12645](https://github.com/vuejs/core/issues/12645)) ([636a861](https://github.com/vuejs/core/commit/636a8619f06c71dfd79f7f6412fd130c4f84226f))
+
+
+
+## [3.5.13](https://github.com/vuejs/core/compare/v3.5.12...v3.5.13) (2024-11-15)
+
+
+### Bug Fixes
+
+* **compiler-core:** handle v-memo + v-for with functional key ([#12014](https://github.com/vuejs/core/issues/12014)) ([99009ee](https://github.com/vuejs/core/commit/99009eee0efc238392daba93792d478525b21afa)), closes [#12013](https://github.com/vuejs/core/issues/12013)
+* **compiler-dom:** properly stringify template string style ([#12392](https://github.com/vuejs/core/issues/12392)) ([2d78539](https://github.com/vuejs/core/commit/2d78539da35322aea5f821b3cf9b02d006abac72)), closes [#12391](https://github.com/vuejs/core/issues/12391)
+* **custom-element:** avoid triggering mutationObserver when relecting props ([352bc88](https://github.com/vuejs/core/commit/352bc88c1bd2fda09c61ab17ea1a5967ffcd7bc0)), closes [#12214](https://github.com/vuejs/core/issues/12214) [#12215](https://github.com/vuejs/core/issues/12215)
+* **deps:** update dependency postcss to ^8.4.48 ([#12356](https://github.com/vuejs/core/issues/12356)) ([b5ff930](https://github.com/vuejs/core/commit/b5ff930089985a58c3553977ef999cec2a6708a4))
+* **hydration:** the component vnode's el should be updated when a mismatch occurs. ([#12255](https://github.com/vuejs/core/issues/12255)) ([a20a4cb](https://github.com/vuejs/core/commit/a20a4cb36a3e717d1f8f259d0d59f133f508ff0a)), closes [#12253](https://github.com/vuejs/core/issues/12253)
+* **reactivity:** avoid unnecessary watcher effect removal from inactive scope ([2193284](https://github.com/vuejs/core/commit/21932840eae72ffcd357a62ec596aaecc7ec224a)), closes [#5783](https://github.com/vuejs/core/issues/5783) [#5806](https://github.com/vuejs/core/issues/5806)
+* **reactivity:** release nested effects/scopes on effect scope stop ([#12373](https://github.com/vuejs/core/issues/12373)) ([bee2f5e](https://github.com/vuejs/core/commit/bee2f5ee62dc0cd04123b737779550726374dd0a)), closes [#12370](https://github.com/vuejs/core/issues/12370)
+* **runtime-dom:** set css vars before user onMounted hooks ([2d5c5e2](https://github.com/vuejs/core/commit/2d5c5e25e9b7a56e883674fb434135ac514429b5)), closes [#11533](https://github.com/vuejs/core/issues/11533)
+* **runtime-dom:** set css vars on update to handle child forcing reflow in onMount ([#11561](https://github.com/vuejs/core/issues/11561)) ([c4312f9](https://github.com/vuejs/core/commit/c4312f9c715c131a09e552ba46e9beb4b36d55e6))
+* **ssr:** avoid updating subtree of async component if it is resolved ([#12363](https://github.com/vuejs/core/issues/12363)) ([da7ad5e](https://github.com/vuejs/core/commit/da7ad5e3d24f3e108401188d909d27a4910da095)), closes [#12362](https://github.com/vuejs/core/issues/12362)
+* **ssr:** ensure v-text updates correctly with custom directives in SSR output ([#12311](https://github.com/vuejs/core/issues/12311)) ([1f75d4e](https://github.com/vuejs/core/commit/1f75d4e6dfe18121ebe443cd3e8105d54f727893)), closes [#12309](https://github.com/vuejs/core/issues/12309)
+* **ssr:** handle initial selected state for select with v-model + v-for option ([#12399](https://github.com/vuejs/core/issues/12399)) ([4f8d807](https://github.com/vuejs/core/commit/4f8d8078221ee52deed266677a227ad2a6d8dd22)), closes [#12395](https://github.com/vuejs/core/issues/12395)
+* **teleport:** handle deferred teleport update before mounted ([#12168](https://github.com/vuejs/core/issues/12168)) ([8bff142](https://github.com/vuejs/core/commit/8bff142f99b646e9dd15897ec75368fbf34f1534)), closes [#12161](https://github.com/vuejs/core/issues/12161)
+* **templateRef:** set ref on cached async component which wrapped in KeepAlive ([#12290](https://github.com/vuejs/core/issues/12290)) ([983eb50](https://github.com/vuejs/core/commit/983eb50a17eac76f1bba4394ad0316c62b72191d)), closes [#4999](https://github.com/vuejs/core/issues/4999) [#5004](https://github.com/vuejs/core/issues/5004)
+* **test:** update snapshot ([#12169](https://github.com/vuejs/core/issues/12169)) ([828d4a4](https://github.com/vuejs/core/commit/828d4a443919fa2aa4e2e92fbd03a5f04b258eea))
+* **Transition:** fix transition memory leak edge case ([#12182](https://github.com/vuejs/core/issues/12182)) ([660132d](https://github.com/vuejs/core/commit/660132df6c6a8c14bf75e593dc47d2fdada30322)), closes [#12181](https://github.com/vuejs/core/issues/12181)
+* **transition:** reflow before leave-active class after leave-from ([#12288](https://github.com/vuejs/core/issues/12288)) ([4b479db](https://github.com/vuejs/core/commit/4b479db61d233b054561402ae94ef08550073ea1)), closes [#2593](https://github.com/vuejs/core/issues/2593)
+* **types:** defineEmits w/ interface declaration ([#12343](https://github.com/vuejs/core/issues/12343)) ([1022eab](https://github.com/vuejs/core/commit/1022eabaa1aaf8436876f5ec5573cb1e4b3959a6)), closes [#8457](https://github.com/vuejs/core/issues/8457)
+* **v-once:** setting hasOnce to current block only when in v-once ([#12374](https://github.com/vuejs/core/issues/12374)) ([37300fc](https://github.com/vuejs/core/commit/37300fc26190a7299efddbf98800ffd96d5cad96)), closes [#12371](https://github.com/vuejs/core/issues/12371)
+
+
+### Performance Improvements
+
+* **reactivity:** do not track inner key `__v_skip`` ([#11690](https://github.com/vuejs/core/issues/11690)) ([d637bd6](https://github.com/vuejs/core/commit/d637bd6c0164c2883e6eabd3c2f1f8c258dedfb1))
+* **runtime-core:** use feature flag for call to resolveMergedOptions ([#12163](https://github.com/vuejs/core/issues/12163)) ([1755ac0](https://github.com/vuejs/core/commit/1755ac0a108ba3486bd8397e56d3bdcd69196594))
+
+
+
+## [3.5.12](https://github.com/vuejs/core/compare/v3.5.11...v3.5.12) (2024-10-11)
+
+
+### Bug Fixes
+
+* **compiler-dom:** avoid stringify option with null value ([#12096](https://github.com/vuejs/core/issues/12096)) ([f6d9926](https://github.com/vuejs/core/commit/f6d99262364b7444ebab8742158599e8cdd79eaa)), closes [#12093](https://github.com/vuejs/core/issues/12093)
+* **compiler-sfc:**  do not skip TSInstantiationExpression when transforming props destructure ([#12064](https://github.com/vuejs/core/issues/12064)) ([d3ecde8](https://github.com/vuejs/core/commit/d3ecde8a696ff62c8d0ab067fd1d7ee0565b63c5))
+* **compiler-sfc:** use sass modern api if available and avoid deprecation warning ([#11992](https://github.com/vuejs/core/issues/11992)) ([4474c11](https://github.com/vuejs/core/commit/4474c113d1fb1c26298dd6794275d5b5c7cc4d93))
+* **compiler:** clone loc to `ifNode` ([#12131](https://github.com/vuejs/core/issues/12131)) ([cde2c06](https://github.com/vuejs/core/commit/cde2c0671b00d4f6111fcbd7aa76e45872f20b0c)), closes [vuejs/language-tools#4911](https://github.com/vuejs/language-tools/issues/4911)
+* **custom-element:** properly remove hyphenated attribute ([#12143](https://github.com/vuejs/core/issues/12143)) ([e16e9a7](https://github.com/vuejs/core/commit/e16e9a7341e7cfb3c443da4e5e5b06e8158712c3)), closes [#12139](https://github.com/vuejs/core/issues/12139)
+* **defineModel:** handle kebab-case model correctly ([#12063](https://github.com/vuejs/core/issues/12063)) ([c0418a3](https://github.com/vuejs/core/commit/c0418a3b8fa96a0b108ab71b7aab5d3388f90557)), closes [#12060](https://github.com/vuejs/core/issues/12060)
+* **deps:** update dependency monaco-editor to ^0.52.0 ([#12119](https://github.com/vuejs/core/issues/12119)) ([f7cbea2](https://github.com/vuejs/core/commit/f7cbea2111c7770a180b640f36f6a5d4d6abc698))
+* **hydration:** provide compat fallback for idle callback hydration strategy ([#11935](https://github.com/vuejs/core/issues/11935)) ([1ae545a](https://github.com/vuejs/core/commit/1ae545a3786abef983be1c969726489685569c92))
+* **reactivity:** trigger reactivity for Map key `undefined` ([#12055](https://github.com/vuejs/core/issues/12055)) ([7ad289e](https://github.com/vuejs/core/commit/7ad289e1e7fea654524008ff91e43a8b8a55ef22)), closes [#12054](https://github.com/vuejs/core/issues/12054)
+* **runtime-core:** allow symbol values for slot prop key ([#12069](https://github.com/vuejs/core/issues/12069)) ([d9d4d4e](https://github.com/vuejs/core/commit/d9d4d4e158cd51a9ddda249f29de8467f60b2792)), closes [#12068](https://github.com/vuejs/core/issues/12068)
+* **runtime-core:** fix required prop check false positive for kebab-case edge cases  ([#12034](https://github.com/vuejs/core/issues/12034)) ([9da1ac1](https://github.com/vuejs/core/commit/9da1ac156552ac449754e1373aac7e349841becb)), closes [#12011](https://github.com/vuejs/core/issues/12011)
+* **runtime-dom:** prevent unnecessary updates in v-model checkbox when value is unchanged ([#12146](https://github.com/vuejs/core/issues/12146)) ([ea943af](https://github.com/vuejs/core/commit/ea943afe404c4ca4b729906c5e8daf7aa2ccde9b)), closes [#12144](https://github.com/vuejs/core/issues/12144)
+* **teleport:** handle disabled teleport with updateCssVars ([#12113](https://github.com/vuejs/core/issues/12113)) ([76a8223](https://github.com/vuejs/core/commit/76a8223199c148b79a5c0ea19e235164809760cd)), closes [#12112](https://github.com/vuejs/core/issues/12112)
+* **transition/ssr:** make transition appear work with Suspense in SSR ([#12047](https://github.com/vuejs/core/issues/12047)) ([f1a4f67](https://github.com/vuejs/core/commit/f1a4f67aedfe83e440c54222213f070774faa421)), closes [#12046](https://github.com/vuejs/core/issues/12046)
+* **types:** ensure `this.$props` type does not include `string` ([#12123](https://github.com/vuejs/core/issues/12123)) ([704173e](https://github.com/vuejs/core/commit/704173e24276706de672cca6c9507e4dd9651197)), closes [#12122](https://github.com/vuejs/core/issues/12122)
+* **types:** retain union type narrowing with defaults applied ([#12108](https://github.com/vuejs/core/issues/12108)) ([05685a9](https://github.com/vuejs/core/commit/05685a9d7c42d4cd37169b867833776b91154fed)), closes [#12106](https://github.com/vuejs/core/issues/12106)
+* **useId:** ensure useId consistency when using serverPrefetch ([#12128](https://github.com/vuejs/core/issues/12128)) ([b4d3534](https://github.com/vuejs/core/commit/b4d35349d8bc39aa15bd3f1094d230e5928b177c)), closes [#12102](https://github.com/vuejs/core/issues/12102)
+* **watch:** watchEffect clean-up with SSR ([#12097](https://github.com/vuejs/core/issues/12097)) ([b094c72](https://github.com/vuejs/core/commit/b094c72b3d40c52c7124f145a9db028509a11202)), closes [#11956](https://github.com/vuejs/core/issues/11956)
+
+
+### Performance Improvements
+
+* **reactivity:** avoid unnecessary recursion in removeSub ([#12135](https://github.com/vuejs/core/issues/12135)) ([ec917cf](https://github.com/vuejs/core/commit/ec917cfdb9d0169cd0835d3a0e28244242657dc9))
+
+
+
+## [3.5.11](https://github.com/vuejs/core/compare/v3.5.10...v3.5.11) (2024-10-03)
+
+
+### Bug Fixes
+
+* **compiler-sfc:** do not skip `TSSatisfiesExpression` when transforming props destructure ([#12062](https://github.com/vuejs/core/issues/12062)) ([2328b05](https://github.com/vuejs/core/commit/2328b051f4efa1f1394b7d4e73b7c3f76e430e7c)), closes [#12061](https://github.com/vuejs/core/issues/12061)
+* **reactivity:** prevent overwriting `next` property during batch processing ([#12075](https://github.com/vuejs/core/issues/12075)) ([d3f5e6e](https://github.com/vuejs/core/commit/d3f5e6e5319b4ffaa55ca9a2ea3d95d78e76fa58)), closes [#12072](https://github.com/vuejs/core/issues/12072)
+* **scheduler:** job ordering when the post queue is flushing ([#12090](https://github.com/vuejs/core/issues/12090)) ([577edca](https://github.com/vuejs/core/commit/577edca8e7795436efd710d1c289ea8ea2642b0e))
+* **types:** correctly infer `TypeProps` when it is `any` ([#12073](https://github.com/vuejs/core/issues/12073)) ([57315ab](https://github.com/vuejs/core/commit/57315ab9688c9741a271d1075bbd28cbe5f71e2f)), closes [#12058](https://github.com/vuejs/core/issues/12058)
+* **types:** should not intersect `PublicProps` with `Props` ([#12077](https://github.com/vuejs/core/issues/12077)) ([6f85894](https://github.com/vuejs/core/commit/6f8589437635706f825ccec51800effba1d2bf5f))
+* **types:** infer the first generic type of `Ref` correctly ([#12094](https://github.com/vuejs/core/issues/12094)) ([c97bb84](https://github.com/vuejs/core/commit/c97bb84d0b0a16b012f886b6498e924415ed63e5))
+
+
+
+## [3.5.10](https://github.com/vuejs/core/compare/v3.5.9...v3.5.10) (2024-09-27)
+
+
+### Bug Fixes
+
+* **custom-element:** properly set kebab-case props on Vue custom elements ([ea3efa0](https://github.com/vuejs/core/commit/ea3efa09e008918c1d9ba7226833a8b1a7a57244)), closes [#12030](https://github.com/vuejs/core/issues/12030) [#12032](https://github.com/vuejs/core/issues/12032)
+* **reactivity:** fix nested batch edge case ([93c95dd](https://github.com/vuejs/core/commit/93c95dd4cd416503f43a98a1455f62658d22b0b2))
+* **reactivity:** only clear notified flags for computed in first batch iteration ([aa9ef23](https://github.com/vuejs/core/commit/aa9ef2386a0cd39a174e5a887ec2b1a3525034fc)), closes [#12045](https://github.com/vuejs/core/issues/12045)
+* **types/ref:** handle nested refs in UnwrapRef ([#12049](https://github.com/vuejs/core/issues/12049)) ([e2c19c2](https://github.com/vuejs/core/commit/e2c19c20cfee9788519a80c0e53e216b78505994)), closes [#12044](https://github.com/vuejs/core/issues/12044)
+
+
+
+## [3.5.9](https://github.com/vuejs/core/compare/v3.5.8...v3.5.9) (2024-09-26)
+
+
+### Bug Fixes
+
+* **reactivity:** fix property dep removal regression ([6001e5c](https://github.com/vuejs/core/commit/6001e5c81a05c894586f9287fbd991677bdd0455)), closes [#12020](https://github.com/vuejs/core/issues/12020) [#12021](https://github.com/vuejs/core/issues/12021)
+* **reactivity:** fix recursive sync watcher on computed edge case ([10ff159](https://github.com/vuejs/core/commit/10ff15924053d9bd95ad706f78ce09e288213fcf)), closes [#12033](https://github.com/vuejs/core/issues/12033) [#12037](https://github.com/vuejs/core/issues/12037)
+* **runtime-core:** avoid rendering plain object as VNode ([#12038](https://github.com/vuejs/core/issues/12038)) ([cb34b28](https://github.com/vuejs/core/commit/cb34b28a4a9bf868be4785b001c526163eda342e)), closes [#12035](https://github.com/vuejs/core/issues/12035) [vitejs/vite-plugin-vue#353](https://github.com/vitejs/vite-plugin-vue/issues/353)
+* **runtime-core:** make useId() always return a string ([a177092](https://github.com/vuejs/core/commit/a177092754642af2f98c33a4feffe8f198c3c950))
+* **types:** correct type inference of union event names ([#12022](https://github.com/vuejs/core/issues/12022)) ([4da6881](https://github.com/vuejs/core/commit/4da688141d9e7c15b622c289deaa81b11845b2c7))
+* **vue:** properly cache runtime compilation ([#12019](https://github.com/vuejs/core/issues/12019)) ([fa0ba24](https://github.com/vuejs/core/commit/fa0ba24b3ace02d7ecab65e57c2bea89a2550dcb))
+
+
+
+## [3.5.8](https://github.com/vuejs/core/compare/v3.5.7...v3.5.8) (2024-09-22)
+
+
+### Bug Fixes
+
+* **reactivity:** do not remove dep from depsMap when cleaning up deps of computed ([#11995](https://github.com/vuejs/core/issues/11995)) ([0267a58](https://github.com/vuejs/core/commit/0267a588017eee4951ac2a877fe1ccae84cad905))
+
+
+
+## [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)
+
+
+### Bug Fixes
+
+* **compile-dom:** should be able to stringify mathML ([#11891](https://github.com/vuejs/core/issues/11891)) ([85c138c](https://github.com/vuejs/core/commit/85c138ced108268f7656b568dfd3036a1e0aae34))
+* **compiler-sfc:** preserve old behavior when using withDefaults with desutructure ([8492c3c](https://github.com/vuejs/core/commit/8492c3c49a922363d6c77ef192c133a8fbce6514)), closes [#11930](https://github.com/vuejs/core/issues/11930)
+* **reactivity:** avoid exponential perf cost and reduce call stack depth for deeply chained computeds ([#11944](https://github.com/vuejs/core/issues/11944)) ([c74bb8c](https://github.com/vuejs/core/commit/c74bb8c2dd9e82aaabb0a2a2b368e900929b513b)), closes [#11928](https://github.com/vuejs/core/issues/11928)
+* **reactivity:** rely on dirty check only when computed has deps ([#11931](https://github.com/vuejs/core/issues/11931)) ([aa5dafd](https://github.com/vuejs/core/commit/aa5dafd2b55d42d6a29316a3bc91aea85c676a0b)), closes [#11929](https://github.com/vuejs/core/issues/11929)
+* **watch:** `once` option should be ignored by watchEffect ([#11884](https://github.com/vuejs/core/issues/11884)) ([49fa673](https://github.com/vuejs/core/commit/49fa673493d93b77ddba2165ab6545bae84fd1ae))
+* **watch:** unwatch should be callable during SSR ([#11925](https://github.com/vuejs/core/issues/11925)) ([2d6adf7](https://github.com/vuejs/core/commit/2d6adf78a047eed091db277ffbd9df0822fb0bdd)), closes [#11924](https://github.com/vuejs/core/issues/11924)
+
+
+
+## [3.5.5](https://github.com/vuejs/core/compare/v3.5.4...v3.5.5) (2024-09-13)
+
+
+### Bug Fixes
+
+* **compiler-core:** fix handling of delimiterOpen in VPre ([#11915](https://github.com/vuejs/core/issues/11915)) ([706d4ac](https://github.com/vuejs/core/commit/706d4ac1d0210b2d9134b3228280187fe02fc971)), closes [#11913](https://github.com/vuejs/core/issues/11913)
+* **compiler-dom:** fix stringify static edge for partially eligible chunks in cached parent ([1d99d61](https://github.com/vuejs/core/commit/1d99d61c1bd77f9ea6743f6214a82add8346a121)), closes [#11879](https://github.com/vuejs/core/issues/11879) [#11890](https://github.com/vuejs/core/issues/11890)
+* **compiler-dom:** should ignore leading newline in <textarea> per spec ([3c4bf76](https://github.com/vuejs/core/commit/3c4bf7627649ec1e3220f8c4e4163c20d2afb367))
+* **compiler-sfc:** nested css supports atrule and comment ([#11899](https://github.com/vuejs/core/issues/11899)) ([0e7bc71](https://github.com/vuejs/core/commit/0e7bc717e6640644f062957ec5031506f0dab215)), closes [#11896](https://github.com/vuejs/core/issues/11896)
+* **custom-element:** handle nested customElement mount w/ shadowRoot false ([#11861](https://github.com/vuejs/core/issues/11861)) ([f2d8019](https://github.com/vuejs/core/commit/f2d801918841e7673ff3f048d0d895592a2f7e23)), closes [#11851](https://github.com/vuejs/core/issues/11851) [#11871](https://github.com/vuejs/core/issues/11871)
+* **hmr:** reload async child wrapped in Suspense + KeepAlive ([#11907](https://github.com/vuejs/core/issues/11907)) ([10a2c60](https://github.com/vuejs/core/commit/10a2c6053bd30d160d0214bb3566f540187e6874)), closes [#11868](https://github.com/vuejs/core/issues/11868)
+* **hydration:** fix mismatch of leading newline in `<textarea>` and `<pre>` ([a5f3c2e](https://github.com/vuejs/core/commit/a5f3c2eb4d2e7fae93ff93ce865b269f01cc825e)), closes [#11873](https://github.com/vuejs/core/issues/11873) [#11874](https://github.com/vuejs/core/issues/11874)
+* **reactivity:** properly clean up deps, fix memory leak ([8ea5d6d](https://github.com/vuejs/core/commit/8ea5d6d6981ab7febda0be43c3c92b18869c3a2a)), closes [#11901](https://github.com/vuejs/core/issues/11901)
+* **runtime-core:** properly update async component nested in KeepAlive ([#11917](https://github.com/vuejs/core/issues/11917)) ([7fe6c79](https://github.com/vuejs/core/commit/7fe6c795a1fc7ddcea5ad91a56141561192373ac)), closes [#11916](https://github.com/vuejs/core/issues/11916)
+* **TransitionGroup:** not warn unkeyed text children with whitespece preserve ([#11888](https://github.com/vuejs/core/issues/11888)) ([7571f20](https://github.com/vuejs/core/commit/7571f20bc3d1854377a146f41d211e05bb68cd47)), closes [#11885](https://github.com/vuejs/core/issues/11885)
+
+
+
+## [3.5.4](https://github.com/vuejs/core/compare/v3.5.3...v3.5.4) (2024-09-10)
+
+
+### Bug Fixes
+
+* **compiler-sfc:** correct scoped injection for nesting selector ([#11854](https://github.com/vuejs/core/issues/11854)) ([b1de75e](https://github.com/vuejs/core/commit/b1de75ed04626b6423085dfde91fb0cb481a25e8)), closes [#10567](https://github.com/vuejs/core/issues/10567)
+* **reactivity:** fix markRaw error on already marked object ([#11864](https://github.com/vuejs/core/issues/11864)) ([67d6596](https://github.com/vuejs/core/commit/67d6596d40b1807b9cd8eb0d9282932ea77be3c0)), closes [#11862](https://github.com/vuejs/core/issues/11862)
+* Revert "fix: Revert "fix(reactivity): self-referencing computed should refresh"" ([e596378](https://github.com/vuejs/core/commit/e596378e0be728dad7d60938449f3fa557ca2ec9))
+* **runtime-core:** handle shallow reactive arrays in renderList correctly ([#11870](https://github.com/vuejs/core/issues/11870)) ([ced59ab](https://github.com/vuejs/core/commit/ced59ab8f2f2e89c13119bab3a0c25a1a1f1c3d6)), closes [#11869](https://github.com/vuejs/core/issues/11869)
+* **types:** correctly infer `TypeEmits` with both tuple and function syntax ([#11840](https://github.com/vuejs/core/issues/11840)) ([dad6738](https://github.com/vuejs/core/commit/dad673809929c084dcb8e42640eb7daa675d4ea4)), closes [#11836](https://github.com/vuejs/core/issues/11836)
+
+
+### Performance Improvements
+
+* **reactivity:** trigger deps directly instead of storing in an array first ([#11695](https://github.com/vuejs/core/issues/11695)) ([f80d447](https://github.com/vuejs/core/commit/f80d447c17662556e9e3f99f6d199967f4c8cf3d))
+
+
+
+## [3.5.3](https://github.com/vuejs/core/compare/v3.5.2...v3.5.3) (2024-09-06)
+
+
+### Bug Fixes
+
+* **hydration:** check __asyncHydrate presence for vue3-lazy-hydration compat ([#11825](https://github.com/vuejs/core/issues/11825)) ([8e6c337](https://github.com/vuejs/core/commit/8e6c3378676be05cea7f53664442acdfb86784f9)), closes [#11793](https://github.com/vuejs/core/issues/11793)
+* Revert "fix(reactivity): self-referencing computed should refresh" ([35c760f](https://github.com/vuejs/core/commit/35c760f82f749f7c6e3f9bfead8221ce498e892f))
+* **ssr:** respect app.config.warnHandler during ssr ([bf3d9a2](https://github.com/vuejs/core/commit/bf3d9a2af41659a743706306fc798b3d215df5af)), closes [#11830](https://github.com/vuejs/core/issues/11830)
+* **Transition:** handle KeepAlive child unmount in Transition out-in mode ([#11833](https://github.com/vuejs/core/issues/11833)) ([6b7901d](https://github.com/vuejs/core/commit/6b7901d28ed3a6a9242c666cc1b8e3c0b0b0fe62)), closes [#11775](https://github.com/vuejs/core/issues/11775)
+* **useId:** make generated IDs selector compatible ([babfb4c](https://github.com/vuejs/core/commit/babfb4cbcbf98601d76c1d7653eae8d250ce2710)), closes [#11828](https://github.com/vuejs/core/issues/11828)
+
+
+
+## [3.5.2](https://github.com/vuejs/core/compare/v3.5.1...v3.5.2) (2024-09-05)
+
+
+### Bug Fixes
+
+* **reactivity:** make toRaw work on proxies created by proxyRef ([46c3ab1](https://github.com/vuejs/core/commit/46c3ab1d714024894fa1d33e495d5d35c7817d4d))
+* **reactivity:** pass oldValue to computed getter ([#11813](https://github.com/vuejs/core/issues/11813)) ([98864a7](https://github.com/vuejs/core/commit/98864a7ef5c8080c407166c8221488a4eacbbc81)), closes [#11812](https://github.com/vuejs/core/issues/11812)
+* **reactivity:** prevent endless recursion in computed getters ([#11797](https://github.com/vuejs/core/issues/11797)) ([716275d](https://github.com/vuejs/core/commit/716275d1b1d2383d8ef0306fcd94558d4d9170f2))
+* **reactivity:** self-referencing computed should refresh ([e84c4a6](https://github.com/vuejs/core/commit/e84c4a608e9dc96fb2a4a29d538bcc64f26103a2)), closes [/github.com/vuejs/core/pull/11797#issuecomment-2330738633](https://github.com//github.com/vuejs/core/pull/11797/issues/issuecomment-2330738633)
+* **scheduler:** prevent duplicate jobs being queued ([#11826](https://github.com/vuejs/core/issues/11826)) ([df56cc5](https://github.com/vuejs/core/commit/df56cc528793b1d6131a1e64095dd5cb95c56bee)), closes [#11712](https://github.com/vuejs/core/issues/11712) [#11807](https://github.com/vuejs/core/issues/11807)
+* **suspense:** avoid updating anchor if activeBranch has not been rendered to the actual container ([#11818](https://github.com/vuejs/core/issues/11818)) ([3c0d531](https://github.com/vuejs/core/commit/3c0d531fa7fe762bfe46fbe63f318adc95221795)), closes [#11806](https://github.com/vuejs/core/issues/11806)
+* **Transition:** handle KeepAlive child unmount in Transition out-in mode ([#11778](https://github.com/vuejs/core/issues/11778)) ([3116553](https://github.com/vuejs/core/commit/311655352931863dfcf520b8cf29cebc5b7e1e00)), closes [#11775](https://github.com/vuejs/core/issues/11775)
+* **types:** add HTMLDialogElement missing close event ([#11811](https://github.com/vuejs/core/issues/11811)) ([3634f7a](https://github.com/vuejs/core/commit/3634f7a4c1649ad2e7e969eb4512512868c61d01))
+* **types:** added name attribute support to details tag ([#11823](https://github.com/vuejs/core/issues/11823)) ([c74176e](https://github.com/vuejs/core/commit/c74176ec7b4d1d34159ce21d600c04b157ac5549)), closes [#11821](https://github.com/vuejs/core/issues/11821)
+* **types:** fix defineComponent props inference when setup() has explicit annotation ([fca20a3](https://github.com/vuejs/core/commit/fca20a39aa4a6f98c8f972bd435ebb7dc535648a)), closes [#11803](https://github.com/vuejs/core/issues/11803)
+* **useTemplateRef:** properly fix readonly warning in dev and ensure prod behavior consistency ([9b7797d](https://github.com/vuejs/core/commit/9b7797d0d1fc773e979e042673d5b9b3151c40fc)), closes [#11808](https://github.com/vuejs/core/issues/11808) [#11816](https://github.com/vuejs/core/issues/11816) [#11810](https://github.com/vuejs/core/issues/11810)
+
+
+### Features
+
+* **compiler-core:** parse modifiers as expression to provide location data ([#11819](https://github.com/vuejs/core/issues/11819)) ([3f13203](https://github.com/vuejs/core/commit/3f13203564164eeb2945bdc0b9ef755c37477d75))
+
+
+
+## [3.5.1](https://github.com/vuejs/core/compare/v3.5.0...v3.5.1) (2024-09-04)
+
+
+### Bug Fixes
+
+* **build:** improve built-in components treeshakability ([4eee630](https://github.com/vuejs/core/commit/4eee630b3122a10d0baf9b91358cfffa92d6fd81))
+* **reactivity:** handle non-array arguments in reactive `concat` method ([#11794](https://github.com/vuejs/core/issues/11794)) ([475977a](https://github.com/vuejs/core/commit/475977a6f76b77392610e0a3ec2b0e076d1e1d59)), closes [#11792](https://github.com/vuejs/core/issues/11792)
+* **Transition:** avoid applying transition hooks on comment vnode ([#11788](https://github.com/vuejs/core/issues/11788)) ([51912f8](https://github.com/vuejs/core/commit/51912f8a02e35f172f6d30ed7a2f3a92c1407cf9)), closes [#11782](https://github.com/vuejs/core/issues/11782)
+* **types:** avoid using intersection type in `Readonly<...>` to fix JSDoc emit ([#11799](https://github.com/vuejs/core/issues/11799)) ([7518bc1](https://github.com/vuejs/core/commit/7518bc19dc73ba46dcf1eef6e23f9e6e75552675))
+* **useTemplateRef:** fix readonly warning when useTemplateRef has same variable name as template ref ([bc63df0](https://github.com/vuejs/core/commit/bc63df01992fdbf0b6749ad234153725697ed896)), closes [#11795](https://github.com/vuejs/core/issues/11795) [#11802](https://github.com/vuejs/core/issues/11802) [#11804](https://github.com/vuejs/core/issues/11804)
+
+
+
+# [3.5.0](https://github.com/vuejs/core/compare/v3.5.0-rc.1...v3.5.0) (2024-09-03)
+
+## Aggregated Features List for 3.5 (alpha to stable)
+
+### Reactivity
+
+- **reactivity**: Refactor reactivity system to use version counting and doubly-linked list tracking ([#10397](https://github.com/vuejs/core/pull/10397)) ([05eb4e0](https://github.com/vuejs/core/commit/05eb4e0fefd585125dd60b7f8fe9c36928d921aa))
+- **reactivity**: Optimize array tracking ([#9511](https://github.com/vuejs/core/pull/9511)) ([70196a4](https://github.com/vuejs/core/commit/70196a40cc078f50fcc1110c38c06fbcc70b205e))
+- **compiler-sfc:** enable reactive props destructure by default ([d2dac0e](https://github.com/vuejs/core/commit/d2dac0e359c47d1ed0aa77eda488e76fd6466d2d))
+- **reactivity:** `onEffectCleanup` API ([2cc5615](https://github.com/vuejs/core/commit/2cc5615590de77126e8df46136de0240dbde5004)), closes [#10173](https://github.com/vuejs/core/issues/10173)
+- **reactivity:** add `failSilently` argument for `onScopeDispose` ([9a936aa](https://github.com/vuejs/core/commit/9a936aaec489c79433a32791ecf5ddb1739a62bd))
+- **reactivity/watch:** base `watch`, `getCurrentWatcher`, and `onWatcherCleanup` ([#9927](https://github.com/vuejs/core/issues/9927)) ([205e5b5](https://github.com/vuejs/core/commit/205e5b5e277243c3af2c937d9bd46cf671296b72))
+- **reactivity/watch:** add pause/resume for ReactiveEffect, EffectScope, and WatchHandle ([#9651](https://github.com/vuejs/core/issues/9651)) ([267093c](https://github.com/vuejs/core/commit/267093c31490050bfcf3ff2b30a2aefee2dad582))
+- **watch:** support passing number to `deep` option to control the watch depth ([#9572](https://github.com/vuejs/core/issues/9572)) ([22f7d96](https://github.com/vuejs/core/commit/22f7d96757956ebe0baafe52256aa327908cc51c))
+- **types:** export `MultiWatchSources` type ([#9563](https://github.com/vuejs/core/issues/9563)) ([998dca5](https://github.com/vuejs/core/commit/998dca59f140420280803233f41707580688562c))
+- **types:** allow computed getter and setter types to be unrelated ([#11472](https://github.com/vuejs/core/issues/11472)) ([a01675e](https://github.com/vuejs/core/commit/a01675ef8f99b5acd6832c53051f4415b18609f2)), closes [#7271](https://github.com/vuejs/core/issues/7271)
+
+### SSR
+
+- **runtime-core:** `useId()` and `app.config.idPrefix` ([#11404](https://github.com/vuejs/core/issues/11404)) ([73ef156](https://github.com/vuejs/core/commit/73ef1561f6905d69f968c094d0180c61824f1247))
+- **hydration:** lazy hydration strategies for async components ([#11458](https://github.com/vuejs/core/issues/11458)) ([d14a11c](https://github.com/vuejs/core/commit/d14a11c1cdcee88452f17ce97758743c863958f4))
+- **hydration:** support suppressing hydration mismatch via data-allow-mismatch ([94fb2b8](https://github.com/vuejs/core/commit/94fb2b8106a66bcca1a3f922a246a29fdd1274b1))
+
+### Custom Element
+
+- **custom-element:** `useHost()` helper ([775103a](https://github.com/vuejs/core/commit/775103af37df69d34c79f12c4c1776c47d07f0a0))
+- **custom-element:** `useShadowRoot()` helper ([5a1a89b](https://github.com/vuejs/core/commit/5a1a89bd6178cc2f84ba91da7d72aee4c6ec1282)), closes [#6113](https://github.com/vuejs/core/issues/6113) [#8195](https://github.com/vuejs/core/issues/8195)
+- **custom-element:** expose `this.$host` in Options API ([1ef8f46](https://github.com/vuejs/core/commit/1ef8f46af0cfdec2fed66376772409e0aa25ad50))
+- **custom-element:** inject child components styles to custom element shadow root ([#11517](https://github.com/vuejs/core/issues/11517)) ([56c76a8](https://github.com/vuejs/core/commit/56c76a8b05c45f782ed3a16ec77c6292b71a17f1)), closes [#4662](https://github.com/vuejs/core/issues/4662) [#7941](https://github.com/vuejs/core/issues/7941) [#7942](https://github.com/vuejs/core/issues/7942)
+- **custom-element:** support configurable app instance in defineCustomElement ([6758c3c](https://github.com/vuejs/core/commit/6758c3cd0427f97394d95168c655dae3b7fa62cd)), closes [#4356](https://github.com/vuejs/core/issues/4356) [#4635](https://github.com/vuejs/core/issues/4635)
+- **custom-element:** support css `:host` selector by applying css vars on host element ([#8830](https://github.com/vuejs/core/issues/8830)) ([03a9ea2](https://github.com/vuejs/core/commit/03a9ea2b88df0842a820e09f7445c4b9189e3fcb)), closes [#8826](https://github.com/vuejs/core/issues/8826)
+- **custom-element:** support emit with options ([e181bff](https://github.com/vuejs/core/commit/e181bff6dc39d5cef92000c10291243c7d6e4d08)), closes [#7605](https://github.com/vuejs/core/issues/7605)
+- **custom-element:** support expose on customElement ([#6256](https://github.com/vuejs/core/issues/6256)) ([af838c1](https://github.com/vuejs/core/commit/af838c1b5ec23552e52e64ffa7db0eb0246c3624)), closes [#5540](https://github.com/vuejs/core/issues/5540)
+- **custom-element:** support `nonce` option for injected style tags ([bb4a02a](https://github.com/vuejs/core/commit/bb4a02a70c30e739a3c705b3d96d09258d7d7ded)), closes [#6530](https://github.com/vuejs/core/issues/6530)
+- **custom-element:** support passing custom-element-specific options via 2nd argument of defineCustomElement ([60a88a2](https://github.com/vuejs/core/commit/60a88a2b129714186cf6ba66f30f31d733d0311e))
+- **custom-element:** support `shadowRoot: false` in `defineCustomElement()` ([37d2ce5](https://github.com/vuejs/core/commit/37d2ce5d8e0fac4a00064f02b05f91f69b2d5d5e)), closes [#4314](https://github.com/vuejs/core/issues/4314) [#4404](https://github.com/vuejs/core/issues/4404)
+
+### Teleport
+
+- **teleport:** support deferred Teleport ([#11387](https://github.com/vuejs/core/issues/11387)) ([59a3e88](https://github.com/vuejs/core/commit/59a3e88903b10ac2278170a44d5a03f24fef23ef)), closes [#2015](https://github.com/vuejs/core/issues/2015) [#11386](https://github.com/vuejs/core/issues/11386)
+- **teleport/transition:** support directly nesting Teleport inside Transition ([#6548](https://github.com/vuejs/core/issues/6548)) ([0e6e3c7](https://github.com/vuejs/core/commit/0e6e3c7eb0e5320b7c1818e025cb4a490fede9c0)), closes [#5836](https://github.com/vuejs/core/issues/5836)
+
+### Misc
+
+- **runtime-core:** `useTemplateRef()` ([3ba70e4](https://github.com/vuejs/core/commit/3ba70e49b5856c53611c314d4855d679a546a7df))
+- **runtime-core:** add `app.onUnmount()` for registering cleanup functions ([#4619](https://github.com/vuejs/core/issues/4619)) ([582a3a3](https://github.com/vuejs/core/commit/582a3a382b1adda565bac576b913a88d9e8d7a9e)), closes [#4516](https://github.com/vuejs/core/issues/4516)
+- **runtime-core:** add `app.config.throwUnhandledErrorInProduction` ([f476b7f](https://github.com/vuejs/core/commit/f476b7f030f2dd427ca655fcea36f4933a4b4da0)), closes [#7876](https://github.com/vuejs/core/issues/7876)
+- **runtime-dom:** Trusted Types compatibility ([#10844](https://github.com/vuejs/core/issues/10844)) ([6d4eb94](https://github.com/vuejs/core/commit/6d4eb94853ed1b2b1675bdd7d5ba9c75cc6daed5))
+- **compiler-core:** support `Symbol` global in template expressions ([#9069](https://github.com/vuejs/core/issues/9069)) ([a501a85](https://github.com/vuejs/core/commit/a501a85a7c910868e01a5c70a2abea4e9d9e87f3))
+- **types:** export more emit related types ([#11017](https://github.com/vuejs/core/issues/11017)) ([189573d](https://github.com/vuejs/core/commit/189573dcee2a16bd3ed36ff5589d43f535e5e733))
+* **types:** add loading prop to iframe ([#11767](https://github.com/vuejs/core/issues/11767)) ([d86fe0e](https://github.com/vuejs/core/commit/d86fe0ec002901dc359a0e85f3a421b4a8538d68))
+
+### Internals
+
+- **reactivity:** store value cache on CustomRefs impls ([#11539](https://github.com/vuejs/core/issues/11539)) ([e044b6e](https://github.com/vuejs/core/commit/e044b6e737efc9433d1d84590036b82280da6292))
+- **types:** provide internal options for directly using user types in language tools ([#10801](https://github.com/vuejs/core/issues/10801)) ([75c8cf6](https://github.com/vuejs/core/commit/75c8cf63a1ef30ac84f91282d66ad3f57c6612e9))
+- **types:** provide internal options for using refs type in language tools ([#11492](https://github.com/vuejs/core/issues/11492)) ([5ffd1a8](https://github.com/vuejs/core/commit/5ffd1a89455807d5069eb2c28eba0379641dca76))
+
+
+
+## Bug Fixes
+
+* **compiler-sfc:** fix import usage check for kebab-case same name shorthand binding ([0f7c0e5](https://github.com/vuejs/core/commit/0f7c0e5dc0eedada7a5194db87fd0a7dbd1d3354)), closes [#11745](https://github.com/vuejs/core/issues/11745) [#11754](https://github.com/vuejs/core/issues/11754)
+* **cssVars:** correctly escape double quotes in SSR ([#11784](https://github.com/vuejs/core/issues/11784)) ([7b5b6e0](https://github.com/vuejs/core/commit/7b5b6e0275f35748dca6d7eb842f8ab2364c6b9a)), closes [#11779](https://github.com/vuejs/core/issues/11779)
+* **deps:** update dependency postcss to ^8.4.44 ([#11774](https://github.com/vuejs/core/issues/11774)) ([cb843e0](https://github.com/vuejs/core/commit/cb843e0be31f9e563ccfc30eca0c06f2a224b505))
+* **hydration:** escape css var name to avoid mismatch ([#11739](https://github.com/vuejs/core/issues/11739)) ([ca12e77](https://github.com/vuejs/core/commit/ca12e776bc53aaa31f2df6bb6edc6be1b2f10c37)), closes [#11735](https://github.com/vuejs/core/issues/11735)
+* **hydration:** handle text nodes with 0 during hydration ([#11772](https://github.com/vuejs/core/issues/11772)) ([c756da2](https://github.com/vuejs/core/commit/c756da24b2d8635cf52b4c7d3abf5bf938852cc5)), closes [#11771](https://github.com/vuejs/core/issues/11771)
+* **reactivity:** correctly handle method calls on user-extended arrays ([#11760](https://github.com/vuejs/core/issues/11760)) ([9817c80](https://github.com/vuejs/core/commit/9817c80187bec6a3344c74d65fac92262de0fcdd)), closes [#11759](https://github.com/vuejs/core/issues/11759)
+* **runtime-dom:** avoid unnecessary prop patch for checkbox ([#11657](https://github.com/vuejs/core/issues/11657)) ([c3ce9fe](https://github.com/vuejs/core/commit/c3ce9fe3d8fc27d864ce7148cd36da882cfc21ab)), closes [#11647](https://github.com/vuejs/core/issues/11647)
+* **runtime-dom:** prevent unnecessary DOM update from v-model ([#11656](https://github.com/vuejs/core/issues/11656)) ([b1be9bd](https://github.com/vuejs/core/commit/b1be9bd64f2c7c4286fecb25bad5d5edd49efce9)), closes [#11647](https://github.com/vuejs/core/issues/11647)
+* **server-renderer:** Fix call to serverPrefetch in server renderer with an async setup ([#10893](https://github.com/vuejs/core/issues/10893)) ([6039e25](https://github.com/vuejs/core/commit/6039e25e04a8c1db5821955f011d57f1615807ab))
+* **server-renderer:** render `className` during SSR ([#11722](https://github.com/vuejs/core/issues/11722)) ([52cdb0f](https://github.com/vuejs/core/commit/52cdb0f991dc154ae32a2900874d5dbc4e078565))
+* **types/defineModel:** allow getter and setter types to be unrelated ([#11699](https://github.com/vuejs/core/issues/11699)) ([fe07f70](https://github.com/vuejs/core/commit/fe07f7073617df358c2f8cbc3de433359e873c96)), closes [#11697](https://github.com/vuejs/core/issues/11697)
+
+
+
+# [3.5.0-rc.1](https://github.com/vuejs/core/compare/v3.5.0-beta.3...v3.5.0-rc.1) (2024-08-29)
+
+
+### Bug Fixes
+
+* **compiler-sfc:** skip circular tsconfig project reference ([#11680](https://github.com/vuejs/core/issues/11680)) ([9c4c2e5](https://github.com/vuejs/core/commit/9c4c2e51b045218d0c5ca64b4fb58b17d5d580cc)), closes [#11382](https://github.com/vuejs/core/issues/11382)
+* **custom-element:** handle keys set on custom elements ([#11655](https://github.com/vuejs/core/issues/11655)) ([f1d1831](https://github.com/vuejs/core/commit/f1d1831f07fe52d5681a5ec9ec310572463abf26)), closes [#11641](https://github.com/vuejs/core/issues/11641)
+* **deps:** update dependency monaco-editor to ^0.51.0 ([#11713](https://github.com/vuejs/core/issues/11713)) ([434f8a9](https://github.com/vuejs/core/commit/434f8a97c77f68aeae050e9e4e1f54f63bc4bd26))
+* **keep-alive:**  reset keep alive flag when the component is removed from include ([#11718](https://github.com/vuejs/core/issues/11718)) ([29c321b](https://github.com/vuejs/core/commit/29c321bfd33f9197244dec3d027077e63b2cdf2f)), closes [#11717](https://github.com/vuejs/core/issues/11717)
+* **reactivity:** avoid infinite recursion when mutating ref wrapped in reactive ([313e4bf](https://github.com/vuejs/core/commit/313e4bf55214ac1e334a99c329a3ba5daca4f156)), closes [#11696](https://github.com/vuejs/core/issues/11696)
+* **reactivity:** ensure watcher with once: true are properly removed from effect scope  ([#11665](https://github.com/vuejs/core/issues/11665)) ([fbc0c42](https://github.com/vuejs/core/commit/fbc0c42bcf6dea5a6ae664223fa19d4375ca39f0))
+* **runtime-dom:** setting innerHTML when patching props should go through trusted types ([d875de5](https://github.com/vuejs/core/commit/d875de54e9e03e0768fe550aa4c4886a4baf3bd7))
+* **types:** GlobalDirective / GlobalComponents should not be records ([42e8df6](https://github.com/vuejs/core/commit/42e8df62030e7f2c287d9103f045e67b34a63e3b))
+
+
+
+# [3.5.0-beta.3](https://github.com/vuejs/core/compare/v3.5.0-beta.2...v3.5.0-beta.3) (2024-08-20)
+
+
+### Bug Fixes
+
+* **reactivity:** extended methods respect reactive ([#11629](https://github.com/vuejs/core/issues/11629)) ([9de1d10](https://github.com/vuejs/core/commit/9de1d101f98bf6081f41038f6974826f190330a0)), closes [#11628](https://github.com/vuejs/core/issues/11628)
+* **runtime-core:** correct type inference for PascalCase emits ([#11579](https://github.com/vuejs/core/issues/11579)) ([d7d0371](https://github.com/vuejs/core/commit/d7d0371e74707ee601020f67de88e091cdae2673)), closes [vuejs/language-tools#4269](https://github.com/vuejs/language-tools/issues/4269)
+* **runtime-core:** ensure suspense content inherit scopeId ([#10652](https://github.com/vuejs/core/issues/10652)) ([ac2a410](https://github.com/vuejs/core/commit/ac2a410e46392db63ca4ed2db3c0fa71ebe1e855)), closes [#5148](https://github.com/vuejs/core/issues/5148)
+* **runtime-core:** pre jobs without an id should run first ([#7746](https://github.com/vuejs/core/issues/7746)) ([b332f80](https://github.com/vuejs/core/commit/b332f80f0edb018229a23b43b93bb402b6368a3c))
+* **ssr:** apply ssr props to the the fallback vnode-based branch in ssr ([#7247](https://github.com/vuejs/core/issues/7247)) ([98b83e8](https://github.com/vuejs/core/commit/98b83e86d16c635547a1e735e5fb675aea2f0f1b)), closes [#6123](https://github.com/vuejs/core/issues/6123)
+* **types/custom-element:** `defineCustomElement` with required props ([#11578](https://github.com/vuejs/core/issues/11578)) ([5e0f6d5](https://github.com/vuejs/core/commit/5e0f6d5f8fe7c4eb8f247357c3e2e281726f36db))
+* **types:** strip non-prop default values from return type of withDefaults ([#9998](https://github.com/vuejs/core/issues/9998)) ([44973bb](https://github.com/vuejs/core/commit/44973bb3e790db7d8aa7af4eda21c80cac73a8de)), closes [#9899](https://github.com/vuejs/core/issues/9899)
+* **watch:** handle errors in computed used as watch source ([#11626](https://github.com/vuejs/core/issues/11626)) ([8bcaad4](https://github.com/vuejs/core/commit/8bcaad4a32cf0f1f89e0259f6a53036620b7fe9f)), closes [#11624](https://github.com/vuejs/core/issues/11624)
+
+
+### Features
+
+* **reactivity:** base `watch`, `getCurrentWatcher`, and `onWatcherCleanup` ([#9927](https://github.com/vuejs/core/issues/9927)) ([205e5b5](https://github.com/vuejs/core/commit/205e5b5e277243c3af2c937d9bd46cf671296b72))
+
+
+### Performance Improvements
+
+* **runtime-core:** use `apply` to avoid spreading. ([#5985](https://github.com/vuejs/core/issues/5985)) ([bb6babc](https://github.com/vuejs/core/commit/bb6babca8f206615d4e246457cd54d21bb3bc5a4))
+
+
+
+# [3.5.0-beta.2](https://github.com/vuejs/core/compare/v3.5.0-beta.1...v3.5.0-beta.2) (2024-08-15)
+
+
+### Bug Fixes
+
+* **build:** revert entities to 4.5 to avoid runtime resolution errors ([e9e0815](https://github.com/vuejs/core/commit/e9e08155bf8d00c3327ed7371330eb2ae467e560)), closes [#11603](https://github.com/vuejs/core/issues/11603)
+* **compiler-core:** use ast-based check for function expressions when possible ([5861229](https://github.com/vuejs/core/commit/58612294757480974e667652ede5bbcf72b1089d)), closes [#11615](https://github.com/vuejs/core/issues/11615)
+* **compiler-sfc:** fix prefixIdentifier default value ([3d6f015](https://github.com/vuejs/core/commit/3d6f01571b3fb61b32da599d0419eff4e3ebb231))
+* **compiler-sfc:** handle keyof operator with index object ([#11581](https://github.com/vuejs/core/issues/11581)) ([fe00815](https://github.com/vuejs/core/commit/fe008152c0612ff3ecc7ad88e7e66a06b1b2bc3f))
+* **custom-element:** keep instance.isCE for backwards compat ([e19fc27](https://github.com/vuejs/core/commit/e19fc270428b59456fee43224990138c4d6ccb2d))
+* **deps:** update dependency postcss to ^8.4.41 ([#11585](https://github.com/vuejs/core/issues/11585)) ([4c4e12a](https://github.com/vuejs/core/commit/4c4e12ae28d67d616924b0601e68adc551959971))
+* **keep-alive:** ensure include/exclude regexp work with global flag ([#11595](https://github.com/vuejs/core/issues/11595)) ([3653bc0](https://github.com/vuejs/core/commit/3653bc0f45d6fedf84e29b64ca52584359c383c0))
+* **reactivity:** ensure extended method arguments are not lost ([#11574](https://github.com/vuejs/core/issues/11574)) ([4085def](https://github.com/vuejs/core/commit/4085def1bae42d01ee3c22c731cc4a02096464ee)), closes [#11570](https://github.com/vuejs/core/issues/11570)
+* **reactivity:** sync watch should be executed correctly ([#11589](https://github.com/vuejs/core/issues/11589)) ([3bda3e8](https://github.com/vuejs/core/commit/3bda3e83fd9e2fbe451a1c79dae82ff6a7467683)), closes [#11577](https://github.com/vuejs/core/issues/11577)
+* **types/computed:** ensure type safety for `WritableComputedRef` ([#11608](https://github.com/vuejs/core/issues/11608)) ([5cf5a16](https://github.com/vuejs/core/commit/5cf5a1620d9a97382d386c277265d9dd051fe484))
+* **types:** add fallback stub for DOM types when DOM lib is absent ([#11598](https://github.com/vuejs/core/issues/11598)) ([fee6697](https://github.com/vuejs/core/commit/fee669764fbf475adce9e47a7a73b4937ab31ffc))
+
+
+### Features
+
+* **deprecated:** remove deprecated parseExpressions option ([#11597](https://github.com/vuejs/core/issues/11597)) ([4e7d5db](https://github.com/vuejs/core/commit/4e7d5db4d276a5d4aaf3af7d43cfd28c171db307))
+
+
+
+# [3.5.0-beta.1](https://github.com/vuejs/core/compare/v3.4.37...v3.5.0-beta.1) (2024-08-08)
+
+
+### Bug Fixes
+
+* **custom-element:** delay mounting of custom elements with async parent ([37ccb9b](https://github.com/vuejs/core/commit/37ccb9b9a0e4381f9465e0fc6459609003030da4)), closes [#8127](https://github.com/vuejs/core/issues/8127) [#9341](https://github.com/vuejs/core/issues/9341) [#9351](https://github.com/vuejs/core/issues/9351) [#9351](https://github.com/vuejs/core/issues/9351)
+* **custom-element:** delete prop on attribute removal ([506c4c5](https://github.com/vuejs/core/commit/506c4c53fdf9766c2ce9517ad58d501ef6b1b9de)), closes [#11276](https://github.com/vuejs/core/issues/11276)
+* **custom-element:** ignore scoped id ([7f2c505](https://github.com/vuejs/core/commit/7f2c505f92026408a8262ba9b5104a465be19446))
+* **custom-element:** reflect prop default value on custom element ([63689ed](https://github.com/vuejs/core/commit/63689ed77601d5f9b78540f810612806c3a5de15)), closes [#9006](https://github.com/vuejs/core/issues/9006) [#10537](https://github.com/vuejs/core/issues/10537)
+* **custom-element:** support early-set domProps for async custom elements ([a07e7bf](https://github.com/vuejs/core/commit/a07e7bf5536a6b3db70ba9bb1c3f366dac1bf5a0)), closes [#11081](https://github.com/vuejs/core/issues/11081) [#11082](https://github.com/vuejs/core/issues/11082)
+* **types/custome-element:** `defineCustomElement` props inference with array emits ([#11384](https://github.com/vuejs/core/issues/11384)) ([e94b01b](https://github.com/vuejs/core/commit/e94b01bd8a1ec740eddc823839ab2627b307c1b0)), closes [#11353](https://github.com/vuejs/core/issues/11353)
+* **types:** allow using InjectionKey as valid property key ([321d807](https://github.com/vuejs/core/commit/321d80758c42fccbd39ecbb63f1a4f6632a1580a)), closes [#5089](https://github.com/vuejs/core/issues/5089)
+
+
+### Features
+
+* **custom-element:** expose this.$host in Options API ([1ef8f46](https://github.com/vuejs/core/commit/1ef8f46af0cfdec2fed66376772409e0aa25ad50))
+* **custom-element:** inject child components styles to custom element shadow root ([#11517](https://github.com/vuejs/core/issues/11517)) ([56c76a8](https://github.com/vuejs/core/commit/56c76a8b05c45f782ed3a16ec77c6292b71a17f1)), closes [#4662](https://github.com/vuejs/core/issues/4662) [#7941](https://github.com/vuejs/core/issues/7941) [#7942](https://github.com/vuejs/core/issues/7942)
+* **custom-element:** support configurable app instance in defineCustomElement ([6758c3c](https://github.com/vuejs/core/commit/6758c3cd0427f97394d95168c655dae3b7fa62cd)), closes [#4356](https://github.com/vuejs/core/issues/4356) [#4635](https://github.com/vuejs/core/issues/4635)
+* **custom-element:** support css `:host` selector by applying css vars on host element ([#8830](https://github.com/vuejs/core/issues/8830)) ([03a9ea2](https://github.com/vuejs/core/commit/03a9ea2b88df0842a820e09f7445c4b9189e3fcb)), closes [#8826](https://github.com/vuejs/core/issues/8826)
+* **custom-element:** support emit with options ([e181bff](https://github.com/vuejs/core/commit/e181bff6dc39d5cef92000c10291243c7d6e4d08)), closes [#7605](https://github.com/vuejs/core/issues/7605)
+* **custom-element:** support for expose on customElement ([#6256](https://github.com/vuejs/core/issues/6256)) ([af838c1](https://github.com/vuejs/core/commit/af838c1b5ec23552e52e64ffa7db0eb0246c3624)), closes [#5540](https://github.com/vuejs/core/issues/5540)
+* **custom-element:** support nonce option for injected style tags ([bb4a02a](https://github.com/vuejs/core/commit/bb4a02a70c30e739a3c705b3d96d09258d7d7ded)), closes [#6530](https://github.com/vuejs/core/issues/6530)
+* **custom-element:** support passing custom-element-specific options via 2nd argument of defineCustomElement ([60a88a2](https://github.com/vuejs/core/commit/60a88a2b129714186cf6ba66f30f31d733d0311e))
+* **custom-element:** support shadowRoot: false in defineCustomElement() ([37d2ce5](https://github.com/vuejs/core/commit/37d2ce5d8e0fac4a00064f02b05f91f69b2d5d5e)), closes [#4314](https://github.com/vuejs/core/issues/4314) [#4404](https://github.com/vuejs/core/issues/4404)
+* **custom-element:** useHost() helper ([775103a](https://github.com/vuejs/core/commit/775103af37df69d34c79f12c4c1776c47d07f0a0))
+* **custom-element:** useShadowRoot() helper ([5a1a89b](https://github.com/vuejs/core/commit/5a1a89bd6178cc2f84ba91da7d72aee4c6ec1282)), closes [#6113](https://github.com/vuejs/core/issues/6113) [#8195](https://github.com/vuejs/core/issues/8195)
+* **hydration:** allow fine tuning of lazy hydration strategy triggers ([#11530](https://github.com/vuejs/core/issues/11530)) ([261c8b1](https://github.com/vuejs/core/commit/261c8b111d046204bd22392a8b920e3c3d4def48))
+* **reactivity/watch:** add pause/resume for ReactiveEffect, EffectScope, and WatchHandle ([#9651](https://github.com/vuejs/core/issues/9651)) ([267093c](https://github.com/vuejs/core/commit/267093c31490050bfcf3ff2b30a2aefee2dad582))
+* **reactivity:** store value cache on CustomRefs impls ([#11539](https://github.com/vuejs/core/issues/11539)) ([e044b6e](https://github.com/vuejs/core/commit/e044b6e737efc9433d1d84590036b82280da6292))
+* **runtime-dom:** Trusted Types compatibility ([#10844](https://github.com/vuejs/core/issues/10844)) ([6d4eb94](https://github.com/vuejs/core/commit/6d4eb94853ed1b2b1675bdd7d5ba9c75cc6daed5))
+* support specifying allowed keys via generic argument in useTemplateRef() ([1fbfa69](https://github.com/vuejs/core/commit/1fbfa6962b48634ff60837084b82dd57f215c109))
+* **types:** allow computed getter and setter types to be unrelated ([#11472](https://github.com/vuejs/core/issues/11472)) ([a01675e](https://github.com/vuejs/core/commit/a01675ef8f99b5acd6832c53051f4415b18609f2)), closes [#7271](https://github.com/vuejs/core/issues/7271)
+* **types:** export `MultiWatchSources` type ([#9563](https://github.com/vuejs/core/issues/9563)) ([998dca5](https://github.com/vuejs/core/commit/998dca59f140420280803233f41707580688562c))
+* **types:** provide internal options for using refs type in language tools ([#11492](https://github.com/vuejs/core/issues/11492)) ([5ffd1a8](https://github.com/vuejs/core/commit/5ffd1a89455807d5069eb2c28eba0379641dca76))
+* **watch:** support passing number to `deep` option to control the watch depth ([#9572](https://github.com/vuejs/core/issues/9572)) ([22f7d96](https://github.com/vuejs/core/commit/22f7d96757956ebe0baafe52256aa327908cc51c))
+
+
+
+# [3.5.0-alpha.5](https://github.com/vuejs/core/compare/v3.4.35...v3.5.0-alpha.5) (2024-07-31)
+
+
+### Features
+
+* **hydration:** support suppressing hydration mismatch via data-allow-mismatch ([94fb2b8](https://github.com/vuejs/core/commit/94fb2b8106a66bcca1a3f922a246a29fdd1274b1))
+* lazy hydration strategies for async components ([#11458](https://github.com/vuejs/core/issues/11458)) ([d14a11c](https://github.com/vuejs/core/commit/d14a11c1cdcee88452f17ce97758743c863958f4))
+
+
+
+# [3.5.0-alpha.4](https://github.com/vuejs/core/compare/v3.4.34...v3.5.0-alpha.4) (2024-07-24)
+
+### Bug Fixes
+
+* **suspense/hydration:** fix hydration timing of async component inside suspense ([1b8e197](https://github.com/vuejs/core/commit/1b8e197a5b65d67a9703b8511786fb81df9aa7cc)), closes [#6638](https://github.com/vuejs/core/issues/6638)
+* **useId:** properly mark async boundary for already resolved async component ([cd28172](https://github.com/vuejs/core/commit/cd281725781ada2ab279e919031ae307e146a9d9))
+
+
+
+# [3.5.0-alpha.3](https://github.com/vuejs/core/compare/v3.4.33...v3.5.0-alpha.3) (2024-07-19)
+
+
+### Bug Fixes
+
+* **build:** enable SSR branches in esm-browser builds ([b14cd9a](https://github.com/vuejs/core/commit/b14cd9a68bab082332b0169be075be357be076ca))
+* **compiler-core:** change node hoisting to caching per instance ([#11067](https://github.com/vuejs/core/issues/11067)) ([cd0ea0d](https://github.com/vuejs/core/commit/cd0ea0d479a276583fa181d8ecbc97fb0e4a9dce)), closes [#5256](https://github.com/vuejs/core/issues/5256) [#9219](https://github.com/vuejs/core/issues/9219) [#10959](https://github.com/vuejs/core/issues/10959)
+* **compiler-sfc:** should properly walk desutructured props when reactive destructure is not enabled ([0fd6193](https://github.com/vuejs/core/commit/0fd6193def2380916eb51a118f37f2d9ec2ace23)), closes [#11325](https://github.com/vuejs/core/issues/11325)
+* **types:** respect props with default on instance type when using __typeProps ([96e4738](https://github.com/vuejs/core/commit/96e473833422342c5ca371ae1aeb186dec9a55e3))
+
+
+### Features
+
+* **runtime-core:** useTemplateRef() ([3ba70e4](https://github.com/vuejs/core/commit/3ba70e49b5856c53611c314d4855d679a546a7df))
+* **runtime-core:** useId() and app.config.idPrefix ([#11404](https://github.com/vuejs/core/issues/11404)) ([73ef156](https://github.com/vuejs/core/commit/73ef1561f6905d69f968c094d0180c61824f1247))
+* **runtime-core:** add app.config.throwUnhandledErrorInProduction ([f476b7f](https://github.com/vuejs/core/commit/f476b7f030f2dd427ca655fcea36f4933a4b4da0)), closes [#7876](https://github.com/vuejs/core/issues/7876)
+* **teleport:** support deferred Teleport ([#11387](https://github.com/vuejs/core/issues/11387)) ([59a3e88](https://github.com/vuejs/core/commit/59a3e88903b10ac2278170a44d5a03f24fef23ef)), closes [#2015](https://github.com/vuejs/core/issues/2015) [#11386](https://github.com/vuejs/core/issues/11386)
+* **compiler-core:** support `Symbol` global in template expressions ([#9069](https://github.com/vuejs/core/issues/9069)) ([a501a85](https://github.com/vuejs/core/commit/a501a85a7c910868e01a5c70a2abea4e9d9e87f3))
+* **types:** export more emit related types ([#11017](https://github.com/vuejs/core/issues/11017)) ([189573d](https://github.com/vuejs/core/commit/189573dcee2a16bd3ed36ff5589d43f535e5e733))
+
+
+
+# [3.5.0-alpha.2](https://github.com/vuejs/core/compare/v3.4.26...v3.5.0-alpha.2) (2024-05-04)
+
+
+### Bug Fixes
+
+* **types:** fix app.component() typing with inline defineComponent ([908f70a](https://github.com/vuejs/core/commit/908f70adc06038d1ea253d96f4024367f4a7545d)), closes [#10843](https://github.com/vuejs/core/issues/10843)
+* **types:** fix compat with generated types that rely on CreateComponentPublicInstance ([c146186](https://github.com/vuejs/core/commit/c146186396d0c1a65423b8c9a21251c5a6467336)), closes [#10842](https://github.com/vuejs/core/issues/10842)
+* **types:** props in defineOptions type should be optional ([124c4ca](https://github.com/vuejs/core/commit/124c4cac833a28ae9bc8edc576c1d0c7c41f5985)), closes [#10841](https://github.com/vuejs/core/issues/10841)
+
+
+### Features
+
+* **runtime-core:** add app.onUnmount() for registering cleanup functions ([#4619](https://github.com/vuejs/core/issues/4619)) ([582a3a3](https://github.com/vuejs/core/commit/582a3a382b1adda565bac576b913a88d9e8d7a9e)), closes [#4516](https://github.com/vuejs/core/issues/4516)
+
+
+
+# [3.5.0-alpha.1](https://github.com/vuejs/core/compare/v3.4.25...v3.5.0-alpha.1) (2024-04-29)
+
+
+### Bug Fixes
+
+* **reactivity:** fix call sequence of ontrigger in effect ([#10501](https://github.com/vuejs/core/issues/10501)) ([28841fe](https://github.com/vuejs/core/commit/28841fee43a45c37905c2c1ed9ace23067539045))
+
+
+### Features
+
+* **compiler-sfc:** enable reactive props destructure by default ([d2dac0e](https://github.com/vuejs/core/commit/d2dac0e359c47d1ed0aa77eda488e76fd6466d2d))
+* **reactivity:** `onEffectCleanup` API ([2cc5615](https://github.com/vuejs/core/commit/2cc5615590de77126e8df46136de0240dbde5004)), closes [#10173](https://github.com/vuejs/core/issues/10173)
+* **reactivity:** add failSilently argument for onScopeDispose ([9a936aa](https://github.com/vuejs/core/commit/9a936aaec489c79433a32791ecf5ddb1739a62bd))
+* **transition:** support directly nesting Teleport inside Transition ([#6548](https://github.com/vuejs/core/issues/6548)) ([0e6e3c7](https://github.com/vuejs/core/commit/0e6e3c7eb0e5320b7c1818e025cb4a490fede9c0)), closes [#5836](https://github.com/vuejs/core/issues/5836)
+* **types:** provide internal options for directly using user types in language tools ([#10801](https://github.com/vuejs/core/issues/10801)) ([75c8cf6](https://github.com/vuejs/core/commit/75c8cf63a1ef30ac84f91282d66ad3f57c6612e9))
+
+
+### Performance Improvements
+
+* **reactivity:** optimize array tracking ([#9511](https://github.com/vuejs/core/issues/9511)) ([70196a4](https://github.com/vuejs/core/commit/70196a40cc078f50fcc1110c38c06fbcc70b205e)), closes [#4318](https://github.com/vuejs/core/issues/4318)
diff --git a/eslint.config.js b/eslint.config.js
index b752b2e19f1..1f5128ec72f 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -106,7 +106,7 @@ export default tseslint.config(
 
   // Packages targeting DOM
   {
-    files: ['packages/{vue,vue-compat,runtime-dom}/**'],
+    files: ['packages/{vue,vue-compat,runtime-dom,runtime-vapor}/**'],
     rules: {
       'no-restricted-globals': ['error', ...NodeGlobals],
     },
@@ -126,6 +126,7 @@ export default tseslint.config(
     files: [
       'packages-private/template-explorer/**',
       'packages-private/sfc-playground/**',
+      'packages-private/local-playground/**',
     ],
     rules: {
       'no-restricted-globals': ['error', ...NodeGlobals],
@@ -152,6 +153,8 @@ export default tseslint.config(
       './*.{js,ts}',
       'packages/*/*.js',
       'packages/vue/*/*.js',
+      'packages-private/benchmark/*',
+      'packages-private/e2e-utils/*',
     ],
     rules: {
       'no-restricted-globals': 'off',
diff --git a/package.json b/package.json
index 08bdc5ac908..4ecb490a478 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "private": true,
-  "version": "3.5.17",
+  "version": "3.6.0-alpha.2",
   "packageManager": "pnpm@10.12.4",
   "type": "module",
   "scripts": {
@@ -9,16 +9,18 @@
     "build-dts": "tsc -p tsconfig.build.json --noCheck && rollup -c rollup.dts.config.js",
     "clean": "rimraf --glob packages/*/dist temp .eslintcache",
     "size": "run-s \"size-*\" && node scripts/usage-size.js",
-    "size-global": "node scripts/build.js vue runtime-dom -f global -p --size",
+    "size-global": "node scripts/build.js vue runtime-dom compiler-dom -f global -p --size",
     "size-esm-runtime": "node scripts/build.js vue -f esm-bundler-runtime",
-    "size-esm": "node scripts/build.js runtime-dom runtime-core reactivity shared -f esm-bundler",
+    "size-esm": "node scripts/build.js runtime-shared runtime-dom runtime-core reactivity shared runtime-vapor -f esm-bundler",
     "check": "tsc --incremental --noEmit",
     "lint": "eslint --cache .",
     "format": "prettier --write --cache .",
     "format-check": "prettier --check --cache .",
     "test": "vitest",
-    "test-unit": "vitest --project unit",
+    "test-unit": "vitest --project unit --project unit-jsdom",
     "test-e2e": "node scripts/build.js vue -f global -d && vitest --project e2e",
+    "test-e2e-vapor": "pnpm run prepare-e2e-vapor && vitest --project e2e-vapor",
+    "prepare-e2e-vapor": "node scripts/build.js -f cjs+esm-bundler+esm-bundler-runtime && pnpm run -C packages-private/vapor-e2e-test build",
     "test-dts": "run-s build-dts test-dts-only",
     "test-dts-only": "tsc -p packages-private/dts-built-test/tsconfig.json && tsc -p ./packages-private/dts-test/tsconfig.test.json",
     "test-coverage": "vitest run --project unit --coverage",
@@ -29,19 +31,17 @@
     "release": "node scripts/release.js",
     "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
     "dev-esm": "node scripts/dev.js -if esm-bundler-runtime",
-    "dev-compiler": "run-p \"dev template-explorer\" serve",
-    "dev-sfc": "run-s dev-sfc-prepare dev-sfc-run",
-    "dev-sfc-prepare": "node scripts/pre-dev-sfc.js || npm run build-all-cjs",
-    "dev-sfc-serve": "vite packages-private/sfc-playground --host",
-    "dev-sfc-run": "run-p \"dev compiler-sfc -f esm-browser\" \"dev vue -if esm-bundler-runtime\" \"dev vue -ipf esm-browser-runtime\" \"dev server-renderer -if esm-bundler\" dev-sfc-serve",
+    "dev-prepare-cjs": "node scripts/prepare-cjs.js || node scripts/build.js -f cjs",
+    "dev-compiler": "run-p \"dev template-explorer\" serve open",
+    "dev-sfc": "run-s dev-prepare-cjs dev-sfc-run",
+    "dev-sfc-serve": "vite packages-private/sfc-playground",
+    "dev-sfc-run": "run-p \"dev compiler-sfc -f esm-browser\" \"dev vue -if esm-browser-vapor\" \"dev vue -ipf esm-browser-vapor\" \"dev server-renderer -if esm-bundler\" dev-sfc-serve",
+    "dev-vapor": "pnpm -C packages-private/local-playground run dev",
     "serve": "serve",
     "open": "open http://localhost:3000/packages-private/template-explorer/local.html",
-    "build-sfc-playground": "run-s build-all-cjs build-runtime-esm build-browser-esm build-ssr-esm build-sfc-playground-self",
-    "build-all-cjs": "node scripts/build.js vue runtime compiler reactivity shared -af cjs",
-    "build-runtime-esm": "node scripts/build.js runtime reactivity shared -af esm-bundler && node scripts/build.js vue -f esm-bundler-runtime && node scripts/build.js vue -f esm-browser-runtime",
-    "build-browser-esm": "node scripts/build.js runtime reactivity shared -af esm-bundler && node scripts/build.js vue -f esm-bundler && node scripts/build.js vue -f esm-browser",
-    "build-ssr-esm": "node scripts/build.js compiler-sfc server-renderer -f esm-browser",
-    "build-sfc-playground-self": "cd packages-private/sfc-playground && npm run build",
+    "build-sfc-playground": "run-s build-sfc-deps build-sfc-playground-self",
+    "build-sfc-deps": "node scripts/build.js -f ~global+global-runtime",
+    "build-sfc-playground-self": "pnpm run -C packages-private/sfc-playground build",
     "preinstall": "npx only-allow pnpm",
     "postinstall": "simple-git-hooks"
   },
@@ -74,6 +74,7 @@
     "@types/node": "^22.16.0",
     "@types/semver": "^7.7.0",
     "@types/serve-handler": "^6.1.4",
+    "@vitest/ui": "^3.0.2",
     "@vitest/coverage-v8": "^3.1.4",
     "@vitest/eslint-plugin": "^1.2.1",
     "@vue/consolidate": "1.0.0",
diff --git a/packages-private/benchmark/.gitignore b/packages-private/benchmark/.gitignore
new file mode 100644
index 00000000000..484ab7e5c61
--- /dev/null
+++ b/packages-private/benchmark/.gitignore
@@ -0,0 +1 @@
+results/*
diff --git a/packages-private/benchmark/client/App.vue b/packages-private/benchmark/client/App.vue
new file mode 100644
index 00000000000..c85deea53ea
--- /dev/null
+++ b/packages-private/benchmark/client/App.vue
@@ -0,0 +1,136 @@
+<script setup>
+import { shallowRef, triggerRef } from 'vue'
+import { buildData } from './data'
+import { defer, wrap } from './profiling'
+
+const selected = shallowRef()
+const rows = shallowRef([])
+
+// Bench Add: https://jsbench.me/45lzxprzmu/1
+const add = wrap('add', () => {
+  rows.value.push(...buildData(1000))
+  triggerRef(rows)
+})
+
+const remove = wrap('remove', id => {
+  rows.value.splice(
+    rows.value.findIndex(d => d.id === id),
+    1,
+  )
+  triggerRef(rows)
+})
+
+const select = wrap('select', id => {
+  selected.value = id
+})
+
+const run = wrap('run', () => {
+  rows.value = buildData()
+  selected.value = undefined
+})
+
+const update = wrap('update', () => {
+  const _rows = rows.value
+  for (let i = 0, len = _rows.length; i < len; i += 10) {
+    _rows[i].label.value += ' !!!'
+  }
+})
+
+const runLots = wrap('runLots', () => {
+  rows.value = buildData(10000)
+  selected.value = undefined
+})
+
+const clear = wrap('clear', () => {
+  rows.value = []
+  selected.value = undefined
+})
+
+const swapRows = wrap('swap', () => {
+  const _rows = rows.value
+  if (_rows.length > 998) {
+    const d1 = _rows[1]
+    const d998 = _rows[998]
+    _rows[1] = d998
+    _rows[998] = d1
+    triggerRef(rows)
+  }
+})
+
+async function bench() {
+  for (let i = 0; i < 30; i++) {
+    rows.value = []
+    await runLots()
+    await defer()
+  }
+}
+
+const globalThis = window
+</script>
+
+<template>
+  <h1>Vue.js (VDOM) Benchmark</h1>
+
+  <div style="display: flex; gap: 4px; margin-bottom: 4px">
+    <label>
+      <input
+        type="checkbox"
+        :value="globalThis.doProfile"
+        @change="globalThis.doProfile = $event.target.checked"
+      />
+      Profiling
+    </label>
+    <label>
+      <input
+        type="checkbox"
+        :value="globalThis.reactivity"
+        @change="globalThis.reactivity = $event.target.checked"
+      />
+      Reactivity Cost
+    </label>
+  </div>
+
+  <div
+    id="control"
+    style="display: flex; flex-direction: column; width: fit-content; gap: 6px"
+  >
+    <button @click="bench">Benchmark mounting</button>
+    <button id="run" @click="run">Create 1,000 rows</button>
+    <button id="runlots" @click="runLots">Create 10,000 rows</button>
+    <button id="add" @click="add">Append 1,000 rows</button>
+    <button id="update" @click="update">Update every 10th row</button>
+    <button id="clear" @click="clear">Clear</button>
+    <button id="swaprows" @click="swapRows">Swap Rows</button>
+  </div>
+  <div id="time"></div>
+  <table class="table table-hover table-striped test-data">
+    <tbody>
+      <tr
+        v-for="row of rows"
+        :key="row.id"
+        :class="selected === row.id ? 'danger' : ''"
+      >
+        <td class="col-md-1">{{ row.id }}</td>
+        <td class="col-md-4">
+          <a @click="select(row.id)">{{ row.label.value }}</a>
+        </td>
+        <td class="col-md-1">
+          <a @click="remove(row.id)">
+            <span class="glyphicon glyphicon-remove" aria-hidden="true">x</span>
+          </a>
+        </td>
+        <td class="col-md-6"></td>
+      </tr>
+    </tbody>
+  </table>
+  <span
+    class="preloadicon glyphicon glyphicon-remove"
+    aria-hidden="true"
+  ></span>
+</template>
+
+<style>
+.danger {
+  background-color: red;
+}
+</style>
diff --git a/packages-private/benchmark/client/AppVapor.vue b/packages-private/benchmark/client/AppVapor.vue
new file mode 100644
index 00000000000..0fd284da3f4
--- /dev/null
+++ b/packages-private/benchmark/client/AppVapor.vue
@@ -0,0 +1,136 @@
+<script setup vapor>
+import { shallowRef, triggerRef } from 'vue'
+import { buildData } from './data'
+import { defer, wrap } from './profiling'
+
+const selected = shallowRef()
+const rows = shallowRef([])
+
+// Bench Add: https://jsbench.me/45lzxprzmu/1
+const add = wrap('add', () => {
+  rows.value.push(...buildData(1000))
+  triggerRef(rows)
+})
+
+const remove = wrap('remove', id => {
+  rows.value.splice(
+    rows.value.findIndex(d => d.id === id),
+    1,
+  )
+  triggerRef(rows)
+})
+
+const select = wrap('select', id => {
+  selected.value = id
+})
+
+const run = wrap('run', () => {
+  rows.value = buildData()
+  selected.value = undefined
+})
+
+const update = wrap('update', () => {
+  const _rows = rows.value
+  for (let i = 0, len = _rows.length; i < len; i += 10) {
+    _rows[i].label.value += ' !!!'
+  }
+})
+
+const runLots = wrap('runLots', () => {
+  rows.value = buildData(10000)
+  selected.value = undefined
+})
+
+const clear = wrap('clear', () => {
+  rows.value = []
+  selected.value = undefined
+})
+
+const swapRows = wrap('swap', () => {
+  const _rows = rows.value
+  if (_rows.length > 998) {
+    const d1 = _rows[1]
+    const d998 = _rows[998]
+    _rows[1] = d998
+    _rows[998] = d1
+    triggerRef(rows)
+  }
+})
+
+async function bench() {
+  for (let i = 0; i < 30; i++) {
+    rows.value = []
+    await runLots()
+    await defer()
+  }
+}
+
+const globalThis = window
+</script>
+
+<template>
+  <h1>Vue.js (Vapor) Benchmark</h1>
+
+  <div style="display: flex; gap: 4px; margin-bottom: 4px">
+    <label>
+      <input
+        type="checkbox"
+        :value="globalThis.doProfile"
+        @change="globalThis.doProfile = $event.target.checked"
+      />
+      Profiling
+    </label>
+    <label>
+      <input
+        type="checkbox"
+        :value="globalThis.reactivity"
+        @change="globalThis.reactivity = $event.target.checked"
+      />
+      Reactivity Cost
+    </label>
+  </div>
+
+  <div
+    id="control"
+    style="display: flex; flex-direction: column; width: fit-content; gap: 6px"
+  >
+    <button @click="bench">Benchmark mounting</button>
+    <button id="run" @click="run">Create 1,000 rows</button>
+    <button id="runlots" @click="runLots">Create 10,000 rows</button>
+    <button id="add" @click="add">Append 1,000 rows</button>
+    <button id="update" @click="update">Update every 10th row</button>
+    <button id="clear" @click="clear">Clear</button>
+    <button id="swaprows" @click="swapRows">Swap Rows</button>
+  </div>
+  <div id="time"></div>
+  <table class="table table-hover table-striped test-data">
+    <tbody>
+      <tr
+        v-for="row of rows"
+        :key="row.id"
+        :class="selected === row.id ? 'danger' : ''"
+      >
+        <td class="col-md-1">{{ row.id }}</td>
+        <td class="col-md-4">
+          <a @click="select(row.id)">{{ row.label.value }}</a>
+        </td>
+        <td class="col-md-1">
+          <a @click="remove(row.id)">
+            <span class="glyphicon glyphicon-remove" aria-hidden="true">x</span>
+          </a>
+        </td>
+        <td class="col-md-6"></td>
+      </tr>
+    </tbody>
+  </table>
+  <span
+    class="preloadicon glyphicon glyphicon-remove"
+    aria-hidden="true"
+  ></span>
+</template>
+
+<style>
+.danger {
+  background-color: red;
+}
+</style>
diff --git a/packages-private/benchmark/client/data.ts b/packages-private/benchmark/client/data.ts
new file mode 100644
index 00000000000..ea5de1451d8
--- /dev/null
+++ b/packages-private/benchmark/client/data.ts
@@ -0,0 +1,78 @@
+import { shallowRef } from 'vue'
+
+let ID = 1
+
+function _random(max: number) {
+  return Math.round(Math.random() * 1000) % max
+}
+
+export function buildData(count = 1000) {
+  const adjectives = [
+    'pretty',
+    'large',
+    'big',
+    'small',
+    'tall',
+    'short',
+    'long',
+    'handsome',
+    'plain',
+    'quaint',
+    'clean',
+    'elegant',
+    'easy',
+    'angry',
+    'crazy',
+    'helpful',
+    'mushy',
+    'odd',
+    'unsightly',
+    'adorable',
+    'important',
+    'inexpensive',
+    'cheap',
+    'expensive',
+    'fancy',
+  ]
+  const colours = [
+    'red',
+    'yellow',
+    'blue',
+    'green',
+    'pink',
+    'brown',
+    'purple',
+    'brown',
+    'white',
+    'black',
+    'orange',
+  ]
+  const nouns = [
+    'table',
+    'chair',
+    'house',
+    'bbq',
+    'desk',
+    'car',
+    'pony',
+    'cookie',
+    'sandwich',
+    'burger',
+    'pizza',
+    'mouse',
+    'keyboard',
+  ]
+  const data = []
+  for (let i = 0; i < count; i++)
+    data.push({
+      id: ID++,
+      label: shallowRef(
+        adjectives[_random(adjectives.length)] +
+          ' ' +
+          colours[_random(colours.length)] +
+          ' ' +
+          nouns[_random(nouns.length)],
+      ),
+    })
+  return data
+}
diff --git a/packages-private/benchmark/client/index.html b/packages-private/benchmark/client/index.html
new file mode 100644
index 00000000000..c3ca4c53590
--- /dev/null
+++ b/packages-private/benchmark/client/index.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Vue Vapor Benchmark</title>
+    <style>
+      html {
+        color-scheme: light dark;
+      }
+    </style>
+  </head>
+  <body class="done">
+    <div id="app"></div>
+    <script type="module" src="./index.ts"></script>
+  </body>
+</html>
diff --git a/packages-private/benchmark/client/index.ts b/packages-private/benchmark/client/index.ts
new file mode 100644
index 00000000000..a12f727a101
--- /dev/null
+++ b/packages-private/benchmark/client/index.ts
@@ -0,0 +1,5 @@
+if (import.meta.env.IS_VAPOR) {
+  import('./vapor')
+} else {
+  import('./vdom')
+}
diff --git a/packages-private/benchmark/client/profiling.ts b/packages-private/benchmark/client/profiling.ts
new file mode 100644
index 00000000000..ee4f38b6090
--- /dev/null
+++ b/packages-private/benchmark/client/profiling.ts
@@ -0,0 +1,94 @@
+/* eslint-disable no-console */
+/* eslint-disable no-restricted-syntax */
+/* eslint-disable no-restricted-globals */
+
+import { nextTick } from 'vue'
+
+declare global {
+  var doProfile: boolean
+  var reactivity: boolean
+  var recordTime: boolean
+  var times: Record<string, number[]>
+}
+
+globalThis.recordTime = true
+globalThis.doProfile = false
+globalThis.reactivity = false
+
+export const defer = () => new Promise(r => requestIdleCallback(r))
+
+const times: Record<string, number[]> = (globalThis.times = {})
+
+export function wrap(
+  id: string,
+  fn: (...args: any[]) => any,
+): (...args: any[]) => Promise<void> {
+  return async (...args) => {
+    if (!globalThis.recordTime) {
+      return fn(...args)
+    }
+
+    document.body.classList.remove('done')
+
+    const { doProfile } = globalThis
+    await nextTick()
+
+    doProfile && console.profile(id)
+    const start = performance.now()
+    fn(...args)
+
+    await nextTick()
+    let time: number
+    if (globalThis.reactivity) {
+      time = performance.measure(
+        'flushJobs-measure',
+        'flushJobs-start',
+        'flushJobs-end',
+      ).duration
+      performance.clearMarks()
+      performance.clearMeasures()
+    } else {
+      time = performance.now() - start
+    }
+    const prevTimes = times[id] || (times[id] = [])
+    prevTimes.push(time)
+
+    const { min, max, median, mean, std } = compute(prevTimes)
+    const msg =
+      `${id}: min: ${min} / ` +
+      `max: ${max} / ` +
+      `median: ${median}ms / ` +
+      `mean: ${mean}ms / ` +
+      `time: ${time.toFixed(2)}ms / ` +
+      `std: ${std} ` +
+      `over ${prevTimes.length} runs`
+    doProfile && console.profileEnd(id)
+    console.log(msg)
+    const timeEl = document.getElementById('time')!
+    timeEl.textContent = msg
+
+    document.body.classList.add('done')
+  }
+}
+
+function compute(array: number[]) {
+  const n = array.length
+  const max = Math.max(...array)
+  const min = Math.min(...array)
+  const mean = array.reduce((a, b) => a + b) / n
+  const std = Math.sqrt(
+    array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n,
+  )
+  const median = array.slice().sort((a, b) => a - b)[Math.floor(n / 2)]
+  return {
+    max: round(max),
+    min: round(min),
+    mean: round(mean),
+    std: round(std),
+    median: round(median),
+  }
+}
+
+function round(n: number) {
+  return +n.toFixed(2)
+}
diff --git a/packages-private/benchmark/client/vapor.ts b/packages-private/benchmark/client/vapor.ts
new file mode 100644
index 00000000000..2574da9dea1
--- /dev/null
+++ b/packages-private/benchmark/client/vapor.ts
@@ -0,0 +1,4 @@
+import { createVaporApp } from 'vue'
+import App from './AppVapor.vue'
+
+createVaporApp(App as any).mount('#app')
diff --git a/packages-private/benchmark/client/vdom.ts b/packages-private/benchmark/client/vdom.ts
new file mode 100644
index 00000000000..01433bca2ac
--- /dev/null
+++ b/packages-private/benchmark/client/vdom.ts
@@ -0,0 +1,4 @@
+import { createApp } from 'vue'
+import App from './App.vue'
+
+createApp(App).mount('#app')
diff --git a/packages-private/benchmark/index.js b/packages-private/benchmark/index.js
new file mode 100644
index 00000000000..3af47eaf499
--- /dev/null
+++ b/packages-private/benchmark/index.js
@@ -0,0 +1,393 @@
+// @ts-check
+import path from 'node:path'
+import { parseArgs } from 'node:util'
+import { mkdir, rm, writeFile } from 'node:fs/promises'
+import Vue from '@vitejs/plugin-vue'
+import { build } from 'vite'
+import connect from 'connect'
+import sirv from 'sirv'
+import { launch } from 'puppeteer'
+import colors from 'picocolors'
+import { exec, getSha } from '../../scripts/utils.js'
+import process from 'node:process'
+import readline from 'node:readline'
+
+// Thanks to https://github.com/krausest/js-framework-benchmark (Apache-2.0 license)
+const {
+  values: {
+    skipLib,
+    skipApp,
+    skipBench,
+    vdom,
+    noVapor,
+    port: portStr,
+    count: countStr,
+    warmupCount: warmupCountStr,
+    noHeadless,
+    noMinify,
+    reference,
+  },
+} = parseArgs({
+  allowNegative: true,
+  allowPositionals: true,
+  options: {
+    skipLib: {
+      type: 'boolean',
+      short: 'l',
+    },
+    skipApp: {
+      type: 'boolean',
+      short: 'a',
+    },
+    skipBench: {
+      type: 'boolean',
+      short: 'b',
+    },
+    noVapor: {
+      type: 'boolean',
+    },
+    vdom: {
+      type: 'boolean',
+      short: 'v',
+    },
+    port: {
+      type: 'string',
+      short: 'p',
+      default: '8193',
+    },
+    count: {
+      type: 'string',
+      short: 'c',
+      default: '30',
+    },
+    warmupCount: {
+      type: 'string',
+      short: 'w',
+      default: '5',
+    },
+    noHeadless: {
+      type: 'boolean',
+    },
+    noMinify: {
+      type: 'boolean',
+    },
+    reference: {
+      type: 'boolean',
+      short: 'r',
+    },
+  },
+})
+
+const port = +(/** @type {string}*/ (portStr))
+const count = +(/** @type {string}*/ (countStr))
+const warmupCount = +(/** @type {string}*/ (warmupCountStr))
+const sha = await getSha(true)
+
+if (!skipLib && !reference) {
+  await buildLib()
+}
+if (!skipApp && !reference) {
+  await rm('client/dist', { recursive: true }).catch(() => {})
+  vdom && (await buildApp(false))
+  !noVapor && (await buildApp(true))
+}
+const server = startServer()
+
+if (!skipBench) {
+  await benchmark()
+  server.close()
+}
+
+async function buildLib() {
+  console.info(colors.blue('Building lib...'))
+
+  /** @type {import('node:child_process').SpawnOptions} */
+  const options = {
+    cwd: path.resolve(import.meta.dirname, '../..'),
+    stdio: 'inherit',
+    env: { ...process.env, BENCHMARK: 'true' },
+  }
+  const [{ ok }, { ok: ok2 }, { ok: ok3 }] = await Promise.all([
+    exec(
+      'pnpm',
+      `run --silent build shared compiler-core compiler-dom -pf cjs`.split(' '),
+      options,
+    ),
+    exec(
+      'pnpm',
+      'run --silent build compiler-sfc compiler-ssr compiler-vapor -f cjs'.split(
+        ' ',
+      ),
+      options,
+    ),
+    exec(
+      'pnpm',
+      `run --silent build shared reactivity runtime-core runtime-dom runtime-vapor vue -f esm-bundler+esm-bundler-runtime`.split(
+        ' ',
+      ),
+      options,
+    ),
+  ])
+
+  if (!ok || !ok2 || !ok3) {
+    console.error('Failed to build')
+    process.exit(1)
+  }
+}
+
+/** @param {boolean} isVapor */
+async function buildApp(isVapor) {
+  console.info(
+    colors.blue(`\nBuilding ${isVapor ? 'Vapor' : 'Virtual DOM'} app...\n`),
+  )
+
+  process.env.NODE_ENV = 'production'
+
+  const CompilerSFC = await import(
+    '../../packages/compiler-sfc/dist/compiler-sfc.cjs.js'
+  )
+
+  const runtimePath = path.resolve(
+    import.meta.dirname,
+    '../../packages/vue/dist/vue.runtime.esm-bundler.js',
+  )
+
+  const mode = isVapor ? 'vapor' : 'vdom'
+  await build({
+    root: './client',
+    base: `/${mode}`,
+    define: {
+      'import.meta.env.IS_VAPOR': String(isVapor),
+    },
+    build: {
+      minify: !noMinify,
+      outDir: path.resolve('./client/dist', mode),
+      rollupOptions: {
+        onwarn(log, handler) {
+          if (log.code === 'INVALID_ANNOTATION') return
+          handler(log)
+        },
+      },
+    },
+    resolve: {
+      alias: {
+        vue: runtimePath,
+      },
+    },
+    clearScreen: false,
+    plugins: [
+      Vue({
+        compiler: CompilerSFC,
+      }),
+    ],
+  })
+}
+
+function startServer() {
+  const server = connect()
+    .use(sirv(reference ? './reference' : './client/dist', { dev: true }))
+    .listen(port)
+  printPort()
+  process.on('SIGTERM', () => server.close())
+  return server
+}
+
+async function benchmark() {
+  console.info(colors.blue(`\nStarting benchmark...`))
+
+  const browser = await initBrowser()
+
+  await mkdir('results', { recursive: true }).catch(() => {})
+  if (!noVapor) {
+    await doBench(browser, true)
+  }
+  if (vdom) {
+    await doBench(browser, false)
+  }
+
+  await browser.close()
+}
+
+/**
+ *  @param {boolean} isVapor
+ */
+function getURL(isVapor) {
+  return `http://localhost:${port}/${reference ? '' : isVapor ? 'vapor' : 'vdom'}/`
+}
+
+/**
+ *
+ * @param {import('puppeteer').Browser} browser
+ * @param {boolean} isVapor
+ */
+async function doBench(browser, isVapor) {
+  const mode = reference ? `reference` : isVapor ? 'vapor' : 'vdom'
+  console.info('\n\nmode:', mode)
+
+  const page = await browser.newPage()
+  page.emulateCPUThrottling(4)
+  await page.goto(getURL(isVapor), {
+    waitUntil: 'networkidle0',
+  })
+
+  await forceGC()
+  const t = performance.now()
+
+  console.log('warmup run')
+  await eachRun(() => withoutRecord(benchOnce), warmupCount)
+
+  console.log('benchmark run')
+  await eachRun(benchOnce, count)
+
+  console.info(
+    'Total time:',
+    colors.cyan(((performance.now() - t) / 1000).toFixed(2)),
+    's',
+  )
+  const times = await getTimes()
+  const result =
+    /** @type {Record<string, typeof compute>} */
+    Object.fromEntries(Object.entries(times).map(([k, v]) => [k, compute(v)]))
+
+  console.table(result)
+  await writeFile(
+    `results/benchmark-${sha}-${mode}.json`,
+    JSON.stringify(result, undefined, 2),
+  )
+  await page.close()
+  return result
+
+  async function benchOnce() {
+    await clickButton('run') // test: create rows
+    await clickButton('update') // partial update
+    await clickButton('swaprows') // swap rows
+    await select() // test: select row, remove row
+    await clickButton('clear') // clear rows
+
+    await withoutRecord(() => clickButton('run'))
+    await clickButton('add') // append rows to large table
+
+    await withoutRecord(() => clickButton('clear'))
+    await clickButton('runlots') // create many rows
+    await withoutRecord(() => clickButton('clear'))
+
+    // TODO replace all rows
+  }
+
+  function getTimes() {
+    return page.evaluate(() => /** @type {any} */ (globalThis).times)
+  }
+
+  async function forceGC() {
+    await page.evaluate(
+      `window.gc({type:'major',execution:'sync',flavor:'last-resort'})`,
+    )
+  }
+
+  /** @param {() => any} fn */
+  async function withoutRecord(fn) {
+    const currentRecordTime = await page.evaluate(() => globalThis.recordTime)
+    await page.evaluate(() => (globalThis.recordTime = false))
+    await fn()
+    await page.evaluate(
+      currentRecordTime => (globalThis.recordTime = currentRecordTime),
+      currentRecordTime,
+    )
+  }
+
+  /** @param {string} id */
+  async function clickButton(id) {
+    await page.click(`#${id}`)
+    await wait()
+  }
+
+  async function select() {
+    for (let i = 1; i <= 10; i++) {
+      await page.click(`tbody > tr:nth-child(2) > td:nth-child(2) > a`)
+      await page.waitForSelector(`tbody > tr:nth-child(2).danger`)
+      await page.click(`tbody > tr:nth-child(2) > td:nth-child(3) > a`)
+      await wait()
+    }
+  }
+
+  async function wait() {
+    await page.waitForSelector('.done')
+  }
+}
+
+/**
+ * @param {Function} bench
+ * @param {number} count
+ */
+async function eachRun(bench, count) {
+  for (let i = 0; i < count; i++) {
+    readline.cursorTo(process.stdout, 0)
+    readline.clearLine(process.stdout, 0)
+    process.stdout.write(`${i + 1}/${count}`)
+    await bench()
+  }
+  if (count === 0) {
+    process.stdout.write('0/0 (skip)')
+  }
+  process.stdout.write('\n')
+}
+
+async function initBrowser() {
+  const disableFeatures = [
+    'Translate', // avoid translation popups
+    'PrivacySandboxSettings4', // avoid privacy popup
+    'IPH_SidePanelGenericMenuFeature', // bookmark popup see https://github.com/krausest/js-framework-benchmark/issues/1688
+  ]
+
+  const args = [
+    '--js-flags=--expose-gc', // needed for gc() function
+    '--no-default-browser-check',
+    '--disable-sync',
+    '--no-first-run',
+    '--ash-no-nudges',
+    '--disable-extensions',
+    `--disable-features=${disableFeatures.join(',')}`,
+  ]
+
+  const headless = !noHeadless
+  console.info('headless:', headless)
+  const browser = await launch({
+    headless: headless,
+    args,
+  })
+  console.log('browser version:', colors.blue(await browser.version()))
+
+  return browser
+}
+
+/** @param {number[]} array */
+function compute(array) {
+  const n = array.length
+  const max = Math.max(...array)
+  const min = Math.min(...array)
+  const mean = array.reduce((a, b) => a + b) / n
+  const std = Math.sqrt(
+    array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n,
+  )
+  const median = array.slice().sort((a, b) => a - b)[Math.floor(n / 2)]
+  return {
+    max: round(max),
+    min: round(min),
+    mean: round(mean),
+    std: round(std),
+    median: round(median),
+  }
+}
+
+/** @param {number} n */
+function round(n) {
+  return +n.toFixed(2)
+}
+
+function printPort() {
+  const vaporLink = !noVapor
+    ? `\n${reference ? `Reference` : `Vapor`}: ${colors.blue(getURL(true))}`
+    : ''
+  const vdomLink = vdom ? `\nvDom:  ${colors.blue(getURL(false))}` : ''
+  console.info(`\n\nServer started at`, vaporLink, vdomLink)
+}
diff --git a/packages-private/benchmark/package.json b/packages-private/benchmark/package.json
new file mode 100644
index 00000000000..e6eb08e9539
--- /dev/null
+++ b/packages-private/benchmark/package.json
@@ -0,0 +1,20 @@
+{
+  "name": "benchmark",
+  "version": "0.0.0",
+  "author": "三咲智子 Kevin Deng <sxzz@sxzz.moe>",
+  "license": "MIT",
+  "type": "module",
+  "scripts": {
+    "dev": "pnpm start --noMinify --skipBench --vdom",
+    "start": "node index.js"
+  },
+  "dependencies": {
+    "@vitejs/plugin-vue": "catalog:",
+    "connect": "^3.7.0",
+    "sirv": "^2.0.4",
+    "vite": "catalog:"
+  },
+  "devDependencies": {
+    "@types/connect": "^3.4.38"
+  }
+}
diff --git a/packages-private/benchmark/tsconfig.json b/packages-private/benchmark/tsconfig.json
new file mode 100644
index 00000000000..4a32149504f
--- /dev/null
+++ b/packages-private/benchmark/tsconfig.json
@@ -0,0 +1,25 @@
+{
+  "compilerOptions": {
+    "target": "esnext",
+    "lib": ["es2022", "dom"],
+    "allowJs": true,
+    "moduleDetection": "force",
+    "module": "preserve",
+    "moduleResolution": "bundler",
+    "resolveJsonModule": true,
+    "types": ["node", "vite/client"],
+    "strict": true,
+    "noUnusedLocals": true,
+    "declaration": true,
+    "esModuleInterop": true,
+    "isolatedModules": true,
+    "verbatimModuleSyntax": true,
+    "skipLibCheck": true,
+    "noEmit": true,
+    "paths": {
+      "vue": ["../packages/vue/src/runtime-with-vapor.ts"],
+      "@vue/*": ["../packages/*/src"]
+    }
+  },
+  "include": ["**/*"]
+}
diff --git a/packages-private/local-playground/index.html b/packages-private/local-playground/index.html
new file mode 100644
index 00000000000..a01ba96dc0b
--- /dev/null
+++ b/packages-private/local-playground/index.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Vue Vapor</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="./src/main.ts"></script>
+  </body>
+</html>
diff --git a/packages-private/local-playground/package.json b/packages-private/local-playground/package.json
new file mode 100644
index 00000000000..37c9818a706
--- /dev/null
+++ b/packages-private/local-playground/package.json
@@ -0,0 +1,22 @@
+{
+  "name": "playground",
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "node ./setup/vite.js",
+    "build": "vite build -c vite.prod.config.ts",
+    "prepreview": "cd ../ && pnpm run build runtime-vapor -f esm-bundler",
+    "preview": "pnpm run build && vite preview -c vite.prod.config.ts"
+  },
+  "dependencies": {
+    "@vueuse/core": "^11.1.0",
+    "vue": "workspace:*"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "catalog:",
+    "@vue/compiler-sfc": "workspace:*",
+    "vite": "catalog:",
+    "vite-hyper-config": "^0.4.0",
+    "vite-plugin-inspect": "^0.8.7"
+  }
+}
diff --git a/packages-private/local-playground/setup/dev.js b/packages-private/local-playground/setup/dev.js
new file mode 100644
index 00000000000..72a7ed4a835
--- /dev/null
+++ b/packages-private/local-playground/setup/dev.js
@@ -0,0 +1,66 @@
+// @ts-check
+import path from 'node:path'
+
+const resolve = (/** @type {string} */ p) =>
+  path.resolve(import.meta.dirname, '../../../packages', p)
+
+/**
+ * @param {Object} [env]
+ * @param {boolean} [env.browser]
+ * @returns {import('vite').Plugin}
+ */
+export function DevPlugin({ browser = false } = {}) {
+  return {
+    name: 'dev-plugin',
+    config() {
+      return {
+        resolve: {
+          alias: {
+            vue: resolve('vue/src/runtime-with-vapor.ts'),
+
+            '@vue/runtime-core': resolve('runtime-core/src'),
+            '@vue/runtime-dom': resolve('runtime-dom/src'),
+            '@vue/runtime-vapor': resolve('runtime-vapor/src'),
+
+            '@vue/compiler-core': resolve('compiler-core/src'),
+            '@vue/compiler-dom': resolve('compiler-dom/src'),
+            '@vue/compiler-vapor': resolve('compiler-vapor/src'),
+
+            '@vue/compiler-sfc': resolve('compiler-sfc/src'),
+            '@vue/compiler-ssr': resolve('compiler-ssr/src'),
+
+            '@vue/reactivity': resolve('reactivity/src'),
+            '@vue/shared': resolve('shared/src'),
+            '@vue/runtime-shared': resolve('runtime-shared/src'),
+          },
+        },
+        define: {
+          __COMMIT__: `"__COMMIT__"`,
+          __VERSION__: `"0.0.0"`,
+          __DEV__: `true`,
+          // this is only used during Vue's internal tests
+          __TEST__: `false`,
+          // If the build is expected to run directly in the browser (global / esm builds)
+          __BROWSER__: String(browser),
+          __GLOBAL__: String(false),
+          __ESM_BUNDLER__: String(true),
+          __ESM_BROWSER__: String(false),
+          // is targeting Node (SSR)?
+          __NODE_JS__: String(false),
+          // need SSR-specific branches?
+          __SSR__: String(false),
+          __BENCHMARK__: 'false',
+
+          // 2.x compat build
+          __COMPAT__: String(false),
+
+          // feature flags
+          __FEATURE_SUSPENSE__: `true`,
+          __FEATURE_OPTIONS_API__: `true`,
+          __FEATURE_PROD_DEVTOOLS__: `false`,
+          __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__: `false`,
+        },
+      }
+    },
+  }
+}
diff --git a/packages-private/local-playground/setup/vite.js b/packages-private/local-playground/setup/vite.js
new file mode 100644
index 00000000000..e6b9a927206
--- /dev/null
+++ b/packages-private/local-playground/setup/vite.js
@@ -0,0 +1,14 @@
+// @ts-check
+
+import { startVite } from 'vite-hyper-config'
+import { DevPlugin } from './dev.js'
+
+startVite(
+  undefined,
+  { plugins: [DevPlugin()] },
+  {
+    deps: {
+      inline: ['@vitejs/plugin-vue'],
+    },
+  },
+)
diff --git a/packages-private/local-playground/src/.gitignore b/packages-private/local-playground/src/.gitignore
new file mode 100644
index 00000000000..2e1b9cfb780
--- /dev/null
+++ b/packages-private/local-playground/src/.gitignore
@@ -0,0 +1,5 @@
+*
+!.gitignore
+!App.vue
+!main.ts
+!style.css
diff --git a/packages-private/local-playground/src/App.vue b/packages-private/local-playground/src/App.vue
new file mode 100644
index 00000000000..b6124e39a35
--- /dev/null
+++ b/packages-private/local-playground/src/App.vue
@@ -0,0 +1,20 @@
+<script setup lang="ts">
+import { ref } from 'vue'
+import VaporComp from './VaporComp.vue'
+
+const msg = ref('hello')
+const passSlot = ref(true)
+</script>
+
+<template>
+  <input v-model="msg" />
+  <button @click="passSlot = !passSlot">toggle #test slot</button>
+  <VaporComp :msg="msg">
+    <template #default="{ foo }">
+      <div>slot props: {{ foo }}</div>
+      <div>component prop: {{ msg }}</div>
+    </template>
+
+    <template #test v-if="passSlot"> A test slot </template>
+  </VaporComp>
+</template>
diff --git a/packages-private/local-playground/src/main.ts b/packages-private/local-playground/src/main.ts
new file mode 100644
index 00000000000..9d682d9ffb6
--- /dev/null
+++ b/packages-private/local-playground/src/main.ts
@@ -0,0 +1 @@
+import './_entry'
diff --git a/packages-private/local-playground/src/style.css b/packages-private/local-playground/src/style.css
new file mode 100644
index 00000000000..832e61618b0
--- /dev/null
+++ b/packages-private/local-playground/src/style.css
@@ -0,0 +1,6 @@
+.red {
+  color: red;
+}
+.green {
+  color: green;
+}
diff --git a/packages-private/local-playground/tsconfig.json b/packages-private/local-playground/tsconfig.json
new file mode 100644
index 00000000000..8ed98192084
--- /dev/null
+++ b/packages-private/local-playground/tsconfig.json
@@ -0,0 +1,8 @@
+{
+  "extends": "../tsconfig.json",
+  "compilerOptions": {
+    "isolatedDeclarations": false,
+    "allowJs": true
+  },
+  "include": ["./**/*", "../../packages/*/src"]
+}
diff --git a/packages-private/local-playground/vite.config.ts b/packages-private/local-playground/vite.config.ts
new file mode 100644
index 00000000000..8b4b1a423f7
--- /dev/null
+++ b/packages-private/local-playground/vite.config.ts
@@ -0,0 +1,19 @@
+import { defineConfig } from 'vite'
+import Inspect from 'vite-plugin-inspect'
+import { DevPlugin } from './setup/dev'
+import Vue from '@vitejs/plugin-vue'
+import * as CompilerSFC from '@vue/compiler-sfc'
+
+export default defineConfig({
+  clearScreen: false,
+  plugins: [
+    Vue({
+      compiler: CompilerSFC,
+    }),
+    DevPlugin(),
+    Inspect(),
+  ],
+  optimizeDeps: {
+    exclude: ['@vueuse/core'],
+  },
+})
diff --git a/packages-private/local-playground/vite.prod.config.ts b/packages-private/local-playground/vite.prod.config.ts
new file mode 100644
index 00000000000..6bafb7a772a
--- /dev/null
+++ b/packages-private/local-playground/vite.prod.config.ts
@@ -0,0 +1,26 @@
+import { defineConfig } from 'vite'
+import Vue from '@vitejs/plugin-vue'
+import * as CompilerSFC from '@vue/compiler-sfc'
+
+export default defineConfig({
+  build: {
+    modulePreload: false,
+    target: 'esnext',
+    minify: 'terser',
+    terserOptions: {
+      format: { comments: false },
+      compress: {
+        pure_getters: true,
+      },
+    },
+  },
+  clearScreen: false,
+  plugins: [
+    Vue({
+      compiler: CompilerSFC,
+      features: {
+        optionsAPI: false,
+      },
+    }),
+  ],
+})
diff --git a/packages-private/sfc-playground/src/App.vue b/packages-private/sfc-playground/src/App.vue
index 455137ba6f6..be4b136012c 100644
--- a/packages-private/sfc-playground/src/App.vue
+++ b/packages-private/sfc-playground/src/App.vue
@@ -1,6 +1,12 @@
 <script setup lang="ts">
 import Header from './Header.vue'
-import { Repl, useStore, SFCOptions, useVueImportMap } from '@vue/repl'
+import {
+  Repl,
+  type SFCOptions,
+  useStore,
+  useVueImportMap,
+  StoreState,
+} from '@vue/repl'
 import Monaco from '@vue/repl/monaco-editor'
 import { ref, watchEffect, onMounted, computed } from 'vue'
 
@@ -20,13 +26,17 @@ const initAutoSave: boolean = JSON.parse(
 )
 const autoSave = ref(initAutoSave)
 
-const { productionMode, vueVersion, importMap } = useVueImportMap({
-  runtimeDev: import.meta.env.PROD
-    ? `${location.origin}/vue.runtime.esm-browser.js`
-    : `${location.origin}/src/vue-dev-proxy`,
-  runtimeProd: import.meta.env.PROD
-    ? `${location.origin}/vue.runtime.esm-browser.prod.js`
-    : `${location.origin}/src/vue-dev-proxy-prod`,
+const { vueVersion, productionMode, importMap } = useVueImportMap({
+  runtimeDev: () => {
+    return import.meta.env.PROD
+      ? `${location.origin}/vue.runtime-with-vapor.esm-browser.js`
+      : `${location.origin}/src/vue-dev-proxy`
+  },
+  runtimeProd: () => {
+    return import.meta.env.PROD
+      ? `${location.origin}/vue.runtime-with-vapor.esm-browser.prod.js`
+      : `${location.origin}/src/vue-dev-proxy-prod`
+  },
   serverRenderer: import.meta.env.PROD
     ? `${location.origin}/server-renderer.esm-browser.js`
     : `${location.origin}/src/vue-server-renderer-dev-proxy`,
@@ -46,6 +56,8 @@ if (hash.startsWith('__SSR__')) {
   useSSRMode.value = true
 }
 
+const files: StoreState['files'] = ref(Object.create(null))
+
 // enable experimental features
 const sfcOptions = computed(
   (): SFCOptions => ({
@@ -53,11 +65,13 @@ const sfcOptions = computed(
       inlineTemplate: productionMode.value,
       isProd: productionMode.value,
       propsDestructure: true,
+      // vapor: useVaporMode.value,
     },
     style: {
       isProd: productionMode.value,
     },
     template: {
+      // vapor: useVaporMode.value,
       isProd: productionMode.value,
       compilerOptions: {
         isCustomElement: (tag: string) =>
@@ -69,8 +83,9 @@ const sfcOptions = computed(
 
 const store = useStore(
   {
-    builtinImportMap: importMap,
+    files,
     vueVersion,
+    builtinImportMap: importMap,
     sfcOptions,
   },
   hash,
@@ -147,8 +162,10 @@ onMounted(() => {
     :clearConsole="false"
     :preview-options="{
       customCode: {
-        importCode: `import { initCustomFormatter } from 'vue'`,
-        useCode: `if (window.devtoolsFormatters) {
+        importCode: `import { initCustomFormatter, vaporInteropPlugin } from 'vue'`,
+        useCode: `
+  app.use(vaporInteropPlugin)
+  if (window.devtoolsFormatters) {
     const index = window.devtoolsFormatters.findIndex((v) => v.__vue_custom_formatter)
     window.devtoolsFormatters.splice(index, 1)
     initCustomFormatter()
diff --git a/packages-private/sfc-playground/src/Header.vue b/packages-private/sfc-playground/src/Header.vue
index bf1c9bad6eb..132453e986d 100644
--- a/packages-private/sfc-playground/src/Header.vue
+++ b/packages-private/sfc-playground/src/Header.vue
@@ -21,6 +21,7 @@ const emit = defineEmits([
   'toggle-theme',
   'toggle-ssr',
   'toggle-prod',
+  'toggle-vapor',
   'toggle-autosave',
   'reload-page',
 ])
@@ -215,6 +216,7 @@ h1 img {
 }
 
 .toggle-prod span,
+.toggle-vapor span,
 .toggle-ssr span,
 .toggle-autosave span {
   font-size: 12px;
@@ -242,6 +244,15 @@ h1 img {
   background-color: var(--green);
 }
 
+.toggle-vapor span {
+  background-color: var(--btn-bg);
+}
+
+.toggle-vapor.enabled span {
+  color: #fff;
+  background-color: var(--green);
+}
+
 .toggle-dark svg {
   width: 18px;
   height: 18px;
diff --git a/packages-private/sfc-playground/src/vue-dev-proxy-prod.ts b/packages-private/sfc-playground/src/vue-dev-proxy-prod.ts
index 3b2faf19533..6448f9d4c2f 100644
--- a/packages-private/sfc-playground/src/vue-dev-proxy-prod.ts
+++ b/packages-private/sfc-playground/src/vue-dev-proxy-prod.ts
@@ -1,2 +1,2 @@
 // serve vue to the iframe sandbox during dev.
-export * from 'vue/dist/vue.runtime.esm-browser.prod.js'
+export * from 'vue/dist/vue.runtime-with-vapor.esm-browser.prod.js'
diff --git a/packages-private/sfc-playground/src/vue-dev-proxy.ts b/packages-private/sfc-playground/src/vue-dev-proxy.ts
index f254416d4fd..427d41621c2 100644
--- a/packages-private/sfc-playground/src/vue-dev-proxy.ts
+++ b/packages-private/sfc-playground/src/vue-dev-proxy.ts
@@ -1,2 +1,2 @@
 // serve vue to the iframe sandbox during dev.
-export * from 'vue'
+export * from 'vue/dist/vue.runtime-with-vapor.esm-browser.js'
diff --git a/packages-private/sfc-playground/src/welcome.vue b/packages-private/sfc-playground/src/welcome.vue
new file mode 100644
index 00000000000..a466ef884d7
--- /dev/null
+++ b/packages-private/sfc-playground/src/welcome.vue
@@ -0,0 +1,9 @@
+<script setup lang="ts">
+import { ref } from 'vue'
+
+const msg = ref('Hello World!')
+</script>
+
+<template>
+  <h1>{{ msg }}</h1>
+</template>
diff --git a/packages-private/sfc-playground/vite.config.ts b/packages-private/sfc-playground/vite.config.ts
index 2e77f1970a7..c1a40fd1ca9 100644
--- a/packages-private/sfc-playground/vite.config.ts
+++ b/packages-private/sfc-playground/vite.config.ts
@@ -53,6 +53,8 @@ function copyVuePlugin(): Plugin {
       copyFile(`vue/dist/vue.esm-browser.prod.js`)
       copyFile(`vue/dist/vue.runtime.esm-browser.js`)
       copyFile(`vue/dist/vue.runtime.esm-browser.prod.js`)
+      copyFile(`vue/dist/vue.runtime-with-vapor.esm-browser.js`)
+      copyFile(`vue/dist/vue.runtime-with-vapor.esm-browser.prod.js`)
       copyFile(`server-renderer/dist/server-renderer.esm-browser.js`)
     },
   }
diff --git a/packages-private/template-explorer/package.json b/packages-private/template-explorer/package.json
index 08da34b173e..c546f4a298d 100644
--- a/packages-private/template-explorer/package.json
+++ b/packages-private/template-explorer/package.json
@@ -11,6 +11,7 @@
     "enableNonBrowserBranches": true
   },
   "dependencies": {
+    "@vue/compiler-vapor": "workspace:^",
     "monaco-editor": "^0.52.2",
     "source-map-js": "^1.2.1"
   }
diff --git a/packages-private/template-explorer/src/index.ts b/packages-private/template-explorer/src/index.ts
index 988712d623c..96619b5a311 100644
--- a/packages-private/template-explorer/src/index.ts
+++ b/packages-private/template-explorer/src/index.ts
@@ -1,15 +1,18 @@
 import type * as m from 'monaco-editor'
+import type { CompilerError } from '@vue/compiler-dom'
+import { compile } from '@vue/compiler-dom'
 import {
-  type CompilerError,
   type CompilerOptions,
-  compile,
-} from '@vue/compiler-dom'
-import { compile as ssrCompile } from '@vue/compiler-ssr'
+  compile as vaporCompile,
+} from '@vue/compiler-vapor'
+// import { compile as ssrCompile } from '@vue/compiler-ssr'
+
 import {
   compilerOptions,
   defaultOptions,
   initOptions,
   ssrMode,
+  vaporMode,
 } from './options'
 import { toRaw, watchEffect } from '@vue/runtime-dom'
 import { SourceMapConsumer } from 'source-map-js'
@@ -77,10 +80,16 @@ window.init = () => {
     console.clear()
     try {
       const errors: CompilerError[] = []
-      const compileFn = ssrMode.value ? ssrCompile : compile
+      const compileFn = /* ssrMode.value ? ssrCompile : */ (
+        vaporMode.value ? vaporCompile : compile
+      ) as typeof vaporCompile
       const start = performance.now()
       const { code, ast, map } = compileFn(source, {
         ...compilerOptions,
+        prefixIdentifiers:
+          compilerOptions.prefixIdentifiers ||
+          compilerOptions.mode === 'module' ||
+          compilerOptions.ssr,
         filename: 'ExampleTemplate.vue',
         sourceMap: true,
         onError: err => {
diff --git a/packages-private/template-explorer/src/options.ts b/packages-private/template-explorer/src/options.ts
index e3cc6173a8a..6e0190ed60c 100644
--- a/packages-private/template-explorer/src/options.ts
+++ b/packages-private/template-explorer/src/options.ts
@@ -1,8 +1,9 @@
 import { createApp, h, reactive, ref } from 'vue'
-import type { CompilerOptions } from '@vue/compiler-dom'
+import type { CompilerOptions } from '@vue/compiler-vapor'
 import { BindingTypes } from '@vue/compiler-core'
 
 export const ssrMode = ref(false)
+export const vaporMode = ref(true)
 
 export const defaultOptions: CompilerOptions = {
   mode: 'module',
@@ -39,7 +40,7 @@ const App = {
         compilerOptions.prefixIdentifiers || compilerOptions.mode === 'module'
 
       return [
-        h('h1', `Vue 3 Template Explorer`),
+        h('h1', `Vue Template Explorer`),
         h(
           'a',
           {
@@ -222,6 +223,18 @@ const App = {
               }),
               h('label', { for: 'compat' }, 'v2 compat mode'),
             ]),
+
+            h('li', [
+              h('input', {
+                type: 'checkbox',
+                id: 'vapor',
+                checked: vaporMode.value,
+                onChange(e: Event) {
+                  vaporMode.value = (e.target as HTMLInputElement).checked
+                },
+              }),
+              h('label', { for: 'vapor' }, 'vapor'),
+            ]),
           ]),
         ]),
       ]
diff --git a/packages-private/tsconfig.json b/packages-private/tsconfig.json
index 1c287a7500c..37a38f53fc6 100644
--- a/packages-private/tsconfig.json
+++ b/packages-private/tsconfig.json
@@ -3,5 +3,5 @@
   "compilerOptions": {
     "isolatedDeclarations": false
   },
-  "include": ["."]
+  "include": [".", "../packages/vue/__tests__/e2e/e2eUtils.ts"]
 }
diff --git a/packages-private/vapor-e2e-test/__tests__/todomvc.spec.ts b/packages-private/vapor-e2e-test/__tests__/todomvc.spec.ts
new file mode 100644
index 00000000000..3de8392e5e2
--- /dev/null
+++ b/packages-private/vapor-e2e-test/__tests__/todomvc.spec.ts
@@ -0,0 +1,195 @@
+import path from 'node:path'
+import {
+  E2E_TIMEOUT,
+  setupPuppeteer,
+} from '../../../packages/vue/__tests__/e2e/e2eUtils'
+import connect from 'connect'
+import sirv from 'sirv'
+
+describe('e2e: todomvc', () => {
+  const {
+    page,
+    click,
+    isVisible,
+    count,
+    text,
+    value,
+    isChecked,
+    isFocused,
+    classList,
+    enterValue,
+    clearValue,
+    timeout,
+  } = setupPuppeteer()
+
+  let server: any
+  const port = '8194'
+  beforeAll(() => {
+    server = connect()
+      .use(sirv(path.resolve(import.meta.dirname, '../dist')))
+      .listen(port)
+    process.on('SIGTERM', () => server && server.close())
+  })
+
+  afterAll(() => {
+    server.close()
+  })
+
+  async function removeItemAt(n: number) {
+    const item = (await page().$('.todo:nth-child(' + n + ')'))!
+    const itemBBox = (await item.boundingBox())!
+    await page().mouse.move(itemBBox.x + 10, itemBBox.y + 10)
+    await click('.todo:nth-child(' + n + ') .destroy')
+  }
+
+  test(
+    'vapor',
+    async () => {
+      const baseUrl = `http://localhost:${port}/todomvc/`
+      await page().goto(baseUrl)
+
+      expect(await isVisible('.main')).toBe(false)
+      expect(await isVisible('.footer')).toBe(false)
+      expect(await count('.filters .selected')).toBe(1)
+      expect(await text('.filters .selected')).toBe('All')
+      expect(await count('.todo')).toBe(0)
+
+      await enterValue('.new-todo', 'test')
+      expect(await count('.todo')).toBe(1)
+      expect(await isVisible('.todo .edit')).toBe(false)
+      expect(await text('.todo label')).toBe('test')
+      expect(await text('.todo-count strong')).toBe('1')
+      expect(await isChecked('.todo .toggle')).toBe(false)
+      expect(await isVisible('.main')).toBe(true)
+      expect(await isVisible('.footer')).toBe(true)
+      expect(await isVisible('.clear-completed')).toBe(false)
+      expect(await value('.new-todo')).toBe('')
+
+      await enterValue('.new-todo', 'test2')
+      expect(await count('.todo')).toBe(2)
+      expect(await text('.todo:nth-child(2) label')).toBe('test2')
+      expect(await text('.todo-count strong')).toBe('2')
+
+      // toggle
+      await click('.todo .toggle')
+      expect(await count('.todo.completed')).toBe(1)
+      expect(await classList('.todo:nth-child(1)')).toContain('completed')
+      expect(await text('.todo-count strong')).toBe('1')
+      expect(await isVisible('.clear-completed')).toBe(true)
+
+      await enterValue('.new-todo', 'test3')
+      expect(await count('.todo')).toBe(3)
+      expect(await text('.todo:nth-child(3) label')).toBe('test3')
+      expect(await text('.todo-count strong')).toBe('2')
+
+      await enterValue('.new-todo', 'test4')
+      await enterValue('.new-todo', 'test5')
+      expect(await count('.todo')).toBe(5)
+      expect(await text('.todo-count strong')).toBe('4')
+
+      // toggle more
+      await click('.todo:nth-child(4) .toggle')
+      await click('.todo:nth-child(5) .toggle')
+      expect(await count('.todo.completed')).toBe(3)
+      expect(await text('.todo-count strong')).toBe('2')
+
+      // remove
+      await removeItemAt(1)
+      expect(await count('.todo')).toBe(4)
+      expect(await count('.todo.completed')).toBe(2)
+      expect(await text('.todo-count strong')).toBe('2')
+      await removeItemAt(2)
+      expect(await count('.todo')).toBe(3)
+      expect(await count('.todo.completed')).toBe(2)
+      expect(await text('.todo-count strong')).toBe('1')
+
+      // remove all
+      await click('.clear-completed')
+      expect(await count('.todo')).toBe(1)
+      expect(await text('.todo label')).toBe('test2')
+      expect(await count('.todo.completed')).toBe(0)
+      expect(await text('.todo-count strong')).toBe('1')
+      expect(await isVisible('.clear-completed')).toBe(false)
+
+      // prepare to test filters
+      await enterValue('.new-todo', 'test')
+      await enterValue('.new-todo', 'test')
+      await click('.todo:nth-child(2) .toggle')
+      await click('.todo:nth-child(3) .toggle')
+
+      // active filter
+      await click('.filters li:nth-child(2) a')
+      await timeout(1)
+      expect(await count('.todo')).toBe(1)
+      expect(await count('.todo.completed')).toBe(0)
+      // add item with filter active
+      await enterValue('.new-todo', 'test')
+      expect(await count('.todo')).toBe(2)
+
+      // completed filter
+      await click('.filters li:nth-child(3) a')
+      await timeout(1)
+      expect(await count('.todo')).toBe(2)
+      expect(await count('.todo.completed')).toBe(2)
+
+      // filter on page load
+      await page().goto(`${baseUrl}#active`)
+      expect(await count('.todo')).toBe(2)
+      expect(await count('.todo.completed')).toBe(0)
+      expect(await text('.todo-count strong')).toBe('2')
+
+      // completed on page load
+      await page().goto(`${baseUrl}#completed`)
+      expect(await count('.todo')).toBe(2)
+      expect(await count('.todo.completed')).toBe(2)
+      expect(await text('.todo-count strong')).toBe('2')
+
+      // toggling with filter active
+      await click('.todo .toggle')
+      expect(await count('.todo')).toBe(1)
+      await click('.filters li:nth-child(2) a')
+      await timeout(1)
+      expect(await count('.todo')).toBe(3)
+      await click('.todo .toggle')
+      expect(await count('.todo')).toBe(2)
+
+      // editing triggered by blur
+      await click('.filters li:nth-child(1) a')
+      await timeout(1)
+      await click('.todo:nth-child(1) label', { clickCount: 2 })
+      expect(await count('.todo.editing')).toBe(1)
+      expect(await isFocused('.todo:nth-child(1) .edit')).toBe(true)
+      await clearValue('.todo:nth-child(1) .edit')
+      await page().type('.todo:nth-child(1) .edit', 'edited!')
+      await click('.new-todo') // blur
+      expect(await count('.todo.editing')).toBe(0)
+      expect(await text('.todo:nth-child(1) label')).toBe('edited!')
+
+      // editing triggered by enter
+      await click('.todo label', { clickCount: 2 })
+      await enterValue('.todo:nth-child(1) .edit', 'edited again!')
+      expect(await count('.todo.editing')).toBe(0)
+      expect(await text('.todo:nth-child(1) label')).toBe('edited again!')
+
+      // cancel
+      await click('.todo label', { clickCount: 2 })
+      await clearValue('.todo:nth-child(1) .edit')
+      await page().type('.todo:nth-child(1) .edit', 'edited!')
+      await page().keyboard.press('Escape')
+      expect(await count('.todo.editing')).toBe(0)
+      expect(await text('.todo:nth-child(1) label')).toBe('edited again!')
+
+      // empty value should remove
+      await click('.todo label', { clickCount: 2 })
+      await enterValue('.todo:nth-child(1) .edit', ' ')
+      expect(await count('.todo')).toBe(3)
+
+      // toggle all
+      await click('.toggle-all+label')
+      expect(await count('.todo.completed')).toBe(3)
+      await click('.toggle-all+label')
+      expect(await count('.todo:not(.completed)')).toBe(3)
+    },
+    E2E_TIMEOUT,
+  )
+})
diff --git a/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts b/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts
new file mode 100644
index 00000000000..360f48085a1
--- /dev/null
+++ b/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts
@@ -0,0 +1,84 @@
+import path from 'node:path'
+import {
+  E2E_TIMEOUT,
+  setupPuppeteer,
+} from '../../../packages/vue/__tests__/e2e/e2eUtils'
+import connect from 'connect'
+import sirv from 'sirv'
+
+describe('vdom / vapor interop', () => {
+  const { page, click, text, enterValue } = setupPuppeteer()
+
+  let server: any
+  const port = '8193'
+  beforeAll(() => {
+    server = connect()
+      .use(sirv(path.resolve(import.meta.dirname, '../dist')))
+      .listen(port)
+    process.on('SIGTERM', () => server && server.close())
+  })
+
+  afterAll(() => {
+    server.close()
+  })
+
+  test(
+    'should work',
+    async () => {
+      const baseUrl = `http://localhost:${port}/interop/`
+      await page().goto(baseUrl)
+
+      expect(await text('.vapor > h2')).toContain('Vapor component in VDOM')
+
+      expect(await text('.vapor-prop')).toContain('hello')
+
+      const t = await text('.vdom-slot-in-vapor-default')
+      expect(t).toContain('slot prop: slot prop')
+      expect(t).toContain('component prop: hello')
+
+      await click('.change-vdom-slot-in-vapor-prop')
+      expect(await text('.vdom-slot-in-vapor-default')).toContain(
+        'slot prop: changed',
+      )
+
+      expect(await text('.vdom-slot-in-vapor-test')).toContain('A test slot')
+
+      await click('.toggle-vdom-slot-in-vapor')
+      expect(await text('.vdom-slot-in-vapor-test')).toContain(
+        'fallback content',
+      )
+
+      await click('.toggle-vdom-slot-in-vapor')
+      expect(await text('.vdom-slot-in-vapor-test')).toContain('A test slot')
+
+      expect(await text('.vdom > h2')).toContain('VDOM component in Vapor')
+
+      expect(await text('.vdom-prop')).toContain('hello')
+
+      const tt = await text('.vapor-slot-in-vdom-default')
+      expect(tt).toContain('slot prop: slot prop')
+      expect(tt).toContain('component prop: hello')
+
+      await click('.change-vapor-slot-in-vdom-prop')
+      expect(await text('.vapor-slot-in-vdom-default')).toContain(
+        'slot prop: changed',
+      )
+
+      expect(await text('.vapor-slot-in-vdom-test')).toContain('fallback')
+
+      await click('.toggle-vapor-slot-in-vdom-default')
+      expect(await text('.vapor-slot-in-vdom-default')).toContain(
+        'default slot fallback',
+      )
+
+      await click('.toggle-vapor-slot-in-vdom-default')
+
+      await enterValue('input', 'bye')
+      expect(await text('.vapor-prop')).toContain('bye')
+      expect(await text('.vdom-slot-in-vapor-default')).toContain('bye')
+      expect(await text('.vdom-prop')).toContain('bye')
+      expect(await text('.vapor-slot-in-vdom-default')).toContain('bye')
+    },
+    E2E_TIMEOUT,
+  )
+})
diff --git a/packages-private/vapor-e2e-test/index.html b/packages-private/vapor-e2e-test/index.html
new file mode 100644
index 00000000000..7dc205e5ab0
--- /dev/null
+++ b/packages-private/vapor-e2e-test/index.html
@@ -0,0 +1,2 @@
+<a href="/interop/">VDOM / Vapor interop</a>
+<a href="/todomvc/">Vapor TodoMVC</a>
diff --git a/packages-private/vapor-e2e-test/interop/App.vue b/packages-private/vapor-e2e-test/interop/App.vue
new file mode 100644
index 00000000000..772a6989dd7
--- /dev/null
+++ b/packages-private/vapor-e2e-test/interop/App.vue
@@ -0,0 +1,22 @@
+<script setup lang="ts">
+import { ref } from 'vue'
+import VaporComp from './VaporComp.vue'
+
+const msg = ref('hello')
+const passSlot = ref(true)
+</script>
+
+<template>
+  <input v-model="msg" />
+  <button class="toggle-vdom-slot-in-vapor" @click="passSlot = !passSlot">
+    toggle #test slot
+  </button>
+  <VaporComp :msg="msg">
+    <template #default="{ foo }">
+      <div>slot prop: {{ foo }}</div>
+      <div>component prop: {{ msg }}</div>
+    </template>
+
+    <template #test v-if="passSlot">A test slot</template>
+  </VaporComp>
+</template>
diff --git a/packages-private/vapor-e2e-test/interop/VaporComp.vue b/packages-private/vapor-e2e-test/interop/VaporComp.vue
new file mode 100644
index 00000000000..88a60c782c0
--- /dev/null
+++ b/packages-private/vapor-e2e-test/interop/VaporComp.vue
@@ -0,0 +1,50 @@
+<script setup vapor lang="ts">
+import { ref } from 'vue'
+import VdomComp from './VdomComp.vue'
+
+defineProps<{
+  msg: string
+}>()
+
+const ok = ref(true)
+const passSlot = ref(true)
+const slotProp = ref('slot prop')
+</script>
+
+<template>
+  <div class="vapor" style="border: 2px solid red; padding: 10px">
+    <h2>This is a Vapor component in VDOM</h2>
+    <p class="vapor-prop">props.msg: {{ msg }}</p>
+
+    <button @click="ok = !ok">Toggle slots</button>
+
+    <div v-if="ok" style="border: 2px solid orange; padding: 10px">
+      <h3>vdom slots in vapor component</h3>
+      <button
+        class="change-vdom-slot-in-vapor-prop"
+        @click="slotProp = 'changed'"
+      >
+        change slot prop
+      </button>
+      <div class="vdom-slot-in-vapor-default">
+        #default: <slot :foo="slotProp" />
+      </div>
+      <div class="vdom-slot-in-vapor-test">
+        #test: <slot name="test">fallback content</slot>
+      </div>
+    </div>
+
+    <button
+      class="toggle-vapor-slot-in-vdom-default"
+      @click="passSlot = !passSlot"
+    >
+      Toggle default slot to vdom
+    </button>
+    <VdomComp :msg="msg">
+      <template #default="{ foo }" v-if="passSlot">
+        <div>slot prop: {{ foo }}</div>
+        <div>component prop: {{ msg }}</div>
+      </template>
+    </VdomComp>
+  </div>
+</template>
diff --git a/packages-private/vapor-e2e-test/interop/VdomComp.vue b/packages-private/vapor-e2e-test/interop/VdomComp.vue
new file mode 100644
index 00000000000..30ec1b2eeb5
--- /dev/null
+++ b/packages-private/vapor-e2e-test/interop/VdomComp.vue
@@ -0,0 +1,28 @@
+<script setup lang="ts">
+import { ref } from 'vue'
+
+defineProps<{
+  msg: string
+}>()
+
+const bar = ref('slot prop')
+</script>
+
+<template>
+  <div class="vdom" style="border: 2px solid blue; padding: 10px">
+    <h2>This is a VDOM component in Vapor</h2>
+    <p class="vdom-prop">props.msg: {{ msg }}</p>
+    <div style="border: 2px solid aquamarine; padding: 10px">
+      <h3>vapor slots in vdom</h3>
+      <button class="change-vapor-slot-in-vdom-prop" @click="bar = 'changed'">
+        Change slot prop
+      </button>
+      <div class="vapor-slot-in-vdom-default">
+        #default: <slot :foo="bar">default slot fallback</slot>
+      </div>
+      <div class="vapor-slot-in-vdom-test">
+        #test <slot name="test">fallback</slot>
+      </div>
+    </div>
+  </div>
+</template>
diff --git a/packages-private/vapor-e2e-test/interop/index.html b/packages-private/vapor-e2e-test/interop/index.html
new file mode 100644
index 00000000000..79052a023ba
--- /dev/null
+++ b/packages-private/vapor-e2e-test/interop/index.html
@@ -0,0 +1,2 @@
+<script type="module" src="./main.ts"></script>
+<div id="app"></div>
diff --git a/packages-private/vapor-e2e-test/interop/main.ts b/packages-private/vapor-e2e-test/interop/main.ts
new file mode 100644
index 00000000000..d5d6d7dcf8c
--- /dev/null
+++ b/packages-private/vapor-e2e-test/interop/main.ts
@@ -0,0 +1,4 @@
+import { createApp, vaporInteropPlugin } from 'vue'
+import App from './App.vue'
+
+createApp(App).use(vaporInteropPlugin).mount('#app')
diff --git a/packages-private/vapor-e2e-test/package.json b/packages-private/vapor-e2e-test/package.json
new file mode 100644
index 00000000000..66ea0457ec9
--- /dev/null
+++ b/packages-private/vapor-e2e-test/package.json
@@ -0,0 +1,18 @@
+{
+  "name": "vapor-e2e-test",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite dev",
+    "build": "vite build"
+  },
+  "devDependencies": {
+    "@types/connect": "^3.4.38",
+    "@vitejs/plugin-vue": "catalog:",
+    "connect": "^3.7.0",
+    "sirv": "^2.0.4",
+    "vite": "catalog:",
+    "vue": "workspace:*"
+  }
+}
diff --git a/packages-private/vapor-e2e-test/todomvc/App.vue b/packages-private/vapor-e2e-test/todomvc/App.vue
new file mode 100644
index 00000000000..910ada51093
--- /dev/null
+++ b/packages-private/vapor-e2e-test/todomvc/App.vue
@@ -0,0 +1,228 @@
+<script setup vapor>
+import {
+  reactive,
+  computed,
+  onMounted,
+  onUnmounted,
+  watchPostEffect,
+} from 'vue'
+
+const STORAGE_KEY = 'todos-vuejs-3.x'
+
+const todoStorage = {
+  fetch() {
+    const todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
+    todos.forEach((todo, index) => {
+      todo.id = index
+    })
+    todoStorage.uid = todos.length
+    return todos
+  },
+  save(todos) {
+    localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
+  },
+}
+
+const filters = {
+  all(todos) {
+    return todos
+  },
+  active(todos) {
+    return todos.filter(todo => {
+      return !todo.completed
+    })
+  },
+  completed(todos) {
+    return todos.filter(function (todo) {
+      return todo.completed
+    })
+  },
+}
+
+function pluralize(n) {
+  return n === 1 ? 'item' : 'items'
+}
+
+const state = reactive({
+  todos: todoStorage.fetch(),
+  editedTodo: null,
+  newTodo: '',
+  beforeEditCache: '',
+  visibility: 'all',
+  remaining: computed(() => {
+    return filters.active(state.todos).length
+  }),
+  remainingText: computed(() => {
+    return ` ${pluralize(state.remaining)} left`
+  }),
+  filteredTodos: computed(() => {
+    return filters[state.visibility](state.todos)
+  }),
+  allDone: computed({
+    get: function () {
+      return state.remaining === 0
+    },
+    set: function (value) {
+      state.todos.forEach(todo => {
+        todo.completed = value
+      })
+    },
+  }),
+})
+
+watchPostEffect(() => {
+  todoStorage.save(state.todos)
+})
+
+onMounted(() => {
+  window.addEventListener('hashchange', onHashChange)
+  onHashChange()
+})
+
+onUnmounted(() => {
+  window.removeEventListener('hashchange', onHashChange)
+})
+
+function onHashChange() {
+  const visibility = window.location.hash.replace(/#\/?/, '')
+  if (filters[visibility]) {
+    state.visibility = visibility
+  } else {
+    window.location.hash = ''
+    state.visibility = 'all'
+  }
+}
+
+function addTodo() {
+  const value = state.newTodo && state.newTodo.trim()
+  if (!value) {
+    return
+  }
+  state.todos.push({
+    id: todoStorage.uid++,
+    title: value,
+    completed: false,
+  })
+  state.newTodo = ''
+}
+
+function removeTodo(todo) {
+  state.todos.splice(state.todos.indexOf(todo), 1)
+}
+
+function editTodo(todo) {
+  state.beforeEditCache = todo.title
+  state.editedTodo = todo
+}
+
+function doneEdit(todo) {
+  if (!state.editedTodo) {
+    return
+  }
+  state.editedTodo = null
+  todo.title = todo.title.trim()
+  if (!todo.title) {
+    removeTodo(todo)
+  }
+}
+
+function cancelEdit(todo) {
+  state.editedTodo = null
+  todo.title = state.beforeEditCache
+}
+
+function removeCompleted() {
+  state.todos = filters.active(state.todos)
+}
+
+// vapor custom directive
+const vTodoFocus = (el, value) => {
+  watchPostEffect(() => value() && el.focus())
+}
+</script>
+
+<template>
+  <section class="todoapp">
+    <header class="header">
+      <h1>todos</h1>
+      <input
+        class="new-todo"
+        autofocus
+        autocomplete="off"
+        placeholder="What needs to be done?"
+        v-model="state.newTodo"
+        @keyup.enter="addTodo"
+      />
+    </header>
+    <section class="main" v-show="state.todos.length">
+      <input
+        id="toggle-all"
+        class="toggle-all"
+        type="checkbox"
+        v-model="state.allDone"
+      />
+      <label for="toggle-all">Mark all as complete</label>
+      <ul class="todo-list">
+        <li
+          v-for="todo in state.filteredTodos"
+          class="todo"
+          :key="todo.id"
+          :class="{
+            completed: todo.completed,
+            editing: todo === state.editedTodo,
+          }"
+        >
+          <div class="view">
+            <input class="toggle" type="checkbox" v-model="todo.completed" />
+            <label @dblclick="editTodo(todo)">{{ todo.title }}</label>
+            <button class="destroy" @click="removeTodo(todo)"></button>
+          </div>
+          <input
+            class="edit"
+            type="text"
+            v-model="todo.title"
+            v-todo-focus="todo === state.editedTodo"
+            @blur="doneEdit(todo)"
+            @keyup.enter="doneEdit(todo)"
+            @keyup.escape="cancelEdit(todo)"
+          />
+        </li>
+      </ul>
+    </section>
+    <footer class="footer" v-show="state.todos.length">
+      <span class="todo-count">
+        <strong>{{ state.remaining }}</strong>
+        <span>{{ state.remainingText }}</span>
+      </span>
+      <ul class="filters">
+        <li>
+          <a href="#/all" :class="{ selected: state.visibility === 'all' }"
+            >All</a
+          >
+        </li>
+        <li>
+          <a
+            href="#/active"
+            :class="{ selected: state.visibility === 'active' }"
+            >Active</a
+          >
+        </li>
+        <li>
+          <a
+            href="#/completed"
+            :class="{ selected: state.visibility === 'completed' }"
+            >Completed</a
+          >
+        </li>
+      </ul>
+
+      <button
+        class="clear-completed"
+        @click="removeCompleted"
+        v-show="state.todos.length > state.remaining"
+      >
+        Clear completed
+      </button>
+    </footer>
+  </section>
+</template>
diff --git a/packages-private/vapor-e2e-test/todomvc/index.html b/packages-private/vapor-e2e-test/todomvc/index.html
new file mode 100644
index 00000000000..79052a023ba
--- /dev/null
+++ b/packages-private/vapor-e2e-test/todomvc/index.html
@@ -0,0 +1,2 @@
+<script type="module" src="./main.ts"></script>
+<div id="app"></div>
diff --git a/packages-private/vapor-e2e-test/todomvc/main.ts b/packages-private/vapor-e2e-test/todomvc/main.ts
new file mode 100644
index 00000000000..42497ab518d
--- /dev/null
+++ b/packages-private/vapor-e2e-test/todomvc/main.ts
@@ -0,0 +1,5 @@
+import { createVaporApp } from 'vue'
+import App from './App.vue'
+import 'todomvc-app-css/index.css'
+
+createVaporApp(App).mount('#app')
diff --git a/packages-private/vapor-e2e-test/vite.config.ts b/packages-private/vapor-e2e-test/vite.config.ts
new file mode 100644
index 00000000000..1e29a4dbd13
--- /dev/null
+++ b/packages-private/vapor-e2e-test/vite.config.ts
@@ -0,0 +1,20 @@
+import { defineConfig } from 'vite'
+import Vue from '@vitejs/plugin-vue'
+import * as CompilerSFC from 'vue/compiler-sfc'
+import { resolve } from 'node:path'
+
+export default defineConfig({
+  plugins: [
+    Vue({
+      compiler: CompilerSFC,
+    }),
+  ],
+  build: {
+    rollupOptions: {
+      input: {
+        interop: resolve(import.meta.dirname, 'interop/index.html'),
+        todomvc: resolve(import.meta.dirname, 'todomvc/index.html'),
+      },
+    },
+  },
+})
diff --git a/packages/compiler-core/package.json b/packages/compiler-core/package.json
index a3c188a6699..e76d747fd61 100644
--- a/packages/compiler-core/package.json
+++ b/packages/compiler-core/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/compiler-core",
-  "version": "3.5.17",
+  "version": "3.6.0-alpha.2",
   "description": "@vue/compiler-core",
   "main": "index.js",
   "module": "dist/compiler-core.esm-bundler.js",
diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts
index 2d6df9d9010..bae13372a98 100644
--- a/packages/compiler-core/src/ast.ts
+++ b/packages/compiler-core/src/ast.ts
@@ -86,6 +86,13 @@ export interface Position {
   column: number
 }
 
+export type AllNode =
+  | ParentNode
+  | ExpressionNode
+  | TemplateChildNode
+  | AttributeNode
+  | DirectiveNode
+
 export type ParentNode = RootNode | ElementNode | IfBranchNode | ForNode
 
 export type ExpressionNode = SimpleExpressionNode | CompoundExpressionNode
diff --git a/packages/compiler-core/src/babelUtils.ts b/packages/compiler-core/src/babelUtils.ts
index 52fabeea896..6ede6bd0386 100644
--- a/packages/compiler-core/src/babelUtils.ts
+++ b/packages/compiler-core/src/babelUtils.ts
@@ -12,6 +12,7 @@ import type {
   Program,
 } from '@babel/types'
 import { walk } from 'estree-walker'
+import { type BindingMetadata, BindingTypes } from './options'
 
 /**
  * Return value indicates whether the AST walked can be a constant
@@ -308,8 +309,8 @@ export const isFunctionType = (node: Node): node is Function => {
   return /Function(?:Expression|Declaration)$|Method$/.test(node.type)
 }
 
-export const isStaticProperty = (node: Node): node is ObjectProperty =>
-  node &&
+export const isStaticProperty = (node?: Node): node is ObjectProperty =>
+  !!node &&
   (node.type === 'ObjectProperty' || node.type === 'ObjectMethod') &&
   !node.computed
 
@@ -510,3 +511,77 @@ export function unwrapTSNode(node: Node): Node {
     return node
   }
 }
+
+export function isStaticNode(node: Node): boolean {
+  node = unwrapTSNode(node)
+
+  switch (node.type) {
+    case 'UnaryExpression': // void 0, !true
+      return isStaticNode(node.argument)
+
+    case 'LogicalExpression': // 1 > 2
+    case 'BinaryExpression': // 1 + 2
+      return isStaticNode(node.left) && isStaticNode(node.right)
+
+    case 'ConditionalExpression': {
+      // 1 ? 2 : 3
+      return (
+        isStaticNode(node.test) &&
+        isStaticNode(node.consequent) &&
+        isStaticNode(node.alternate)
+      )
+    }
+
+    case 'SequenceExpression': // (1, 2)
+    case 'TemplateLiteral': // `foo${1}`
+      return node.expressions.every(expr => isStaticNode(expr))
+
+    case 'ParenthesizedExpression': // (1)
+      return isStaticNode(node.expression)
+
+    case 'StringLiteral':
+    case 'NumericLiteral':
+    case 'BooleanLiteral':
+    case 'NullLiteral':
+    case 'BigIntLiteral':
+      return true
+  }
+  return false
+}
+
+export function isConstantNode(node: Node, bindings: BindingMetadata): boolean {
+  if (isStaticNode(node)) return true
+
+  node = unwrapTSNode(node)
+  switch (node.type) {
+    case 'Identifier':
+      const type = bindings[node.name]
+      return type === BindingTypes.LITERAL_CONST
+    case 'RegExpLiteral':
+      return true
+    case 'ObjectExpression':
+      return node.properties.every(prop => {
+        // { bar() {} } object methods are not considered static nodes
+        if (prop.type === 'ObjectMethod') return false
+        // { ...{ foo: 1 } }
+        if (prop.type === 'SpreadElement')
+          return isConstantNode(prop.argument, bindings)
+        // { foo: 1 }
+        return (
+          (!prop.computed || isConstantNode(prop.key, bindings)) &&
+          isConstantNode(prop.value, bindings)
+        )
+      })
+    case 'ArrayExpression':
+      return node.elements.every(element => {
+        // [1, , 3]
+        if (element === null) return true
+        // [1, ...[2, 3]]
+        if (element.type === 'SpreadElement')
+          return isConstantNode(element.argument, bindings)
+        // [1, 2]
+        return isConstantNode(element, bindings)
+      })
+  }
+  return false
+}
diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts
index 6b4559fabb2..99020bcf1ae 100644
--- a/packages/compiler-core/src/codegen.ts
+++ b/packages/compiler-core/src/codegen.ts
@@ -105,22 +105,38 @@ const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
 
 type CodegenNode = TemplateChildNode | JSChildNode | SSRCodegenNode
 
-export interface CodegenResult {
+export interface BaseCodegenResult {
   code: string
   preamble: string
-  ast: RootNode
+  ast: unknown
   map?: RawSourceMap
+  helpers?: Set<string> | Set<symbol>
+}
+
+export interface CodegenResult extends BaseCodegenResult {
+  ast: RootNode
+  helpers: Set<symbol>
 }
 
-enum NewlineType {
+export enum NewlineType {
+  /** Start with `\n` */
   Start = 0,
+  /** Ends with `\n` */
   End = -1,
+  /** No `\n` included */
   None = -2,
+  /** Don't know, calc it */
   Unknown = -3,
 }
 
 export interface CodegenContext
-  extends Omit<Required<CodegenOptions>, 'bindingMetadata' | 'inline'> {
+  extends Omit<
+    Required<CodegenOptions>,
+    | 'bindingMetadata'
+    | 'inline'
+    | 'vaporRuntimeModuleName'
+    | 'expressionPlugins'
+  > {
   source: string
   code: string
   line: number
@@ -400,6 +416,7 @@ export function generate(
     code: context.code,
     preamble: isSetupInlined ? preambleContext.code : ``,
     map: context.map ? context.map.toJSON() : undefined,
+    helpers: ast.helpers,
   }
 }
 
diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts
index 29e5f681300..e54b0c3a498 100644
--- a/packages/compiler-core/src/index.ts
+++ b/packages/compiler-core/src/index.ts
@@ -17,21 +17,26 @@ export {
   createTransformContext,
   traverseNode,
   createStructuralDirectiveTransform,
+  getSelfName,
   type NodeTransform,
   type StructuralDirectiveTransform,
   type DirectiveTransform,
 } from './transform'
 export {
   generate,
+  NewlineType,
   type CodegenContext,
   type CodegenResult,
   type CodegenSourceMapGenerator,
   type RawSourceMap,
+  type BaseCodegenResult,
 } from './codegen'
 export {
   ErrorCodes,
   errorMessages,
   createCompilerError,
+  defaultOnError,
+  defaultOnWarn,
   type CoreCompilerError,
   type CompilerError,
 } from './errors'
@@ -52,6 +57,7 @@ export {
   transformExpression,
   processExpression,
   stringifyExpression,
+  isLiteralWhitelisted,
 } from './transforms/transformExpression'
 export {
   buildSlots,
@@ -75,4 +81,5 @@ export {
   checkCompatEnabled,
   warnDeprecation,
   CompilerDeprecationTypes,
+  type CompilerCompatOptions,
 } from './compat/compatConfig'
diff --git a/packages/compiler-core/src/options.ts b/packages/compiler-core/src/options.ts
index 1de865f42eb..9983071609e 100644
--- a/packages/compiler-core/src/options.ts
+++ b/packages/compiler-core/src/options.ts
@@ -174,6 +174,12 @@ interface SharedTransformCodegenOptions {
    * @default mode === 'module'
    */
   prefixIdentifiers?: boolean
+  /**
+   * A list of parser plugins to enable for `@babel/parser`, which is used to
+   * parse expressions in bindings and interpolations.
+   * https://babeljs.io/docs/en/next/babel-parser#plugins
+   */
+  expressionPlugins?: ParserPlugin[]
   /**
    * Control whether generate SSR-optimized render functions instead.
    * The resulting function must be attached to the component via the
@@ -272,12 +278,6 @@ export interface TransformOptions
    * @default false
    */
   cacheHandlers?: boolean
-  /**
-   * A list of parser plugins to enable for `@babel/parser`, which is used to
-   * parse expressions in bindings and interpolations.
-   * https://babeljs.io/docs/en/next/babel-parser#plugins
-   */
-  expressionPlugins?: ParserPlugin[]
   /**
    * SFC scoped styles ID
    */
diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts
index 9d8fd842935..10121fb5d5c 100644
--- a/packages/compiler-core/src/transform.ts
+++ b/packages/compiler-core/src/transform.ts
@@ -123,6 +123,11 @@ export interface TransformContext
   filters?: Set<string>
 }
 
+export function getSelfName(filename: string): string | null {
+  const nameMatch = filename.replace(/\?.*$/, '').match(/([^/\\]+)\.\w+$/)
+  return nameMatch ? capitalize(camelize(nameMatch[1])) : null
+}
+
 export function createTransformContext(
   root: RootNode,
   {
@@ -150,11 +155,10 @@ export function createTransformContext(
     compatConfig,
   }: TransformOptions,
 ): TransformContext {
-  const nameMatch = filename.replace(/\?.*$/, '').match(/([^/\\]+)\.\w+$/)
   const context: TransformContext = {
     // options
     filename,
-    selfName: nameMatch && capitalize(camelize(nameMatch[1])),
+    selfName: getSelfName(filename),
     prefixIdentifiers,
     hoistStatic,
     hmr,
diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts
index 9ae8897e674..9012c2701f7 100644
--- a/packages/compiler-core/src/transforms/transformExpression.ts
+++ b/packages/compiler-core/src/transforms/transformExpression.ts
@@ -44,7 +44,8 @@ import { parseExpression } from '@babel/parser'
 import { IS_REF, UNREF } from '../runtimeHelpers'
 import { BindingTypes } from '../options'
 
-const isLiteralWhitelisted = /*@__PURE__*/ makeMap('true,false,null,this')
+export const isLiteralWhitelisted: (key: string) => boolean =
+  /*@__PURE__*/ makeMap('true,false,null,this')
 
 export const transformExpression: NodeTransform = (node, context) => {
   if (node.type === NodeTypes.INTERPOLATION) {
diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts
index b49d70bb2fb..b90a7018c8b 100644
--- a/packages/compiler-core/src/utils.ts
+++ b/packages/compiler-core/src/utils.ts
@@ -160,7 +160,7 @@ export const isMemberExpressionBrowser = (exp: ExpressionNode): boolean => {
 
 export const isMemberExpressionNode: (
   exp: ExpressionNode,
-  context: TransformContext,
+  context: Pick<TransformContext, 'expressionPlugins'>,
 ) => boolean = __BROWSER__
   ? (NOOP as any)
   : (exp, context) => {
@@ -185,7 +185,7 @@ export const isMemberExpressionNode: (
 
 export const isMemberExpression: (
   exp: ExpressionNode,
-  context: TransformContext,
+  context: Pick<TransformContext, 'expressionPlugins'>,
 ) => boolean = __BROWSER__ ? isMemberExpressionBrowser : isMemberExpressionNode
 
 const fnExpRE =
@@ -196,7 +196,7 @@ export const isFnExpressionBrowser: (exp: ExpressionNode) => boolean = exp =>
 
 export const isFnExpressionNode: (
   exp: ExpressionNode,
-  context: TransformContext,
+  context: Pick<TransformContext, 'expressionPlugins'>,
 ) => boolean = __BROWSER__
   ? (NOOP as any)
   : (exp, context) => {
@@ -227,7 +227,7 @@ export const isFnExpressionNode: (
 
 export const isFnExpression: (
   exp: ExpressionNode,
-  context: TransformContext,
+  context: Pick<TransformContext, 'expressionPlugins'>,
 ) => boolean = __BROWSER__ ? isFnExpressionBrowser : isFnExpressionNode
 
 export function advancePositionWithClone(
@@ -279,6 +279,7 @@ export function assert(condition: boolean, msg?: string): void {
   }
 }
 
+/** find directive */
 export function findDir(
   node: ElementNode,
   name: string | RegExp,
diff --git a/packages/compiler-dom/package.json b/packages/compiler-dom/package.json
index 95a56f045d0..3224f5bdbcd 100644
--- a/packages/compiler-dom/package.json
+++ b/packages/compiler-dom/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/compiler-dom",
-  "version": "3.5.17",
+  "version": "3.6.0-alpha.2",
   "description": "@vue/compiler-dom",
   "main": "index.js",
   "module": "dist/compiler-dom.esm-bundler.js",
diff --git a/packages/compiler-dom/src/errors.ts b/packages/compiler-dom/src/errors.ts
index b47624840ab..15641e531af 100644
--- a/packages/compiler-dom/src/errors.ts
+++ b/packages/compiler-dom/src/errors.ts
@@ -48,7 +48,7 @@ if (__TEST__) {
   }
 }
 
-export const DOMErrorMessages: { [code: number]: string } = {
+export const DOMErrorMessages: Record<DOMErrorCodes, string> = {
   [DOMErrorCodes.X_V_HTML_NO_EXPRESSION]: `v-html is missing expression.`,
   [DOMErrorCodes.X_V_HTML_WITH_CHILDREN]: `v-html will override element children.`,
   [DOMErrorCodes.X_V_TEXT_NO_EXPRESSION]: `v-text is missing expression.`,
@@ -60,4 +60,7 @@ export const DOMErrorMessages: { [code: number]: string } = {
   [DOMErrorCodes.X_V_SHOW_NO_EXPRESSION]: `v-show is missing expression.`,
   [DOMErrorCodes.X_TRANSITION_INVALID_CHILDREN]: `<Transition> expects exactly one child element or component.`,
   [DOMErrorCodes.X_IGNORED_SIDE_EFFECT_TAG]: `Tags with side effect (<script> and <style>) are ignored in client component templates.`,
+
+  // just to fulfill types
+  [DOMErrorCodes.__EXTEND_POINT__]: ``,
 }
diff --git a/packages/compiler-dom/src/index.ts b/packages/compiler-dom/src/index.ts
index 809f3708023..950901e7bf9 100644
--- a/packages/compiler-dom/src/index.ts
+++ b/packages/compiler-dom/src/index.ts
@@ -74,4 +74,6 @@ export {
   DOMErrorCodes,
   DOMErrorMessages,
 } from './errors'
+export { resolveModifiers } from './transforms/vOn'
+export { isValidHTMLNesting } from './htmlNesting'
 export * from '@vue/compiler-core'
diff --git a/packages/compiler-dom/src/transforms/vOn.ts b/packages/compiler-dom/src/transforms/vOn.ts
index 1bb5763188b..723229cf34b 100644
--- a/packages/compiler-dom/src/transforms/vOn.ts
+++ b/packages/compiler-dom/src/transforms/vOn.ts
@@ -15,7 +15,7 @@ import {
   isStaticExp,
 } from '@vue/compiler-core'
 import { V_ON_WITH_KEYS, V_ON_WITH_MODIFIERS } from '../runtimeHelpers'
-import { capitalize, makeMap } from '@vue/shared'
+import { capitalize, isString, makeMap } from '@vue/shared'
 
 const isEventOptionModifier = /*@__PURE__*/ makeMap(`passive,once,capture`)
 const isNonKeyModifier = /*@__PURE__*/ makeMap(
@@ -30,12 +30,16 @@ const isNonKeyModifier = /*@__PURE__*/ makeMap(
 const maybeKeyModifier = /*@__PURE__*/ makeMap('left,right')
 const isKeyboardEvent = /*@__PURE__*/ makeMap(`onkeyup,onkeydown,onkeypress`)
 
-const resolveModifiers = (
-  key: ExpressionNode,
+export const resolveModifiers = (
+  key: ExpressionNode | string,
   modifiers: SimpleExpressionNode[],
-  context: TransformContext,
+  context: TransformContext | null,
   loc: SourceLocation,
-) => {
+): {
+  keyModifiers: string[]
+  nonKeyModifiers: string[]
+  eventOptionModifiers: string[]
+} => {
   const keyModifiers = []
   const nonKeyModifiers = []
   const eventOptionModifiers = []
@@ -46,6 +50,7 @@ const resolveModifiers = (
     if (
       __COMPAT__ &&
       modifier === 'native' &&
+      context &&
       checkCompatEnabled(
         CompilerDeprecationTypes.COMPILER_V_ON_NATIVE,
         context,
@@ -58,12 +63,16 @@ const resolveModifiers = (
       // e.g. .passive & .capture
       eventOptionModifiers.push(modifier)
     } else {
+      const keyString = isString(key)
+        ? key
+        : isStaticExp(key)
+          ? key.content
+          : null
+
       // runtimeModifiers: modifiers that needs runtime guards
       if (maybeKeyModifier(modifier)) {
-        if (isStaticExp(key)) {
-          if (
-            isKeyboardEvent((key as SimpleExpressionNode).content.toLowerCase())
-          ) {
+        if (keyString) {
+          if (isKeyboardEvent(keyString.toLowerCase())) {
             keyModifiers.push(modifier)
           } else {
             nonKeyModifiers.push(modifier)
diff --git a/packages/compiler-sfc/README.md b/packages/compiler-sfc/README.md
index 4f8ff3ac8b3..4d2ae6e9248 100644
--- a/packages/compiler-sfc/README.md
+++ b/packages/compiler-sfc/README.md
@@ -11,7 +11,6 @@ This package contains lower level utilities that you can use if you are writing
 The API is intentionally low-level due to the various considerations when integrating Vue SFCs in a build system:
 
 - Separate hot-module replacement (HMR) for script, template and styles
-
   - template updates should not reset component state
   - style updates should be performed without component re-render
 
diff --git a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/definePropsDestructure.spec.ts.snap b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/definePropsDestructure.spec.ts.snap
index 9306d31da97..fd34e7a32de 100644
--- a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/definePropsDestructure.spec.ts.snap
+++ b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/definePropsDestructure.spec.ts.snap
@@ -117,6 +117,8 @@ return () => {}
 
 exports[`sfc reactive props destructure > default values w/ type declaration & key is string 1`] = `
 "import { defineComponent as _defineComponent } from 'vue'
+import { toDisplayString as _toDisplayString } from "vue"
+
 
 export default /*@__PURE__*/_defineComponent({
   props: {
@@ -129,7 +131,9 @@ export default /*@__PURE__*/_defineComponent({
 
       
       
-return () => {}
+return (_ctx: any,_cache: any) => {
+  return _toDisplayString(__props.foo)
+}
 }
 
 })"
diff --git a/packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts b/packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts
index 25dd817bbe5..5709cfbe5bc 100644
--- a/packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts
+++ b/packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts
@@ -155,6 +155,7 @@ describe('sfc reactive props destructure', () => {
         "onUpdate:modelValue": (val: number) => void  // double-quoted string containing symbols
       }>()
       </script>
+      <template>{{ foo }}</template>
     `)
     expect(bindings).toStrictEqual({
       __propsAliases: {
@@ -173,6 +174,7 @@ describe('sfc reactive props destructure', () => {
     "foo:bar": { type: String, required: true, default: 'foo-bar' },
     "onUpdate:modelValue": { type: Function, required: true }
   },`)
+    expect(content).toMatch(`__props.foo`)
     assertCode(content)
   })
 
diff --git a/packages/compiler-sfc/__tests__/parse.spec.ts b/packages/compiler-sfc/__tests__/parse.spec.ts
index 265655e47ef..82b8cf98f11 100644
--- a/packages/compiler-sfc/__tests__/parse.spec.ts
+++ b/packages/compiler-sfc/__tests__/parse.spec.ts
@@ -381,6 +381,17 @@ h1 { color: red }
     })
   })
 
+  describe('vapor mode', () => {
+    test('on empty script', () => {
+      const { descriptor } = parse(`<script vapor></script>`)
+      expect(descriptor.vapor).toBe(true)
+    })
+    test('on template', () => {
+      const { descriptor } = parse(`<template vapor><div/></template>`)
+      expect(descriptor.vapor).toBe(true)
+    })
+  })
+
   describe('warnings', () => {
     function assertWarning(errors: Error[], msg: string) {
       expect(errors.some(e => e.message.match(msg))).toBe(true)
diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json
index 45468177a94..77d4e38b69e 100644
--- a/packages/compiler-sfc/package.json
+++ b/packages/compiler-sfc/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/compiler-sfc",
-  "version": "3.5.17",
+  "version": "3.6.0-alpha.2",
   "description": "@vue/compiler-sfc",
   "main": "dist/compiler-sfc.cjs.js",
   "module": "dist/compiler-sfc.esm-browser.js",
@@ -46,6 +46,7 @@
     "@vue/compiler-core": "workspace:*",
     "@vue/compiler-dom": "workspace:*",
     "@vue/compiler-ssr": "workspace:*",
+    "@vue/compiler-vapor": "workspace:*",
     "@vue/shared": "workspace:*",
     "estree-walker": "catalog:",
     "magic-string": "catalog:",
diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts
index 2e1e0ec34de..54ca260bdd6 100644
--- a/packages/compiler-sfc/src/compileScript.ts
+++ b/packages/compiler-sfc/src/compileScript.ts
@@ -2,6 +2,7 @@ import {
   BindingTypes,
   UNREF,
   isFunctionType,
+  isStaticNode,
   unwrapTSNode,
   walkIdentifiers,
 } from '@vue/compiler-dom'
@@ -18,6 +19,7 @@ import type {
   Declaration,
   ExportSpecifier,
   Identifier,
+  LVal,
   Node,
   ObjectPattern,
   Statement,
@@ -129,6 +131,10 @@ export interface SFCScriptCompileOptions {
    * Transform Vue SFCs into custom elements.
    */
   customElement?: boolean | ((filename: string) => boolean)
+  /**
+   * Force to use of Vapor mode.
+   */
+  vapor?: boolean
 }
 
 export interface ImportBinding {
@@ -173,6 +179,8 @@ export function compileScript(
   const scopeId = options.id ? options.id.replace(/^data-v-/, '') : ''
   const scriptLang = script && script.lang
   const scriptSetupLang = scriptSetup && scriptSetup.lang
+  const vapor = sfc.vapor || options.vapor
+  const ssr = options.templateOptions?.ssr
 
   if (!scriptSetup) {
     if (!script) {
@@ -540,7 +548,7 @@ export function compileScript(
           }
 
           // defineProps
-          const isDefineProps = processDefineProps(ctx, init, decl.id)
+          const isDefineProps = processDefineProps(ctx, init, decl.id as LVal)
           if (ctx.propsDestructureRestId) {
             setupBindings[ctx.propsDestructureRestId] =
               BindingTypes.SETUP_REACTIVE_CONST
@@ -548,10 +556,10 @@ export function compileScript(
 
           // defineEmits
           const isDefineEmits =
-            !isDefineProps && processDefineEmits(ctx, init, decl.id)
+            !isDefineProps && processDefineEmits(ctx, init, decl.id as LVal)
           !isDefineEmits &&
-            (processDefineSlots(ctx, init, decl.id) ||
-              processDefineModel(ctx, init, decl.id))
+            (processDefineSlots(ctx, init, decl.id as LVal) ||
+              processDefineModel(ctx, init, decl.id as LVal))
 
           if (
             isDefineProps &&
@@ -747,7 +755,7 @@ export function compileScript(
   if (
     sfc.cssVars.length &&
     // no need to do this when targeting SSR
-    !options.templateOptions?.ssr
+    !ssr
   ) {
     ctx.helperImports.add(CSS_VARS_HELPER)
     ctx.helperImports.add('unref')
@@ -858,12 +866,12 @@ export function compileScript(
   } else {
     // inline mode
     if (sfc.template && !sfc.template.src) {
-      if (options.templateOptions && options.templateOptions.ssr) {
+      if (ssr) {
         hasInlinedSsrRenderFn = true
       }
       // inline render function mode - we are going to compile the template and
       // inline it right here
-      const { code, ast, preamble, tips, errors, map } = compileTemplate({
+      const { code, preamble, tips, errors, helpers, map } = compileTemplate({
         filename,
         ast: sfc.template.ast,
         source: sfc.template.content,
@@ -873,6 +881,7 @@ export function compileScript(
         scoped: sfc.styles.some(s => s.scoped),
         isProd: options.isProd,
         ssrCssVars: sfc.cssVars,
+        vapor,
         compilerOptions: {
           ...(options.templateOptions &&
             options.templateOptions.compilerOptions),
@@ -909,7 +918,7 @@ export function compileScript(
       // avoid duplicated unref import
       // as this may get injected by the render function preamble OR the
       // css vars codegen
-      if (ast && ast.helpers.has(UNREF)) {
+      if (helpers && helpers.has(UNREF)) {
         ctx.helperImports.delete('unref')
       }
       returned = code
@@ -929,7 +938,11 @@ export function compileScript(
         `\n}\n\n`,
     )
   } else {
-    ctx.s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`)
+    ctx.s.appendRight(
+      endOffset,
+      // vapor mode generates its own return when inlined
+      `\n${vapor && !ssr ? `` : `return `}${returned}\n}\n\n`,
+    )
   }
 
   // 10. finalize default export
@@ -978,13 +991,17 @@ export function compileScript(
     ctx.s.prependLeft(
       startOffset,
       `\n${genDefaultAs} /*@__PURE__*/${ctx.helper(
-        `defineComponent`,
+        vapor && !ssr ? `defineVaporComponent` : `defineComponent`,
       )}({${def}${runtimeOptions}\n  ${
         hasAwait ? `async ` : ``
       }setup(${args}) {\n${exposeCall}`,
     )
     ctx.s.appendRight(endOffset, `})`)
   } else {
+    // in TS, defineVaporComponent adds the option already
+    if (vapor) {
+      runtimeOptions += `\n  __vapor: true,`
+    }
     if (defaultExport || definedOptions) {
       // without TS, can't rely on rest spread, so we use Object.assign
       // export default Object.assign(__default__, { ... })
@@ -1263,43 +1280,6 @@ function canNeverBeRef(node: Node, userReactiveImport?: string): boolean {
   }
 }
 
-function isStaticNode(node: Node): boolean {
-  node = unwrapTSNode(node)
-
-  switch (node.type) {
-    case 'UnaryExpression': // void 0, !true
-      return isStaticNode(node.argument)
-
-    case 'LogicalExpression': // 1 > 2
-    case 'BinaryExpression': // 1 + 2
-      return isStaticNode(node.left) && isStaticNode(node.right)
-
-    case 'ConditionalExpression': {
-      // 1 ? 2 : 3
-      return (
-        isStaticNode(node.test) &&
-        isStaticNode(node.consequent) &&
-        isStaticNode(node.alternate)
-      )
-    }
-
-    case 'SequenceExpression': // (1, 2)
-    case 'TemplateLiteral': // `foo${1}`
-      return node.expressions.every(expr => isStaticNode(expr))
-
-    case 'ParenthesizedExpression': // (1)
-      return isStaticNode(node.expression)
-
-    case 'StringLiteral':
-    case 'NumericLiteral':
-    case 'BooleanLiteral':
-    case 'NullLiteral':
-    case 'BigIntLiteral':
-      return true
-  }
-  return false
-}
-
 export function mergeSourceMaps(
   scriptMap: RawSourceMap,
   templateMap: RawSourceMap,
diff --git a/packages/compiler-sfc/src/compileTemplate.ts b/packages/compiler-sfc/src/compileTemplate.ts
index b043cf813d7..29d1853d2d6 100644
--- a/packages/compiler-sfc/src/compileTemplate.ts
+++ b/packages/compiler-sfc/src/compileTemplate.ts
@@ -1,5 +1,5 @@
 import {
-  type CodegenResult,
+  type BaseCodegenResult,
   type CompilerError,
   type CompilerOptions,
   type ElementNode,
@@ -24,24 +24,29 @@ import {
 } from './template/transformSrcset'
 import { generateCodeFrame, isObject } from '@vue/shared'
 import * as CompilerDOM from '@vue/compiler-dom'
+import * as CompilerVapor from '@vue/compiler-vapor'
 import * as CompilerSSR from '@vue/compiler-ssr'
 import consolidate from '@vue/consolidate'
 import { warnOnce } from './warn'
 import { genCssVarsFromList } from './style/cssVars'
 
 export interface TemplateCompiler {
-  compile(source: string | RootNode, options: CompilerOptions): CodegenResult
+  compile(
+    source: string | RootNode,
+    options: CompilerOptions,
+  ): BaseCodegenResult
   parse(template: string, options: ParserOptions): RootNode
 }
 
 export interface SFCTemplateCompileResults {
   code: string
-  ast?: RootNode
+  ast?: unknown
   preamble?: string
   source: string
   tips: string[]
   errors: (string | CompilerError)[]
   map?: RawSourceMap
+  helpers?: Set<string | symbol>
 }
 
 export interface SFCTemplateCompileOptions {
@@ -52,6 +57,7 @@ export interface SFCTemplateCompileOptions {
   scoped?: boolean
   slotted?: boolean
   isProd?: boolean
+  vapor?: boolean
   ssr?: boolean
   ssrCssVars?: string[]
   inMap?: RawSourceMap
@@ -168,6 +174,7 @@ function doCompileTemplate({
   source,
   ast: inAST,
   ssr = false,
+  vapor = false,
   ssrCssVars,
   isProd = false,
   compiler,
@@ -202,7 +209,11 @@ function doCompileTemplate({
   const shortId = id.replace(/^data-v-/, '')
   const longId = `data-v-${shortId}`
 
-  const defaultCompiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM
+  const defaultCompiler = ssr
+    ? (CompilerSSR as TemplateCompiler)
+    : vapor
+      ? (CompilerVapor as TemplateCompiler)
+      : CompilerDOM
   compiler = compiler || defaultCompiler
 
   if (compiler !== defaultCompiler) {
@@ -227,25 +238,30 @@ function doCompileTemplate({
     inAST = createRoot(template.children, inAST.source)
   }
 
-  let { code, ast, preamble, map } = compiler.compile(inAST || source, {
-    mode: 'module',
-    prefixIdentifiers: true,
-    hoistStatic: true,
-    cacheHandlers: true,
-    ssrCssVars:
-      ssr && ssrCssVars && ssrCssVars.length
-        ? genCssVarsFromList(ssrCssVars, shortId, isProd, true)
-        : '',
-    scopeId: scoped ? longId : undefined,
-    slotted,
-    sourceMap: true,
-    ...compilerOptions,
-    hmr: !isProd,
-    nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
-    filename,
-    onError: e => errors.push(e),
-    onWarn: w => warnings.push(w),
-  })
+  let { code, ast, preamble, map, helpers } = compiler.compile(
+    inAST || source,
+    {
+      mode: 'module',
+      prefixIdentifiers: true,
+      hoistStatic: true,
+      cacheHandlers: true,
+      ssrCssVars:
+        ssr && ssrCssVars && ssrCssVars.length
+          ? genCssVarsFromList(ssrCssVars, shortId, isProd, true)
+          : '',
+      scopeId: scoped ? longId : undefined,
+      slotted,
+      sourceMap: true,
+      ...compilerOptions,
+      hmr: !isProd,
+      nodeTransforms: nodeTransforms.concat(
+        compilerOptions.nodeTransforms || [],
+      ),
+      filename,
+      onError: e => errors.push(e),
+      onWarn: w => warnings.push(w),
+    },
+  )
 
   // inMap should be the map produced by ./parse.ts which is a simple line-only
   // mapping. If it is present, we need to adjust the final map and errors to
@@ -271,7 +287,16 @@ function doCompileTemplate({
     return msg
   })
 
-  return { code, ast, preamble, source, errors, tips, map }
+  return {
+    code,
+    ast,
+    preamble,
+    source,
+    errors,
+    tips,
+    map,
+    helpers,
+  }
 }
 
 function mapLines(oldMap: RawSourceMap, newMap: RawSourceMap): RawSourceMap {
diff --git a/packages/compiler-sfc/src/parse.ts b/packages/compiler-sfc/src/parse.ts
index c8be865508f..01d0794e0d8 100644
--- a/packages/compiler-sfc/src/parse.ts
+++ b/packages/compiler-sfc/src/parse.ts
@@ -84,6 +84,8 @@ export interface SFCDescriptor {
    */
   slotted: boolean
 
+  vapor?: boolean
+
   /**
    * compare with an existing descriptor to determine whether HMR should perform
    * a reload vs. re-render.
@@ -137,6 +139,7 @@ export function parse(
     customBlocks: [],
     cssVars: [],
     slotted: false,
+    vapor: false,
     shouldForceReload: prevImports => hmrShouldReload(prevImports, descriptor),
   }
 
@@ -159,8 +162,9 @@ export function parse(
       ignoreEmpty &&
       node.tag !== 'template' &&
       isEmpty(node) &&
-      !hasSrc(node)
+      !hasAttr(node, 'src')
     ) {
+      descriptor.vapor ||= hasAttr(node, 'vapor')
       return
     }
     switch (node.tag) {
@@ -171,6 +175,7 @@ export function parse(
             source,
             false,
           ) as SFCTemplateBlock)
+          descriptor.vapor ||= !!templateBlock.attrs.vapor
 
           if (!templateBlock.attrs.src) {
             templateBlock.ast = createRoot(node.children, source)
@@ -195,7 +200,8 @@ export function parse(
         break
       case 'script':
         const scriptBlock = createBlock(node, source, pad) as SFCScriptBlock
-        const isSetup = !!scriptBlock.attrs.setup
+        descriptor.vapor ||= !!scriptBlock.attrs.vapor
+        const isSetup = !!(scriptBlock.attrs.setup || scriptBlock.attrs.vapor)
         if (isSetup && !descriptor.scriptSetup) {
           descriptor.scriptSetup = scriptBlock
           break
@@ -404,13 +410,8 @@ function padContent(
   }
 }
 
-function hasSrc(node: ElementNode) {
-  return node.props.some(p => {
-    if (p.type !== NodeTypes.ATTRIBUTE) {
-      return false
-    }
-    return p.name === 'src'
-  })
+function hasAttr(node: ElementNode, name: string) {
+  return node.props.some(p => p.type === NodeTypes.ATTRIBUTE && p.name === name)
 }
 
 /**
diff --git a/packages/compiler-sfc/src/script/defineProps.ts b/packages/compiler-sfc/src/script/defineProps.ts
index 9a4880a1a54..ac5226168e4 100644
--- a/packages/compiler-sfc/src/script/defineProps.ts
+++ b/packages/compiler-sfc/src/script/defineProps.ts
@@ -79,6 +79,15 @@ export function processDefineProps(
       )
     }
     ctx.propsTypeDecl = node.typeParameters.params[0]
+    // register bindings
+    const { props } = resolveTypeElements(ctx, ctx.propsTypeDecl)
+    if (props) {
+      for (const key in props) {
+        if (!(key in ctx.bindingMetadata)) {
+          ctx.bindingMetadata[key] = BindingTypes.PROPS
+        }
+      }
+    }
   }
 
   // handle props destructure
@@ -190,10 +199,6 @@ export function extractRuntimeProps(
 
   for (const prop of props) {
     propStrings.push(genRuntimePropFromType(ctx, prop, hasStaticDefaults))
-    // register bindings
-    if ('bindingMetadata' in ctx && !(prop.key in ctx.bindingMetadata)) {
-      ctx.bindingMetadata[prop.key] = BindingTypes.PROPS
-    }
   }
 
   let propsDecls = `{
diff --git a/packages/compiler-ssr/package.json b/packages/compiler-ssr/package.json
index 31681468f3b..38b08c4f6df 100644
--- a/packages/compiler-ssr/package.json
+++ b/packages/compiler-ssr/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/compiler-ssr",
-  "version": "3.5.17",
+  "version": "3.6.0-alpha.2",
   "description": "@vue/compiler-ssr",
   "main": "dist/compiler-ssr.cjs.js",
   "types": "dist/compiler-ssr.d.ts",
diff --git a/packages/compiler-vapor/LICENSE b/packages/compiler-vapor/LICENSE
new file mode 100644
index 00000000000..15f1f7e7a49
--- /dev/null
+++ b/packages/compiler-vapor/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2018-present, Yuxi (Evan) You
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/packages/compiler-vapor/README.md b/packages/compiler-vapor/README.md
new file mode 100644
index 00000000000..c2f11323d19
--- /dev/null
+++ b/packages/compiler-vapor/README.md
@@ -0,0 +1 @@
+# @vue/compiler-vapor
diff --git a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap
new file mode 100644
index 00000000000..b10a98d32cb
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap
@@ -0,0 +1,310 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compile > bindings 1`] = `
+"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div> </div>", true)
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = t0()
+  const x0 = _child(n0)
+  _renderEffect(() => _setText(x0, "count is " + _toDisplayString(_ctx.count) + "."))
+  return n0
+}"
+`;
+
+exports[`compile > custom directive > basic 1`] = `
+"import { resolveDirective as _resolveDirective, withVaporDirectives as _withVaporDirectives, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const _directive_test = _resolveDirective("test")
+  const _directive_hello = _resolveDirective("hello")
+  const n0 = t0()
+  _withVaporDirectives(n0, [[_directive_test], [_directive_hello, void 0, void 0, { world: true }]])
+  return n0
+}"
+`;
+
+exports[`compile > custom directive > component 1`] = `
+"import { resolveComponent as _resolveComponent, resolveDirective as _resolveDirective, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, withVaporDirectives as _withVaporDirectives, createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+
+export function render(_ctx) {
+  const _component_Bar = _resolveComponent("Bar")
+  const _component_Comp = _resolveComponent("Comp")
+  const _directive_hello = _resolveDirective("hello")
+  const _directive_test = _resolveDirective("test")
+  const n4 = _createComponentWithFallback(_component_Comp, null, {
+    "default": () => {
+      const n0 = _createIf(() => (true), () => {
+        const n3 = t0()
+        _setInsertionState(n3)
+        const n2 = _createComponentWithFallback(_component_Bar)
+        _withVaporDirectives(n2, [[_directive_hello, void 0, void 0, { world: true }]])
+        return n3
+      })
+      return n0
+    }
+  }, true)
+  _withVaporDirectives(n4, [[_directive_test]])
+  return n4
+}"
+`;
+
+exports[`compile > directives > custom directive > basic 1`] = `
+"import { withVaporDirectives as _withVaporDirectives, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = t0()
+  _withVaporDirectives(n0, [[_ctx.vExample]])
+  return n0
+}"
+`;
+
+exports[`compile > directives > custom directive > binding value 1`] = `
+"import { withVaporDirectives as _withVaporDirectives, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = t0()
+  _withVaporDirectives(n0, [[_ctx.vExample, () => _ctx.msg]])
+  return n0
+}"
+`;
+
+exports[`compile > directives > custom directive > dynamic parameters 1`] = `
+"import { withVaporDirectives as _withVaporDirectives, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = t0()
+  _withVaporDirectives(n0, [[_ctx.vExample, () => _ctx.msg, _ctx.foo]])
+  return n0
+}"
+`;
+
+exports[`compile > directives > custom directive > modifiers 1`] = `
+"import { withVaporDirectives as _withVaporDirectives, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = t0()
+  _withVaporDirectives(n0, [[_ctx.vExample, () => _ctx.msg, void 0, { bar: true }]])
+  return n0
+}"
+`;
+
+exports[`compile > directives > custom directive > modifiers w/o binding 1`] = `
+"import { withVaporDirectives as _withVaporDirectives, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = t0()
+  _withVaporDirectives(n0, [[_ctx.vExample, void 0, void 0, { "foo-bar": true }]])
+  return n0
+}"
+`;
+
+exports[`compile > directives > custom directive > static parameters 1`] = `
+"import { withVaporDirectives as _withVaporDirectives, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = t0()
+  _withVaporDirectives(n0, [[_ctx.vExample, () => _ctx.msg, "foo"]])
+  return n0
+}"
+`;
+
+exports[`compile > directives > custom directive > static parameters and modifiers 1`] = `
+"import { withVaporDirectives as _withVaporDirectives, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = t0()
+  _withVaporDirectives(n0, [[_ctx.vExample, () => _ctx.msg, "foo", { bar: true }]])
+  return n0
+}"
+`;
+
+exports[`compile > directives > v-cloak > basic 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<div>test</div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compile > directives > v-pre > basic 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<div :id=\\"foo\\"><Comp></Comp>{{ bar }}</div>", true)
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compile > directives > v-pre > should not affect siblings after it 1`] = `
+"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, child as _child, setProp as _setProp, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div :id=\\"foo\\"><Comp></Comp>{{ bar }}</div>")
+const t1 = _template("<div> </div>")
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n0 = t0()
+  const n3 = t1()
+  const n2 = _child(n3)
+  _setInsertionState(n3, 0)
+  const n1 = _createComponentWithFallback(_component_Comp)
+  _renderEffect(() => {
+    _setProp(n3, "id", _ctx.foo)
+    _setText(n2, _toDisplayString(_ctx.bar))
+  })
+  return [n0, n3]
+}"
+`;
+
+exports[`compile > dynamic root 1`] = `
+"import { toDisplayString as _toDisplayString, setText as _setText, template as _template } from 'vue';
+const t0 = _template(" ")
+
+export function render(_ctx) {
+  const n0 = t0()
+  _setText(n0, _toDisplayString(1) + _toDisplayString(2))
+  return n0
+}"
+`;
+
+exports[`compile > dynamic root nodes and interpolation 1`] = `
+"import { child as _child, setProp as _setProp, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<button> </button>", true)
+_delegateEvents("click")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const x0 = _child(n0)
+  n0.$evtclick = e => _ctx.handleClick(e)
+  _renderEffect(() => {
+    const _count = _ctx.count
+    _setProp(n0, "id", _count)
+    _setText(x0, _toDisplayString(_count) + "foo" + _toDisplayString(_count) + "foo" + _toDisplayString(_count))
+  })
+  return n0
+}"
+`;
+
+exports[`compile > execution order > basic 1`] = `
+"import { child as _child, setProp as _setProp, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div> </div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  const x0 = _child(n0)
+  _renderEffect(() => {
+    _setProp(n0, "id", _ctx.foo)
+    _setText(x0, _toDisplayString(_ctx.bar))
+  })
+  return n0
+}"
+`;
+
+exports[`compile > execution order > with v-once 1`] = `
+"import { child as _child, next as _next, nthChild as _nthChild, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div><span> </span> <br> </div>", true)
+
+export function render(_ctx) {
+  const n3 = t0()
+  const n0 = _child(n3)
+  const n1 = _next(n0)
+  const n2 = _nthChild(n3, 3)
+  const x0 = _child(n0)
+  _setText(x0, _toDisplayString(_ctx.foo))
+  _renderEffect(() => {
+    _setText(n1, " " + _toDisplayString(_ctx.bar))
+    _setText(n2, " " + _toDisplayString(_ctx.baz))
+  })
+  return n3
+}"
+`;
+
+exports[`compile > expression parsing > interpolation 1`] = `
+"
+  const n0 = t0()
+  _renderEffect(() => _setText(n0, _toDisplayString(a + b.value)))
+  return n0
+"
+`;
+
+exports[`compile > expression parsing > v-bind 1`] = `
+"
+  const n0 = t0()
+  _renderEffect(() => {
+    const _key = key.value
+    _setDynamicProps(n0, [{ [_key+1]: _unref(foo)[_key+1]() }], true)
+  })
+  return n0
+"
+`;
+
+exports[`compile > fragment 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<p></p>")
+const t1 = _template("<span></span>")
+const t2 = _template("<div></div>")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t1()
+  const n2 = t2()
+  return [n0, n1, n2]
+}"
+`;
+
+exports[`compile > setInsertionState > next, child and nthChild should be above the setInsertionState 1`] = `
+"import { resolveComponent as _resolveComponent, child as _child, next as _next, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, nthChild as _nthChild, createIf as _createIf, setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+const t1 = _template("<div><div></div><!><div></div><!><div><button></button></div></div>", true)
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n6 = t1()
+  const n5 = _next(_child(n6))
+  const n7 = _nthChild(n6, 3)
+  const p0 = _next(n7)
+  const n4 = _child(p0)
+  _setInsertionState(n6, n5)
+  const n0 = _createComponentWithFallback(_component_Comp)
+  _setInsertionState(n6, n7)
+  const n1 = _createIf(() => (true), () => {
+    const n3 = t0()
+    return n3
+  })
+  _renderEffect(() => _setProp(n4, "disabled", _ctx.foo))
+  return n6
+}"
+`;
+
+exports[`compile > static + dynamic root 1`] = `
+"import { toDisplayString as _toDisplayString, setText as _setText, template as _template } from 'vue';
+const t0 = _template(" ")
+
+export function render(_ctx) {
+  const n0 = t0()
+  _setText(n0, _toDisplayString(1) + _toDisplayString(2) + "3" + _toDisplayString(4) + _toDisplayString(5) + "6" + _toDisplayString(7) + _toDisplayString(8) + "9" + 'A' + 'B')
+  return n0
+}"
+`;
+
+exports[`compile > static template 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<div><p>hello</p><input><span></span></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
diff --git a/packages/compiler-vapor/__tests__/abbreviation.spec.ts b/packages/compiler-vapor/__tests__/abbreviation.spec.ts
new file mode 100644
index 00000000000..6159c6ca5fd
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/abbreviation.spec.ts
@@ -0,0 +1,44 @@
+/**
+ * @vitest-environment jsdom
+ */
+
+const parser: DOMParser = new DOMParser()
+
+function parseHTML(html: string): string {
+  return parser.parseFromString(html, 'text/html').body.innerHTML
+}
+
+function checkAbbr(
+  template: string,
+  abbrevation: string,
+  expected: string,
+): void {
+  // TODO do some optimzations to make sure template === abbrevation
+  expect(parseHTML(abbrevation)).toBe(expected)
+}
+
+test('template abbreviation', () => {
+  checkAbbr('<div>hello</div>', '<div>hello', '<div>hello</div>')
+  checkAbbr(
+    '<div><div>hello</div></div>',
+    '<div><div>hello',
+    '<div><div>hello</div></div>',
+  )
+  checkAbbr(
+    '<div><span>foo</span><span/></div>',
+    '<div><span>foo</span><span>',
+    '<div><span>foo</span><span></span></div>',
+  )
+  checkAbbr(
+    '<div><hr/><div/></div>',
+    '<div><hr><div>',
+    '<div><hr><div></div></div>',
+  )
+  checkAbbr(
+    '<div><div/><hr/></div>',
+    '<div><div></div><hr>',
+    '<div><div></div><hr></div>',
+  )
+
+  checkAbbr('<span/>hello', '<span></span>hello', '<span></span>hello')
+})
diff --git a/packages/compiler-vapor/__tests__/compile.spec.ts b/packages/compiler-vapor/__tests__/compile.spec.ts
new file mode 100644
index 00000000000..178021d13dd
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/compile.spec.ts
@@ -0,0 +1,265 @@
+import { BindingTypes, type RootNode } from '@vue/compiler-dom'
+import { type CompilerOptions, compile as _compile } from '../src'
+
+// TODO This is a temporary test case for initial implementation.
+// Remove it once we have more comprehensive tests.
+// DO NOT ADD MORE TESTS HERE.
+
+function compile(template: string | RootNode, options: CompilerOptions = {}) {
+  let { code } = _compile(template, {
+    ...options,
+    mode: 'module',
+    prefixIdentifiers: true,
+  })
+  return code
+}
+
+describe('compile', () => {
+  test('static template', () => {
+    const code = compile(
+      `<div>
+        <p>hello</p>
+        <input />
+        <span />
+      </div>`,
+    )
+    expect(code).matchSnapshot()
+  })
+
+  test('dynamic root', () => {
+    const code = compile(`{{ 1 }}{{ 2 }}`)
+    expect(code).matchSnapshot()
+  })
+
+  test('dynamic root nodes and interpolation', () => {
+    const code = compile(
+      `<button @click="handleClick" :id="count">{{count}}foo{{count}}foo{{count}} </button>`,
+    )
+    expect(code).matchSnapshot()
+  })
+
+  test('static + dynamic root', () => {
+    const code = compile(
+      `{{ 1 }}{{ 2 }}3{{ 4 }}{{ 5 }}6{{ 7 }}{{ 8 }}9{{ 'A' }}{{ 'B' }}`,
+    )
+    expect(code).matchSnapshot()
+  })
+
+  test('fragment', () => {
+    const code = compile(`<p/><span/><div/>`)
+    expect(code).matchSnapshot()
+  })
+
+  test('bindings', () => {
+    const code = compile(`<div>count is {{ count }}.</div>`, {
+      bindingMetadata: {
+        count: BindingTypes.SETUP_REF,
+      },
+    })
+    expect(code).matchSnapshot()
+  })
+
+  describe('directives', () => {
+    describe('v-pre', () => {
+      test('basic', () => {
+        const code = compile(`<div v-pre :id="foo"><Comp/>{{ bar }}</div>\n`, {
+          bindingMetadata: {
+            foo: BindingTypes.SETUP_REF,
+            bar: BindingTypes.SETUP_REF,
+          },
+        })
+
+        expect(code).toMatchSnapshot()
+        expect(code).contains(
+          JSON.stringify('<div :id="foo"><Comp></Comp>{{ bar }}</div>'),
+        )
+        expect(code).not.contains('effect')
+      })
+
+      test('should not affect siblings after it', () => {
+        const code = compile(
+          `<div v-pre :id="foo"><Comp/>{{ bar }}</div>\n` +
+            `<div :id="foo"><Comp/>{{ bar }}</div>`,
+          {
+            bindingMetadata: {
+              foo: BindingTypes.SETUP_REF,
+              bar: BindingTypes.SETUP_REF,
+            },
+          },
+        )
+
+        expect(code).toMatchSnapshot()
+        // Waiting for TODO, There should be more here.
+      })
+    })
+
+    describe('v-cloak', () => {
+      test('basic', () => {
+        const code = compile(`<div v-cloak>test</div>`)
+        expect(code).toMatchSnapshot()
+        expect(code).not.contains('v-cloak')
+      })
+    })
+
+    describe('custom directive', () => {
+      test('basic', () => {
+        const code = compile(`<div v-example></div>`, {
+          bindingMetadata: {
+            vExample: BindingTypes.SETUP_CONST,
+          },
+        })
+        expect(code).matchSnapshot()
+      })
+
+      test('binding value', () => {
+        const code = compile(`<div v-example="msg"></div>`, {
+          bindingMetadata: {
+            msg: BindingTypes.SETUP_REF,
+            vExample: BindingTypes.SETUP_CONST,
+          },
+        })
+        expect(code).matchSnapshot()
+      })
+
+      test('static parameters', () => {
+        const code = compile(`<div v-example:foo="msg"></div>`, {
+          bindingMetadata: {
+            msg: BindingTypes.SETUP_REF,
+            vExample: BindingTypes.SETUP_CONST,
+          },
+        })
+        expect(code).matchSnapshot()
+      })
+
+      test('modifiers', () => {
+        const code = compile(`<div v-example.bar="msg"></div>`, {
+          bindingMetadata: {
+            msg: BindingTypes.SETUP_REF,
+            vExample: BindingTypes.SETUP_CONST,
+          },
+        })
+        expect(code).matchSnapshot()
+      })
+
+      test('modifiers w/o binding', () => {
+        const code = compile(`<div v-example.foo-bar></div>`, {
+          bindingMetadata: {
+            vExample: BindingTypes.SETUP_CONST,
+          },
+        })
+        expect(code).matchSnapshot()
+      })
+
+      test('static parameters and modifiers', () => {
+        const code = compile(`<div v-example:foo.bar="msg"></div>`, {
+          bindingMetadata: {
+            msg: BindingTypes.SETUP_REF,
+            vExample: BindingTypes.SETUP_CONST,
+          },
+        })
+        expect(code).matchSnapshot()
+      })
+
+      test('dynamic parameters', () => {
+        const code = compile(`<div v-example:[foo]="msg"></div>`, {
+          bindingMetadata: {
+            foo: BindingTypes.SETUP_REF,
+            vExample: BindingTypes.SETUP_CONST,
+          },
+        })
+        expect(code).matchSnapshot()
+      })
+    })
+  })
+
+  describe('expression parsing', () => {
+    test('interpolation', () => {
+      const code = compile(`{{ a + b }}`, {
+        inline: true,
+        bindingMetadata: {
+          b: BindingTypes.SETUP_REF,
+        },
+      })
+      expect(code).matchSnapshot()
+      expect(code).contains('a + b.value')
+    })
+
+    test('v-bind', () => {
+      const code = compile(`<div :[key+1]="foo[key+1]()" />`, {
+        inline: true,
+        bindingMetadata: {
+          key: BindingTypes.SETUP_REF,
+          foo: BindingTypes.SETUP_MAYBE_REF,
+        },
+      })
+      expect(code).matchSnapshot()
+      expect(code).contains('const _key = key.value')
+      expect(code).contains('_key+1')
+      expect(code).contains(
+        '_setDynamicProps(n0, [{ [_key+1]: _unref(foo)[_key+1]() }], true)',
+      )
+    })
+
+    // TODO: add more test for expression parsing (v-on, v-slot, v-for)
+  })
+
+  describe('custom directive', () => {
+    test('basic', () => {
+      const code = compile(`<div v-test v-hello.world />`)
+      expect(code).matchSnapshot()
+    })
+
+    test('component', () => {
+      const code = compile(`
+      <Comp v-test>
+        <div v-if="true">
+          <Bar v-hello.world />
+        </div>
+      </Comp>
+      `)
+      expect(code).matchSnapshot()
+    })
+  })
+
+  describe('setInsertionState', () => {
+    test('next, child and nthChild should be above the setInsertionState', () => {
+      const code = compile(`
+      <div>
+        <div />
+        <Comp />
+        <div />
+        <div v-if="true" />
+        <div>
+          <button :disabled="foo" />
+        </div>
+      </div>
+      `)
+      expect(code).toMatchSnapshot()
+    })
+  })
+
+  describe('execution order', () => {
+    test('basic', () => {
+      const code = compile(`<div :id="foo">{{ bar }}</div>`)
+      expect(code).matchSnapshot()
+      expect(code).contains(
+        `_setProp(n0, "id", _ctx.foo)
+    _setText(x0, _toDisplayString(_ctx.bar))`,
+      )
+    })
+    test('with v-once', () => {
+      const code = compile(
+        `<div>
+          <span v-once>{{ foo }}</span>
+          {{ bar }}<br>
+          {{ baz }}
+        </div>`,
+      )
+      expect(code).matchSnapshot()
+      expect(code).contains(
+        `_setText(n1, " " + _toDisplayString(_ctx.bar))
+    _setText(n2, " " + _toDisplayString(_ctx.baz))`,
+      )
+    })
+  })
+})
diff --git a/packages/compiler-vapor/__tests__/scopeId.spec.ts b/packages/compiler-vapor/__tests__/scopeId.spec.ts
new file mode 100644
index 00000000000..da17ef5552f
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/scopeId.spec.ts
@@ -0,0 +1,3 @@
+// import { compile } from '../src/compile'
+
+describe.todo('scopeId compiler support', () => {})
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/expression.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/expression.spec.ts.snap
new file mode 100644
index 00000000000..fda0121d632
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/expression.spec.ts.snap
@@ -0,0 +1,102 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler: expression > basic 1`] = `
+"import { toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template(" ")
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setText(n0, _toDisplayString(_ctx.a)))
+  return n0
+}"
+`;
+
+exports[`compiler: expression > empty interpolation 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template(" ")
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compiler: expression > empty interpolation 2`] = `
+"import { template as _template } from 'vue';
+const t0 = _template(" ")
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compiler: expression > empty interpolation 3`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compiler: expression > empty interpolation 4`] = `
+"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div> </div>", true)
+
+export function render(_ctx) {
+  const n1 = t0()
+  const n0 = _child(n1)
+  const x1 = _child(n1)
+  _renderEffect(() => {
+    const _foo = _ctx.foo
+    _setText(n0, _toDisplayString(_foo))
+    _setText(x1, _toDisplayString(_foo))
+  })
+  return n1
+}"
+`;
+
+exports[`compiler: expression > props 1`] = `
+"import { toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template(" ")
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = t0()
+  _renderEffect(() => _setText(n0, _toDisplayString($props.foo)))
+  return n0
+}"
+`;
+
+exports[`compiler: expression > props aliased 1`] = `
+"import { toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template(" ")
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = t0()
+  _renderEffect(() => _setText(n0, _toDisplayString($props['bar'])))
+  return n0
+}"
+`;
+
+exports[`compiler: expression > update expression 1`] = `
+"import { child as _child, setProp as _setProp, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div> </div>", true)
+
+export function render(_ctx) {
+  const n1 = t0()
+  const n0 = _child(n1)
+  const x1 = _child(n1)
+  _renderEffect(() => {
+    const _String = String
+    const _foo = _ctx.foo
+    _setProp(n1, "id", _String(_foo.id++))
+    _setProp(n1, "foo", _foo)
+    _setProp(n1, "bar", _ctx.bar++)
+    _setText(n0, _toDisplayString(_String(_foo.id++)) + " " + _toDisplayString(_foo) + " " + _toDisplayString(_ctx.bar))
+    _setText(x1, _toDisplayString(_String(_foo.id++)) + " " + _toDisplayString(_foo) + " " + _toDisplayString(_ctx.bar))
+  })
+  return n1
+}"
+`;
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformChildren.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformChildren.spec.ts.snap
new file mode 100644
index 00000000000..4a8caa65948
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformChildren.spec.ts.snap
@@ -0,0 +1,76 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler: children transform > anchor insertion in middle 1`] = `
+"import { child as _child, next as _next, setInsertionState as _setInsertionState, createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+const t1 = _template("<div><div></div><!><div></div></div>", true)
+
+export function render(_ctx) {
+  const n4 = t1()
+  const n3 = _next(_child(n4))
+  _setInsertionState(n4, n3)
+  const n0 = _createIf(() => (1), () => {
+    const n2 = t0()
+    return n2
+  }, null, true)
+  return n4
+}"
+`;
+
+exports[`compiler: children transform > children & sibling references 1`] = `
+"import { child as _child, next as _next, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div><p> </p> <p> </p></div>", true)
+
+export function render(_ctx) {
+  const n3 = t0()
+  const n0 = _child(n3)
+  const n1 = _next(n0)
+  const n2 = _next(n1)
+  const x0 = _child(n0)
+  const x2 = _child(n2)
+  _renderEffect(() => {
+    _setText(x0, _toDisplayString(_ctx.first))
+    _setText(n1, " " + _toDisplayString(_ctx.second) + " " + _toDisplayString(_ctx.third) + " ")
+    _setText(x2, _toDisplayString(_ctx.forth))
+  })
+  return n3
+}"
+`;
+
+exports[`compiler: children transform > efficient find 1`] = `
+"import { child as _child, nthChild as _nthChild, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div><div>x</div><div>x</div><div> </div></div>", true)
+
+export function render(_ctx) {
+  const n1 = t0()
+  const n0 = _nthChild(n1, 2)
+  const x0 = _child(n0)
+  _renderEffect(() => _setText(x0, _toDisplayString(_ctx.msg)))
+  return n1
+}"
+`;
+
+exports[`compiler: children transform > efficient traversal 1`] = `
+"import { child as _child, next as _next, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div><div>x</div><div><span> </span></div><div><span> </span></div><div><span> </span></div></div>", true)
+
+export function render(_ctx) {
+  const n3 = t0()
+  const p0 = _next(_child(n3))
+  const p1 = _next(p0)
+  const p2 = _next(p1)
+  const n0 = _child(p0)
+  const n1 = _child(p1)
+  const n2 = _child(p2)
+  const x0 = _child(n0)
+  const x1 = _child(n1)
+  const x2 = _child(n2)
+  _renderEffect(() => {
+    const _msg = _ctx.msg
+    _setText(x0, _toDisplayString(_msg))
+    _setText(x1, _toDisplayString(_msg))
+    _setText(x2, _toDisplayString(_msg))
+  })
+  return n3
+}"
+`;
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap
new file mode 100644
index 00000000000..7aa56aa9c2f
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap
@@ -0,0 +1,463 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler: element transform > component > cache v-on expression with unique handler name 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Foo = _resolveComponent("Foo")
+  const _component_Bar = _resolveComponent("Bar")
+  const _on_bar = $event => (_ctx.handleBar($event))
+  const n0 = _createComponentWithFallback(_component_Foo, { onBar: () => _on_bar })
+  const _on_bar1 = () => _ctx.handler
+  const n1 = _createComponentWithFallback(_component_Bar, { onBar: () => _on_bar1 })
+  return [n0, n1]
+}"
+`;
+
+exports[`compiler: element transform > component > do not resolve component from non-script-setup bindings 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const _component_Example = _resolveComponent("Example")
+  const n0 = _createComponentWithFallback(_component_Example, null, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > component > generate multi root component 1`] = `
+"import { createComponent as _createComponent, template as _template } from 'vue';
+const t0 = _template("123")
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = _createComponent(_ctx.Comp)
+  const n1 = t0()
+  return [n0, n1]
+}"
+`;
+
+exports[`compiler: element transform > component > generate single root component 1`] = `
+"import { createComponent as _createComponent } from 'vue';
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = _createComponent(_ctx.Comp, null, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > component > import + resolve component 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Foo = _resolveComponent("Foo")
+  const n0 = _createComponentWithFallback(_component_Foo, null, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > component > resolve component from setup bindings (inline const) 1`] = `
+"
+  const n0 = _createComponent(Example, null, null, true)
+  return n0
+"
+`;
+
+exports[`compiler: element transform > component > resolve component from setup bindings (inline) 1`] = `
+"
+  const n0 = _createComponent(_unref(Example), null, null, true)
+  return n0
+"
+`;
+
+exports[`compiler: element transform > component > resolve component from setup bindings 1`] = `
+"import { createComponent as _createComponent } from 'vue';
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = _createComponent(_ctx.Example, null, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > component > resolve implicitly self-referencing component 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Example__self = _resolveComponent("Example", true)
+  const n0 = _createComponentWithFallback(_component_Example__self, null, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > component > resolve namespaced component from props bindings (inline) 1`] = `
+"
+  const n0 = _createComponent(Foo.Example, null, null, true)
+  return n0
+"
+`;
+
+exports[`compiler: element transform > component > resolve namespaced component from props bindings (non-inline) 1`] = `
+"import { createComponent as _createComponent } from 'vue';
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = _createComponent(_ctx.Foo.Example, null, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > component > resolve namespaced component from setup bindings (inline const) 1`] = `
+"
+  const n0 = _createComponent(Foo.Example, null, null, true)
+  return n0
+"
+`;
+
+exports[`compiler: element transform > component > resolve namespaced component from setup bindings 1`] = `
+"import { createComponent as _createComponent } from 'vue';
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = _createComponent(_ctx.Foo.Example, null, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > component > static props 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Foo = _resolveComponent("Foo")
+  const n0 = _createComponentWithFallback(_component_Foo, {
+    id: () => ("foo"), 
+    class: () => ("bar")
+  }, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > component > v-bind="obj" 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Foo = _resolveComponent("Foo")
+  const n0 = _createComponentWithFallback(_component_Foo, { $: [
+    () => (_ctx.obj)
+  ] }, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > component > v-bind="obj" after static prop 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Foo = _resolveComponent("Foo")
+  const n0 = _createComponentWithFallback(_component_Foo, {
+    id: () => ("foo"), 
+    $: [
+      () => (_ctx.obj)
+    ]
+  }, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > component > v-bind="obj" before static prop 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Foo = _resolveComponent("Foo")
+  const n0 = _createComponentWithFallback(_component_Foo, { $: [
+    () => (_ctx.obj), 
+    { id: () => ("foo") }
+  ] }, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > component > v-bind="obj" between static props 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Foo = _resolveComponent("Foo")
+  const n0 = _createComponentWithFallback(_component_Foo, {
+    id: () => ("foo"), 
+    $: [
+      () => (_ctx.obj), 
+      { class: () => ("bar") }
+    ]
+  }, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > component > v-for on component should not mark as single root 1`] = `
+"import { createComponent as _createComponent, createFor as _createFor } from 'vue';
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = _createFor(() => (_ctx.items), (_for_item0) => {
+    const n2 = _createComponent(_ctx.Comp)
+    return n2
+  }, (item) => (item), 2)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > component > v-on expression is a function call 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Foo = _resolveComponent("Foo")
+  const _on_bar = $event => (_ctx.handleBar($event))
+  const n0 = _createComponentWithFallback(_component_Foo, { onBar: () => _on_bar }, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > component > v-on expression is inline statement 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Foo = _resolveComponent("Foo")
+  const _on_bar = () => _ctx.handler
+  const n0 = _createComponentWithFallback(_component_Foo, { onBar: () => _on_bar }, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > component > v-on="obj" 1`] = `
+"import { resolveComponent as _resolveComponent, toHandlers as _toHandlers, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Foo = _resolveComponent("Foo")
+  const n0 = _createComponentWithFallback(_component_Foo, { $: [
+    () => (_toHandlers(_ctx.obj))
+  ] }, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > component dynamic event with once modifier 1`] = `
+"import { resolveComponent as _resolveComponent, toHandlerKey as _toHandlerKey, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Foo = _resolveComponent("Foo")
+  const n0 = _createComponentWithFallback(_component_Foo, { $: [
+    () => ({ [_toHandlerKey(_ctx.foo) + "Once"]: () => _ctx.bar })
+  ] }, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > component event with once modifier 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Foo = _resolveComponent("Foo")
+  const n0 = _createComponentWithFallback(_component_Foo, { onFooOnce: () => _ctx.bar }, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > component with dynamic event arguments 1`] = `
+"import { resolveComponent as _resolveComponent, toHandlerKey as _toHandlerKey, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Foo = _resolveComponent("Foo")
+  const n0 = _createComponentWithFallback(_component_Foo, { $: [
+    () => ({ [_toHandlerKey(_ctx.foo-_ctx.bar)]: () => _ctx.bar }), 
+    () => ({ [_toHandlerKey(_ctx.baz)]: () => _ctx.qux })
+  ] }, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > component with dynamic prop arguments 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Foo = _resolveComponent("Foo")
+  const n0 = _createComponentWithFallback(_component_Foo, { $: [
+    () => ({ [_ctx.foo-_ctx.bar]: _ctx.bar }), 
+    () => ({ [_ctx.baz]: _ctx.qux })
+  ] }, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > dynamic component > capitalized version w/ static binding 1`] = `
+"import { resolveDynamicComponent as _resolveDynamicComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const n0 = _createComponentWithFallback(_resolveDynamicComponent("foo"), null, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > dynamic component > dynamic binding 1`] = `
+"import { createDynamicComponent as _createDynamicComponent } from 'vue';
+
+export function render(_ctx) {
+  const n0 = _createDynamicComponent(() => (_ctx.foo), null, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > dynamic component > dynamic binding shorthand 1`] = `
+"import { createDynamicComponent as _createDynamicComponent } from 'vue';
+
+export function render(_ctx) {
+  const n0 = _createDynamicComponent(() => (_ctx.is), null, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > dynamic component > normal component with is prop 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_custom_input = _resolveComponent("custom-input")
+  const n0 = _createComponentWithFallback(_component_custom_input, { is: () => ("foo") }, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > dynamic component > static binding 1`] = `
+"import { resolveDynamicComponent as _resolveDynamicComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const n0 = _createComponentWithFallback(_resolveDynamicComponent("foo"), null, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > empty template 1`] = `
+"
+export function render(_ctx) {
+  return null
+}"
+`;
+
+exports[`compiler: element transform > invalid html nesting 1`] = `
+"import { insert as _insert, template as _template } from 'vue';
+const t0 = _template("<div>123</div>")
+const t1 = _template("<p></p>")
+const t2 = _template("<form></form>")
+
+export function render(_ctx) {
+  const n1 = t1()
+  const n3 = t2()
+  const n0 = t0()
+  const n2 = t2()
+  _insert(n0, n1)
+  _insert(n2, n3)
+  return [n1, n3]
+}"
+`;
+
+exports[`compiler: element transform > props + children 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<div id=\\"foo\\"><span></span></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > props merging: class 1`] = `
+"import { setClass as _setClass, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setClass(n0, ["foo", { bar: _ctx.isBar }]))
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > props merging: event handlers 1`] = `
+"import { withKeys as _withKeys, delegate as _delegate, delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+_delegateEvents("click")
+
+export function render(_ctx) {
+  const n0 = t0()
+  _delegate(n0, "click", _withKeys(e => _ctx.a(e), ["foo"]))
+  _delegate(n0, "click", _withKeys(e => _ctx.b(e), ["bar"]))
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > props merging: style 1`] = `
+"import { setStyle as _setStyle, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _setStyle(n0, ["color: green", { color: 'red' }])
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > static props 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<div id=\\"foo\\" class=\\"bar\\"></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > v-bind="obj" 1`] = `
+"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setDynamicProps(n0, [_ctx.obj], true))
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > v-bind="obj" after static prop 1`] = `
+"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setDynamicProps(n0, [{ id: "foo" }, _ctx.obj], true))
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > v-bind="obj" before static prop 1`] = `
+"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setDynamicProps(n0, [_ctx.obj, { id: "foo" }], true))
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > v-bind="obj" between static props 1`] = `
+"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setDynamicProps(n0, [{ id: "foo" }, _ctx.obj, { class: "bar" }], true))
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > v-on="obj" 1`] = `
+"import { setDynamicEvents as _setDynamicEvents, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setDynamicEvents(n0, _ctx.obj))
+  return n0
+}"
+`;
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformRef.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformRef.spec.ts.snap
new file mode 100644
index 00000000000..a59b17d9429
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformRef.spec.ts.snap
@@ -0,0 +1,52 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler: template ref transform > dynamic ref 1`] = `
+"import { renderEffect as _renderEffect, setRef as _setRef, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+
+export function render(_ctx) {
+  const n0 = t0()
+  let r0
+  _renderEffect(() => r0 = _setRef(n0, _ctx.foo, r0))
+  return n0
+}"
+`
+
+exports[`compiler: template ref transform > ref + v-for 1`] = `
+"import { setRef as _setRef, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+
+export function render(_ctx) {
+  const n0 = _createFor(() => ([1,2,3]), (_block) => {
+    const n2 = t0()
+    _setRef(n2, "foo", void 0, true)
+    return [n2, () => {}]
+  })
+  return n0
+}"
+`
+
+exports[`compiler: template ref transform > ref + v-if 1`] = `
+"import { setRef as _setRef, createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+
+export function render(_ctx) {
+  const n0 = _createIf(() => (true), () => {
+    const n2 = t0()
+    _setRef(n2, "foo")
+    return n2
+  })
+  return n0
+}"
+`
+
+exports[`compiler: template ref transform > static ref 1`] = `
+"import { setRef as _setRef, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+
+export function render(_ctx) {
+  const n0 = t0()
+  _setRef(n0, "foo")
+  return n0
+}"
+`
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap
new file mode 100644
index 00000000000..6d62b9c4e35
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap
@@ -0,0 +1,163 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler: transform <slot> outlets > default slot outlet 1`] = `
+"import { createSlot as _createSlot } from 'vue';
+
+export function render(_ctx) {
+  const n0 = _createSlot("default", null)
+  return n0
+}"
+`;
+
+exports[`compiler: transform <slot> outlets > default slot outlet with fallback 1`] = `
+"import { createSlot as _createSlot, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = _createSlot("default", null, () => {
+    const n2 = t0()
+    return n2
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: transform <slot> outlets > default slot outlet with props & fallback 1`] = `
+"import { createSlot as _createSlot, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = _createSlot("default", { foo: () => (_ctx.bar) }, () => {
+    const n2 = t0()
+    return n2
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: transform <slot> outlets > default slot outlet with props 1`] = `
+"import { createSlot as _createSlot } from 'vue';
+
+export function render(_ctx) {
+  const n0 = _createSlot("default", {
+    foo: () => ("bar"), 
+    baz: () => (_ctx.qux), 
+    fooBar: () => (_ctx.foo-_ctx.bar)
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: transform <slot> outlets > dynamically named slot outlet 1`] = `
+"import { createSlot as _createSlot } from 'vue';
+
+export function render(_ctx) {
+  const n0 = _createSlot(() => (_ctx.foo + _ctx.bar), null)
+  return n0
+}"
+`;
+
+exports[`compiler: transform <slot> outlets > dynamically named slot outlet with v-bind shorthand 1`] = `
+"import { createSlot as _createSlot } from 'vue';
+
+export function render(_ctx) {
+  const n0 = _createSlot(() => (_ctx.name), null)
+  return n0
+}"
+`;
+
+exports[`compiler: transform <slot> outlets > error on unexpected custom directive on <slot> 1`] = `
+"import { resolveDirective as _resolveDirective, createSlot as _createSlot } from 'vue';
+
+export function render(_ctx) {
+  const _directive_foo = _resolveDirective("foo")
+  const n0 = _createSlot("default", null)
+  return n0
+}"
+`;
+
+exports[`compiler: transform <slot> outlets > error on unexpected custom directive with v-show on <slot> 1`] = `
+"import { createSlot as _createSlot } from 'vue';
+
+export function render(_ctx) {
+  const n0 = _createSlot("default", null)
+  return n0
+}"
+`;
+
+exports[`compiler: transform <slot> outlets > named slot outlet with fallback 1`] = `
+"import { createSlot as _createSlot, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = _createSlot("foo", null, () => {
+    const n2 = t0()
+    return n2
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: transform <slot> outlets > named slot outlet with props & fallback 1`] = `
+"import { createSlot as _createSlot, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = _createSlot("foo", { foo: () => (_ctx.bar) }, () => {
+    const n2 = t0()
+    return n2
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: transform <slot> outlets > statically named slot outlet 1`] = `
+"import { createSlot as _createSlot } from 'vue';
+
+export function render(_ctx) {
+  const n0 = _createSlot("foo", null)
+  return n0
+}"
+`;
+
+exports[`compiler: transform <slot> outlets > statically named slot outlet with props 1`] = `
+"import { createSlot as _createSlot } from 'vue';
+
+export function render(_ctx) {
+  const n0 = _createSlot("foo", {
+    foo: () => ("bar"), 
+    baz: () => (_ctx.qux)
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: transform <slot> outlets > statically named slot outlet with v-bind="obj" 1`] = `
+"import { createSlot as _createSlot } from 'vue';
+
+export function render(_ctx) {
+  const n0 = _createSlot("foo", {
+    foo: () => ("bar"), 
+    $: [
+      () => (_ctx.obj), 
+      { baz: () => (_ctx.qux) }
+    ]
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: transform <slot> outlets > statically named slot outlet with v-on 1`] = `
+"import { createSlot as _createSlot, toHandlers as _toHandlers } from 'vue';
+
+export function render(_ctx) {
+  const n0 = _createSlot("default", {
+    onClick: () => _ctx.foo, 
+    $: [
+      () => (_toHandlers(_ctx.bar)), 
+      { baz: () => (_ctx.qux) }
+    ]
+  })
+  return n0
+}"
+`;
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap
new file mode 100644
index 00000000000..7184446fc09
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap
@@ -0,0 +1,85 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler: template ref transform > dynamic ref 1`] = `
+"import { createTemplateRefSetter as _createTemplateRefSetter, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const _setTemplateRef = _createTemplateRefSetter()
+  const n0 = t0()
+  let r0
+  _renderEffect(() => r0 = _setTemplateRef(n0, _ctx.foo, r0))
+  return n0
+}"
+`;
+
+exports[`compiler: template ref transform > function ref 1`] = `
+"import { createTemplateRefSetter as _createTemplateRefSetter, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const _setTemplateRef = _createTemplateRefSetter()
+  const n0 = t0()
+  let r0
+  _renderEffect(() => {
+    const _foo = _ctx.foo
+    r0 = _setTemplateRef(n0, bar => {
+        _foo.value = bar
+        ;({ baz: _ctx.baz } = bar)
+        console.log(_foo.value, _ctx.baz)
+      }, r0)
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: template ref transform > ref + v-for 1`] = `
+"import { createTemplateRefSetter as _createTemplateRefSetter, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const _setTemplateRef = _createTemplateRefSetter()
+  const n0 = _createFor(() => ([1,2,3]), (_for_item0) => {
+    const n2 = t0()
+    _setTemplateRef(n2, "foo", void 0, true)
+    return n2
+  }, undefined, 4)
+  return n0
+}"
+`;
+
+exports[`compiler: template ref transform > ref + v-if 1`] = `
+"import { createTemplateRefSetter as _createTemplateRefSetter, createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const _setTemplateRef = _createTemplateRefSetter()
+  const n0 = _createIf(() => (true), () => {
+    const n2 = t0()
+    _setTemplateRef(n2, "foo")
+    return n2
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: template ref transform > static ref (inline mode) 1`] = `
+"
+  const _setTemplateRef = _createTemplateRefSetter()
+  const n0 = t0()
+  _setTemplateRef(n0, foo)
+  return n0
+"
+`;
+
+exports[`compiler: template ref transform > static ref 1`] = `
+"import { createTemplateRefSetter as _createTemplateRefSetter, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const _setTemplateRef = _createTemplateRefSetter()
+  const n0 = t0()
+  _setTemplateRef(n0, "foo")
+  return n0
+}"
+`;
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformText.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformText.spec.ts.snap
new file mode 100644
index 00000000000..f18f8520353
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformText.spec.ts.snap
@@ -0,0 +1,23 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler: text transform > consecutive text 1`] = `
+"import { toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template(" ")
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setText(n0, _toDisplayString(_ctx.msg)))
+  return n0
+}"
+`;
+
+exports[`compiler: text transform > no consecutive text 1`] = `
+"import { setText as _setText, template as _template } from 'vue';
+const t0 = _template(" ")
+
+export function render(_ctx) {
+  const n0 = t0()
+  _setText(n0, "hello world")
+  return n0
+}"
+`;
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap
new file mode 100644
index 00000000000..4ea0db55fe5
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap
@@ -0,0 +1,658 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`cache multiple access > cache variable used in both property shorthand and normal binding 1`] = `
+"import { setStyle as _setStyle, setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => {
+    const _color = _ctx.color
+    _setStyle(n0, {color: _color})
+    _setProp(n0, "id", _color)
+  })
+  return n0
+}"
+`;
+
+exports[`cache multiple access > dynamic key bindings with expressions 1`] = `
+"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => {
+    const _key = _ctx.key
+    _setDynamicProps(n0, [{ [_key+1]: _ctx.foo[_key+1]() }], true)
+  })
+  return n0
+}"
+`;
+
+exports[`cache multiple access > dynamic property access 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => {
+    const _obj = _ctx.obj
+    _setProp(n0, "id", _obj[1][_ctx.baz] + _obj.bar)
+  })
+  return n0
+}"
+`;
+
+exports[`cache multiple access > function calls with arguments 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t0()
+  const n2 = t0()
+  _renderEffect(() => {
+    const _foo = _ctx.foo
+    const _bar = _ctx.bar
+    const _foo_bar_baz = _foo[_bar(_ctx.baz)]
+    _setProp(n0, "id", _foo_bar_baz)
+    _setProp(n1, "id", _foo_bar_baz)
+    _setProp(n2, "id", _bar() + _foo)
+  })
+  return [n0, n1, n2]
+}"
+`;
+
+exports[`cache multiple access > not cache variable and member expression with the same name 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setProp(n0, "id", _ctx.bar + _ctx.obj.bar))
+  return n0
+}"
+`;
+
+exports[`cache multiple access > not cache variable in function expression 1`] = `
+"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setDynamicProps(n0, [{ foo: bar => _ctx.foo = bar }], true))
+  return n0
+}"
+`;
+
+exports[`cache multiple access > not cache variable only used in property shorthand 1`] = `
+"import { setStyle as _setStyle, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setStyle(n0, {color: _ctx.color}))
+  return n0
+}"
+`;
+
+exports[`cache multiple access > object property chain access 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t0()
+  _renderEffect(() => {
+    const _obj = _ctx.obj
+    const _obj_foo_baz_obj_bar = _obj['foo']['baz'] + _obj.bar
+    _setProp(n0, "id", _obj_foo_baz_obj_bar)
+    _setProp(n1, "id", _obj_foo_baz_obj_bar)
+  })
+  return [n0, n1]
+}"
+`;
+
+exports[`cache multiple access > object property name substring cases 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => {
+    const _p = _ctx.p
+    const _p_title = _p.title
+    _setProp(n0, "id", _p_title + _p.titles + _p_title)
+  })
+  return n0
+}"
+`;
+
+exports[`cache multiple access > optional chaining 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => {
+    const _obj = _ctx.obj
+    _setProp(n0, "id", _obj?.foo + _obj?.bar)
+  })
+  return n0
+}"
+`;
+
+exports[`cache multiple access > repeated expression in expressions 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t0()
+  const n2 = t0()
+  _renderEffect(() => {
+    const _foo = _ctx.foo
+    const _foo_bar = _foo + _ctx.bar
+    _setProp(n0, "id", _foo_bar)
+    _setProp(n1, "id", _foo_bar)
+    _setProp(n2, "id", _foo + _foo_bar)
+  })
+  return [n0, n1, n2]
+}"
+`;
+
+exports[`cache multiple access > repeated expressions 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t0()
+  _renderEffect(() => {
+    const _foo_bar = _ctx.foo + _ctx.bar
+    _setProp(n0, "id", _foo_bar)
+    _setProp(n1, "id", _foo_bar)
+  })
+  return [n0, n1]
+}"
+`;
+
+exports[`cache multiple access > repeated variable in expressions 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t0()
+  _renderEffect(() => {
+    const _foo = _ctx.foo
+    _setProp(n0, "id", _foo + _foo + _ctx.bar)
+    _setProp(n1, "id", _foo)
+  })
+  return [n0, n1]
+}"
+`;
+
+exports[`cache multiple access > repeated variables 1`] = `
+"import { setClass as _setClass, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t0()
+  _renderEffect(() => {
+    const _foo = _ctx.foo
+    _setClass(n0, _foo)
+    _setClass(n1, _foo)
+  })
+  return [n0, n1]
+}"
+`;
+
+exports[`cache multiple access > variable name substring edge cases 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => {
+    const _title = _ctx.title
+    _setProp(n0, "id", _title + _ctx.titles + _title)
+  })
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > .attr modifier 1`] = `
+"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setAttr(n0, "foo-bar", _ctx.id))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > .attr modifier w/ innerHTML 1`] = `
+"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setAttr(n0, "innerHTML", _ctx.foo))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > .attr modifier w/ no expression 1`] = `
+"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setAttr(n0, "foo-bar", _ctx.fooBar))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > .attr modifier w/ progress value 1`] = `
+"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<progress></progress>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setAttr(n0, "value", _ctx.foo))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > .attr modifier w/ textContent 1`] = `
+"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setAttr(n0, "textContent", _ctx.foo))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > .attr modifier w/ value 1`] = `
+"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setAttr(n0, "value", _ctx.foo))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > .camel modifier 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setProp(n0, "fooBar", _ctx.id))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > .camel modifier w/ dynamic arg 1`] = `
+"import { camelize as _camelize, setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setDynamicProps(n0, [{ [_camelize(_ctx.foo)]: _ctx.id }], true))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > .camel modifier w/ no expression 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setProp(n0, "fooBar", _ctx.fooBar))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > .prop modifier (shorthand) 1`] = `
+"import { setDOMProp as _setDOMProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setDOMProp(n0, "fooBar", _ctx.id))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > .prop modifier (shorthand) w/ innerHTML 1`] = `
+"import { setHtml as _setHtml, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setHtml(n0, _ctx.foo))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > .prop modifier (shorthand) w/ no expression 1`] = `
+"import { setDOMProp as _setDOMProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setDOMProp(n0, "fooBar", _ctx.fooBar))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > .prop modifier (shorthand) w/ progress value 1`] = `
+"import { setDOMProp as _setDOMProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<progress></progress>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setDOMProp(n0, "value", _ctx.foo))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > .prop modifier (shorthand) w/ textContent 1`] = `
+"import { setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setText(n0, _ctx.foo))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > .prop modifier (shorthand) w/ value 1`] = `
+"import { setValue as _setValue, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setValue(n0, _ctx.foo))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > .prop modifier 1`] = `
+"import { setDOMProp as _setDOMProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setDOMProp(n0, "fooBar", _ctx.id))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > .prop modifier w/ dynamic arg 1`] = `
+"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setDynamicProps(n0, [{ ["." + _ctx.fooBar]: _ctx.id }], true))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > .prop modifier w/ innerHTML 1`] = `
+"import { setHtml as _setHtml, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setHtml(n0, _ctx.foo))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > .prop modifier w/ no expression 1`] = `
+"import { setDOMProp as _setDOMProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setDOMProp(n0, "fooBar", _ctx.fooBar))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > .prop modifier w/ progress value 1`] = `
+"import { setDOMProp as _setDOMProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<progress></progress>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setDOMProp(n0, "value", _ctx.foo))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > .prop modifier w/ textContent 1`] = `
+"import { setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setText(n0, _ctx.foo))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > .prop modifier w/ value 1`] = `
+"import { setValue as _setValue, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setValue(n0, _ctx.foo))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > :innerHTML 1`] = `
+"import { setHtml as _setHtml, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setHtml(n0, _ctx.foo))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > :textContext 1`] = `
+"import { setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setText(n0, _ctx.foo))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > :value 1`] = `
+"import { setValue as _setValue, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<input>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setValue(n0, _ctx.foo))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > :value w/ progress 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<progress></progress>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setProp(n0, "value", _ctx.foo))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > attributes must be set as attribute 1`] = `
+"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+const t1 = _template("<input>")
+const t2 = _template("<textarea></textarea>")
+const t3 = _template("<img>")
+const t4 = _template("<video></video>")
+const t5 = _template("<canvas></canvas>")
+const t6 = _template("<source>")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t1()
+  const n2 = t2()
+  const n3 = t3()
+  const n4 = t4()
+  const n5 = t5()
+  const n6 = t6()
+  _renderEffect(() => {
+    const _width = _ctx.width
+    const _height = _ctx.height
+    _setAttr(n0, "spellcheck", _ctx.spellcheck)
+    _setAttr(n0, "draggable", _ctx.draggable)
+    _setAttr(n0, "translate", _ctx.translate)
+    _setAttr(n0, "form", _ctx.form)
+    _setAttr(n1, "list", _ctx.list)
+    _setAttr(n2, "type", _ctx.type)
+    _setAttr(n3, "width", _width)
+    _setAttr(n3, "height", _height)
+    _setAttr(n4, "width", _width)
+    _setAttr(n4, "height", _height)
+    _setAttr(n5, "width", _width)
+    _setAttr(n5, "height", _height)
+    _setAttr(n6, "width", _width)
+    _setAttr(n6, "height", _height)
+  })
+  return [n0, n1, n2, n3, n4, n5, n6]
+}"
+`;
+
+exports[`compiler v-bind > basic 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setProp(n0, "id", _ctx.id))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > dynamic arg 1`] = `
+"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => {
+    const _id = _ctx.id
+    const _title = _ctx.title
+    _setDynamicProps(n0, [{ [_id]: _id, [_title]: _title }], true)
+  })
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > dynamic arg w/ static attribute 1`] = `
+"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => {
+    const _id = _ctx.id
+    _setDynamicProps(n0, [{ [_id]: _id, foo: "bar", checked: "" }], true)
+  })
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > no expression (shorthand) 1`] = `
+"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setAttr(n0, "camel-case", _ctx.camelCase))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > no expression 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setProp(n0, "id", _ctx.id))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > number value 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n0 = _createComponentWithFallback(_component_Comp, { depth: () => (0) }, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > should error if empty expression 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<div arg></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > with constant value 1`] = `
+"import { setProp as _setProp, template as _template } from 'vue';
+const t0 = _template("<div f=\\"foo1\\" h=\\"1\\"></div>", true)
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = t0()
+  _setProp(n0, "a", void 0)
+  _setProp(n0, "b", 1 > 2)
+  _setProp(n0, "c", 1 + 2)
+  _setProp(n0, "d", 1 ? 2 : 3)
+  _setProp(n0, "e", (2))
+  _setProp(n0, "g", 1)
+  _setProp(n0, "i", true)
+  _setProp(n0, "j", null)
+  _setProp(n0, "k", _ctx.x)
+  _setProp(n0, "l", { foo: 1 })
+  _setProp(n0, "m", { [_ctx.x]: 1 })
+  _setProp(n0, "n", { ...{ foo: 1 } })
+  _setProp(n0, "o", [1, , 3])
+  _setProp(n0, "p", [1, ...[2, 3]])
+  _setProp(n0, "q", [1, 2])
+  _setProp(n0, "r", /\\s+/)
+  return n0
+}"
+`;
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap
new file mode 100644
index 00000000000..69c695a246a
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap
@@ -0,0 +1,249 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler: v-for > array de-structured value (with rest) 1`] = `
+"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("<div> </div>", true)
+
+export function render(_ctx) {
+  const n0 = _createFor(() => (_ctx.list), (_for_item0, _for_key0) => {
+    const n2 = t0()
+    const x2 = _child(n2)
+    _renderEffect(() => _setText(x2, _toDisplayString(_for_item0.value[0] + _for_item0.value.slice(1) + _for_key0.value)))
+    return n2
+  }, ([id, ...other], index) => (id))
+  return n0
+}"
+`;
+
+exports[`compiler: v-for > array de-structured value 1`] = `
+"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("<div> </div>", true)
+
+export function render(_ctx) {
+  const n0 = _createFor(() => (_ctx.list), (_for_item0, _for_key0) => {
+    const n2 = t0()
+    const x2 = _child(n2)
+    _renderEffect(() => _setText(x2, _toDisplayString(_for_item0.value[0] + _for_item0.value[1] + _for_key0.value)))
+    return n2
+  }, ([id, other], index) => (id))
+  return n0
+}"
+`;
+
+exports[`compiler: v-for > basic v-for 1`] = `
+"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div> </div>", true)
+_delegateEvents("click")
+
+export function render(_ctx) {
+  const n0 = _createFor(() => (_ctx.items), (_for_item0) => {
+    const n2 = t0()
+    const x2 = _child(n2)
+    n2.$evtclick = () => (_ctx.remove(_for_item0.value))
+    _renderEffect(() => _setText(x2, _toDisplayString(_for_item0.value)))
+    return n2
+  }, (item) => (item.id))
+  return n0
+}"
+`;
+
+exports[`compiler: v-for > key only binding pattern 1`] = `
+"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("<tr> </tr>", true)
+
+export function render(_ctx) {
+  const n0 = _createFor(() => (_ctx.rows), (_for_item0) => {
+    const n2 = t0()
+    const x2 = _child(n2)
+    _setText(x2, _toDisplayString(_for_item0.value.id + _for_item0.value.id))
+    return n2
+  }, (row) => (row.id))
+  return n0
+}"
+`;
+
+exports[`compiler: v-for > multi effect 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = _createFor(() => (_ctx.items), (_for_item0, _for_key0) => {
+    const n2 = t0()
+    _renderEffect(() => {
+      _setProp(n2, "item", _for_item0.value)
+      _setProp(n2, "index", _for_key0.value)
+    })
+    return n2
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: v-for > nested v-for 1`] = `
+"import { setInsertionState as _setInsertionState, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("<span> </span>")
+const t1 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
+    const n5 = t1()
+    _setInsertionState(n5)
+    const n2 = _createFor(() => (_for_item0.value), (_for_item1) => {
+      const n4 = t0()
+      const x4 = _child(n4)
+      _renderEffect(() => _setText(x4, _toDisplayString(_for_item1.value+_for_item0.value)))
+      return n4
+    }, undefined, 1)
+    return n5
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: v-for > object de-structured value (with rest) 1`] = `
+"import { getRestElement as _getRestElement, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("<div> </div>", true)
+
+export function render(_ctx) {
+  const n0 = _createFor(() => (_ctx.list), (_for_item0, _for_key0) => {
+    const n2 = t0()
+    const x2 = _child(n2)
+    _renderEffect(() => _setText(x2, _toDisplayString(_for_item0.value.id + _getRestElement(_for_item0.value, ["id"]) + _for_key0.value)))
+    return n2
+  }, ({ id, ...other }, index) => (id))
+  return n0
+}"
+`;
+
+exports[`compiler: v-for > object de-structured value 1`] = `
+"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("<span> </span>", true)
+
+export function render(_ctx) {
+  const n0 = _createFor(() => (_ctx.items), (_for_item0) => {
+    const n2 = t0()
+    const x2 = _child(n2)
+    _renderEffect(() => _setText(x2, _toDisplayString(_for_item0.value.id) + _toDisplayString(_for_item0.value.value)))
+    return n2
+  }, ({ id, value }) => (id))
+  return n0
+}"
+`;
+
+exports[`compiler: v-for > object value, key and index 1`] = `
+"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("<div> </div>", true)
+
+export function render(_ctx) {
+  const n0 = _createFor(() => (_ctx.list), (_for_item0, _for_key0, _for_index0) => {
+    const n2 = t0()
+    const x2 = _child(n2)
+    _renderEffect(() => _setText(x2, _toDisplayString(_for_item0.value + _for_key0.value + _for_index0.value)))
+    return n2
+  }, (value, key, index) => (key))
+  return n0
+}"
+`;
+
+exports[`compiler: v-for > selector pattern 1`] = `
+"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("<tr> </tr>", true)
+
+export function render(_ctx) {
+  let _selector0_0
+  const n0 = _createFor(() => (_ctx.rows), (_for_item0) => {
+    const n2 = t0()
+    const x2 = _child(n2)
+    _selector0_0(() => {
+      _setText(x2, _toDisplayString(_ctx.selected === _for_item0.value.id ? 'danger' : ''))
+    })
+    return n2
+  }, (row) => (row.id), undefined, ({ createSelector }) => {
+    _selector0_0 = createSelector(() => _ctx.selected)
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: v-for > selector pattern 2`] = `
+"import { setClass as _setClass, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("<tr></tr>", true)
+
+export function render(_ctx) {
+  let _selector0_0
+  const n0 = _createFor(() => (_ctx.rows), (_for_item0) => {
+    const n2 = t0()
+    _selector0_0(() => {
+      _setClass(n2, _ctx.selected === _for_item0.value.id ? 'danger' : '')
+    })
+    return n2
+  }, (row) => (row.id), undefined, ({ createSelector }) => {
+    _selector0_0 = createSelector(() => _ctx.selected)
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: v-for > selector pattern 3`] = `
+"import { setClass as _setClass, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("<tr></tr>", true)
+
+export function render(_ctx) {
+  const n0 = _createFor(() => (_ctx.rows), (_for_item0) => {
+    const n2 = t0()
+    _renderEffect(() => {
+      const _row = _for_item0.value
+      _setClass(n2, _row.label === _row.id ? 'danger' : '')
+    })
+    return n2
+  }, (row) => (row.id))
+  return n0
+}"
+`;
+
+exports[`compiler: v-for > selector pattern 4`] = `
+"import { setClass as _setClass, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("<tr></tr>", true)
+
+export function render(_ctx) {
+  let _selector0_0
+  const n0 = _createFor(() => (_ctx.rows), (_for_item0) => {
+    const n2 = t0()
+    _selector0_0(() => {
+      _setClass(n2, { danger: _for_item0.value.id === _ctx.selected })
+    })
+    return n2
+  }, (row) => (row.id), undefined, ({ createSelector }) => {
+    _selector0_0 = createSelector(() => _ctx.selected)
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: v-for > v-for aliases w/ complex expressions 1`] = `
+"import { getDefaultValue as _getDefaultValue, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("<div> </div>", true)
+
+export function render(_ctx) {
+  const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
+    const n2 = t0()
+    const x2 = _child(n2)
+    _renderEffect(() => _setText(x2, _toDisplayString(_getDefaultValue(_for_item0.value.foo, _ctx.bar) + _ctx.bar + _ctx.baz + _getDefaultValue(_for_item0.value.baz[0], _ctx.quux) + _ctx.quux)))
+    return n2
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: v-for > w/o value 1`] = `
+"import { createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("<div>item</div>", true)
+
+export function render(_ctx) {
+  const n0 = _createFor(() => (_ctx.items), (_for_item0) => {
+    const n2 = t0()
+    return n2
+  })
+  return n0
+}"
+`;
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vHtml.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vHtml.spec.ts.snap
new file mode 100644
index 00000000000..ecf886d7cbb
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vHtml.spec.ts.snap
@@ -0,0 +1,34 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`v-html > should convert v-html to innerHTML 1`] = `
+"import { setHtml as _setHtml, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = t0()
+  _renderEffect(() => _setHtml(n0, _ctx.code))
+  return n0
+}"
+`;
+
+exports[`v-html > should raise error and ignore children when v-html is present 1`] = `
+"import { setHtml as _setHtml, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => _setHtml(n0, _ctx.test))
+  return n0
+}"
+`;
+
+exports[`v-html > should raise error if has no expression 1`] = `
+"import { setHtml as _setHtml, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _setHtml(n0, "")
+  return n0
+}"
+`;
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap
new file mode 100644
index 00000000000..c41dc9226c5
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap
@@ -0,0 +1,162 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler: v-if > basic v-if 1`] = `
+"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div> </div>", true)
+
+export function render(_ctx) {
+  const n0 = _createIf(() => (_ctx.ok), () => {
+    const n2 = t0()
+    const x2 = _child(n2)
+    _renderEffect(() => _setText(x2, _toDisplayString(_ctx.msg)))
+    return n2
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: v-if > comment between branches 1`] = `
+"import { createIf as _createIf, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+const t1 = _template("<!--foo-->")
+const t2 = _template("<p></p>")
+const t3 = _template("<!--bar-->")
+const t4 = _template("fine")
+const t5 = _template("<div> </div>")
+
+export function render(_ctx) {
+  const n0 = _createIf(() => (_ctx.ok), () => {
+    const n2 = t0()
+    return n2
+  }, () => _createIf(() => (_ctx.orNot), () => {
+    const n5 = t1()
+    const n6 = t2()
+    return [n5, n6]
+  }, () => {
+    const n10 = t3()
+    const n11 = t4()
+    return [n10, n11]
+  }))
+  const n13 = t5()
+  const x13 = _child(n13)
+  _renderEffect(() => _setText(x13, _toDisplayString(_ctx.text)))
+  return [n0, n13]
+}"
+`;
+
+exports[`compiler: v-if > dedupe same template 1`] = `
+"import { createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div>hello</div>")
+
+export function render(_ctx) {
+  const n0 = _createIf(() => (_ctx.ok), () => {
+    const n2 = t0()
+    return n2
+  })
+  const n3 = _createIf(() => (_ctx.ok), () => {
+    const n5 = t0()
+    return n5
+  })
+  return [n0, n3]
+}"
+`;
+
+exports[`compiler: v-if > template v-if 1`] = `
+"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+const t1 = _template("hello")
+const t2 = _template("<p> </p>", true)
+
+export function render(_ctx) {
+  const n0 = _createIf(() => (_ctx.ok), () => {
+    const n2 = t0()
+    const n3 = t1()
+    const n4 = t2()
+    const x4 = _child(n4)
+    _renderEffect(() => _setText(x4, _toDisplayString(_ctx.msg)))
+    return [n2, n3, n4]
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: v-if > v-if + v-else 1`] = `
+"import { createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+const t1 = _template("<p></p>")
+
+export function render(_ctx) {
+  const n0 = _createIf(() => (_ctx.ok), () => {
+    const n2 = t0()
+    return n2
+  }, () => {
+    const n4 = t1()
+    return n4
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: v-if > v-if + v-else-if + v-else 1`] = `
+"import { createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+const t1 = _template("<p></p>")
+const t2 = _template("fine")
+
+export function render(_ctx) {
+  const n0 = _createIf(() => (_ctx.ok), () => {
+    const n2 = t0()
+    return n2
+  }, () => _createIf(() => (_ctx.orNot), () => {
+    const n4 = t1()
+    return n4
+  }, () => {
+    const n7 = t2()
+    return n7
+  }))
+  return n0
+}"
+`;
+
+exports[`compiler: v-if > v-if + v-else-if 1`] = `
+"import { createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+const t1 = _template("<p></p>")
+
+export function render(_ctx) {
+  const n0 = _createIf(() => (_ctx.ok), () => {
+    const n2 = t0()
+    return n2
+  }, () => _createIf(() => (_ctx.orNot), () => {
+    const n4 = t1()
+    return n4
+  }))
+  return n0
+}"
+`;
+
+exports[`compiler: v-if > v-if + v-if / v-else[-if] 1`] = `
+"import { setInsertionState as _setInsertionState, createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<span>foo</span>")
+const t1 = _template("<span>bar</span>")
+const t2 = _template("<span>baz</span>")
+const t3 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n8 = t3()
+  _setInsertionState(n8)
+  const n0 = _createIf(() => (_ctx.foo), () => {
+    const n2 = t0()
+    return n2
+  })
+  _setInsertionState(n8)
+  const n3 = _createIf(() => (_ctx.bar), () => {
+    const n5 = t1()
+    return n5
+  }, () => {
+    const n7 = t2()
+    return n7
+  })
+  return n8
+}"
+`;
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap
new file mode 100644
index 00000000000..5ef064974c0
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap
@@ -0,0 +1,242 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler: vModel transform > component > v-model for component should generate modelModifiers 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n0 = _createComponentWithFallback(_component_Comp, { modelValue: () => (_ctx.foo),
+  "onUpdate:modelValue": () => _value => (_ctx.foo = _value),
+  modelModifiers: () => ({ trim: true, "bar-baz": true }) }, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: vModel transform > component > v-model for component should work 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n0 = _createComponentWithFallback(_component_Comp, { modelValue: () => (_ctx.foo),
+  "onUpdate:modelValue": () => _value => (_ctx.foo = _value) }, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: vModel transform > component > v-model with arguments for component should generate modelModifiers 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n0 = _createComponentWithFallback(_component_Comp, {
+    foo: () => (_ctx.foo),
+    "onUpdate:foo": () => _value => (_ctx.foo = _value),
+    fooModifiers: () => ({ trim: true }), 
+    bar: () => (_ctx.bar),
+    "onUpdate:bar": () => _value => (_ctx.bar = _value),
+    barModifiers: () => ({ number: true })
+  }, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: vModel transform > component > v-model with arguments for component should work 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n0 = _createComponentWithFallback(_component_Comp, { bar: () => (_ctx.foo),
+  "onUpdate:bar": () => _value => (_ctx.foo = _value) }, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: vModel transform > component > v-model with dynamic arguments for component should generate modelModifiers  1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n0 = _createComponentWithFallback(_component_Comp, { $: [
+    () => ({ [_ctx.foo]: _ctx.foo,
+    ["onUpdate:" + _ctx.foo]: () => _value => (_ctx.foo = _value),
+    [_ctx.foo + "Modifiers"]: () => ({ trim: true }) }), 
+    () => ({ [_ctx.bar]: _ctx.bar,
+    ["onUpdate:" + _ctx.bar]: () => _value => (_ctx.bar = _value),
+    [_ctx.bar + "Modifiers"]: () => ({ number: true }) })
+  ] }, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: vModel transform > component > v-model with dynamic arguments for component should work 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n0 = _createComponentWithFallback(_component_Comp, { $: [
+    () => ({ [_ctx.arg]: _ctx.foo,
+    ["onUpdate:" + _ctx.arg]: () => _value => (_ctx.foo = _value) })
+  ] }, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: vModel transform > modifiers > .lazy 1`] = `
+"import { applyTextModel as _applyTextModel, template as _template } from 'vue';
+const t0 = _template("<input>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _applyTextModel(n0, () => (_ctx.model), _value => (_ctx.model = _value), { lazy: true })
+  return n0
+}"
+`;
+
+exports[`compiler: vModel transform > modifiers > .number 1`] = `
+"import { applyTextModel as _applyTextModel, template as _template } from 'vue';
+const t0 = _template("<input>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _applyTextModel(n0, () => (_ctx.model), _value => (_ctx.model = _value), { number: true })
+  return n0
+}"
+`;
+
+exports[`compiler: vModel transform > modifiers > .trim 1`] = `
+"import { applyTextModel as _applyTextModel, template as _template } from 'vue';
+const t0 = _template("<input>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _applyTextModel(n0, () => (_ctx.model), _value => (_ctx.model = _value), { trim: true })
+  return n0
+}"
+`;
+
+exports[`compiler: vModel transform > should support input (checkbox) 1`] = `
+"import { applyCheckboxModel as _applyCheckboxModel, template as _template } from 'vue';
+const t0 = _template("<input type=\\"checkbox\\">", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _applyCheckboxModel(n0, () => (_ctx.model), _value => (_ctx.model = _value))
+  return n0
+}"
+`;
+
+exports[`compiler: vModel transform > should support input (dynamic type) 1`] = `
+"import { applyDynamicModel as _applyDynamicModel, template as _template } from 'vue';
+const t0 = _template("<input>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _applyDynamicModel(n0, () => (_ctx.model), _value => (_ctx.model = _value))
+  return n0
+}"
+`;
+
+exports[`compiler: vModel transform > should support input (radio) 1`] = `
+"import { applyRadioModel as _applyRadioModel, template as _template } from 'vue';
+const t0 = _template("<input type=\\"radio\\">", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _applyRadioModel(n0, () => (_ctx.model), _value => (_ctx.model = _value))
+  return n0
+}"
+`;
+
+exports[`compiler: vModel transform > should support input (text) 1`] = `
+"import { applyTextModel as _applyTextModel, template as _template } from 'vue';
+const t0 = _template("<input type=\\"text\\">", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _applyTextModel(n0, () => (_ctx.model), _value => (_ctx.model = _value))
+  return n0
+}"
+`;
+
+exports[`compiler: vModel transform > should support member expression 1`] = `
+"import { applyTextModel as _applyTextModel, template as _template } from 'vue';
+const t0 = _template("<input>")
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = t0()
+  const n1 = t0()
+  const n2 = t0()
+  _applyTextModel(n0, () => (_ctx.setupRef.child), _value => (_ctx.setupRef.child = _value))
+  _applyTextModel(n1, () => (_ctx.setupLet.child), _value => (_ctx.setupLet.child = _value))
+  _applyTextModel(n2, () => (_ctx.setupMaybeRef.child), _value => (_ctx.setupMaybeRef.child = _value))
+  return [n0, n1, n2]
+}"
+`;
+
+exports[`compiler: vModel transform > should support member expression w/ inline 1`] = `
+"
+  const n0 = t0()
+  const n1 = t0()
+  const n2 = t0()
+  _applyTextModel(n0, () => (setupRef.value.child), _value => (setupRef.value.child = _value))
+  _applyTextModel(n1, () => (_unref(setupLet).child), _value => (_unref(setupLet).child = _value))
+  _applyTextModel(n2, () => (_unref(setupMaybeRef).child), _value => (_unref(setupMaybeRef).child = _value))
+  return [n0, n1, n2]
+"
+`;
+
+exports[`compiler: vModel transform > should support select 1`] = `
+"import { applySelectModel as _applySelectModel, template as _template } from 'vue';
+const t0 = _template("<select></select>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _applySelectModel(n0, () => (_ctx.model), _value => (_ctx.model = _value))
+  return n0
+}"
+`;
+
+exports[`compiler: vModel transform > should support simple expression 1`] = `
+"import { applyTextModel as _applyTextModel, template as _template } from 'vue';
+const t0 = _template("<input>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _applyTextModel(n0, () => (_ctx.model), _value => (_ctx.model = _value))
+  return n0
+}"
+`;
+
+exports[`compiler: vModel transform > should support textarea 1`] = `
+"import { applyTextModel as _applyTextModel, template as _template } from 'vue';
+const t0 = _template("<textarea></textarea>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _applyTextModel(n0, () => (_ctx.model), _value => (_ctx.model = _value))
+  return n0
+}"
+`;
+
+exports[`compiler: vModel transform > should support w/ dynamic v-bind 1`] = `
+"import { applyDynamicModel as _applyDynamicModel, setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<input>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _applyDynamicModel(n0, () => (_ctx.model), _value => (_ctx.model = _value))
+  _renderEffect(() => _setDynamicProps(n0, [_ctx.obj], true))
+  return n0
+}"
+`;
+
+exports[`compiler: vModel transform > should support w/ dynamic v-bind 2`] = `
+"import { applyDynamicModel as _applyDynamicModel, template as _template } from 'vue';
+const t0 = _template("<input>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _applyDynamicModel(n0, () => (_ctx.model), _value => (_ctx.model = _value))
+  return n0
+}"
+`;
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap
new file mode 100644
index 00000000000..e7a2b30e69c
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap
@@ -0,0 +1,487 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`v-on > complex member expression w/ prefixIdentifiers: true 1`] = `
+"import { delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+_delegateEvents("click")
+
+export function render(_ctx) {
+  const n0 = t0()
+  n0.$evtclick = e => _ctx.a['b' + _ctx.c](e)
+  return n0
+}"
+`;
+
+exports[`v-on > component event with special characters 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Foo = _resolveComponent("Foo")
+  const _on_update_model = () => {}
+  const _on_update_model1 = () => {}
+  const n0 = _createComponentWithFallback(_component_Foo, {
+    "onUpdate:model": () => _on_update_model, 
+    "onUpdate-model": () => _on_update_model1
+  }, null, true)
+  return n0
+}"
+`;
+
+exports[`v-on > dynamic arg 1`] = `
+"import { on as _on, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => {
+    
+    _on(n0, _ctx.event, e => _ctx.handler(e), {
+      effect: true
+    })
+  })
+  return n0
+}"
+`;
+
+exports[`v-on > dynamic arg with complex exp prefixing 1`] = `
+"import { on as _on, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => {
+    
+    _on(n0, _ctx.event(_ctx.foo), e => _ctx.handler(e), {
+      effect: true
+    })
+  })
+  return n0
+}"
+`;
+
+exports[`v-on > dynamic arg with prefixing 1`] = `
+"import { on as _on, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => {
+    
+    _on(n0, _ctx.event, e => _ctx.handler(e), {
+      effect: true
+    })
+  })
+  return n0
+}"
+`;
+
+exports[`v-on > event modifier 1`] = `
+"import { withModifiers as _withModifiers, on as _on, withKeys as _withKeys, delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<a></a>")
+const t1 = _template("<form></form>")
+const t2 = _template("<div></div>")
+const t3 = _template("<input>")
+_delegateEvents("click", "contextmenu", "mouseup", "keyup")
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = t0()
+  const n1 = t1()
+  const n2 = t0()
+  const n3 = t2()
+  const n4 = t2()
+  const n5 = t0()
+  const n6 = t2()
+  const n7 = t3()
+  const n8 = t3()
+  const n9 = t3()
+  const n10 = t3()
+  const n11 = t3()
+  const n12 = t3()
+  const n13 = t3()
+  const n14 = t3()
+  const n15 = t3()
+  const n16 = t3()
+  const n17 = t3()
+  const n18 = t3()
+  const n19 = t3()
+  const n20 = t3()
+  const n21 = t3()
+  n0.$evtclick = _withModifiers(_ctx.handleEvent, ["stop"])
+  _on(n1, "submit", _withModifiers(_ctx.handleEvent, ["prevent"]))
+  n2.$evtclick = _withModifiers(_ctx.handleEvent, ["stop","prevent"])
+  n3.$evtclick = _withModifiers(_ctx.handleEvent, ["self"])
+  _on(n4, "click", _ctx.handleEvent, {
+    capture: true
+  })
+  _on(n5, "click", _ctx.handleEvent, {
+    once: true
+  })
+  _on(n6, "scroll", _ctx.handleEvent, {
+    passive: true
+  })
+  n7.$evtcontextmenu = _withModifiers(_ctx.handleEvent, ["right"])
+  n8.$evtclick = _withModifiers(_ctx.handleEvent, ["left"])
+  n9.$evtmouseup = _withModifiers(_ctx.handleEvent, ["middle"])
+  n10.$evtcontextmenu = _withKeys(_withModifiers(_ctx.handleEvent, ["right"]), ["enter"])
+  n11.$evtkeyup = _withKeys(_ctx.handleEvent, ["enter"])
+  n12.$evtkeyup = _withKeys(_ctx.handleEvent, ["tab"])
+  n13.$evtkeyup = _withKeys(_ctx.handleEvent, ["delete"])
+  n14.$evtkeyup = _withKeys(_ctx.handleEvent, ["esc"])
+  n15.$evtkeyup = _withKeys(_ctx.handleEvent, ["space"])
+  n16.$evtkeyup = _withKeys(_ctx.handleEvent, ["up"])
+  n17.$evtkeyup = _withKeys(_ctx.handleEvent, ["down"])
+  n18.$evtkeyup = _withKeys(_ctx.handleEvent, ["left"])
+  n19.$evtkeyup = _withModifiers(e => _ctx.submit(e), ["middle"])
+  n20.$evtkeyup = _withModifiers(e => _ctx.submit(e), ["middle","self"])
+  n21.$evtkeyup = _withKeys(_withModifiers(_ctx.handleEvent, ["self"]), ["enter"])
+  return [n0, n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12, n13, n14, n15, n16, n17, n18, n19, n20, n21]
+}"
+`;
+
+exports[`v-on > expression with type 1`] = `
+"import { delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+_delegateEvents("click")
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = t0()
+  n0.$evtclick = e => _ctx.handleClick(e)
+  return n0
+}"
+`;
+
+exports[`v-on > function expression w/ prefixIdentifiers: true 1`] = `
+"import { delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+_delegateEvents("click")
+
+export function render(_ctx) {
+  const n0 = t0()
+  n0.$evtclick = e => _ctx.foo(e)
+  return n0
+}"
+`;
+
+exports[`v-on > inline statement w/ prefixIdentifiers: true 1`] = `
+"import { delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+_delegateEvents("click")
+
+export function render(_ctx) {
+  const n0 = t0()
+  n0.$evtclick = $event => (_ctx.foo($event))
+  return n0
+}"
+`;
+
+exports[`v-on > multiple inline statements w/ prefixIdentifiers: true 1`] = `
+"import { delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+_delegateEvents("click")
+
+export function render(_ctx) {
+  const n0 = t0()
+  n0.$evtclick = $event => {_ctx.foo($event);_ctx.bar()}
+  return n0
+}"
+`;
+
+exports[`v-on > should NOT add a prefix to $event if the expression is a function expression 1`] = `
+"import { delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+_delegateEvents("click")
+
+export function render(_ctx) {
+  const n0 = t0()
+  n0.$evtclick = $event => {_ctx.i++;_ctx.foo($event)}
+  return n0
+}"
+`;
+
+exports[`v-on > should NOT wrap as function if expression is already function expression (with Typescript) 1`] = `
+"import { delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+_delegateEvents("click")
+
+export function render(_ctx) {
+  const n0 = t0()
+  n0.$evtclick = (e: any): any => _ctx.foo(e)
+  return n0
+}"
+`;
+
+exports[`v-on > should NOT wrap as function if expression is already function expression (with newlines) 1`] = `
+"import { delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+_delegateEvents("click")
+
+export function render(_ctx) {
+  const n0 = t0()
+  n0.$evtclick = 
+      $event => {
+        _ctx.foo($event)
+      }
+    
+  return n0
+}"
+`;
+
+exports[`v-on > should NOT wrap as function if expression is already function expression 1`] = `
+"import { delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+_delegateEvents("click")
+
+export function render(_ctx) {
+  const n0 = t0()
+  n0.$evtclick = $event => _ctx.foo($event)
+  return n0
+}"
+`;
+
+exports[`v-on > should NOT wrap as function if expression is complex member expression 1`] = `
+"import { delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+_delegateEvents("click")
+
+export function render(_ctx) {
+  const n0 = t0()
+  n0.$evtclick = e => _ctx.a['b' + _ctx.c](e)
+  return n0
+}"
+`;
+
+exports[`v-on > should delegate event 1`] = `
+"import { delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+_delegateEvents("click")
+
+export function render(_ctx) {
+  const n0 = t0()
+  n0.$evtclick = e => _ctx.test(e)
+  return n0
+}"
+`;
+
+exports[`v-on > should handle multi-line statement 1`] = `
+"import { delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+_delegateEvents("click")
+
+export function render(_ctx) {
+  const n0 = t0()
+  n0.$evtclick = () => {
+_ctx.foo();
+_ctx.bar()
+}
+  return n0
+}"
+`;
+
+exports[`v-on > should handle multiple inline statement 1`] = `
+"import { delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+_delegateEvents("click")
+
+export function render(_ctx) {
+  const n0 = t0()
+  n0.$evtclick = () => {_ctx.foo();_ctx.bar()}
+  return n0
+}"
+`;
+
+exports[`v-on > should not prefix member expression 1`] = `
+"import { delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+_delegateEvents("click")
+
+export function render(_ctx) {
+  const n0 = t0()
+  n0.$evtclick = e => _ctx.foo.bar(e)
+  return n0
+}"
+`;
+
+exports[`v-on > should not wrap keys guard if no key modifier is present 1`] = `
+"import { withModifiers as _withModifiers, delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+_delegateEvents("keyup")
+
+export function render(_ctx) {
+  const n0 = t0()
+  n0.$evtkeyup = _withModifiers(e => _ctx.test(e), ["exact"])
+  return n0
+}"
+`;
+
+exports[`v-on > should support multiple events and modifiers options w/ prefixIdentifiers: true 1`] = `
+"import { withModifiers as _withModifiers, withKeys as _withKeys, delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+_delegateEvents("click", "keyup")
+
+export function render(_ctx) {
+  const n0 = t0()
+  n0.$evtclick = _withModifiers(e => _ctx.test(e), ["stop"])
+  n0.$evtkeyup = _withKeys(e => _ctx.test(e), ["enter"])
+  return n0
+}"
+`;
+
+exports[`v-on > should support multiple modifiers and event options w/ prefixIdentifiers: true 1`] = `
+"import { withModifiers as _withModifiers, on as _on, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _on(n0, "click", _withModifiers(e => _ctx.test(e), ["stop","prevent"]), {
+    capture: true, 
+    once: true
+  })
+  return n0
+}"
+`;
+
+exports[`v-on > should transform click.middle 1`] = `
+"import { withModifiers as _withModifiers, delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+_delegateEvents("mouseup")
+
+export function render(_ctx) {
+  const n0 = t0()
+  n0.$evtmouseup = _withModifiers(e => _ctx.test(e), ["middle"])
+  return n0
+}"
+`;
+
+exports[`v-on > should transform click.middle 2`] = `
+"import { withModifiers as _withModifiers, on as _on, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => {
+    
+    _on(n0, (_ctx.event) === "click" ? "mouseup" : (_ctx.event), _withModifiers(e => _ctx.test(e), ["middle"]), {
+      effect: true
+    })
+  })
+  return n0
+}"
+`;
+
+exports[`v-on > should transform click.right 1`] = `
+"import { withModifiers as _withModifiers, delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+_delegateEvents("contextmenu")
+
+export function render(_ctx) {
+  const n0 = t0()
+  n0.$evtcontextmenu = _withModifiers(e => _ctx.test(e), ["right"])
+  return n0
+}"
+`;
+
+exports[`v-on > should transform click.right 2`] = `
+"import { withModifiers as _withModifiers, withKeys as _withKeys, on as _on, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => {
+    
+    _on(n0, (_ctx.event) === "click" ? "contextmenu" : (_ctx.event), _withKeys(_withModifiers(e => _ctx.test(e), ["right"]), ["right"]), {
+      effect: true
+    })
+  })
+  return n0
+}"
+`;
+
+exports[`v-on > should use delegate helper when have multiple events of same name 1`] = `
+"import { delegate as _delegate, withModifiers as _withModifiers, delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+_delegateEvents("click")
+
+export function render(_ctx) {
+  const n0 = t0()
+  _delegate(n0, "click", e => _ctx.test(e))
+  _delegate(n0, "click", _withModifiers(e => _ctx.test(e), ["stop"]))
+  return n0
+}"
+`;
+
+exports[`v-on > should wrap as function if expression is inline statement 1`] = `
+"import { delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+_delegateEvents("click")
+
+export function render(_ctx) {
+  const n0 = t0()
+  n0.$evtclick = () => (_ctx.i++)
+  return n0
+}"
+`;
+
+exports[`v-on > should wrap both for dynamic key event w/ left/right modifiers 1`] = `
+"import { withModifiers as _withModifiers, withKeys as _withKeys, on as _on, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => {
+    
+    _on(n0, _ctx.e, _withKeys(_withModifiers(e => _ctx.test(e), ["left"]), ["left"]), {
+      effect: true
+    })
+  })
+  return n0
+}"
+`;
+
+exports[`v-on > should wrap in unref if identifier is setup-maybe-ref w/ inline: true 1`] = `
+"
+  const n0 = t0()
+  const n1 = t0()
+  const n2 = t0()
+  n0.$evtclick = () => (x.value=_unref(y))
+  n1.$evtclick = () => (x.value++)
+  n2.$evtclick = () => ({ x: x.value } = _unref(y))
+  return [n0, n1, n2]
+"
+`;
+
+exports[`v-on > should wrap keys guard for keyboard events or dynamic events 1`] = `
+"import { withModifiers as _withModifiers, withKeys as _withKeys, on as _on, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _on(n0, "keydown", _withKeys(_withModifiers(e => _ctx.test(e), ["stop","ctrl"]), ["a"]), {
+    capture: true
+  })
+  return n0
+}"
+`;
+
+exports[`v-on > should wrap keys guard for static key event w/ left/right modifiers 1`] = `
+"import { withKeys as _withKeys, delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+_delegateEvents("keyup")
+
+export function render(_ctx) {
+  const n0 = t0()
+  n0.$evtkeyup = _withKeys(e => _ctx.test(e), ["left"])
+  return n0
+}"
+`;
+
+exports[`v-on > simple expression 1`] = `
+"import { delegateEvents as _delegateEvents, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+_delegateEvents("click")
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = t0()
+  n0.$evtclick = _ctx.handleClick
+  return n0
+}"
+`;
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
new file mode 100644
index 00000000000..b6107d5a1a1
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
@@ -0,0 +1,104 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler: v-once > as root node 1`] = `
+"import { setProp as _setProp, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _setProp(n0, "id", _ctx.foo)
+  return n0
+}"
+`;
+
+exports[`compiler: v-once > basic 1`] = `
+"import { child as _child, next as _next, toDisplayString as _toDisplayString, setText as _setText, setClass as _setClass, template as _template } from 'vue';
+const t0 = _template("<div> <span></span></div>", true)
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n2 = t0()
+  const n0 = _child(n2)
+  const n1 = _next(n0)
+  _setText(n0, _toDisplayString(_ctx.msg) + " ")
+  _setClass(n1, _ctx.clz)
+  return n2
+}"
+`;
+
+exports[`compiler: v-once > inside v-once 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<div><div></div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compiler: v-once > on component 1`] = `
+"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n1 = t0()
+  _setInsertionState(n1)
+  const n0 = _createComponentWithFallback(_component_Comp, { id: () => (_ctx.foo) }, null, null, true)
+  return n1
+}"
+`;
+
+exports[`compiler: v-once > on nested plain element 1`] = `
+"import { child as _child, setProp as _setProp, template as _template } from 'vue';
+const t0 = _template("<div><div></div></div>", true)
+
+export function render(_ctx) {
+  const n1 = t0()
+  const n0 = _child(n1)
+  _setProp(n0, "id", _ctx.foo)
+  return n1
+}"
+`;
+
+exports[`compiler: v-once > with v-for 1`] = `
+"import { createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
+    const n2 = t0()
+    return n2
+  }, undefined, 4)
+  return n0
+}"
+`;
+
+exports[`compiler: v-once > with v-if 1`] = `
+"import { createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = _createIf(() => (_ctx.expr), () => {
+    const n2 = t0()
+    return n2
+  }, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: v-once > with v-if/else 1`] = `
+"import { createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+const t1 = _template("<p></p>")
+
+export function render(_ctx) {
+  const n0 = _createIf(() => (_ctx.expr), () => {
+    const n2 = t0()
+    return n2
+  }, () => {
+    const n4 = t1()
+    return n4
+  }, true)
+  return n0
+}"
+`;
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vShow.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vShow.spec.ts.snap
new file mode 100644
index 00000000000..f595da5ef8f
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vShow.spec.ts.snap
@@ -0,0 +1,12 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler: v-show transform > simple expression 1`] = `
+"import { applyVShow as _applyVShow, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _applyVShow(n0, () => (_ctx.foo))
+  return n0
+}"
+`;
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap
new file mode 100644
index 00000000000..f296d7257d0
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap
@@ -0,0 +1,341 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler: transform slot > dynamic slots name 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template("foo")
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n2 = _createComponentWithFallback(_component_Comp, null, {
+    $: [
+      () => ({
+        name: _ctx.name, 
+        fn: () => {
+          const n0 = t0()
+          return n0
+        }
+      })
+    ]
+  }, true)
+  return n2
+}"
+`;
+
+exports[`compiler: transform slot > dynamic slots name w/ v-for 1`] = `
+"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createForSlots as _createForSlots, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template(" ")
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n2 = _createComponentWithFallback(_component_Comp, null, {
+    $: [
+      () => (_createForSlots(_ctx.list, (item) => ({
+        name: item, 
+        fn: (_slotProps0) => {
+          const n0 = t0()
+          _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["bar"])))
+          return n0
+        }
+      })))
+    ]
+  }, true)
+  return n2
+}"
+`;
+
+exports[`compiler: transform slot > dynamic slots name w/ v-for and provide absent key 1`] = `
+"import { resolveComponent as _resolveComponent, createForSlots as _createForSlots, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template("foo")
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n2 = _createComponentWithFallback(_component_Comp, null, {
+    $: [
+      () => (_createForSlots(_ctx.list, (_, __, index) => ({
+        name: index, 
+        fn: () => {
+          const n0 = t0()
+          return n0
+        }
+      })))
+    ]
+  }, true)
+  return n2
+}"
+`;
+
+exports[`compiler: transform slot > dynamic slots name w/ v-if / v-else[-if] 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template("condition slot")
+const t1 = _template("another condition")
+const t2 = _template("else condition")
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n6 = _createComponentWithFallback(_component_Comp, null, {
+    $: [
+      () => (_ctx.condition
+        ? {
+          name: "condition", 
+          fn: () => {
+            const n0 = t0()
+            return n0
+          }
+        }
+        : _ctx.anotherCondition
+          ? {
+            name: "condition", 
+            fn: (_slotProps0) => {
+              const n2 = t1()
+              return n2
+            }
+          }
+          : {
+            name: "condition", 
+            fn: () => {
+              const n4 = t2()
+              return n4
+            }
+          })
+    ]
+  }, true)
+  return n6
+}"
+`;
+
+exports[`compiler: transform slot > implicit default slot 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n1 = _createComponentWithFallback(_component_Comp, null, {
+    "default": () => {
+      const n0 = t0()
+      return n0
+    }
+  }, true)
+  return n1
+}"
+`;
+
+exports[`compiler: transform slot > named slots w/ implicit default slot 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template("foo")
+const t1 = _template("bar")
+const t2 = _template("<span></span>")
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n4 = _createComponentWithFallback(_component_Comp, null, {
+    "one": () => {
+      const n0 = t0()
+      return n0
+    }, 
+    "default": () => {
+      const n2 = t1()
+      const n3 = t2()
+      return [n2, n3]
+    }
+  }, true)
+  return n4
+}"
+`;
+
+exports[`compiler: transform slot > nested component slot 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_B = _resolveComponent("B")
+  const _component_A = _resolveComponent("A")
+  const n1 = _createComponentWithFallback(_component_A, null, {
+    "default": () => {
+      const n0 = _createComponentWithFallback(_component_B)
+      return n0
+    }
+  }, true)
+  return n1
+}"
+`;
+
+exports[`compiler: transform slot > nested slots scoping 1`] = `
+"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template(" ")
+
+export function render(_ctx) {
+  const _component_Inner = _resolveComponent("Inner")
+  const _component_Comp = _resolveComponent("Comp")
+  const n5 = _createComponentWithFallback(_component_Comp, null, {
+    "default": (_slotProps0) => {
+      const n1 = _createComponentWithFallback(_component_Inner, null, {
+        "default": (_slotProps1) => {
+          const n0 = t0()
+          _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _slotProps1["bar"] + _ctx.baz)))
+          return n0
+        }
+      })
+      const n3 = t0()
+      _renderEffect(() => _setText(n3, " " + _toDisplayString(_slotProps0["foo"] + _ctx.bar + _ctx.baz)))
+      return [n1, n3]
+    }
+  }, true)
+  return n5
+}"
+`;
+
+exports[`compiler: transform slot > on component dynamically named slot 1`] = `
+"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template(" ")
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n1 = _createComponentWithFallback(_component_Comp, null, {
+    $: [
+      () => ({
+        name: _ctx.named, 
+        fn: (_slotProps0) => {
+          const n0 = t0()
+          _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _ctx.bar)))
+          return n0
+        }
+      })
+    ]
+  }, true)
+  return n1
+}"
+`;
+
+exports[`compiler: transform slot > on component named slot 1`] = `
+"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template(" ")
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n1 = _createComponentWithFallback(_component_Comp, null, {
+    "named": (_slotProps0) => {
+      const n0 = t0()
+      _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _ctx.bar)))
+      return n0
+    }
+  }, true)
+  return n1
+}"
+`;
+
+exports[`compiler: transform slot > on-component default slot 1`] = `
+"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template(" ")
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n1 = _createComponentWithFallback(_component_Comp, null, {
+    "default": (_slotProps0) => {
+      const n0 = t0()
+      _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _ctx.bar)))
+      return n0
+    }
+  }, true)
+  return n1
+}"
+`;
+
+exports[`compiler: transform slot > quote slot name 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n1 = _createComponentWithFallback(_component_Comp, null, {
+    "nav-bar-title-before": () => {
+      return null
+    }
+  }, true)
+  return n1
+}"
+`;
+
+exports[`compiler: transform slot > slot + v-if / v-else[-if] should not cause error 1`] = `
+"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createSlot as _createSlot, createComponentWithFallback as _createComponentWithFallback, createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const _component_Foo = _resolveComponent("Foo")
+  const _component_Bar = _resolveComponent("Bar")
+  const n6 = t0()
+  _setInsertionState(n6)
+  const n0 = _createSlot("foo", null)
+  _setInsertionState(n6)
+  const n1 = _createIf(() => (true), () => {
+    const n3 = _createComponentWithFallback(_component_Foo)
+    return n3
+  }, () => {
+    const n5 = _createComponentWithFallback(_component_Bar)
+    return n5
+  })
+  return n6
+}"
+`;
+
+exports[`compiler: transform slot > with whitespace: 'preserve' > implicit default slot 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template(" Header ")
+const t1 = _template(" ")
+const t2 = _template("<p></p>")
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n4 = _createComponentWithFallback(_component_Comp, null, {
+    "header": () => {
+      const n0 = t0()
+      return n0
+    }, 
+    "default": () => {
+      const n2 = t1()
+      const n3 = t2()
+      return [n2, n3]
+    }
+  }, true)
+  return n4
+}"
+`;
+
+exports[`compiler: transform slot > with whitespace: 'preserve' > named default slot + implicit whitespace content 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template(" Header ")
+const t1 = _template(" Default ")
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n5 = _createComponentWithFallback(_component_Comp, null, {
+    "header": () => {
+      const n0 = t0()
+      return n0
+    }, 
+    "default": () => {
+      const n3 = t1()
+      return n3
+    }
+  }, true)
+  return n5
+}"
+`;
+
+exports[`compiler: transform slot > with whitespace: 'preserve' > should not generate whitespace only default slot 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template(" Header ")
+const t1 = _template(" Footer ")
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n5 = _createComponentWithFallback(_component_Comp, null, {
+    "header": () => {
+      const n0 = t0()
+      return n0
+    }, 
+    "footer": () => {
+      const n3 = t1()
+      return n3
+    }
+  }, true)
+  return n5
+}"
+`;
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap
new file mode 100644
index 00000000000..9a3b88acba3
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap
@@ -0,0 +1,35 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`v-text > should convert v-text to setText 1`] = `
+"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div> </div>", true)
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+  const n0 = t0()
+  const x0 = _child(n0)
+  _renderEffect(() => _setText(x0, _toDisplayString(_ctx.str)))
+  return n0
+}"
+`;
+
+exports[`v-text > should raise error and ignore children when v-text is present 1`] = `
+"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div> </div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  const x0 = _child(n0)
+  _renderEffect(() => _setText(x0, _toDisplayString(_ctx.test)))
+  return n0
+}"
+`;
+
+exports[`v-text > should raise error if has no expression 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
diff --git a/packages/compiler-vapor/__tests__/transforms/_utils.ts b/packages/compiler-vapor/__tests__/transforms/_utils.ts
new file mode 100644
index 00000000000..1b6e3f18ab1
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/_utils.ts
@@ -0,0 +1,37 @@
+import type { RootNode } from '@vue/compiler-dom'
+import {
+  type CompilerOptions,
+  type RootIRNode,
+  generate,
+  parse,
+  transform,
+} from '../../src'
+
+export function makeCompile(options: CompilerOptions = {}) {
+  return (
+    template: string,
+    overrideOptions: CompilerOptions = {},
+  ): {
+    ast: RootNode
+    ir: RootIRNode
+    code: string
+    helpers: Set<string>
+  } => {
+    const ast = parse(template, {
+      prefixIdentifiers: true,
+      ...options,
+      ...overrideOptions,
+    })
+    const ir = transform(ast, {
+      prefixIdentifiers: true,
+      ...options,
+      ...overrideOptions,
+    })
+    const { code, helpers } = generate(ir, {
+      prefixIdentifiers: true,
+      ...options,
+      ...overrideOptions,
+    })
+    return { ast, ir, code, helpers }
+  }
+}
diff --git a/packages/compiler-vapor/__tests__/transforms/expression.spec.ts b/packages/compiler-vapor/__tests__/transforms/expression.spec.ts
new file mode 100644
index 00000000000..9257a714b13
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/expression.spec.ts
@@ -0,0 +1,71 @@
+import { BindingTypes } from '@vue/compiler-dom'
+import {
+  transformChildren,
+  transformElement,
+  transformText,
+  transformVBind,
+} from '../../src'
+import { makeCompile } from './_utils'
+
+const compileWithExpression = makeCompile({
+  nodeTransforms: [transformElement, transformChildren, transformText],
+  directiveTransforms: { bind: transformVBind },
+})
+
+describe('compiler: expression', () => {
+  test('basic', () => {
+    const { code } = compileWithExpression(`{{ a }}`)
+    expect(code).toMatchSnapshot()
+    expect(code).contains(`ctx.a`)
+  })
+
+  test('props', () => {
+    const { code } = compileWithExpression(`{{ foo }}`, {
+      bindingMetadata: { foo: BindingTypes.PROPS },
+    })
+    expect(code).toMatchSnapshot()
+    expect(code).contains(`$props.foo`)
+  })
+
+  test('props aliased', () => {
+    const { code } = compileWithExpression(`{{ foo }}`, {
+      bindingMetadata: {
+        foo: BindingTypes.PROPS_ALIASED,
+        __propsAliases: { foo: 'bar' } as any,
+      },
+    })
+    expect(code).toMatchSnapshot()
+    expect(code).contains(`$props['bar']`)
+  })
+
+  test('update expression', () => {
+    const { code } = compileWithExpression(`
+      <div :id="String(foo.id++)" :foo="foo" :bar="bar++">
+        {{ String(foo.id++) }} {{ foo }} {{ bar }}
+      </div>
+    `)
+    expect(code).toMatchSnapshot()
+    expect(code).contains(`_String(_foo.id++)`)
+  })
+
+  test('empty interpolation', () => {
+    const { code } = compileWithExpression(`{{}}`)
+    const { code: code2 } = compileWithExpression(`{{ }}`)
+    const { code: code3 } = compileWithExpression(`<div>{{ }}</div>`)
+    const { code: code4 } = compileWithExpression(`<div>{{ foo }}{{ }}</div>`)
+
+    expect(code).toMatchSnapshot()
+    expect(code).not.toContain(`_toDisplayString`)
+    expect(code).not.toContain(`_setText`)
+
+    expect(code2).toMatchSnapshot()
+    expect(code2).not.toContain(`_toDisplayString`)
+    expect(code2).not.toContain(`_setText`)
+
+    expect(code3).toMatchSnapshot()
+    expect(code3).not.toContain(`_toDisplayString`)
+    expect(code3).not.toContain(`_setText`)
+
+    expect(code4).toMatchSnapshot()
+  })
+})
diff --git a/packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts
new file mode 100644
index 00000000000..2d8ae8c960d
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts
@@ -0,0 +1,76 @@
+import { makeCompile } from './_utils'
+import {
+  transformChildren,
+  transformElement,
+  transformText,
+  transformVIf,
+} from '../../src'
+
+const compileWithElementTransform = makeCompile({
+  nodeTransforms: [
+    transformText,
+    transformVIf,
+    transformElement,
+    transformChildren,
+  ],
+})
+
+describe('compiler: children transform', () => {
+  test('children & sibling references', () => {
+    const { code, helpers } = compileWithElementTransform(
+      `<div>
+        <p>{{ first }}</p>
+        {{ second }}
+        {{ third }}
+        <p>{{ forth }}</p>
+      </div>`,
+    )
+    expect(code).toMatchSnapshot()
+    expect(Array.from(helpers)).containSubset([
+      'child',
+      'toDisplayString',
+      'renderEffect',
+      'next',
+      'setText',
+      'template',
+    ])
+  })
+
+  test('efficient traversal', () => {
+    const { code } = compileWithElementTransform(
+      `<div>
+    <div>x</div>
+    <div><span>{{ msg }}</span></div>
+    <div><span>{{ msg }}</span></div>
+    <div><span>{{ msg }}</span></div>
+  </div>`,
+    )
+    expect(code).toMatchSnapshot()
+  })
+
+  test('efficient find', () => {
+    const { code } = compileWithElementTransform(
+      `<div>
+        <div>x</div>
+        <div>x</div>
+        <div>{{ msg }}</div>
+      </div>`,
+    )
+    expect(code).contains(`const n0 = _nthChild(n1, 2)`)
+    expect(code).toMatchSnapshot()
+  })
+
+  test('anchor insertion in middle', () => {
+    const { code } = compileWithElementTransform(
+      `<div>
+        <div></div>
+        <div v-if="1"></div>
+        <div></div>
+      </div>`,
+    )
+    // ensure the insertion anchor is generated before the insertion statement
+    expect(code).toMatch(`const n3 = _next(_child(n4))`)
+    expect(code).toMatch(`_setInsertionState(n4, n3)`)
+    expect(code).toMatchSnapshot()
+  })
+})
diff --git a/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts
new file mode 100644
index 00000000000..a693db4ad39
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts
@@ -0,0 +1,959 @@
+import { makeCompile } from './_utils'
+import {
+  IRDynamicPropsKind,
+  IRNodeTypes,
+  transformChildren,
+  transformElement,
+  transformText,
+  transformVBind,
+  transformVFor,
+  transformVOn,
+} from '../../src'
+import {
+  type BindingMetadata,
+  BindingTypes,
+  NodeTypes,
+} from '@vue/compiler-dom'
+
+const compileWithElementTransform = makeCompile({
+  nodeTransforms: [
+    transformVFor,
+    transformElement,
+    transformChildren,
+    transformText,
+  ],
+  directiveTransforms: {
+    bind: transformVBind,
+    on: transformVOn,
+  },
+})
+
+describe('compiler: element transform', () => {
+  describe('component', () => {
+    test('import + resolve component', () => {
+      const { code, ir, helpers } = compileWithElementTransform(`<Foo/>`)
+      expect(code).toMatchSnapshot()
+      expect(helpers).contains.all.keys('resolveComponent')
+      expect(helpers).contains.all.keys('createComponentWithFallback')
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        id: 0,
+        tag: 'Foo',
+        asset: true,
+        root: true,
+        props: [[]],
+      })
+    })
+
+    test('resolve implicitly self-referencing component', () => {
+      const { code, helpers } = compileWithElementTransform(`<Example/>`, {
+        filename: `/foo/bar/Example.vue?vue&type=template`,
+      })
+      expect(code).toMatchSnapshot()
+      expect(code).toContain('_resolveComponent("Example", true)')
+      expect(helpers).toContain('resolveComponent')
+    })
+
+    test('resolve component from setup bindings', () => {
+      const { code, ir, helpers } = compileWithElementTransform(`<Example/>`, {
+        bindingMetadata: {
+          Example: BindingTypes.SETUP_MAYBE_REF,
+        },
+      })
+      expect(code).toMatchSnapshot()
+      expect(helpers).not.toContain('resolveComponent')
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        tag: 'Example',
+        asset: false,
+      })
+    })
+
+    test('resolve component from setup bindings (inline)', () => {
+      const { code, helpers } = compileWithElementTransform(`<Example/>`, {
+        inline: true,
+        bindingMetadata: {
+          Example: BindingTypes.SETUP_MAYBE_REF,
+        },
+      })
+      expect(code).toMatchSnapshot()
+      expect(code).contains(`unref(Example)`)
+      expect(helpers).not.toContain('resolveComponent')
+      expect(helpers).toContain('unref')
+    })
+
+    test('resolve component from setup bindings (inline const)', () => {
+      const { code, helpers } = compileWithElementTransform(`<Example/>`, {
+        inline: true,
+        bindingMetadata: {
+          Example: BindingTypes.SETUP_CONST,
+        },
+      })
+      expect(code).toMatchSnapshot()
+      expect(helpers).not.toContain('resolveComponent')
+    })
+
+    test('resolve namespaced component from setup bindings', () => {
+      const { code, helpers } = compileWithElementTransform(`<Foo.Example/>`, {
+        bindingMetadata: {
+          Foo: BindingTypes.SETUP_MAYBE_REF,
+        },
+      })
+      expect(code).toMatchSnapshot()
+      expect(code).contains(`_ctx.Foo.Example`)
+      expect(helpers).not.toContain('resolveComponent')
+    })
+
+    test('resolve namespaced component from setup bindings (inline const)', () => {
+      const { code, helpers } = compileWithElementTransform(`<Foo.Example/>`, {
+        inline: true,
+        bindingMetadata: {
+          Foo: BindingTypes.SETUP_CONST,
+        },
+      })
+      expect(code).toMatchSnapshot()
+      expect(code).contains(`Foo.Example`)
+      expect(helpers).not.toContain('resolveComponent')
+    })
+
+    test('resolve namespaced component from props bindings (inline)', () => {
+      const { code, helpers } = compileWithElementTransform(`<Foo.Example/>`, {
+        inline: true,
+        bindingMetadata: {
+          Foo: BindingTypes.PROPS,
+        },
+      })
+      expect(code).toMatchSnapshot()
+      expect(code).contains(`Foo.Example`)
+      expect(helpers).not.toContain('resolveComponent')
+    })
+
+    test('resolve namespaced component from props bindings (non-inline)', () => {
+      const { code, helpers } = compileWithElementTransform(`<Foo.Example/>`, {
+        inline: false,
+        bindingMetadata: {
+          Foo: BindingTypes.PROPS,
+        },
+      })
+      expect(code).toMatchSnapshot()
+      expect(code).contains('_ctx.Foo.Example')
+      expect(helpers).not.toContain('resolveComponent')
+    })
+
+    test('do not resolve component from non-script-setup bindings', () => {
+      const bindingMetadata: BindingMetadata = {
+        Example: BindingTypes.SETUP_MAYBE_REF,
+      }
+      Object.defineProperty(bindingMetadata, '__isScriptSetup', {
+        value: false,
+      })
+      const { code, ir, helpers } = compileWithElementTransform(`<Example/>`, {
+        bindingMetadata,
+      })
+      expect(code).toMatchSnapshot()
+      expect(helpers).toContain('resolveComponent')
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        id: 0,
+        tag: 'Example',
+        asset: true,
+      })
+    })
+
+    test('generate single root component', () => {
+      const { code } = compileWithElementTransform(`<Comp/>`, {
+        bindingMetadata: { Comp: BindingTypes.SETUP_CONST },
+      })
+      expect(code).toMatchSnapshot()
+      expect(code).contains('_createComponent(_ctx.Comp, null, null, true)')
+    })
+
+    test('generate multi root component', () => {
+      const { code } = compileWithElementTransform(`<Comp/>123`, {
+        bindingMetadata: { Comp: BindingTypes.SETUP_CONST },
+      })
+      expect(code).toMatchSnapshot()
+      expect(code).contains('_createComponent(_ctx.Comp)')
+    })
+
+    test('v-for on component should not mark as single root', () => {
+      const { code } = compileWithElementTransform(
+        `<Comp v-for="item in items" :key="item"/>`,
+        {
+          bindingMetadata: { Comp: BindingTypes.SETUP_CONST },
+        },
+      )
+      expect(code).toMatchSnapshot()
+      expect(code).contains('_createComponent(_ctx.Comp)')
+    })
+
+    test('static props', () => {
+      const { code, ir } = compileWithElementTransform(
+        `<Foo id="foo" class="bar" />`,
+      )
+
+      expect(code).toMatchSnapshot()
+      expect(code).contains(`{
+    id: () => ("foo"), 
+    class: () => ("bar")
+  }`)
+
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        tag: 'Foo',
+        asset: true,
+        root: true,
+        props: [
+          [
+            {
+              key: {
+                type: NodeTypes.SIMPLE_EXPRESSION,
+                content: 'id',
+                isStatic: true,
+              },
+              values: [
+                {
+                  type: NodeTypes.SIMPLE_EXPRESSION,
+                  content: 'foo',
+                  isStatic: true,
+                },
+              ],
+            },
+            {
+              key: {
+                type: NodeTypes.SIMPLE_EXPRESSION,
+                content: 'class',
+                isStatic: true,
+              },
+              values: [
+                {
+                  type: NodeTypes.SIMPLE_EXPRESSION,
+                  content: 'bar',
+                  isStatic: true,
+                },
+              ],
+            },
+          ],
+        ],
+      })
+    })
+
+    test('v-bind="obj"', () => {
+      const { code, ir } = compileWithElementTransform(`<Foo v-bind="obj" />`)
+      expect(code).toMatchSnapshot()
+      expect(code).contains(`[
+    () => (_ctx.obj)
+  ]`)
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        tag: 'Foo',
+        props: [
+          {
+            kind: IRDynamicPropsKind.EXPRESSION,
+            value: { content: 'obj', isStatic: false },
+          },
+        ],
+      })
+    })
+
+    test('v-bind="obj" after static prop', () => {
+      const { code, ir } = compileWithElementTransform(
+        `<Foo id="foo" v-bind="obj" />`,
+      )
+      expect(code).toMatchSnapshot()
+      expect(code).contains(`{
+    id: () => ("foo"), 
+    $: [
+      () => (_ctx.obj)
+    ]
+  }`)
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        tag: 'Foo',
+        props: [
+          [{ key: { content: 'id' }, values: [{ content: 'foo' }] }],
+          {
+            kind: IRDynamicPropsKind.EXPRESSION,
+            value: { content: 'obj' },
+          },
+        ],
+      })
+    })
+
+    test('v-bind="obj" before static prop', () => {
+      const { code, ir } = compileWithElementTransform(
+        `<Foo v-bind="obj" id="foo" />`,
+      )
+      expect(code).toMatchSnapshot()
+      expect(code).contains(`[
+    () => (_ctx.obj), 
+    { id: () => ("foo") }
+  ]`)
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        tag: 'Foo',
+        props: [
+          {
+            kind: IRDynamicPropsKind.EXPRESSION,
+            value: { content: 'obj' },
+          },
+          [{ key: { content: 'id' }, values: [{ content: 'foo' }] }],
+        ],
+      })
+    })
+
+    test('v-bind="obj" between static props', () => {
+      const { code, ir } = compileWithElementTransform(
+        `<Foo id="foo" v-bind="obj" class="bar" />`,
+      )
+      expect(code).toMatchSnapshot()
+      expect(code).contains(`{
+    id: () => ("foo"), 
+    $: [
+      () => (_ctx.obj), 
+      { class: () => ("bar") }
+    ]
+  }`)
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        tag: 'Foo',
+        props: [
+          [{ key: { content: 'id' }, values: [{ content: 'foo' }] }],
+          {
+            kind: IRDynamicPropsKind.EXPRESSION,
+            value: { content: 'obj' },
+          },
+          [{ key: { content: 'class' }, values: [{ content: 'bar' }] }],
+        ],
+      })
+    })
+
+    test.todo('props merging: event handlers', () => {
+      const { code, ir } = compileWithElementTransform(
+        `<Foo @click.foo="a" @click.bar="b" />`,
+      )
+      expect(code).toMatchSnapshot()
+      expect(code).contains('onClick: () => [_ctx.a, _ctx.b]')
+      expect(ir.block.operation).toMatchObject([
+        {
+          type: IRNodeTypes.CREATE_COMPONENT_NODE,
+          tag: 'Foo',
+          props: [
+            [
+              {
+                key: { content: 'onClick', isStatic: true },
+                values: [{ content: 'a' }, { content: 'b' }],
+              },
+            ],
+          ],
+        },
+      ])
+    })
+
+    test.todo('props merging: style', () => {
+      const { code } = compileWithElementTransform(
+        `<Foo style="color: green" :style="{ color: 'red' }" />`,
+      )
+      expect(code).toMatchSnapshot()
+    })
+
+    test.todo('props merging: class', () => {
+      const { code } = compileWithElementTransform(
+        `<Foo class="foo" :class="{ bar: isBar }" />`,
+      )
+      expect(code).toMatchSnapshot()
+    })
+
+    test('v-on="obj"', () => {
+      const { code, ir } = compileWithElementTransform(`<Foo v-on="obj" />`)
+      expect(code).toMatchSnapshot()
+      expect(code).contains(`[
+    () => (_toHandlers(_ctx.obj))
+  ]`)
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        tag: 'Foo',
+        props: [
+          {
+            kind: IRDynamicPropsKind.EXPRESSION,
+            value: { content: 'obj' },
+            handler: true,
+          },
+        ],
+      })
+    })
+
+    test('v-on expression is inline statement', () => {
+      const { code, ir } = compileWithElementTransform(
+        `<Foo v-on:bar="() => handler" />`,
+      )
+      expect(code).toMatchSnapshot()
+      expect(code).contains(`onBar: () => _on_bar`)
+      expect(code).contains(`const _on_bar = () => _ctx.handler`)
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        tag: 'Foo',
+        props: [
+          [
+            {
+              key: { content: 'bar' },
+              handler: true,
+              values: [{ content: '_on_bar' }],
+            },
+          ],
+        ],
+      })
+    })
+
+    test('v-on expression is a function call', () => {
+      const { code, ir } = compileWithElementTransform(
+        `<Foo v-on:bar="handleBar($event)" />`,
+      )
+      expect(code).toMatchSnapshot()
+      expect(code).contains(`onBar: () => _on_bar`)
+      expect(code).contains(
+        `const _on_bar = $event => (_ctx.handleBar($event))`,
+      )
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        tag: 'Foo',
+        props: [
+          [
+            {
+              key: { content: 'bar' },
+              handler: true,
+              values: [{ content: '_on_bar' }],
+            },
+          ],
+        ],
+      })
+    })
+
+    test('cache v-on expression with unique handler name', () => {
+      const { code, ir } = compileWithElementTransform(
+        `<Foo v-on:bar="handleBar($event)" /><Bar v-on:bar="() => handler" />`,
+      )
+      expect(code).toMatchSnapshot()
+      expect(code).contains(`onBar: () => _on_bar`)
+      expect(code).contains(
+        `const _on_bar = $event => (_ctx.handleBar($event))`,
+      )
+      expect(code).contains(`onBar: () => _on_bar1`)
+      expect(code).contains(`const _on_bar1 = () => _ctx.handler`)
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        tag: 'Foo',
+        props: [
+          [
+            {
+              key: { content: 'bar' },
+              handler: true,
+              values: [{ content: '_on_bar' }],
+            },
+          ],
+        ],
+      })
+
+      expect(ir.block.dynamic.children[1].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        tag: 'Bar',
+        props: [
+          [
+            {
+              key: { content: 'bar' },
+              handler: true,
+              values: [{ content: '_on_bar1' }],
+            },
+          ],
+        ],
+      })
+    })
+  })
+
+  describe('dynamic component', () => {
+    test('static binding', () => {
+      const { code, ir, helpers } = compileWithElementTransform(
+        `<component is="foo" />`,
+      )
+      expect(code).toMatchSnapshot()
+      expect(helpers).toContain('resolveDynamicComponent')
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        tag: 'component',
+        asset: true,
+        root: true,
+        props: [[]],
+        dynamic: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'foo',
+          isStatic: true,
+        },
+      })
+    })
+
+    test('capitalized version w/ static binding', () => {
+      const { code, ir, helpers } = compileWithElementTransform(
+        `<Component is="foo" />`,
+      )
+      expect(code).toMatchSnapshot()
+      expect(helpers).toContain('resolveDynamicComponent')
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        tag: 'Component',
+        asset: true,
+        root: true,
+        props: [[]],
+        dynamic: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'foo',
+          isStatic: true,
+        },
+      })
+    })
+
+    test('dynamic binding', () => {
+      const { code, ir, helpers } = compileWithElementTransform(
+        `<component :is="foo" />`,
+      )
+      expect(code).toMatchSnapshot()
+      expect(helpers).toContain('createDynamicComponent')
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        tag: 'component',
+        asset: true,
+        root: true,
+        props: [[]],
+        dynamic: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'foo',
+          isStatic: false,
+        },
+      })
+    })
+
+    test('dynamic binding shorthand', () => {
+      const { code, ir, helpers } =
+        compileWithElementTransform(`<component :is />`)
+      expect(code).toMatchSnapshot()
+      expect(helpers).toContain('createDynamicComponent')
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        tag: 'component',
+        asset: true,
+        root: true,
+        props: [[]],
+        dynamic: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'is',
+          isStatic: false,
+        },
+      })
+    })
+
+    // #3934
+    test('normal component with is prop', () => {
+      const { code, ir, helpers } = compileWithElementTransform(
+        `<custom-input is="foo" />`,
+        {
+          isNativeTag: () => false,
+        },
+      )
+      expect(code).toMatchSnapshot()
+      expect(helpers).toContain('resolveComponent')
+      expect(helpers).not.toContain('resolveDynamicComponent')
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        tag: 'custom-input',
+        asset: true,
+        root: true,
+        props: [[{ key: { content: 'is' }, values: [{ content: 'foo' }] }]],
+      })
+    })
+  })
+
+  test('static props', () => {
+    const { code, ir } = compileWithElementTransform(
+      `<div id="foo" class="bar" />`,
+    )
+
+    const template = '<div id="foo" class="bar"></div>'
+    expect(code).toMatchSnapshot()
+    expect(code).contains(JSON.stringify(template))
+    expect(ir.template).toMatchObject([template])
+    expect(ir.block.effect).lengthOf(0)
+  })
+
+  test('props + children', () => {
+    const { code, ir } = compileWithElementTransform(
+      `<div id="foo"><span/></div>`,
+    )
+
+    const template = '<div id="foo"><span></span></div>'
+    expect(code).toMatchSnapshot()
+    expect(code).contains(JSON.stringify(template))
+    expect(ir.template).toMatchObject([template])
+    expect(ir.block.effect).lengthOf(0)
+  })
+
+  test('v-bind="obj"', () => {
+    const { code, ir } = compileWithElementTransform(`<div v-bind="obj" />`)
+    expect(code).toMatchSnapshot()
+    expect(ir.block.effect).toMatchObject([
+      {
+        expressions: [
+          {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: 'obj',
+            isStatic: false,
+          },
+        ],
+        operations: [
+          {
+            type: IRNodeTypes.SET_DYNAMIC_PROPS,
+            element: 0,
+            props: [
+              {
+                kind: IRDynamicPropsKind.EXPRESSION,
+                value: {
+                  type: NodeTypes.SIMPLE_EXPRESSION,
+                  content: 'obj',
+                  isStatic: false,
+                },
+              },
+            ],
+          },
+        ],
+      },
+    ])
+    expect(code).contains('_setDynamicProps(n0, [_ctx.obj], true)')
+  })
+
+  test('v-bind="obj" after static prop', () => {
+    const { code, ir } = compileWithElementTransform(
+      `<div id="foo" v-bind="obj" />`,
+    )
+    expect(code).toMatchSnapshot()
+    expect(ir.block.effect).toMatchObject([
+      {
+        expressions: [
+          {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: 'obj',
+            isStatic: false,
+          },
+        ],
+        operations: [
+          {
+            type: IRNodeTypes.SET_DYNAMIC_PROPS,
+            element: 0,
+            props: [
+              [{ key: { content: 'id' }, values: [{ content: 'foo' }] }],
+              {
+                kind: IRDynamicPropsKind.EXPRESSION,
+                value: {
+                  type: NodeTypes.SIMPLE_EXPRESSION,
+                  content: 'obj',
+                  isStatic: false,
+                },
+              },
+            ],
+          },
+        ],
+      },
+    ])
+    expect(code).contains(
+      '_setDynamicProps(n0, [{ id: "foo" }, _ctx.obj], true)',
+    )
+  })
+
+  test('v-bind="obj" before static prop', () => {
+    const { code, ir } = compileWithElementTransform(
+      `<div v-bind="obj" id="foo" />`,
+    )
+    expect(code).toMatchSnapshot()
+    expect(ir.block.effect).toMatchObject([
+      {
+        expressions: [{ content: 'obj' }],
+        operations: [
+          {
+            type: IRNodeTypes.SET_DYNAMIC_PROPS,
+            element: 0,
+            props: [
+              {
+                kind: IRDynamicPropsKind.EXPRESSION,
+                value: { content: 'obj' },
+              },
+              [{ key: { content: 'id' }, values: [{ content: 'foo' }] }],
+            ],
+          },
+        ],
+      },
+    ])
+    expect(code).contains(
+      '_setDynamicProps(n0, [_ctx.obj, { id: "foo" }], true)',
+    )
+  })
+
+  test('v-bind="obj" between static props', () => {
+    const { code, ir } = compileWithElementTransform(
+      `<div id="foo" v-bind="obj" class="bar" />`,
+    )
+    expect(code).toMatchSnapshot()
+    expect(ir.block.effect).toMatchObject([
+      {
+        expressions: [{ content: 'obj' }],
+        operations: [
+          {
+            type: IRNodeTypes.SET_DYNAMIC_PROPS,
+            element: 0,
+            props: [
+              [{ key: { content: 'id' }, values: [{ content: 'foo' }] }],
+              {
+                kind: IRDynamicPropsKind.EXPRESSION,
+                value: { content: 'obj' },
+              },
+              [{ key: { content: 'class' }, values: [{ content: 'bar' }] }],
+            ],
+          },
+        ],
+      },
+    ])
+    expect(code).contains(
+      '_setDynamicProps(n0, [{ id: "foo" }, _ctx.obj, { class: "bar" }], true)',
+    )
+  })
+
+  test('props merging: event handlers', () => {
+    const { code, ir } = compileWithElementTransform(
+      `<div @click.foo="a" @click.bar="b" />`,
+    )
+    expect(code).toMatchSnapshot()
+
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        element: 0,
+        key: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'click',
+          isStatic: true,
+        },
+        value: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'a',
+          isStatic: false,
+        },
+        keyOverride: undefined,
+        delegate: true,
+        effect: false,
+      },
+      {
+        type: IRNodeTypes.SET_EVENT,
+        element: 0,
+        key: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'click',
+          isStatic: true,
+        },
+        value: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'b',
+          isStatic: false,
+        },
+        keyOverride: undefined,
+        delegate: true,
+        effect: false,
+      },
+    ])
+  })
+
+  test('props merging: style', () => {
+    const { code, ir } = compileWithElementTransform(
+      `<div style="color: green" :style="{ color: 'red' }" />`,
+    )
+    expect(code).toMatchSnapshot()
+
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_PROP,
+        element: 0,
+        prop: {
+          key: {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: 'style',
+            isStatic: true,
+          },
+          values: [
+            {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: 'color: green',
+              isStatic: true,
+            },
+            {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: `{ color: 'red' }`,
+              isStatic: false,
+            },
+          ],
+        },
+      },
+    ])
+  })
+
+  test('props merging: class', () => {
+    const { code, ir } = compileWithElementTransform(
+      `<div class="foo" :class="{ bar: isBar }" />`,
+    )
+
+    expect(code).toMatchSnapshot()
+
+    expect(ir.block.effect).toMatchObject([
+      {
+        expressions: [
+          {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: `{ bar: isBar }`,
+            isStatic: false,
+          },
+        ],
+        operations: [
+          {
+            type: IRNodeTypes.SET_PROP,
+            element: 0,
+            prop: {
+              key: {
+                type: NodeTypes.SIMPLE_EXPRESSION,
+                content: 'class',
+                isStatic: true,
+              },
+              values: [
+                {
+                  type: NodeTypes.SIMPLE_EXPRESSION,
+                  content: `foo`,
+                  isStatic: true,
+                },
+                {
+                  type: NodeTypes.SIMPLE_EXPRESSION,
+                  content: `{ bar: isBar }`,
+                  isStatic: false,
+                },
+              ],
+            },
+          },
+        ],
+      },
+    ])
+  })
+
+  test('v-on="obj"', () => {
+    const { code, ir } = compileWithElementTransform(`<div v-on="obj" />`)
+    expect(code).toMatchSnapshot()
+    expect(ir.block.effect).toMatchObject([
+      {
+        expressions: [
+          {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: 'obj',
+            isStatic: false,
+          },
+        ],
+        operations: [
+          {
+            type: IRNodeTypes.SET_DYNAMIC_EVENTS,
+            element: 0,
+            event: {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: 'obj',
+              isStatic: false,
+            },
+          },
+        ],
+      },
+    ])
+    expect(code).contains('_setDynamicEvents(n0, _ctx.obj)')
+  })
+
+  test('component with dynamic prop arguments', () => {
+    const { code, ir } = compileWithElementTransform(
+      `<Foo :[foo-bar]="bar" :[baz]="qux" />`,
+    )
+    expect(code).toMatchSnapshot()
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.CREATE_COMPONENT_NODE,
+      tag: 'Foo',
+      props: [
+        {
+          kind: IRDynamicPropsKind.ATTRIBUTE,
+          key: { content: 'foo-bar' },
+          values: [{ content: 'bar' }],
+        },
+        {
+          kind: IRDynamicPropsKind.ATTRIBUTE,
+          key: { content: 'baz' },
+          values: [{ content: 'qux' }],
+        },
+      ],
+    })
+  })
+
+  test('component with dynamic event arguments', () => {
+    const { code, ir } = compileWithElementTransform(
+      `<Foo @[foo-bar]="bar" @[baz]="qux" />`,
+    )
+    expect(code).toMatchSnapshot()
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.CREATE_COMPONENT_NODE,
+      tag: 'Foo',
+      props: [
+        {
+          kind: IRDynamicPropsKind.ATTRIBUTE,
+          key: { content: 'foo-bar' },
+          values: [{ content: 'bar' }],
+          handler: true,
+        },
+        {
+          kind: IRDynamicPropsKind.ATTRIBUTE,
+          key: { content: 'baz' },
+          values: [{ content: 'qux' }],
+          handler: true,
+        },
+      ],
+    })
+  })
+
+  test('component event with once modifier', () => {
+    const { code } = compileWithElementTransform(`<Foo @foo.once="bar" />`)
+    expect(code).toMatchSnapshot()
+  })
+
+  test('component dynamic event with once modifier', () => {
+    const { code } = compileWithElementTransform(`<Foo @[foo].once="bar" />`)
+    expect(code).toMatchSnapshot()
+  })
+
+  test('invalid html nesting', () => {
+    const { code, ir } = compileWithElementTransform(
+      `<p><div>123</div></p>
+      <form><form/></form>`,
+    )
+    expect(code).toMatchSnapshot()
+    expect(ir.template).toEqual(['<div>123</div>', '<p></p>', '<form></form>'])
+    expect(ir.block.dynamic).toMatchObject({
+      children: [
+        { id: 1, template: 1, children: [{ id: 0, template: 0 }] },
+        { id: 3, template: 2, children: [{ id: 2, template: 2 }] },
+      ],
+    })
+
+    expect(ir.block.operation).toMatchObject([
+      { type: IRNodeTypes.INSERT_NODE, parent: 1, elements: [0] },
+      { type: IRNodeTypes.INSERT_NODE, parent: 3, elements: [2] },
+    ])
+  })
+
+  test('empty template', () => {
+    const { code } = compileWithElementTransform('')
+    expect(code).toMatchSnapshot()
+    expect(code).contain('return null')
+  })
+})
diff --git a/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts
new file mode 100644
index 00000000000..389c665a12f
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts
@@ -0,0 +1,280 @@
+import { ErrorCodes, NodeTypes } from '@vue/compiler-dom'
+import {
+  IRNodeTypes,
+  transformChildren,
+  transformElement,
+  transformSlotOutlet,
+  transformText,
+  transformVBind,
+  transformVOn,
+  transformVShow,
+} from '../../src'
+import { makeCompile } from './_utils'
+
+const compileWithSlotsOutlet = makeCompile({
+  nodeTransforms: [
+    transformText,
+    transformSlotOutlet,
+    transformElement,
+    transformChildren,
+  ],
+  directiveTransforms: {
+    bind: transformVBind,
+    on: transformVOn,
+    show: transformVShow,
+  },
+})
+
+describe('compiler: transform <slot> outlets', () => {
+  test('default slot outlet', () => {
+    const { ir, code, helpers } = compileWithSlotsOutlet(`<slot />`)
+    expect(code).toMatchSnapshot()
+    expect(helpers).toContain('createSlot')
+    expect(ir.block.effect).toEqual([])
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.SLOT_OUTLET_NODE,
+      id: 0,
+      name: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'default',
+        isStatic: true,
+      },
+      props: [],
+      fallback: undefined,
+    })
+  })
+
+  test('statically named slot outlet', () => {
+    const { ir, code } = compileWithSlotsOutlet(`<slot name="foo" />`)
+    expect(code).toMatchSnapshot()
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.SLOT_OUTLET_NODE,
+      id: 0,
+      name: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'foo',
+        isStatic: true,
+      },
+    })
+  })
+
+  test('dynamically named slot outlet', () => {
+    const { ir, code } = compileWithSlotsOutlet(`<slot :name="foo + bar" />`)
+    expect(code).toMatchSnapshot()
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.SLOT_OUTLET_NODE,
+      id: 0,
+      name: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'foo + bar',
+        isStatic: false,
+      },
+    })
+  })
+
+  test('dynamically named slot outlet with v-bind shorthand', () => {
+    const { ir, code } = compileWithSlotsOutlet(`<slot :name />`)
+    expect(code).toMatchSnapshot()
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.SLOT_OUTLET_NODE,
+      id: 0,
+      name: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'name',
+        isStatic: false,
+      },
+    })
+  })
+
+  test('default slot outlet with props', () => {
+    const { ir, code } = compileWithSlotsOutlet(
+      `<slot foo="bar" :baz="qux" :foo-bar="foo-bar" />`,
+    )
+    expect(code).toMatchSnapshot()
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.SLOT_OUTLET_NODE,
+      name: { content: 'default' },
+      props: [
+        [
+          { key: { content: 'foo' }, values: [{ content: 'bar' }] },
+          { key: { content: 'baz' }, values: [{ content: 'qux' }] },
+          { key: { content: 'fooBar' }, values: [{ content: 'foo-bar' }] },
+        ],
+      ],
+    })
+  })
+
+  test('statically named slot outlet with props', () => {
+    const { ir, code } = compileWithSlotsOutlet(
+      `<slot name="foo" foo="bar" :baz="qux" />`,
+    )
+    expect(code).toMatchSnapshot()
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.SLOT_OUTLET_NODE,
+      name: { content: 'foo' },
+      props: [
+        [
+          { key: { content: 'foo' }, values: [{ content: 'bar' }] },
+          { key: { content: 'baz' }, values: [{ content: 'qux' }] },
+        ],
+      ],
+    })
+  })
+
+  test('statically named slot outlet with v-bind="obj"', () => {
+    const { ir, code } = compileWithSlotsOutlet(
+      `<slot name="foo" foo="bar" v-bind="obj" :baz="qux" />`,
+    )
+    expect(code).toMatchSnapshot()
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.SLOT_OUTLET_NODE,
+      name: { content: 'foo' },
+      props: [
+        [{ key: { content: 'foo' }, values: [{ content: 'bar' }] }],
+        { value: { content: 'obj', isStatic: false } },
+        [{ key: { content: 'baz' }, values: [{ content: 'qux' }] }],
+      ],
+    })
+  })
+
+  test('statically named slot outlet with v-on', () => {
+    const { ir, code } = compileWithSlotsOutlet(
+      `<slot @click="foo" v-on="bar" :baz="qux" />`,
+    )
+    expect(code).toMatchSnapshot()
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.SLOT_OUTLET_NODE,
+      props: [
+        [{ key: { content: 'click' }, values: [{ content: 'foo' }] }],
+        { value: { content: 'bar' }, handler: true },
+        [{ key: { content: 'baz' }, values: [{ content: 'qux' }] }],
+      ],
+    })
+  })
+
+  test('default slot outlet with fallback', () => {
+    const { ir, code } = compileWithSlotsOutlet(`<slot><div/></slot>`)
+    expect(code).toMatchSnapshot()
+    expect(ir.template[0]).toBe('<div></div>')
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.SLOT_OUTLET_NODE,
+      id: 0,
+      name: { content: 'default' },
+      fallback: {
+        type: IRNodeTypes.BLOCK,
+        dynamic: {
+          children: [{ template: 0, id: 2 }],
+        },
+        returns: [2],
+      },
+    })
+  })
+
+  test('named slot outlet with fallback', () => {
+    const { ir, code } = compileWithSlotsOutlet(
+      `<slot name="foo"><div/></slot>`,
+    )
+    expect(code).toMatchSnapshot()
+    expect(ir.template[0]).toBe('<div></div>')
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.SLOT_OUTLET_NODE,
+      id: 0,
+      name: { content: 'foo' },
+      fallback: {
+        type: IRNodeTypes.BLOCK,
+        dynamic: {
+          children: [{ template: 0, id: 2 }],
+        },
+        returns: [2],
+      },
+    })
+  })
+
+  test('default slot outlet with props & fallback', () => {
+    const { ir, code } = compileWithSlotsOutlet(
+      `<slot :foo="bar"><div/></slot>`,
+    )
+    expect(code).toMatchSnapshot()
+    expect(ir.template[0]).toBe('<div></div>')
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.SLOT_OUTLET_NODE,
+      id: 0,
+      name: { content: 'default' },
+      props: [[{ key: { content: 'foo' }, values: [{ content: 'bar' }] }]],
+      fallback: {
+        type: IRNodeTypes.BLOCK,
+        dynamic: {
+          children: [{ template: 0, id: 2 }],
+        },
+        returns: [2],
+      },
+    })
+  })
+
+  test('named slot outlet with props & fallback', () => {
+    const { ir, code } = compileWithSlotsOutlet(
+      `<slot name="foo" :foo="bar"><div/></slot>`,
+    )
+    expect(code).toMatchSnapshot()
+    expect(ir.template[0]).toBe('<div></div>')
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.SLOT_OUTLET_NODE,
+      id: 0,
+      name: { content: 'foo' },
+      props: [[{ key: { content: 'foo' }, values: [{ content: 'bar' }] }]],
+      fallback: {
+        type: IRNodeTypes.BLOCK,
+        dynamic: {
+          children: [{ template: 0, id: 2 }],
+        },
+        returns: [2],
+      },
+    })
+  })
+
+  test('error on unexpected custom directive on <slot>', () => {
+    const onError = vi.fn()
+    const source = `<slot v-foo />`
+    const index = source.indexOf('v-foo')
+    const { code } = compileWithSlotsOutlet(source, { onError })
+    expect(code).toMatchSnapshot()
+    expect(onError.mock.calls[0][0]).toMatchObject({
+      code: ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
+      loc: {
+        start: {
+          offset: index,
+          line: 1,
+          column: index + 1,
+        },
+        end: {
+          offset: index + 5,
+          line: 1,
+          column: index + 6,
+        },
+      },
+    })
+  })
+
+  test('error on unexpected custom directive with v-show on <slot>', () => {
+    const onError = vi.fn()
+    const source = `<slot v-show="ok" />`
+    const index = source.indexOf('v-show="ok"')
+    const { code } = compileWithSlotsOutlet(source, { onError })
+    expect(code).toMatchSnapshot()
+    expect(onError.mock.calls[0][0]).toMatchObject({
+      code: ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
+      loc: {
+        start: {
+          offset: index,
+          line: 1,
+          column: index + 1,
+        },
+        end: {
+          offset: index + 11,
+          line: 1,
+          column: index + 12,
+        },
+      },
+    })
+  })
+})
diff --git a/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts
new file mode 100644
index 00000000000..2c883d10cc6
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts
@@ -0,0 +1,183 @@
+import { BindingTypes } from '@vue/compiler-dom'
+import {
+  DynamicFlag,
+  type ForIRNode,
+  IRNodeTypes,
+  type IfIRNode,
+  transformChildren,
+  transformElement,
+  transformTemplateRef,
+  transformVFor,
+  transformVIf,
+} from '../../src'
+import { makeCompile } from './_utils'
+
+const compileWithTransformRef = makeCompile({
+  nodeTransforms: [
+    transformVIf,
+    transformVFor,
+    transformTemplateRef,
+    transformElement,
+    transformChildren,
+  ],
+})
+
+describe('compiler: template ref transform', () => {
+  test('static ref', () => {
+    const { ir, code } = compileWithTransformRef(`<div ref="foo" />`)
+
+    expect(ir.block.dynamic.children[0]).toMatchObject({
+      id: 0,
+      flags: DynamicFlag.REFERENCED,
+    })
+    expect(ir.template).toEqual(['<div></div>'])
+    expect(ir.block.operation).lengthOf(1)
+    expect(ir.block.operation[0]).toMatchObject({
+      type: IRNodeTypes.SET_TEMPLATE_REF,
+      element: 0,
+      value: {
+        content: 'foo',
+        isStatic: true,
+        loc: {
+          start: { line: 1, column: 10, offset: 9 },
+          end: { line: 1, column: 15, offset: 14 },
+        },
+      },
+    })
+    expect(code).matchSnapshot()
+    expect(code).contains('const _setTemplateRef = _createTemplateRefSetter()')
+    expect(code).contains('_setTemplateRef(n0, "foo")')
+  })
+
+  test('static ref (inline mode)', () => {
+    const { code } = compileWithTransformRef(`<div ref="foo" />`, {
+      inline: true,
+      bindingMetadata: { foo: BindingTypes.SETUP_REF },
+    })
+    expect(code).matchSnapshot()
+    // pass the actual ref
+    expect(code).contains('_setTemplateRef(n0, foo)')
+  })
+
+  test('dynamic ref', () => {
+    const { ir, code } = compileWithTransformRef(`<div :ref="foo" />`)
+
+    expect(ir.block.dynamic.children[0]).toMatchObject({
+      id: 0,
+      flags: DynamicFlag.REFERENCED,
+    })
+    expect(ir.template).toEqual(['<div></div>'])
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.DECLARE_OLD_REF,
+        id: 0,
+      },
+    ])
+    expect(ir.block.effect).toMatchObject([
+      {
+        operations: [
+          {
+            type: IRNodeTypes.SET_TEMPLATE_REF,
+            element: 0,
+            value: {
+              content: 'foo',
+              isStatic: false,
+            },
+          },
+        ],
+      },
+    ])
+    expect(code).matchSnapshot()
+    expect(code).contains('const _setTemplateRef = _createTemplateRefSetter()')
+    expect(code).contains('_setTemplateRef(n0, _ctx.foo, r0)')
+  })
+
+  test('function ref', () => {
+    const { ir, code } = compileWithTransformRef(
+      `<div :ref="bar => {
+        foo.value = bar
+        ;({ baz } = bar)
+        console.log(foo.value, baz)
+      }" />`,
+    )
+    expect(ir.block.dynamic.children[0]).toMatchObject({
+      id: 0,
+      flags: DynamicFlag.REFERENCED,
+    })
+    expect(ir.template).toEqual(['<div></div>'])
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.DECLARE_OLD_REF,
+        id: 0,
+      },
+    ])
+    expect(ir.block.effect).toMatchObject([
+      {
+        operations: [
+          {
+            type: IRNodeTypes.SET_TEMPLATE_REF,
+            element: 0,
+            value: {
+              isStatic: false,
+            },
+          },
+        ],
+      },
+    ])
+    expect(code).toMatchSnapshot()
+    expect(code).contains('const _setTemplateRef = _createTemplateRefSetter()')
+    expect(code).contains(`_setTemplateRef(n0, bar => {
+        _foo.value = bar
+        ;({ baz: _ctx.baz } = bar)
+        console.log(_foo.value, _ctx.baz)
+      }, r0)`)
+  })
+
+  test('ref + v-if', () => {
+    const { ir, code } = compileWithTransformRef(
+      `<div ref="foo" v-if="true" />`,
+    )
+
+    const op = ir.block.dynamic.children[0].operation as IfIRNode
+    expect(op.type).toBe(IRNodeTypes.IF)
+
+    const { positive } = op
+    expect(positive.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_TEMPLATE_REF,
+        element: 2,
+        value: {
+          content: 'foo',
+          isStatic: true,
+        },
+        effect: false,
+      },
+    ])
+    expect(code).matchSnapshot()
+    expect(code).contains('const _setTemplateRef = _createTemplateRefSetter()')
+    expect(code).contains('_setTemplateRef(n2, "foo")')
+  })
+
+  test('ref + v-for', () => {
+    const { ir, code } = compileWithTransformRef(
+      `<div ref="foo" v-for="item in [1,2,3]" />`,
+    )
+
+    const { render } = ir.block.dynamic.children[0].operation as ForIRNode
+    expect(render.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_TEMPLATE_REF,
+        element: 2,
+        value: {
+          content: 'foo',
+          isStatic: true,
+        },
+        refFor: true,
+        effect: false,
+      },
+    ])
+    expect(code).matchSnapshot()
+    expect(code).contains('const _setTemplateRef = _createTemplateRefSetter()')
+    expect(code).contains('_setTemplateRef(n2, "foo", void 0, true)')
+  })
+})
diff --git a/packages/compiler-vapor/__tests__/transforms/transformText.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformText.spec.ts
new file mode 100644
index 00000000000..20fa6d1fd00
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/transformText.spec.ts
@@ -0,0 +1,51 @@
+// TODO: add tests for this transform
+import { NodeTypes } from '@vue/compiler-dom'
+import {
+  IRNodeTypes,
+  transformChildren,
+  transformElement,
+  transformText,
+  transformVBind,
+  transformVOn,
+} from '../../src'
+
+import { makeCompile } from './_utils'
+
+const compileWithTextTransform = makeCompile({
+  nodeTransforms: [transformElement, transformChildren, transformText],
+  directiveTransforms: {
+    bind: transformVBind,
+    on: transformVOn,
+  },
+})
+
+describe('compiler: text transform', () => {
+  it('no consecutive text', () => {
+    const { code, ir, helpers } = compileWithTextTransform(
+      '{{ "hello world" }}',
+    )
+    expect(code).toMatchSnapshot()
+    expect(helpers).contains.all.keys('setText', 'template')
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_TEXT,
+        element: 0,
+        values: [
+          {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: '"hello world"',
+            isStatic: false,
+          },
+        ],
+      },
+    ])
+  })
+
+  it('consecutive text', () => {
+    const { code, ir, helpers } = compileWithTextTransform('{{ msg }}')
+    expect(code).toMatchSnapshot()
+    expect(helpers).contains.all.keys('setText', 'template')
+    expect(ir.block.operation).toMatchObject([])
+    expect(ir.block.effect.length).toBe(1)
+  })
+})
diff --git a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts
new file mode 100644
index 00000000000..e96186c275c
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts
@@ -0,0 +1,846 @@
+import { BindingTypes, ErrorCodes, NodeTypes } from '@vue/compiler-dom'
+import {
+  DynamicFlag,
+  IRNodeTypes,
+  transformChildren,
+  transformElement,
+  transformVBind,
+} from '../../src'
+import { makeCompile } from './_utils'
+
+const compileWithVBind = makeCompile({
+  nodeTransforms: [transformElement, transformChildren],
+  directiveTransforms: {
+    bind: transformVBind,
+  },
+})
+
+describe('compiler v-bind', () => {
+  test('basic', () => {
+    const { ir, code } = compileWithVBind(`<div v-bind:id="id"/>`)
+
+    expect(ir.block.dynamic.children[0]).toMatchObject({
+      id: 0,
+      flags: DynamicFlag.REFERENCED,
+    })
+    expect(ir.template).toEqual(['<div></div>'])
+    expect(ir.block.effect).lengthOf(1)
+    expect(ir.block.effect[0].expressions).lengthOf(1)
+    expect(ir.block.effect[0].operations).lengthOf(1)
+    expect(ir.block.effect[0]).toMatchObject({
+      expressions: [
+        {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'id',
+          isStatic: false,
+        },
+      ],
+      operations: [
+        {
+          type: IRNodeTypes.SET_PROP,
+          element: 0,
+          prop: {
+            key: {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: 'id',
+              isStatic: true,
+              loc: {
+                start: { line: 1, column: 13, offset: 12 },
+                end: { line: 1, column: 15, offset: 14 },
+                source: 'id',
+              },
+            },
+            values: [
+              {
+                type: NodeTypes.SIMPLE_EXPRESSION,
+                content: 'id',
+                isStatic: false,
+                loc: {
+                  source: 'id',
+                  start: { line: 1, column: 17, offset: 16 },
+                  end: { line: 1, column: 19, offset: 18 },
+                },
+              },
+            ],
+            loc: {
+              start: { column: 6, line: 1, offset: 5 },
+              end: { column: 20, line: 1, offset: 19 },
+              source: 'v-bind:id="id"',
+            },
+            runtimeCamelize: false,
+          },
+        },
+      ],
+    })
+
+    expect(code).matchSnapshot()
+    expect(code).contains('_setProp(n0, "id", _ctx.id')
+  })
+
+  test('no expression', () => {
+    const { ir, code } = compileWithVBind(`<div v-bind:id />`)
+
+    expect(code).matchSnapshot()
+    expect(ir.block.effect[0].operations[0]).toMatchObject({
+      type: IRNodeTypes.SET_PROP,
+      prop: {
+        key: {
+          content: `id`,
+          isStatic: true,
+          loc: {
+            start: { line: 1, column: 13, offset: 12 },
+            end: { line: 1, column: 15, offset: 14 },
+          },
+        },
+        values: [
+          {
+            content: `id`,
+            isStatic: false,
+            loc: {
+              start: { line: 1, column: 13, offset: 12 },
+              end: { line: 1, column: 15, offset: 14 },
+            },
+          },
+        ],
+      },
+    })
+    expect(code).contains('_setProp(n0, "id", _ctx.id)')
+  })
+
+  test('no expression (shorthand)', () => {
+    const { ir, code } = compileWithVBind(`<div :camel-case />`)
+
+    expect(code).matchSnapshot()
+    expect(ir.block.effect[0].operations[0]).toMatchObject({
+      type: IRNodeTypes.SET_PROP,
+      prop: {
+        key: {
+          content: `camel-case`,
+          isStatic: true,
+        },
+        values: [
+          {
+            content: `camelCase`,
+            isStatic: false,
+          },
+        ],
+      },
+    })
+    expect(code).contains('_setAttr(n0, "camel-case", _ctx.camelCase)')
+  })
+
+  test('dynamic arg', () => {
+    const { ir, code } = compileWithVBind(
+      `<div v-bind:[id]="id" v-bind:[title]="title" />`,
+    )
+    expect(code).matchSnapshot()
+    expect(ir.block.effect[0].operations[0]).toMatchObject({
+      type: IRNodeTypes.SET_DYNAMIC_PROPS,
+      element: 0,
+      props: [
+        [
+          {
+            key: {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: 'id',
+              isStatic: false,
+            },
+            values: [
+              {
+                type: NodeTypes.SIMPLE_EXPRESSION,
+                content: 'id',
+                isStatic: false,
+              },
+            ],
+          },
+          {
+            key: {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: 'title',
+              isStatic: false,
+            },
+            values: [
+              {
+                type: NodeTypes.SIMPLE_EXPRESSION,
+                content: 'title',
+                isStatic: false,
+              },
+            ],
+          },
+        ],
+      ],
+    })
+    expect(code).contains(
+      '_setDynamicProps(n0, [{ [_id]: _id, [_title]: _title }], true)',
+    )
+  })
+
+  test('dynamic arg w/ static attribute', () => {
+    const { ir, code } = compileWithVBind(
+      `<div v-bind:[id]="id" foo="bar" checked />`,
+    )
+    expect(code).matchSnapshot()
+    expect(ir.block.effect[0].operations[0]).toMatchObject({
+      type: IRNodeTypes.SET_DYNAMIC_PROPS,
+      element: 0,
+      props: [
+        [
+          {
+            key: {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: 'id',
+              isStatic: false,
+            },
+            values: [
+              {
+                type: NodeTypes.SIMPLE_EXPRESSION,
+                content: 'id',
+                isStatic: false,
+              },
+            ],
+          },
+          {
+            key: {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: 'foo',
+              isStatic: true,
+            },
+            values: [
+              {
+                type: NodeTypes.SIMPLE_EXPRESSION,
+                content: 'bar',
+                isStatic: true,
+              },
+            ],
+          },
+          {
+            key: {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: 'checked',
+              isStatic: true,
+            },
+          },
+        ],
+      ],
+    })
+    expect(code).contains(
+      '_setDynamicProps(n0, [{ [_id]: _id, foo: "bar", checked: "" }], true)',
+    )
+  })
+
+  test('should error if empty expression', () => {
+    const onError = vi.fn()
+    const { ir, code } = compileWithVBind(`<div v-bind:arg="" />`, {
+      onError,
+    })
+
+    expect(onError.mock.calls[0][0]).toMatchObject({
+      code: ErrorCodes.X_V_BIND_NO_EXPRESSION,
+      loc: {
+        start: { line: 1, column: 6 },
+        end: { line: 1, column: 19 },
+      },
+    })
+    expect(ir.template).toEqual(['<div arg></div>'])
+
+    expect(code).matchSnapshot()
+    expect(code).contains(JSON.stringify('<div arg></div>'))
+  })
+
+  test('error on invalid argument for same-name shorthand', () => {
+    const onError = vi.fn()
+    compileWithVBind(`<div v-bind:[arg] />`, { onError })
+    expect(onError.mock.calls[0][0]).toMatchObject({
+      code: ErrorCodes.X_V_BIND_INVALID_SAME_NAME_ARGUMENT,
+      loc: {
+        start: {
+          line: 1,
+          column: 13,
+        },
+        end: {
+          line: 1,
+          column: 18,
+        },
+      },
+    })
+  })
+
+  test('.camel modifier', () => {
+    const { ir, code } = compileWithVBind(`<div v-bind:foo-bar.camel="id"/>`)
+
+    expect(ir.block.effect[0].operations[0]).toMatchObject({
+      prop: {
+        key: {
+          content: `fooBar`,
+          isStatic: true,
+        },
+        values: [
+          {
+            content: `id`,
+            isStatic: false,
+          },
+        ],
+        runtimeCamelize: false,
+        modifier: undefined,
+      },
+    })
+
+    expect(code).matchSnapshot()
+    expect(code).contains('_setProp(n0, "fooBar", _ctx.id)')
+  })
+
+  test('.camel modifier w/ no expression', () => {
+    const { ir, code } = compileWithVBind(`<div v-bind:foo-bar.camel />`)
+
+    expect(code).matchSnapshot()
+    expect(ir.block.effect[0].operations[0]).toMatchObject({
+      prop: {
+        key: {
+          content: `fooBar`,
+          isStatic: true,
+        },
+        values: [
+          {
+            content: `fooBar`,
+            isStatic: false,
+          },
+        ],
+        runtimeCamelize: false,
+        modifier: undefined,
+      },
+    })
+    expect(code).contains('renderEffect')
+    expect(code).contains('_setProp(n0, "fooBar", _ctx.fooBar)')
+  })
+
+  test('.camel modifier w/ dynamic arg', () => {
+    const { ir, code } = compileWithVBind(`<div v-bind:[foo].camel="id"/>`)
+
+    expect(ir.block.effect[0].operations[0]).toMatchObject({
+      type: IRNodeTypes.SET_DYNAMIC_PROPS,
+      props: [
+        [
+          {
+            key: {
+              content: `foo`,
+              isStatic: false,
+            },
+            values: [
+              {
+                content: `id`,
+                isStatic: false,
+              },
+            ],
+            runtimeCamelize: true,
+            modifier: undefined,
+          },
+        ],
+      ],
+    })
+
+    expect(code).matchSnapshot()
+    expect(code).contains('renderEffect')
+    expect(code).contains(
+      `_setDynamicProps(n0, [{ [_camelize(_ctx.foo)]: _ctx.id }], true)`,
+    )
+  })
+
+  test.todo('.camel modifier w/ dynamic arg + prefixIdentifiers')
+
+  test('.prop modifier', () => {
+    const { ir, code } = compileWithVBind(`<div v-bind:fooBar.prop="id"/>`)
+
+    expect(code).matchSnapshot()
+    expect(ir.block.effect[0].operations[0]).toMatchObject({
+      prop: {
+        key: {
+          content: `fooBar`,
+          isStatic: true,
+        },
+        values: [
+          {
+            content: `id`,
+            isStatic: false,
+          },
+        ],
+        runtimeCamelize: false,
+        modifier: '.',
+      },
+    })
+    expect(code).contains('renderEffect')
+    expect(code).contains('_setDOMProp(n0, "fooBar", _ctx.id)')
+  })
+
+  test('.prop modifier w/ no expression', () => {
+    const { ir, code } = compileWithVBind(`<div v-bind:fooBar.prop />`)
+
+    expect(code).matchSnapshot()
+    expect(ir.block.effect[0].operations[0]).toMatchObject({
+      prop: {
+        key: {
+          content: `fooBar`,
+          isStatic: true,
+        },
+        values: [
+          {
+            content: `fooBar`,
+            isStatic: false,
+          },
+        ],
+        runtimeCamelize: false,
+        modifier: '.',
+      },
+    })
+    expect(code).contains('renderEffect')
+    expect(code).contains('_setDOMProp(n0, "fooBar", _ctx.fooBar)')
+  })
+
+  test('.prop modifier w/ dynamic arg', () => {
+    const { ir, code } = compileWithVBind(`<div v-bind:[fooBar].prop="id"/>`)
+
+    expect(code).matchSnapshot()
+    expect(ir.block.effect[0].operations[0]).toMatchObject({
+      type: IRNodeTypes.SET_DYNAMIC_PROPS,
+      props: [
+        [
+          {
+            key: {
+              content: `fooBar`,
+              isStatic: false,
+            },
+            values: [
+              {
+                content: `id`,
+                isStatic: false,
+              },
+            ],
+            runtimeCamelize: false,
+            modifier: '.',
+          },
+        ],
+      ],
+    })
+    expect(code).contains('renderEffect')
+    expect(code).contains(
+      `_setDynamicProps(n0, [{ ["." + _ctx.fooBar]: _ctx.id }], true)`,
+    )
+  })
+
+  test.todo('.prop modifier w/ dynamic arg + prefixIdentifiers')
+
+  test('.prop modifier (shorthand)', () => {
+    const { ir, code } = compileWithVBind(`<div .fooBar="id"/>`)
+
+    expect(code).matchSnapshot()
+    expect(ir.block.effect[0].operations[0]).toMatchObject({
+      prop: {
+        key: {
+          content: `fooBar`,
+          isStatic: true,
+        },
+        values: [
+          {
+            content: `id`,
+            isStatic: false,
+          },
+        ],
+        runtimeCamelize: false,
+        modifier: '.',
+      },
+    })
+    expect(code).contains('renderEffect')
+    expect(code).contains(' _setDOMProp(n0, "fooBar", _ctx.id)')
+  })
+
+  test('.prop modifier (shorthand) w/ no expression', () => {
+    const { ir, code } = compileWithVBind(`<div .fooBar />`)
+
+    expect(code).matchSnapshot()
+    expect(ir.block.effect[0].operations[0]).toMatchObject({
+      prop: {
+        key: {
+          content: `fooBar`,
+          isStatic: true,
+        },
+        values: [
+          {
+            content: `fooBar`,
+            isStatic: false,
+          },
+        ],
+        runtimeCamelize: false,
+        modifier: '.',
+      },
+    })
+    expect(code).contains('renderEffect')
+    expect(code).contains('_setDOMProp(n0, "fooBar", _ctx.fooBar)')
+  })
+
+  test('.prop modifier w/ innerHTML', () => {
+    const { code } = compileWithVBind(`<div :innerHTML.prop="foo" />`)
+    expect(code).matchSnapshot()
+    expect(code).contains('_setHtml(n0, _ctx.foo)')
+  })
+
+  test('.prop modifier (shorthand) w/ innerHTML', () => {
+    const { code } = compileWithVBind(`<div .innerHTML="foo" />`)
+    expect(code).matchSnapshot()
+    expect(code).contains('_setHtml(n0, _ctx.foo)')
+  })
+
+  test('.prop modifier w/ textContent', () => {
+    const { code } = compileWithVBind(`<div :textContent.prop="foo" />`)
+    expect(code).matchSnapshot()
+    expect(code).contains('_setText(n0, _ctx.foo)')
+  })
+
+  test('.prop modifier (shorthand) w/ textContent', () => {
+    const { code } = compileWithVBind(`<div .textContent="foo" />`)
+    expect(code).matchSnapshot()
+    expect(code).contains('_setText(n0, _ctx.foo)')
+  })
+
+  test('.prop modifier w/ value', () => {
+    const { code } = compileWithVBind(`<div :value.prop="foo" />`)
+    expect(code).matchSnapshot()
+    expect(code).contains('_setValue(n0, _ctx.foo)')
+  })
+
+  test('.prop modifier (shorthand) w/ value', () => {
+    const { code } = compileWithVBind(`<div .value="foo" />`)
+    expect(code).matchSnapshot()
+    expect(code).contains('_setValue(n0, _ctx.foo)')
+  })
+
+  test('.prop modifier w/ progress value', () => {
+    const { code } = compileWithVBind(`<progress :value.prop="foo" />`)
+    expect(code).matchSnapshot()
+    expect(code).contains('_setDOMProp(n0, "value", _ctx.foo)')
+  })
+
+  test('.prop modifier (shorthand) w/ progress value', () => {
+    const { code } = compileWithVBind(`<progress .value="foo" />`)
+    expect(code).matchSnapshot()
+    expect(code).contains('_setDOMProp(n0, "value", _ctx.foo)')
+  })
+
+  test('.attr modifier', () => {
+    const { ir, code } = compileWithVBind(`<div v-bind:foo-bar.attr="id"/>`)
+
+    expect(code).matchSnapshot()
+    expect(ir.block.effect[0].operations[0]).toMatchObject({
+      prop: {
+        key: {
+          content: `foo-bar`,
+          isStatic: true,
+        },
+        values: [
+          {
+            content: `id`,
+            isStatic: false,
+          },
+        ],
+        runtimeCamelize: false,
+        modifier: '^',
+      },
+    })
+    expect(code).contains('renderEffect')
+    expect(code).contains('_setAttr(n0, "foo-bar", _ctx.id)')
+  })
+
+  test('.attr modifier w/ no expression', () => {
+    const { ir, code } = compileWithVBind(`<div v-bind:foo-bar.attr />`)
+
+    expect(code).matchSnapshot()
+    expect(ir.block.effect[0].operations[0]).toMatchObject({
+      prop: {
+        key: {
+          content: `foo-bar`,
+          isStatic: true,
+        },
+        values: [
+          {
+            content: `fooBar`,
+            isStatic: false,
+          },
+        ],
+        runtimeCamelize: false,
+        modifier: '^',
+      },
+    })
+
+    expect(code).contains('renderEffect')
+    expect(code).contains('_setAttr(n0, "foo-bar", _ctx.fooBar)')
+  })
+
+  test('.attr modifier w/ innerHTML', () => {
+    const { code } = compileWithVBind(`<div :innerHTML.attr="foo" />`)
+    expect(code).matchSnapshot()
+    expect(code).contains('_setAttr(n0, "innerHTML", _ctx.foo)')
+  })
+
+  test('.attr modifier w/ textContent', () => {
+    const { code } = compileWithVBind(`<div :textContent.attr="foo" />`)
+    expect(code).matchSnapshot()
+    expect(code).contains('_setAttr(n0, "textContent", _ctx.foo)')
+  })
+
+  test('.attr modifier w/ value', () => {
+    const { code } = compileWithVBind(`<div :value.attr="foo" />`)
+    expect(code).matchSnapshot()
+    expect(code).contains('_setAttr(n0, "value", _ctx.foo)')
+  })
+
+  test('.attr modifier w/ progress value', () => {
+    const { code } = compileWithVBind(`<progress :value.attr="foo" />`)
+    expect(code).matchSnapshot()
+    expect(code).contains('_setAttr(n0, "value", _ctx.foo)')
+  })
+
+  test('attributes must be set as attribute', () => {
+    const { code } = compileWithVBind(`
+      <div :spellcheck :draggable :translate :form />
+      <input :list="list" />
+      <textarea :type="type" />
+      <img :width="width" :height="height"/>
+      <video :width="width" :height="height"/>
+      <canvas :width="width" :height="height"/>
+      <source :width="width" :height="height"/>
+    `)
+
+    expect(code).matchSnapshot()
+    expect(code).contains('_setAttr(n0, "spellcheck", _ctx.spellcheck)')
+    expect(code).contains('_setAttr(n0, "draggable", _ctx.draggable)')
+    expect(code).contains('_setAttr(n0, "translate", _ctx.translate)')
+    expect(code).contains('_setAttr(n0, "form", _ctx.form)')
+    expect(code).contains('_setAttr(n1, "list", _ctx.list)')
+    expect(code).contains('_setAttr(n2, "type", _ctx.type)')
+    expect(code).contains('_setAttr(n3, "width", _width)')
+    expect(code).contains('_setAttr(n3, "height", _height)')
+    expect(code).contains('_setAttr(n4, "width", _width)')
+    expect(code).contains('_setAttr(n4, "height", _height)')
+    expect(code).contains('_setAttr(n5, "width", _width)')
+    expect(code).contains('_setAttr(n5, "height", _height)')
+    expect(code).contains(' _setAttr(n6, "width", _width)')
+  })
+
+  test(':innerHTML', () => {
+    const { code } = compileWithVBind(`
+      <div :innerHTML="foo"/>
+    `)
+    expect(code).matchSnapshot()
+    expect(code).contains('_setHtml(n0, _ctx.foo)')
+  })
+
+  test(':textContext', () => {
+    const { code } = compileWithVBind(`
+      <div :textContent="foo"/>
+    `)
+    expect(code).matchSnapshot()
+    expect(code).contains('_setText(n0, _ctx.foo)')
+  })
+
+  test(':value', () => {
+    const { code } = compileWithVBind(`
+      <input :value="foo"/>
+    `)
+    expect(code).matchSnapshot()
+    expect(code).contains('_setValue(n0, _ctx.foo)')
+  })
+
+  test(':value w/ progress', () => {
+    const { code } = compileWithVBind(`
+      <progress :value="foo"/>
+    `)
+    expect(code).matchSnapshot()
+    expect(code).contains('_setProp(n0, "value", _ctx.foo)')
+  })
+
+  test('number value', () => {
+    const { code } = compileWithVBind(`<Comp :depth="0" />`)
+    expect(code).matchSnapshot()
+    expect(code).contains('{ depth: () => (0) }')
+  })
+
+  test('with constant value', () => {
+    const { code } = compileWithVBind(
+      `
+        <div
+          :a="void 0" 
+          :b="1 > 2" 
+          :c="1 + 2" 
+          :d="1 ? 2 : 3" 
+          :e="(2)" 
+          :f="\`foo${1}\`"
+          :g="1"
+          :h="'1'"
+          :i="true"
+          :j="null"
+          :k="x"
+          :l="{ foo: 1 }"
+          :m="{ [x]: 1 }"
+          :n="{ ...{ foo: 1 } }"
+          :o="[1, , 3]"
+          :p="[1, ...[2, 3]]"
+          :q="[1, 2]"
+          :r="/\\s+/"
+        />`,
+      {
+        bindingMetadata: {
+          x: BindingTypes.LITERAL_CONST,
+        },
+      },
+    )
+    expect(code).matchSnapshot()
+  })
+})
+
+describe('cache multiple access', () => {
+  test('repeated variables', () => {
+    const { code } = compileWithVBind(`
+        <div :class="foo"></div>
+        <div :class="foo"></div>
+      `)
+    expect(code).matchSnapshot()
+    expect(code).contains('const _foo = _ctx.foo')
+    expect(code).contains('setClass(n0, _foo)')
+    expect(code).contains('setClass(n1, _foo)')
+  })
+
+  test('repeated expressions', () => {
+    const { code } = compileWithVBind(`
+        <div :id="foo + bar"></div>
+        <div :id="foo + bar"></div>
+      `)
+    expect(code).matchSnapshot()
+    expect(code).contains('const _foo_bar = _ctx.foo + _ctx.bar')
+    expect(code).contains('_setProp(n0, "id", _foo_bar)')
+    expect(code).contains('_setProp(n1, "id", _foo_bar)')
+  })
+
+  test('repeated variable in expressions', () => {
+    const { code } = compileWithVBind(`
+        <div :id="foo + foo + bar"></div>
+        <div :id="foo"></div>
+      `)
+    expect(code).matchSnapshot()
+    expect(code).contains('const _foo = _ctx.foo')
+    expect(code).contains('_setProp(n0, "id", _foo + _foo + _ctx.bar)')
+    expect(code).contains('_setProp(n1, "id", _foo)')
+  })
+
+  test('repeated expression in expressions', () => {
+    const { code } = compileWithVBind(`
+        <div :id="foo + bar"></div>
+        <div :id="foo + bar"></div>
+        <div :id="foo + foo + bar"></div>
+      `)
+    expect(code).matchSnapshot()
+    expect(code).contains('const _foo_bar = _foo + _ctx.bar')
+    expect(code).contains('_setProp(n0, "id", _foo_bar)')
+    expect(code).contains('_setProp(n2, "id", _foo + _foo_bar)')
+  })
+
+  test('function calls with arguments', () => {
+    const { code } = compileWithVBind(`
+        <div :id="foo[bar(baz)]"></div>
+        <div :id="foo[bar(baz)]"></div>
+        <div :id="bar() + foo"></div>
+      `)
+    expect(code).matchSnapshot()
+    expect(code).contains('const _foo_bar_baz = _foo[_bar(_ctx.baz)]')
+    expect(code).contains('_setProp(n0, "id", _foo_bar_baz)')
+    expect(code).contains('_setProp(n1, "id", _foo_bar_baz)')
+    expect(code).contains('_setProp(n2, "id", _bar() + _foo)')
+  })
+
+  test('dynamic key bindings with expressions', () => {
+    const { code } = compileWithVBind(`
+        <div :[key+1]="foo[key+1]()" />
+      `)
+    expect(code).matchSnapshot()
+    expect(code).contains('const _key = _ctx.key')
+    expect(code).contains('[{ [_key+1]: _ctx.foo[_key+1]() }]')
+  })
+
+  test('object property chain access', () => {
+    const { code } = compileWithVBind(`
+        <div :id="obj['foo']['baz'] + obj.bar"></div>
+        <div :id="obj['foo']['baz'] + obj.bar"></div>
+      `)
+    expect(code).matchSnapshot()
+    expect(code).contains(
+      "const _obj_foo_baz_obj_bar = _obj['foo']['baz'] + _obj.bar",
+    )
+    expect(code).contains('_setProp(n0, "id", _obj_foo_baz_obj_bar)')
+    expect(code).contains('_setProp(n1, "id", _obj_foo_baz_obj_bar)')
+  })
+
+  test('dynamic property access', () => {
+    const { code } = compileWithVBind(`
+        <div :id="obj[1][baz] + obj.bar"></div>
+      `)
+    expect(code).matchSnapshot()
+    expect(code).contains('const _obj = _ctx.obj')
+    expect(code).contains('_setProp(n0, "id", _obj[1][_ctx.baz] + _obj.bar)')
+  })
+
+  test('variable name substring edge cases', () => {
+    const { code } = compileWithVBind(
+      `<div :id="title + titles + title"></div>`,
+    )
+    expect(code).matchSnapshot()
+    expect(code).contains('const _title = _ctx.title')
+    expect(code).contains('_setProp(n0, "id", _title + _ctx.titles + _title)')
+  })
+
+  test('object property name substring cases', () => {
+    const { code } = compileWithVBind(
+      `<div :id="p.title + p.titles + p.title"></div>`,
+    )
+    expect(code).matchSnapshot()
+    expect(code).contains('const _p = _ctx.p')
+    expect(code).contains('const _p_title = _p.title')
+    expect(code).contains('_setProp(n0, "id", _p_title + _p.titles + _p_title)')
+  })
+
+  test('cache variable used in both property shorthand and normal binding', () => {
+    const { code } = compileWithVBind(`
+        <div :style="{color}" :id="color"/>
+      `)
+    expect(code).matchSnapshot()
+    expect(code).contains('const _color = _ctx.color')
+    expect(code).contains('_setStyle(n0, {color: _color})')
+  })
+
+  test('optional chaining', () => {
+    const { code } = compileWithVBind(`<div :id="obj?.foo + obj?.bar"></div>`)
+    expect(code).matchSnapshot()
+    expect(code).contains('const _obj = _ctx.obj')
+    expect(code).contains('_setProp(n0, "id", _obj?.foo + _obj?.bar)')
+  })
+
+  test('not cache variable only used in property shorthand', () => {
+    const { code } = compileWithVBind(`
+        <div :style="{color}" />
+      `)
+    expect(code).matchSnapshot()
+    expect(code).not.contains('const _color = _ctx.color')
+  })
+
+  test('not cache variable and member expression with the same name', () => {
+    const { code } = compileWithVBind(`
+        <div :id="bar + obj.bar"></div>
+      `)
+    expect(code).matchSnapshot()
+    expect(code).not.contains('const _bar = _ctx.bar')
+  })
+
+  test('not cache variable in function expression', () => {
+    const { code } = compileWithVBind(`
+        <div v-bind="{ foo: bar => foo = bar }"></div>
+      `)
+    expect(code).matchSnapshot()
+    expect(code).not.contains('const _bar = _ctx.bar')
+  })
+})
diff --git a/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts b/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts
new file mode 100644
index 00000000000..7357ad84fef
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts
@@ -0,0 +1,371 @@
+import { makeCompile } from './_utils'
+import {
+  type ForIRNode,
+  IRNodeTypes,
+  transformChildren,
+  transformElement,
+  transformText,
+  transformVBind,
+  transformVFor,
+  transformVOn,
+} from '../../src'
+import { NodeTypes } from '@vue/compiler-dom'
+
+const compileWithVFor = makeCompile({
+  nodeTransforms: [
+    transformVFor,
+    transformText,
+    transformElement,
+    transformChildren,
+  ],
+  directiveTransforms: {
+    bind: transformVBind,
+    on: transformVOn,
+  },
+})
+
+describe('compiler: v-for', () => {
+  test('basic v-for', () => {
+    const { code, ir, helpers } = compileWithVFor(
+      `<div v-for="item of items" :key="item.id" @click="remove(item)">{{ item }}</div>`,
+    )
+
+    expect(code).matchSnapshot()
+    expect(helpers).contains('createFor')
+    expect(ir.template).toEqual(['<div> </div>'])
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.FOR,
+      id: 0,
+      source: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'items',
+      },
+      value: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'item',
+      },
+      key: undefined,
+      index: undefined,
+      render: {
+        type: IRNodeTypes.BLOCK,
+        dynamic: {
+          children: [{ template: 0 }],
+        },
+      },
+      keyProp: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'item.id',
+      },
+    })
+    expect(ir.block.returns).toEqual([0])
+    expect(ir.block.dynamic).toMatchObject({
+      children: [{ id: 0 }],
+    })
+    expect(ir.block.effect).toEqual([])
+    expect(
+      (ir.block.dynamic.children[0].operation as ForIRNode).render.effect,
+    ).lengthOf(1)
+  })
+
+  test('key only binding pattern', () => {
+    expect(
+      compileWithVFor(
+        `
+          <tr
+            v-for="row of rows"
+            :key="row.id"
+          >
+            {{ row.id + row.id }}
+          </tr>
+      `,
+      ).code,
+    ).matchSnapshot()
+  })
+
+  test('selector pattern', () => {
+    expect(
+      compileWithVFor(
+        `
+          <tr
+            v-for="row of rows"
+            :key="row.id"
+          >
+            {{ selected === row.id ? 'danger' : '' }}
+          </tr>
+      `,
+      ).code,
+    ).matchSnapshot()
+
+    expect(
+      compileWithVFor(
+        `
+          <tr
+            v-for="row of rows"
+            :key="row.id"
+            :class="selected === row.id ? 'danger' : ''"
+          ></tr>
+      `,
+      ).code,
+    ).matchSnapshot()
+
+    // Should not be optimized because row.label is not from parent scope
+    expect(
+      compileWithVFor(
+        `
+          <tr
+            v-for="row of rows"
+            :key="row.id"
+            :class="row.label === row.id ? 'danger' : ''"
+          ></tr>
+      `,
+      ).code,
+    ).matchSnapshot()
+
+    expect(
+      compileWithVFor(
+        `
+          <tr
+            v-for="row of rows"
+            :key="row.id"
+            :class="{ danger: row.id === selected }"
+          ></tr>
+      `,
+      ).code,
+    ).matchSnapshot()
+  })
+
+  test('multi effect', () => {
+    const { code } = compileWithVFor(
+      `<div v-for="(item, index) of items" :item="item" :index="index" />`,
+    )
+    expect(code).matchSnapshot()
+  })
+
+  test('w/o value', () => {
+    const { code } = compileWithVFor(`<div v-for=" of items">item</div>`)
+    expect(code).matchSnapshot()
+  })
+
+  test('nested v-for', () => {
+    const { code, ir } = compileWithVFor(
+      `<div v-for="i in list"><span v-for="j in i">{{ j+i }}</span></div>`,
+    )
+    expect(code).matchSnapshot()
+    expect(code).contains(`_createFor(() => (_ctx.list), (_for_item0) => {`)
+    expect(code).contains(
+      `_createFor(() => (_for_item0.value), (_for_item1) => {`,
+    )
+    expect(code).contains(`_for_item1.value+_for_item0.value`)
+    expect(ir.template).toEqual(['<span> </span>', '<div></div>'])
+    const parentOp = ir.block.dynamic.children[0].operation
+    expect(parentOp).toMatchObject({
+      type: IRNodeTypes.FOR,
+      id: 0,
+      source: { content: 'list' },
+      value: { content: 'i' },
+      render: {
+        type: IRNodeTypes.BLOCK,
+        dynamic: {
+          children: [{ template: 1 }],
+        },
+      },
+    })
+    expect(
+      (parentOp as any).render.dynamic.children[0].children[0].operation,
+    ).toMatchObject({
+      type: IRNodeTypes.FOR,
+      id: 2,
+      source: { content: 'i' },
+      value: { content: 'j' },
+      render: {
+        type: IRNodeTypes.BLOCK,
+        dynamic: {
+          children: [{ template: 0 }],
+        },
+      },
+    })
+  })
+
+  test('object value, key and index', () => {
+    const { code, ir } = compileWithVFor(
+      `<div v-for="(value, key, index) in list" :key="key">{{ value + key + index }}</div>`,
+    )
+    expect(code).matchSnapshot()
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.FOR,
+      source: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'list',
+      },
+      value: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'value',
+      },
+      key: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'key',
+      },
+      index: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'index',
+      },
+    })
+  })
+
+  test('object de-structured value', () => {
+    const { code, ir } = compileWithVFor(
+      '<span v-for="({ id, value }) in items" :key="id">{{ id }}{{ value }}</span>',
+    )
+    expect(code).matchSnapshot()
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.FOR,
+      source: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'items',
+      },
+      value: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: '{ id, value }',
+        ast: {
+          type: 'ArrowFunctionExpression',
+          params: [
+            {
+              type: 'ObjectPattern',
+            },
+          ],
+        },
+      },
+      key: undefined,
+      index: undefined,
+    })
+  })
+
+  test('object de-structured value (with rest)', () => {
+    const { code, ir } = compileWithVFor(
+      `<div v-for="(  { id, ...other }, index) in list" :key="id">{{ id + other + index }}</div>`,
+    )
+    expect(code).matchSnapshot()
+    expect(code).toContain('_getRestElement(_for_item0.value, ["id"])')
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.FOR,
+      source: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'list',
+      },
+      value: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: '{ id, ...other }',
+        ast: {
+          type: 'ArrowFunctionExpression',
+          params: [
+            {
+              type: 'ObjectPattern',
+            },
+          ],
+        },
+      },
+      key: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'index',
+      },
+      index: undefined,
+    })
+  })
+
+  test('array de-structured value', () => {
+    const { code, ir } = compileWithVFor(
+      `<div v-for="([id, other], index) in list" :key="id">{{ id + other + index }}</div>`,
+    )
+    expect(code).matchSnapshot()
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.FOR,
+      source: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'list',
+      },
+      value: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: '[id, other]',
+        ast: {
+          type: 'ArrowFunctionExpression',
+          params: [
+            {
+              type: 'ArrayPattern',
+            },
+          ],
+        },
+      },
+      key: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'index',
+      },
+      index: undefined,
+    })
+  })
+
+  test('array de-structured value (with rest)', () => {
+    const { code, ir } = compileWithVFor(
+      `<div v-for="([id, ...other], index) in list" :key="id">{{ id + other + index }}</div>`,
+    )
+    expect(code).matchSnapshot()
+    expect(code).toContain('_for_item0.value.slice(1)')
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.FOR,
+      source: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'list',
+      },
+      value: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: '[id, ...other]',
+        ast: {
+          type: 'ArrowFunctionExpression',
+          params: [
+            {
+              type: 'ArrayPattern',
+            },
+          ],
+        },
+      },
+      key: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'index',
+      },
+      index: undefined,
+    })
+  })
+
+  test('v-for aliases w/ complex expressions', () => {
+    const { code, ir } = compileWithVFor(
+      `<div v-for="({ foo = bar, baz: [qux = quux] }) in list">
+        {{ foo + bar + baz + qux + quux }}
+      </div>`,
+    )
+    expect(code).matchSnapshot()
+    expect(code).toContain(`_getDefaultValue(_for_item0.value.foo, _ctx.bar)`)
+    expect(code).toContain(
+      `_getDefaultValue(_for_item0.value.baz[0], _ctx.quux)`,
+    )
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.FOR,
+      source: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'list',
+      },
+      value: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: '{ foo = bar, baz: [qux = quux] }',
+        ast: {
+          type: 'ArrowFunctionExpression',
+          params: [
+            {
+              type: 'ObjectPattern',
+            },
+          ],
+        },
+      },
+      key: undefined,
+      index: undefined,
+    })
+  })
+})
diff --git a/packages/compiler-vapor/__tests__/transforms/vHtml.spec.ts b/packages/compiler-vapor/__tests__/transforms/vHtml.spec.ts
new file mode 100644
index 00000000000..0de0b6abca6
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/vHtml.spec.ts
@@ -0,0 +1,114 @@
+import { BindingTypes, DOMErrorCodes, NodeTypes } from '@vue/compiler-dom'
+import {
+  IRNodeTypes,
+  transformChildren,
+  transformElement,
+  transformVHtml,
+} from '../../src'
+import { makeCompile } from './_utils'
+
+const compileWithVHtml = makeCompile({
+  nodeTransforms: [transformElement, transformChildren],
+  directiveTransforms: {
+    html: transformVHtml,
+  },
+})
+
+describe('v-html', () => {
+  test('should convert v-html to innerHTML', () => {
+    const { code, ir, helpers } = compileWithVHtml(
+      `<div v-html="code"></div>`,
+      {
+        bindingMetadata: {
+          code: BindingTypes.SETUP_REF,
+        },
+      },
+    )
+
+    expect(helpers).contains('setHtml')
+
+    expect(ir.block.operation).toMatchObject([])
+    expect(ir.block.effect).toMatchObject([
+      {
+        expressions: [
+          {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: 'code',
+            isStatic: false,
+          },
+        ],
+        operations: [
+          {
+            type: IRNodeTypes.SET_HTML,
+            element: 0,
+            value: {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: 'code',
+              isStatic: false,
+            },
+          },
+        ],
+      },
+    ])
+
+    expect(code).matchSnapshot()
+  })
+
+  test('should raise error and ignore children when v-html is present', () => {
+    const onError = vi.fn()
+    const { code, ir, helpers } = compileWithVHtml(
+      `<div v-html="test">hello</div>`,
+      {
+        onError,
+      },
+    )
+
+    expect(helpers).contains('setHtml')
+
+    // children should have been removed
+    expect(ir.template).toEqual(['<div></div>'])
+
+    expect(ir.block.operation).toMatchObject([])
+    expect(ir.block.effect).toMatchObject([
+      {
+        expressions: [
+          {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: 'test',
+            isStatic: false,
+          },
+        ],
+        operations: [
+          {
+            type: IRNodeTypes.SET_HTML,
+            element: 0,
+            value: {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: 'test',
+              isStatic: false,
+            },
+          },
+        ],
+      },
+    ])
+
+    expect(onError.mock.calls).toMatchObject([
+      [{ code: DOMErrorCodes.X_V_HTML_WITH_CHILDREN }],
+    ])
+
+    expect(code).matchSnapshot()
+    // children should have been removed
+    expect(code).contains('template("<div></div>", true)')
+  })
+
+  test('should raise error if has no expression', () => {
+    const onError = vi.fn()
+    const { code } = compileWithVHtml(`<div v-html></div>`, {
+      onError,
+    })
+    expect(code).matchSnapshot()
+    expect(onError.mock.calls).toMatchObject([
+      [{ code: DOMErrorCodes.X_V_HTML_NO_EXPRESSION }],
+    ])
+  })
+})
diff --git a/packages/compiler-vapor/__tests__/transforms/vIf.spec.ts b/packages/compiler-vapor/__tests__/transforms/vIf.spec.ts
new file mode 100644
index 00000000000..e5fd61add2e
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/vIf.spec.ts
@@ -0,0 +1,253 @@
+import { makeCompile } from './_utils'
+import {
+  IRNodeTypes,
+  type IfIRNode,
+  transformChildren,
+  transformComment,
+  transformElement,
+  transformText,
+  transformVIf,
+  transformVOnce,
+  transformVText,
+} from '../../src'
+import { NodeTypes } from '@vue/compiler-dom'
+
+const compileWithVIf = makeCompile({
+  nodeTransforms: [
+    transformVOnce,
+    transformVIf,
+    transformText,
+    transformElement,
+    transformComment,
+    transformChildren,
+  ],
+  directiveTransforms: {
+    text: transformVText,
+  },
+})
+
+describe('compiler: v-if', () => {
+  test('basic v-if', () => {
+    const { code, helpers, ir } = compileWithVIf(`<div v-if="ok">{{msg}}</div>`)
+
+    expect(helpers).contains('createIf')
+
+    expect(ir.template).toEqual(['<div> </div>'])
+
+    const op = ir.block.dynamic.children[0].operation
+    expect(op).toMatchObject({
+      type: IRNodeTypes.IF,
+      id: 0,
+      condition: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'ok',
+        isStatic: false,
+      },
+      positive: {
+        type: IRNodeTypes.BLOCK,
+        dynamic: {
+          children: [{ template: 0 }],
+        },
+      },
+    })
+    expect(ir.block.returns).toEqual([0])
+
+    expect(ir.block.dynamic).toMatchObject({
+      children: [{ id: 0 }],
+    })
+
+    expect(ir.block.effect).toEqual([])
+    expect((op as IfIRNode).positive.effect).lengthOf(1)
+
+    expect(code).matchSnapshot()
+  })
+
+  test('template v-if', () => {
+    const { code, ir } = compileWithVIf(
+      `<template v-if="ok"><div/>hello<p v-text="msg"/></template>`,
+    )
+    expect(code).matchSnapshot()
+
+    expect(ir.template).toEqual(['<div></div>', 'hello', '<p> </p>'])
+    expect(ir.block.effect).toEqual([])
+    const op = ir.block.dynamic.children[0].operation as IfIRNode
+    expect(op.positive.effect).toMatchObject([
+      {
+        operations: [
+          {
+            type: IRNodeTypes.SET_TEXT,
+            element: 4,
+            values: [
+              {
+                content: 'msg',
+                type: NodeTypes.SIMPLE_EXPRESSION,
+                isStatic: false,
+              },
+            ],
+          },
+        ],
+      },
+    ])
+    expect(op.positive.dynamic).toMatchObject({
+      id: 1,
+      children: {
+        2: {
+          id: 4,
+        },
+      },
+    })
+  })
+
+  test('dedupe same template', () => {
+    const { code, ir } = compileWithVIf(
+      `<div v-if="ok">hello</div><div v-if="ok">hello</div>`,
+    )
+    expect(code).matchSnapshot()
+    expect(ir.template).toEqual(['<div>hello</div>'])
+    expect(ir.block.returns).toEqual([0, 3])
+  })
+
+  test.todo('v-if with v-once')
+  test.todo('component v-if')
+
+  test('v-if + v-else', () => {
+    const { code, ir, helpers } = compileWithVIf(`<div v-if="ok"/><p v-else/>`)
+    expect(code).matchSnapshot()
+    expect(ir.template).toEqual(['<div></div>', '<p></p>'])
+
+    expect(helpers).contains('createIf')
+    expect(ir.block.effect).lengthOf(0)
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.IF,
+      id: 0,
+      condition: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'ok',
+        isStatic: false,
+      },
+      positive: {
+        type: IRNodeTypes.BLOCK,
+        dynamic: {
+          children: [{ template: 0 }],
+        },
+      },
+      negative: {
+        type: IRNodeTypes.BLOCK,
+        dynamic: {
+          children: [{ template: 1 }],
+        },
+      },
+    })
+    expect(ir.block.returns).toEqual([0])
+  })
+
+  test('v-if + v-else-if', () => {
+    const { code, ir } = compileWithVIf(
+      `<div v-if="ok"/><p v-else-if="orNot"/>`,
+    )
+    expect(code).matchSnapshot()
+    expect(ir.template).toEqual(['<div></div>', '<p></p>'])
+
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.IF,
+      id: 0,
+      condition: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'ok',
+        isStatic: false,
+      },
+      positive: {
+        type: IRNodeTypes.BLOCK,
+        dynamic: {
+          children: [{ template: 0 }],
+        },
+      },
+      negative: {
+        type: IRNodeTypes.IF,
+        condition: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'orNot',
+          isStatic: false,
+        },
+        positive: {
+          type: IRNodeTypes.BLOCK,
+          dynamic: {
+            children: [{ template: 1 }],
+          },
+        },
+      },
+    })
+    expect(ir.block.returns).toEqual([0])
+  })
+
+  test('v-if + v-else-if + v-else', () => {
+    const { code, ir } = compileWithVIf(
+      `<div v-if="ok"/><p v-else-if="orNot"/><template v-else>fine</template>`,
+    )
+    expect(code).matchSnapshot()
+    expect(ir.template).toEqual(['<div></div>', '<p></p>', 'fine'])
+
+    expect(ir.block.returns).toEqual([0])
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.IF,
+      id: 0,
+      positive: {
+        type: IRNodeTypes.BLOCK,
+        dynamic: {
+          children: [{ template: 0 }],
+        },
+      },
+      negative: {
+        type: IRNodeTypes.IF,
+        positive: {
+          type: IRNodeTypes.BLOCK,
+          dynamic: {
+            children: [{ template: 1 }],
+          },
+        },
+        negative: {
+          type: IRNodeTypes.BLOCK,
+          dynamic: {
+            children: [{ template: 2 }],
+          },
+        },
+      },
+    })
+  })
+
+  test('v-if + v-if / v-else[-if]', () => {
+    const { code } = compileWithVIf(
+      `<div>
+        <span v-if="foo">foo</span>
+        <span v-if="bar">bar</span>
+        <span v-else>baz</span>
+      </div>`,
+    )
+    expect(code).toMatchSnapshot()
+  })
+
+  test('comment between branches', () => {
+    const { code, ir } = compileWithVIf(`
+      <div v-if="ok"/>
+      <!--foo-->
+      <p v-else-if="orNot"/>
+      <!--bar-->
+      <template v-else>fine</template>
+      <div v-text="text" />
+    `)
+    expect(code).matchSnapshot()
+    expect(ir.template).toEqual([
+      '<div></div>',
+      '<!--foo-->',
+      '<p></p>',
+      '<!--bar-->',
+      'fine',
+
+      '<div> </div>',
+    ])
+  })
+
+  describe.todo('errors')
+  describe.todo('codegen')
+  test.todo('v-on with v-if')
+})
diff --git a/packages/compiler-vapor/__tests__/transforms/vModel.spec.ts b/packages/compiler-vapor/__tests__/transforms/vModel.spec.ts
new file mode 100644
index 00000000000..51eaa9e0230
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/vModel.spec.ts
@@ -0,0 +1,349 @@
+import { makeCompile } from './_utils'
+import {
+  IRNodeTypes,
+  transformChildren,
+  transformElement,
+  transformVModel,
+} from '../../src'
+import { BindingTypes, DOMErrorCodes } from '@vue/compiler-dom'
+
+const compileWithVModel = makeCompile({
+  nodeTransforms: [transformElement, transformChildren],
+  directiveTransforms: {
+    model: transformVModel,
+  },
+})
+
+describe('compiler: vModel transform', () => {
+  test('should support simple expression', () => {
+    const { code, helpers } = compileWithVModel('<input v-model="model" />')
+    expect(code).toMatchSnapshot()
+    expect(helpers).toContain('applyTextModel')
+  })
+
+  test('should support input (text)', () => {
+    const { code, helpers } = compileWithVModel(
+      '<input type="text" v-model="model" />',
+    )
+    expect(code).toMatchSnapshot()
+    expect(helpers).toContain('applyTextModel')
+  })
+
+  test('should support input (radio)', () => {
+    const { code, helpers } = compileWithVModel(
+      '<input type="radio" v-model="model" />',
+    )
+    expect(code).toMatchSnapshot()
+    expect(helpers).toContain('applyRadioModel')
+  })
+
+  test('should support input (checkbox)', () => {
+    const { code, helpers } = compileWithVModel(
+      '<input type="checkbox" v-model="model" />',
+    )
+    expect(code).toMatchSnapshot()
+    expect(helpers).toContain('applyCheckboxModel')
+  })
+
+  test('should support select', () => {
+    const { code, helpers } = compileWithVModel('<select v-model="model" />')
+    expect(code).toMatchSnapshot()
+    expect(helpers).toContain('applySelectModel')
+  })
+
+  test('should support textarea', () => {
+    const { code, helpers } = compileWithVModel('<textarea v-model="model" />')
+    expect(code).toMatchSnapshot()
+    expect(helpers).toContain('applyTextModel')
+  })
+
+  test('should support input (dynamic type)', () => {
+    const { code, helpers } = compileWithVModel(
+      '<input :type="foo" v-model="model" />',
+    )
+    expect(code).toMatchSnapshot()
+    expect(helpers).toContain('applyDynamicModel')
+  })
+
+  test('should support w/ dynamic v-bind', () => {
+    const root1 = compileWithVModel('<input v-bind="obj" v-model="model" />')
+    expect(root1.code).toMatchSnapshot()
+    expect(root1.helpers).toContain('applyDynamicModel')
+
+    const root2 = compileWithVModel(
+      '<input v-bind:[key]="val" v-model="model" />',
+    )
+    expect(root2.code).toMatchSnapshot()
+    expect(root2.helpers).toContain('applyDynamicModel')
+  })
+
+  describe('errors', () => {
+    test('invalid element', () => {
+      const onError = vi.fn()
+      compileWithVModel('<span v-model="model" />', { onError })
+
+      expect(onError).toHaveBeenCalledTimes(1)
+      expect(onError).toHaveBeenCalledWith(
+        expect.objectContaining({
+          code: DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT,
+        }),
+      )
+    })
+
+    test('plain elements with argument', () => {
+      const onError = vi.fn()
+      compileWithVModel('<input v-model:value="model" />', { onError })
+
+      expect(onError).toHaveBeenCalledTimes(1)
+      expect(onError).toHaveBeenCalledWith(
+        expect.objectContaining({
+          code: DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT,
+        }),
+      )
+    })
+
+    // TODO: component
+    test.todo('should allow usage on custom element', () => {
+      const onError = vi.fn()
+      const root = compileWithVModel('<my-input v-model="model" />', {
+        onError,
+        isCustomElement: tag => tag.startsWith('my-'),
+      })
+      expect(root.helpers).toContain('applyTextModel')
+      expect(onError).not.toHaveBeenCalled()
+    })
+
+    test('should raise error if used file input element', () => {
+      const onError = vi.fn()
+      compileWithVModel(`<input type="file" v-model="test"/>`, {
+        onError,
+      })
+      expect(onError).toHaveBeenCalledWith(
+        expect.objectContaining({
+          code: DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT,
+        }),
+      )
+    })
+
+    test('should error on dynamic value binding alongside v-model', () => {
+      const onError = vi.fn()
+      compileWithVModel(`<input v-model="test" :value="test" />`, {
+        onError,
+      })
+      expect(onError).toHaveBeenCalledWith(
+        expect.objectContaining({
+          code: DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE,
+        }),
+      )
+    })
+
+    // #3596
+    test('should NOT error on static value binding alongside v-model', () => {
+      const onError = vi.fn()
+      compileWithVModel(`<input v-model="test" value="test" />`, {
+        onError,
+      })
+      expect(onError).not.toHaveBeenCalled()
+    })
+  })
+
+  describe('modifiers', () => {
+    test('.number', () => {
+      const { code } = compileWithVModel('<input v-model.number="model" />')
+
+      expect(code).toMatchSnapshot()
+    })
+
+    test('.trim', () => {
+      const { code } = compileWithVModel('<input v-model.trim="model" />')
+
+      expect(code).toMatchSnapshot()
+    })
+
+    test('.lazy', () => {
+      const { code } = compileWithVModel('<input v-model.lazy="model" />')
+
+      expect(code).toMatchSnapshot()
+    })
+  })
+
+  test('should support member expression', () => {
+    const { code } = compileWithVModel(
+      '<input v-model="setupRef.child" /><input v-model="setupLet.child" /><input v-model="setupMaybeRef.child" />',
+      {
+        bindingMetadata: {
+          setupRef: BindingTypes.SETUP_REF,
+          setupLet: BindingTypes.SETUP_LET,
+          setupMaybeRef: BindingTypes.SETUP_MAYBE_REF,
+        },
+      },
+    )
+
+    expect(code).toMatchSnapshot()
+  })
+
+  test('should support member expression w/ inline', () => {
+    const { code } = compileWithVModel(
+      '<input v-model="setupRef.child" /><input v-model="setupLet.child" /><input v-model="setupMaybeRef.child" />',
+      {
+        bindingMetadata: {
+          setupRef: BindingTypes.SETUP_REF,
+          setupLet: BindingTypes.SETUP_LET,
+          setupMaybeRef: BindingTypes.SETUP_MAYBE_REF,
+        },
+        inline: true,
+      },
+    )
+
+    expect(code).toMatchSnapshot()
+  })
+
+  describe('component', () => {
+    test('v-model for component should work', () => {
+      const { code, ir } = compileWithVModel('<Comp v-model="foo" />')
+      expect(code).toMatchSnapshot()
+      expect(code).contains(`modelValue: () => (_ctx.foo),`)
+      expect(code).contains(
+        `"onUpdate:modelValue": () => _value => (_ctx.foo = _value)`,
+      )
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        tag: 'Comp',
+        props: [
+          [
+            {
+              key: { content: 'modelValue', isStatic: true },
+              model: true,
+              modelModifiers: [],
+              values: [{ content: 'foo', isStatic: false }],
+            },
+          ],
+        ],
+      })
+    })
+
+    test('v-model with arguments for component should work', () => {
+      const { code, ir } = compileWithVModel('<Comp v-model:bar="foo" />')
+      expect(code).toMatchSnapshot()
+      expect(code).contains(`bar: () => (_ctx.foo),`)
+      expect(code).contains(
+        `"onUpdate:bar": () => _value => (_ctx.foo = _value)`,
+      )
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        tag: 'Comp',
+        props: [
+          [
+            {
+              key: { content: 'bar', isStatic: true },
+              model: true,
+              modelModifiers: [],
+              values: [{ content: 'foo', isStatic: false }],
+            },
+          ],
+        ],
+      })
+    })
+
+    test('v-model with dynamic arguments for component should work', () => {
+      const { code, ir } = compileWithVModel('<Comp v-model:[arg]="foo" />')
+      expect(code).toMatchSnapshot()
+      expect(code).contains(
+        `[_ctx.arg]: _ctx.foo,
+    ["onUpdate:" + _ctx.arg]: () => _value => (_ctx.foo = _value)`,
+      )
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        tag: 'Comp',
+        props: [
+          {
+            key: { content: 'arg', isStatic: false },
+            values: [{ content: 'foo', isStatic: false }],
+            model: true,
+            modelModifiers: [],
+          },
+        ],
+      })
+    })
+
+    test('v-model for component should generate modelModifiers', () => {
+      const { code, ir } = compileWithVModel(
+        '<Comp v-model.trim.bar-baz="foo" />',
+      )
+      expect(code).toMatchSnapshot()
+      expect(code).contain(
+        `modelModifiers: () => ({ trim: true, "bar-baz": true })`,
+      )
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        tag: 'Comp',
+        props: [
+          [
+            {
+              key: { content: 'modelValue', isStatic: true },
+              values: [{ content: 'foo', isStatic: false }],
+              model: true,
+              modelModifiers: ['trim', 'bar-baz'],
+            },
+          ],
+        ],
+      })
+    })
+
+    test('v-model with arguments for component should generate modelModifiers', () => {
+      const { code, ir } = compileWithVModel(
+        '<Comp v-model:foo.trim="foo" v-model:bar.number="bar" />',
+      )
+      expect(code).toMatchSnapshot()
+      expect(code).contain(`fooModifiers: () => ({ trim: true })`)
+      expect(code).contain(`barModifiers: () => ({ number: true })`)
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        tag: 'Comp',
+        props: [
+          [
+            {
+              key: { content: 'foo', isStatic: true },
+              values: [{ content: 'foo', isStatic: false }],
+              model: true,
+              modelModifiers: ['trim'],
+            },
+            {
+              key: { content: 'bar', isStatic: true },
+              values: [{ content: 'bar', isStatic: false }],
+              model: true,
+              modelModifiers: ['number'],
+            },
+          ],
+        ],
+      })
+    })
+
+    test('v-model with dynamic arguments for component should generate modelModifiers ', () => {
+      const { code, ir } = compileWithVModel(
+        '<Comp v-model:[foo].trim="foo" v-model:[bar].number="bar" />',
+      )
+      expect(code).toMatchSnapshot()
+      expect(code).contain(`[_ctx.foo + "Modifiers"]: () => ({ trim: true })`)
+      expect(code).contain(`[_ctx.bar + "Modifiers"]: () => ({ number: true })`)
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        tag: 'Comp',
+        props: [
+          {
+            key: { content: 'foo', isStatic: false },
+            values: [{ content: 'foo', isStatic: false }],
+            model: true,
+            modelModifiers: ['trim'],
+          },
+          {
+            key: { content: 'bar', isStatic: false },
+            values: [{ content: 'bar', isStatic: false }],
+            model: true,
+            modelModifiers: ['number'],
+          },
+        ],
+      })
+    })
+  })
+})
diff --git a/packages/compiler-vapor/__tests__/transforms/vOn.spec.ts b/packages/compiler-vapor/__tests__/transforms/vOn.spec.ts
new file mode 100644
index 00000000000..963f46ad2a3
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/vOn.spec.ts
@@ -0,0 +1,710 @@
+import { BindingTypes, ErrorCodes, NodeTypes } from '@vue/compiler-dom'
+import {
+  IRNodeTypes,
+  transformChildren,
+  transformElement,
+  transformVOn,
+} from '../../src'
+import { makeCompile } from './_utils'
+
+const compileWithVOn = makeCompile({
+  nodeTransforms: [transformElement, transformChildren],
+  directiveTransforms: {
+    on: transformVOn,
+  },
+})
+
+describe('v-on', () => {
+  test('simple expression', () => {
+    const { code, ir, helpers } = compileWithVOn(
+      `<div @click="handleClick"></div>`,
+      {
+        bindingMetadata: {
+          handleClick: BindingTypes.SETUP_CONST,
+        },
+      },
+    )
+
+    expect(code).matchSnapshot()
+    expect(helpers).not.contains('delegate') // optimized as direct attachment
+    expect(ir.block.effect).toEqual([])
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        element: 0,
+        key: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'click',
+          isStatic: true,
+        },
+        value: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'handleClick',
+          isStatic: false,
+        },
+        modifiers: { keys: [], nonKeys: [], options: [] },
+        keyOverride: undefined,
+        delegate: true,
+      },
+    ])
+  })
+
+  test('event modifier', () => {
+    const { code } = compileWithVOn(
+      `<a @click.stop="handleEvent"></a>
+        <form @submit.prevent="handleEvent"></form>
+        <a @click.stop.prevent="handleEvent"></a>
+        <div @click.self="handleEvent"></div>
+        <div @click.capture="handleEvent"></div>
+        <a @click.once="handleEvent"></a>
+        <div @scroll.passive="handleEvent"></div>
+        <input @click.right="handleEvent" />
+        <input @click.left="handleEvent" />
+        <input @click.middle="handleEvent" />
+        <input @click.enter.right="handleEvent" />
+        <input @keyup.enter="handleEvent" />
+        <input @keyup.tab="handleEvent" />
+        <input @keyup.delete="handleEvent" />
+        <input @keyup.esc="handleEvent" />
+        <input @keyup.space="handleEvent" />
+        <input @keyup.up="handleEvent" />
+        <input @keyup.down="handleEvent" />
+        <input @keyup.left="handleEvent" />
+        <input @keyup.middle="submit" />
+        <input @keyup.middle.self="submit" />
+        <input @keyup.self.enter="handleEvent" />`,
+      {
+        bindingMetadata: {
+          handleEvent: BindingTypes.SETUP_CONST,
+        },
+      },
+    )
+    expect(code).matchSnapshot()
+  })
+
+  test('dynamic arg', () => {
+    const { code, ir, helpers } = compileWithVOn(
+      `<div v-on:[event]="handler"/>`,
+    )
+
+    expect(helpers).contains('on')
+    expect(helpers).contains('renderEffect')
+    expect(ir.block.operation).toMatchObject([])
+
+    expect(ir.block.effect[0].operations[0]).toMatchObject({
+      type: IRNodeTypes.SET_EVENT,
+      element: 0,
+      key: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'event',
+        isStatic: false,
+      },
+      value: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'handler',
+        isStatic: false,
+      },
+    })
+
+    expect(code).matchSnapshot()
+  })
+
+  test('dynamic arg with prefixing', () => {
+    const { code } = compileWithVOn(`<div v-on:[event]="handler"/>`, {
+      prefixIdentifiers: true,
+    })
+
+    expect(code).matchSnapshot()
+  })
+
+  test('dynamic arg with complex exp prefixing', () => {
+    const { ir, code, helpers } = compileWithVOn(
+      `<div v-on:[event(foo)]="handler"/>`,
+      {
+        prefixIdentifiers: true,
+      },
+    )
+
+    expect(helpers).contains('on')
+    expect(helpers).contains('renderEffect')
+    expect(ir.block.operation).toMatchObject([])
+
+    expect(ir.block.effect[0].operations[0]).toMatchObject({
+      type: IRNodeTypes.SET_EVENT,
+      element: 0,
+      key: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'event(foo)',
+        isStatic: false,
+      },
+      value: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'handler',
+        isStatic: false,
+      },
+    })
+
+    expect(code).matchSnapshot()
+  })
+
+  test('should wrap as function if expression is inline statement', () => {
+    const { code, ir, helpers } = compileWithVOn(`<div @click="i++"/>`)
+
+    expect(code).matchSnapshot()
+    expect(helpers).not.contains('delegate')
+    expect(ir.block.effect).toEqual([])
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        element: 0,
+        value: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'i++',
+          isStatic: false,
+        },
+        delegate: true,
+      },
+    ])
+    expect(code).contains(`n0.$evtclick = () => (_ctx.i++)`)
+  })
+
+  test('should wrap in unref if identifier is setup-maybe-ref w/ inline: true', () => {
+    const { code, helpers } = compileWithVOn(
+      `<div @click="x=y"/><div @click="x++"/><div @click="{ x } = y"/>`,
+      {
+        mode: 'module',
+        inline: true,
+        bindingMetadata: {
+          x: BindingTypes.SETUP_MAYBE_REF,
+          y: BindingTypes.SETUP_MAYBE_REF,
+        },
+      },
+    )
+    expect(code).matchSnapshot()
+    expect(helpers).contains('unref')
+    expect(code).contains(`n0.$evtclick = () => (x.value=_unref(y))`)
+    expect(code).contains(`n1.$evtclick = () => (x.value++)`)
+    expect(code).contains(`n2.$evtclick = () => ({ x: x.value } = _unref(y))`)
+  })
+
+  test('should handle multiple inline statement', () => {
+    const { ir, code } = compileWithVOn(`<div @click="foo();bar()"/>`)
+
+    expect(code).matchSnapshot()
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        value: { content: 'foo();bar()' },
+      },
+    ])
+    // should wrap with `{` for multiple statements
+    // in this case the return value is discarded and the behavior is
+    // consistent with 2.x
+    expect(code).contains(`n0.$evtclick = () => {_ctx.foo();_ctx.bar()}`)
+  })
+
+  test('should handle multi-line statement', () => {
+    const { code, ir } = compileWithVOn(`<div @click="\nfoo();\nbar()\n"/>`)
+
+    expect(code).matchSnapshot()
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        value: { content: '\nfoo();\nbar()\n' },
+      },
+    ])
+    // should wrap with `{` for multiple statements
+    // in this case the return value is discarded and the behavior is
+    // consistent with 2.x
+    expect(code).contains(`n0.$evtclick = () => {\n_ctx.foo();\n_ctx.bar()\n}`)
+  })
+
+  test('inline statement w/ prefixIdentifiers: true', () => {
+    const { code, ir } = compileWithVOn(`<div @click="foo($event)"/>`, {
+      prefixIdentifiers: true,
+    })
+
+    expect(code).matchSnapshot()
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        value: { content: 'foo($event)' },
+      },
+    ])
+    // should NOT prefix $event
+    expect(code).contains(`n0.$evtclick = $event => (_ctx.foo($event))`)
+  })
+
+  test('multiple inline statements w/ prefixIdentifiers: true', () => {
+    const { ir, code } = compileWithVOn(`<div @click="foo($event);bar()"/>`, {
+      prefixIdentifiers: true,
+    })
+
+    expect(code).matchSnapshot()
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        value: { content: 'foo($event);bar()' },
+      },
+    ])
+    // should NOT prefix $event
+    expect(code).contains(
+      `n0.$evtclick = $event => {_ctx.foo($event);_ctx.bar()}`,
+    )
+  })
+
+  test('should NOT wrap as function if expression is already function expression', () => {
+    const { code, ir } = compileWithVOn(`<div @click="$event => foo($event)"/>`)
+
+    expect(code).matchSnapshot()
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        value: { content: '$event => foo($event)' },
+      },
+    ])
+    expect(code).contains(`n0.$evtclick = $event => _ctx.foo($event)`)
+  })
+
+  test('should NOT wrap as function if expression is already function expression (with Typescript)', () => {
+    const { ir, code } = compileWithVOn(
+      `<div @click="(e: any): any => foo(e)"/>`,
+      { expressionPlugins: ['typescript'] },
+    )
+
+    expect(code).matchSnapshot()
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        value: { content: '(e: any): any => foo(e)' },
+      },
+    ])
+    expect(code).contains(`n0.$evtclick = (e: any): any => _ctx.foo(e)`)
+  })
+
+  test('should NOT wrap as function if expression is already function expression (with newlines)', () => {
+    const { ir, code } = compileWithVOn(
+      `<div @click="
+      $event => {
+        foo($event)
+      }
+    "/>`,
+    )
+
+    expect(code).matchSnapshot()
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        value: {
+          content: `
+      $event => {
+        foo($event)
+      }
+    `,
+        },
+      },
+    ])
+  })
+
+  test('should NOT add a prefix to $event if the expression is a function expression', () => {
+    const { ir, code } = compileWithVOn(
+      `<div @click="$event => {i++;foo($event)}"></div>`,
+      {
+        prefixIdentifiers: true,
+      },
+    )
+
+    expect(ir.block.operation[0]).toMatchObject({
+      type: IRNodeTypes.SET_EVENT,
+      value: { content: '$event => {i++;foo($event)}' },
+    })
+
+    expect(code).matchSnapshot()
+  })
+
+  test('should NOT wrap as function if expression is complex member expression', () => {
+    const { ir, code } = compileWithVOn(`<div @click="a['b' + c]"/>`)
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        value: { content: `a['b' + c]` },
+      },
+    ])
+
+    expect(code).matchSnapshot()
+  })
+
+  test('complex member expression w/ prefixIdentifiers: true', () => {
+    const { ir, code } = compileWithVOn(`<div @click="a['b' + c]"/>`)
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        value: { content: `a['b' + c]` },
+      },
+    ])
+
+    expect(code).matchSnapshot()
+    expect(code).contains(`n0.$evtclick = e => _ctx.a['b' + _ctx.c](e)`)
+  })
+
+  test('function expression w/ prefixIdentifiers: true', () => {
+    const { code, ir } = compileWithVOn(`<div @click="e => foo(e)"/>`, {
+      prefixIdentifiers: true,
+    })
+
+    expect(code).matchSnapshot()
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        value: { content: `e => foo(e)` },
+      },
+    ])
+    expect(code).contains(`n0.$evtclick = e => _ctx.foo(e)`)
+  })
+
+  test('should error if no expression AND no modifier', () => {
+    const onError = vi.fn()
+    compileWithVOn(`<div v-on:click />`, { onError })
+    expect(onError.mock.calls[0][0]).toMatchObject({
+      code: ErrorCodes.X_V_ON_NO_EXPRESSION,
+      loc: {
+        start: {
+          line: 1,
+          column: 6,
+        },
+        end: {
+          line: 1,
+          column: 16,
+        },
+      },
+    })
+  })
+
+  test('should NOT error if no expression but has modifier', () => {
+    const onError = vi.fn()
+    compileWithVOn(`<div v-on:click.prevent />`, { onError })
+    expect(onError).not.toHaveBeenCalled()
+  })
+
+  test('should support multiple modifiers and event options w/ prefixIdentifiers: true', () => {
+    const { code, ir, helpers } = compileWithVOn(
+      `<div @click.stop.prevent.capture.once="test"/>`,
+      {
+        prefixIdentifiers: true,
+      },
+    )
+
+    expect(code).matchSnapshot()
+    expect(helpers).contains('on')
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        value: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'test',
+          isStatic: false,
+        },
+        modifiers: {
+          keys: [],
+          nonKeys: ['stop', 'prevent'],
+          options: ['capture', 'once'],
+        },
+        keyOverride: undefined,
+        delegate: false,
+      },
+    ])
+    expect(code).contains(
+      `_on(n0, "click", _withModifiers(e => _ctx.test(e), ["stop","prevent"]), {
+    capture: true, 
+    once: true
+  })`,
+    )
+  })
+
+  test('should support multiple events and modifiers options w/ prefixIdentifiers: true', () => {
+    const { code, ir } = compileWithVOn(
+      `<div @click.stop="test" @keyup.enter="test" />`,
+      {
+        prefixIdentifiers: true,
+      },
+    )
+
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        key: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'click',
+          isStatic: true,
+        },
+        value: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'test',
+          isStatic: false,
+        },
+        modifiers: {
+          keys: [],
+          nonKeys: ['stop'],
+          options: [],
+        },
+      },
+      {
+        type: IRNodeTypes.SET_EVENT,
+        key: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'keyup',
+          isStatic: true,
+        },
+        value: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'test',
+          isStatic: false,
+        },
+        modifiers: {
+          keys: ['enter'],
+          nonKeys: [],
+          options: [],
+        },
+      },
+    ])
+
+    expect(code).matchSnapshot()
+    expect(code).contains(
+      `n0.$evtclick = _withModifiers(e => _ctx.test(e), ["stop"])
+  n0.$evtkeyup = _withKeys(e => _ctx.test(e), ["enter"])`,
+    )
+  })
+
+  test('should wrap keys guard for keyboard events or dynamic events', () => {
+    const { code, ir } = compileWithVOn(
+      `<div @keydown.stop.capture.ctrl.a="test"/>`,
+      {
+        prefixIdentifiers: true,
+      },
+    )
+
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        element: 0,
+        key: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'keydown',
+          isStatic: true,
+        },
+        value: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'test',
+          isStatic: false,
+        },
+        modifiers: {
+          keys: ['a'],
+          nonKeys: ['stop', 'ctrl'],
+          options: ['capture'],
+        },
+      },
+    ])
+
+    expect(code).matchSnapshot()
+  })
+
+  test('should not wrap keys guard if no key modifier is present', () => {
+    const { code, ir } = compileWithVOn(`<div @keyup.exact="test"/>`, {
+      prefixIdentifiers: true,
+    })
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        modifiers: { nonKeys: ['exact'] },
+      },
+    ])
+
+    expect(code).matchSnapshot()
+  })
+
+  test('should wrap keys guard for static key event w/ left/right modifiers', () => {
+    const { code, ir } = compileWithVOn(`<div @keyup.left="test"/>`, {
+      prefixIdentifiers: true,
+    })
+
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        modifiers: {
+          keys: ['left'],
+          nonKeys: [],
+          options: [],
+        },
+      },
+    ])
+
+    expect(code).matchSnapshot()
+  })
+
+  test('should wrap both for dynamic key event w/ left/right modifiers', () => {
+    const { code, ir } = compileWithVOn(`<div @[e].left="test"/>`, {
+      prefixIdentifiers: true,
+    })
+
+    expect(ir.block.effect[0].operations).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        key: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'e',
+          isStatic: false,
+        },
+        modifiers: {
+          keys: ['left'],
+          nonKeys: ['left'],
+          options: [],
+        },
+      },
+    ])
+
+    expect(code).matchSnapshot()
+  })
+
+  test('should transform click.right', () => {
+    const { code, ir } = compileWithVOn(`<div @click.right="test"/>`)
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        key: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'contextmenu',
+          isStatic: true,
+        },
+        modifiers: { nonKeys: ['right'] },
+        keyOverride: undefined,
+      },
+    ])
+
+    expect(code).matchSnapshot()
+    expect(code).contains('"contextmenu"')
+
+    // dynamic
+    const { code: code2, ir: ir2 } = compileWithVOn(
+      `<div @[event].right="test"/>`,
+    )
+    expect(ir2.block.effect[0].operations).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        key: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'event',
+          isStatic: false,
+        },
+        modifiers: { nonKeys: ['right'] },
+        keyOverride: ['click', 'contextmenu'],
+      },
+    ])
+
+    expect(code2).matchSnapshot()
+    expect(code2).contains(
+      '(_ctx.event) === "click" ? "contextmenu" : (_ctx.event)',
+    )
+  })
+
+  test('should transform click.middle', () => {
+    const { code, ir } = compileWithVOn(`<div @click.middle="test"/>`)
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        key: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'mouseup',
+          isStatic: true,
+        },
+        modifiers: { nonKeys: ['middle'] },
+        keyOverride: undefined,
+      },
+    ])
+
+    expect(code).matchSnapshot()
+    expect(code).contains('"mouseup"')
+
+    // dynamic
+    const { code: code2, ir: ir2 } = compileWithVOn(
+      `<div @[event].middle="test"/>`,
+    )
+
+    expect(ir2.block.effect[0].operations).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        key: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'event',
+          isStatic: false,
+        },
+        modifiers: { nonKeys: ['middle'] },
+        keyOverride: ['click', 'mouseup'],
+      },
+    ])
+
+    expect(code2).matchSnapshot()
+    expect(code2).contains(
+      '(_ctx.event) === "click" ? "mouseup" : (_ctx.event)',
+    )
+  })
+
+  test('should not prefix member expression', () => {
+    const { code } = compileWithVOn(`<div @click="foo.bar"/>`, {
+      prefixIdentifiers: true,
+    })
+
+    expect(code).matchSnapshot()
+    expect(code).contains(`n0.$evtclick = e => _ctx.foo.bar(e)`)
+  })
+
+  test('should delegate event', () => {
+    const { code, ir, helpers } = compileWithVOn(`<div @click="test"/>`)
+
+    expect(code).matchSnapshot()
+    expect(code).contains('_delegateEvents("click")')
+    expect(helpers).contains('delegateEvents')
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        delegate: true,
+      },
+    ])
+  })
+
+  test('should use delegate helper when have multiple events of same name', () => {
+    const { code, helpers } = compileWithVOn(
+      `<div @click="test" @click.stop="test" />`,
+    )
+    expect(helpers).contains('delegate')
+    expect(code).toMatchSnapshot()
+    expect(code).contains('_delegate(n0, "click", e => _ctx.test(e))')
+    expect(code).contains(
+      '_delegate(n0, "click", _withModifiers(e => _ctx.test(e), ["stop"]))',
+    )
+  })
+
+  test('expression with type', () => {
+    const { code } = compileWithVOn(
+      `<div @click="(<number>handleClick as any)"></div>`,
+      {
+        bindingMetadata: {
+          handleClick: BindingTypes.SETUP_CONST,
+        },
+      },
+    )
+    expect(code).matchSnapshot()
+    expect(code).include('n0.$evtclick = e => _ctx.handleClick(e)')
+  })
+
+  test('component event with special characters', () => {
+    const { code } = compileWithVOn(
+      `<Foo @update:model="() => {}" @update-model="() => {}" />`,
+    )
+
+    expect(code).matchSnapshot()
+    expect(code).contains('const _on_update_model = () => {}')
+    expect(code).contains('const _on_update_model1 = () => {}')
+    expect(code).contains('"onUpdate:model": () => _on_update_model')
+    expect(code).contains('"onUpdate-model": () => _on_update_model1')
+  })
+})
diff --git a/packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts b/packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts
new file mode 100644
index 00000000000..97f6ee62a11
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts
@@ -0,0 +1,215 @@
+import { BindingTypes, NodeTypes } from '@vue/compiler-dom'
+import { IRNodeTypes } from '../../src'
+import { getBaseTransformPreset } from '../../src/compile'
+import { makeCompile } from './_utils'
+
+const [nodeTransforms, directiveTransforms] = getBaseTransformPreset()
+const compileWithOnce = makeCompile({
+  nodeTransforms,
+  directiveTransforms,
+})
+
+describe('compiler: v-once', () => {
+  test('basic', () => {
+    const { ir, code } = compileWithOnce(
+      `<div v-once>
+        {{ msg }}
+        <span :class="clz" />
+      </div>`,
+      {
+        bindingMetadata: {
+          msg: BindingTypes.SETUP_REF,
+          clz: BindingTypes.SETUP_REF,
+        },
+      },
+    )
+
+    expect(code).toMatchSnapshot()
+    expect(ir.block.effect).lengthOf(0)
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_TEXT,
+        element: 0,
+        values: [
+          {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: 'msg',
+            isStatic: false,
+          },
+          {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: ' ',
+            isStatic: true,
+          },
+        ],
+      },
+      {
+        element: 1,
+        type: IRNodeTypes.SET_PROP,
+        prop: {
+          key: {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: 'class',
+            isStatic: true,
+          },
+          values: [
+            {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: 'clz',
+              isStatic: false,
+            },
+          ],
+        },
+      },
+    ])
+  })
+
+  test('as root node', () => {
+    const { ir, code } = compileWithOnce(`<div :id="foo" v-once />`)
+
+    expect(code).toMatchSnapshot()
+
+    expect(ir.block.effect).lengthOf(0)
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_PROP,
+        element: 0,
+        prop: {
+          key: {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: 'id',
+            isStatic: true,
+          },
+          values: [
+            {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: 'foo',
+              isStatic: false,
+            },
+          ],
+        },
+      },
+    ])
+    expect(code).not.contains('effect')
+  })
+
+  test('on nested plain element', () => {
+    const { ir, code } = compileWithOnce(`<div><div :id="foo" v-once /></div>`)
+
+    expect(code).toMatchSnapshot()
+
+    expect(ir.block.effect).lengthOf(0)
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_PROP,
+        element: 0,
+        prop: {
+          runtimeCamelize: false,
+          key: {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: 'id',
+            isStatic: true,
+          },
+          values: [
+            {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: 'foo',
+              isStatic: false,
+            },
+          ],
+        },
+      },
+    ])
+  })
+
+  test('on component', () => {
+    const { ir, code } = compileWithOnce(`<div><Comp :id="foo" v-once /></div>`)
+    expect(code).toMatchSnapshot()
+    expect(ir.block.effect).lengthOf(0)
+    expect(ir.block.dynamic.children[0].children[0].operation).toMatchObject({
+      type: IRNodeTypes.CREATE_COMPONENT_NODE,
+      id: 0,
+      tag: 'Comp',
+      once: true,
+      parent: 1,
+    })
+  })
+
+  test.todo('on slot outlet')
+
+  test('inside v-once', () => {
+    const { ir, code } = compileWithOnce(`<div v-once><div v-once/></div>`)
+
+    expect(code).toMatchSnapshot()
+
+    expect(ir.block.effect).lengthOf(0)
+    expect(ir.block.operation).lengthOf(0)
+  })
+
+  test.todo('with hoistStatic: true')
+
+  test('with v-if', () => {
+    const { ir, code } = compileWithOnce(`<div v-if="expr" v-once />`)
+    expect(code).toMatchSnapshot()
+
+    expect(ir.block.effect).lengthOf(0)
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.IF,
+      id: 0,
+      once: true,
+      condition: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'expr',
+        isStatic: false,
+      },
+      positive: {
+        type: IRNodeTypes.BLOCK,
+        dynamic: {
+          children: [{ template: 0 }],
+        },
+      },
+    })
+  })
+
+  test('with v-if/else', () => {
+    const { ir, code } = compileWithOnce(
+      `<div v-if="expr" v-once /><p v-else/>`,
+    )
+    expect(code).toMatchSnapshot()
+
+    expect(ir.block.effect).lengthOf(0)
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.IF,
+      id: 0,
+      once: true,
+      condition: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'expr',
+        isStatic: false,
+      },
+      positive: {
+        type: IRNodeTypes.BLOCK,
+        dynamic: {
+          children: [{ template: 0 }],
+        },
+      },
+      negative: {
+        type: IRNodeTypes.BLOCK,
+        dynamic: {
+          children: [{ template: 1 }],
+        },
+      },
+    })
+  })
+
+  test('with v-for', () => {
+    const { ir, code } = compileWithOnce(`<div v-for="i in list" v-once />`)
+    expect(code).toMatchSnapshot()
+    expect(ir.block.effect).lengthOf(0)
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.FOR,
+      id: 0,
+      once: true,
+    })
+  })
+})
diff --git a/packages/compiler-vapor/__tests__/transforms/vShow.spec.ts b/packages/compiler-vapor/__tests__/transforms/vShow.spec.ts
new file mode 100644
index 00000000000..800f64cc0b4
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/vShow.spec.ts
@@ -0,0 +1,29 @@
+import { makeCompile } from './_utils'
+import { transformChildren, transformElement, transformVShow } from '../../src'
+import { DOMErrorCodes } from '@vue/compiler-dom'
+
+const compileWithVShow = makeCompile({
+  nodeTransforms: [transformElement, transformChildren],
+  directiveTransforms: {
+    show: transformVShow,
+  },
+})
+
+describe('compiler: v-show transform', () => {
+  test('simple expression', () => {
+    const { code } = compileWithVShow(`<div v-show="foo"/>`)
+    expect(code).toMatchSnapshot()
+  })
+
+  test('should raise error if has no expression', () => {
+    const onError = vi.fn()
+    compileWithVShow(`<div v-show/>`, { onError })
+
+    expect(onError).toHaveBeenCalledTimes(1)
+    expect(onError).toHaveBeenCalledWith(
+      expect.objectContaining({
+        code: DOMErrorCodes.X_V_SHOW_NO_EXPRESSION,
+      }),
+    )
+  })
+})
diff --git a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts
new file mode 100644
index 00000000000..909162fe3ca
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts
@@ -0,0 +1,568 @@
+import { ErrorCodes, NodeTypes } from '@vue/compiler-dom'
+import {
+  IRNodeTypes,
+  IRSlotType,
+  transformChildren,
+  transformElement,
+  transformSlotOutlet,
+  transformText,
+  transformVBind,
+  transformVFor,
+  transformVIf,
+  transformVOn,
+  transformVSlot,
+} from '../../src'
+import { makeCompile } from './_utils'
+
+const compileWithSlots = makeCompile({
+  nodeTransforms: [
+    transformText,
+    transformVIf,
+    transformVFor,
+    transformSlotOutlet,
+    transformElement,
+    transformVSlot,
+    transformChildren,
+  ],
+  directiveTransforms: {
+    bind: transformVBind,
+    on: transformVOn,
+  },
+})
+
+describe('compiler: transform slot', () => {
+  test('implicit default slot', () => {
+    const { ir, code } = compileWithSlots(`<Comp><div/></Comp>`)
+    expect(code).toMatchSnapshot()
+
+    expect(ir.template).toEqual(['<div></div>'])
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.CREATE_COMPONENT_NODE,
+      id: 1,
+      tag: 'Comp',
+      props: [[]],
+      slots: [
+        {
+          slotType: IRSlotType.STATIC,
+          slots: {
+            default: {
+              type: IRNodeTypes.BLOCK,
+              dynamic: {
+                children: [{ template: 0 }],
+              },
+            },
+          },
+        },
+      ],
+    })
+    expect(ir.block.returns).toEqual([1])
+    expect(ir.block.dynamic).toMatchObject({
+      children: [{ id: 1 }],
+    })
+  })
+
+  test('on-component default slot', () => {
+    const { ir, code } = compileWithSlots(
+      `<Comp v-slot="{ foo }">{{ foo + bar }}</Comp>`,
+    )
+    expect(code).toMatchSnapshot()
+
+    expect(code).contains(`"default": (_slotProps0) =>`)
+    expect(code).contains(`_slotProps0["foo"] + _ctx.bar`)
+
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.CREATE_COMPONENT_NODE,
+      tag: 'Comp',
+      props: [[]],
+      slots: [
+        {
+          slotType: IRSlotType.STATIC,
+          slots: {
+            default: {
+              type: IRNodeTypes.BLOCK,
+              props: {
+                type: NodeTypes.SIMPLE_EXPRESSION,
+                content: '{ foo }',
+                ast: {
+                  type: 'ArrowFunctionExpression',
+                  params: [{ type: 'ObjectPattern' }],
+                },
+              },
+            },
+          },
+        },
+      ],
+    })
+  })
+
+  test('on component named slot', () => {
+    const { ir, code } = compileWithSlots(
+      `<Comp v-slot:named="{ foo }">{{ foo + bar }}</Comp>`,
+    )
+    expect(code).toMatchSnapshot()
+
+    expect(code).contains(`"named": (_slotProps0) =>`)
+    expect(code).contains(`_slotProps0["foo"] + _ctx.bar`)
+
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.CREATE_COMPONENT_NODE,
+      tag: 'Comp',
+      slots: [
+        {
+          slotType: IRSlotType.STATIC,
+          slots: {
+            named: {
+              type: IRNodeTypes.BLOCK,
+              props: {
+                type: NodeTypes.SIMPLE_EXPRESSION,
+                content: '{ foo }',
+              },
+            },
+          },
+        },
+      ],
+    })
+  })
+
+  test('on component dynamically named slot', () => {
+    const { ir, code } = compileWithSlots(
+      `<Comp v-slot:[named]="{ foo }">{{ foo + bar }}</Comp>`,
+    )
+    expect(code).toMatchSnapshot()
+
+    expect(code).contains(`fn: (_slotProps0) =>`)
+    expect(code).contains(`_slotProps0["foo"] + _ctx.bar`)
+
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.CREATE_COMPONENT_NODE,
+      tag: 'Comp',
+      slots: [
+        {
+          name: {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: 'named',
+            isStatic: false,
+          },
+          fn: {
+            type: IRNodeTypes.BLOCK,
+            props: {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: '{ foo }',
+            },
+          },
+        },
+      ],
+    })
+  })
+
+  test('named slots w/ implicit default slot', () => {
+    const { ir, code } = compileWithSlots(
+      `<Comp>
+        <template #one>foo</template>bar<span/>
+      </Comp>`,
+    )
+    expect(code).toMatchSnapshot()
+
+    expect(ir.template).toEqual(['foo', 'bar', '<span></span>'])
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.CREATE_COMPONENT_NODE,
+      id: 4,
+      tag: 'Comp',
+      props: [[]],
+      slots: [
+        {
+          slotType: IRSlotType.STATIC,
+          slots: {
+            one: {
+              type: IRNodeTypes.BLOCK,
+              dynamic: {
+                children: [{ template: 0 }],
+              },
+            },
+            default: {
+              type: IRNodeTypes.BLOCK,
+              dynamic: {
+                children: [{}, { template: 1 }, { template: 2 }],
+              },
+            },
+          },
+        },
+      ],
+    })
+  })
+
+  test('nested slots scoping', () => {
+    const { ir, code } = compileWithSlots(
+      `<Comp>
+        <template #default="{ foo }">
+          <Inner v-slot="{ bar }">
+            {{ foo + bar + baz }}
+          </Inner>
+          {{ foo + bar + baz }}
+        </template>
+      </Comp>`,
+    )
+    expect(code).toMatchSnapshot()
+
+    expect(code).contains(`"default": (_slotProps0) =>`)
+    expect(code).contains(`"default": (_slotProps1) =>`)
+    expect(code).contains(`_slotProps0["foo"] + _slotProps1["bar"] + _ctx.baz`)
+    expect(code).contains(`_slotProps0["foo"] + _ctx.bar + _ctx.baz`)
+
+    const outerOp = ir.block.dynamic.children[0].operation
+    expect(outerOp).toMatchObject({
+      type: IRNodeTypes.CREATE_COMPONENT_NODE,
+      tag: 'Comp',
+      props: [[]],
+      slots: [
+        {
+          slotType: IRSlotType.STATIC,
+          slots: {
+            default: {
+              type: IRNodeTypes.BLOCK,
+              props: {
+                type: NodeTypes.SIMPLE_EXPRESSION,
+                content: '{ foo }',
+              },
+            },
+          },
+        },
+      ],
+    })
+    expect(
+      (outerOp as any).slots[0].slots.default.dynamic.children[0].operation,
+    ).toMatchObject({
+      type: IRNodeTypes.CREATE_COMPONENT_NODE,
+      tag: 'Inner',
+      slots: [
+        {
+          slotType: IRSlotType.STATIC,
+          slots: {
+            default: {
+              type: IRNodeTypes.BLOCK,
+              props: {
+                type: NodeTypes.SIMPLE_EXPRESSION,
+                content: '{ bar }',
+              },
+            },
+          },
+        },
+      ],
+    })
+  })
+
+  test('dynamic slots name', () => {
+    const { ir, code } = compileWithSlots(
+      `<Comp>
+        <template #[name]>foo</template>
+      </Comp>`,
+    )
+    expect(code).toMatchSnapshot()
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.CREATE_COMPONENT_NODE,
+      tag: 'Comp',
+      slots: [
+        {
+          name: {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: 'name',
+            isStatic: false,
+          },
+          fn: { type: IRNodeTypes.BLOCK },
+        },
+      ],
+    })
+  })
+
+  test('dynamic slots name w/ v-for', () => {
+    const { ir, code } = compileWithSlots(
+      `<Comp>
+        <template v-for="item in list" #[item]="{ bar }">{{ bar }}</template>
+      </Comp>`,
+    )
+    expect(code).toMatchSnapshot()
+
+    expect(code).contains(`fn: (_slotProps0) =>`)
+    expect(code).contains(`_setText(n0, _toDisplayString(_slotProps0["bar"]))`)
+
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.CREATE_COMPONENT_NODE,
+      tag: 'Comp',
+      slots: [
+        {
+          name: {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: 'item',
+            isStatic: false,
+          },
+          fn: { type: IRNodeTypes.BLOCK },
+          loop: {
+            source: { content: 'list' },
+            value: { content: 'item' },
+            index: undefined,
+          },
+        },
+      ],
+    })
+  })
+
+  test('dynamic slots name w/ v-for and provide absent key', () => {
+    const { ir, code } = compileWithSlots(
+      `<Comp>
+        <template v-for="(,,index) in list" #[index]>foo</template>
+      </Comp>`,
+    )
+    expect(code).toMatchSnapshot()
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.CREATE_COMPONENT_NODE,
+      tag: 'Comp',
+      slots: [
+        {
+          name: {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: 'index',
+            isStatic: false,
+          },
+          fn: { type: IRNodeTypes.BLOCK },
+          loop: {
+            source: { content: 'list' },
+            value: undefined,
+            index: {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+            },
+          },
+        },
+      ],
+    })
+  })
+
+  test('dynamic slots name w/ v-if / v-else[-if]', () => {
+    const { ir, code } = compileWithSlots(
+      `<Comp>
+        <template v-if="condition" #condition>condition slot</template>
+        <template v-else-if="anotherCondition" #condition="{ foo, bar }">another condition</template>
+        <template v-else #condition>else condition</template>
+      </Comp>`,
+    )
+    expect(code).toMatchSnapshot()
+
+    expect(code).contains(`fn: (_slotProps0) =>`)
+
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.CREATE_COMPONENT_NODE,
+      tag: 'Comp',
+      slots: [
+        {
+          slotType: IRSlotType.CONDITIONAL,
+          condition: { content: 'condition' },
+          positive: {
+            slotType: IRSlotType.DYNAMIC,
+          },
+          negative: {
+            slotType: IRSlotType.CONDITIONAL,
+            condition: { content: 'anotherCondition' },
+            positive: {
+              slotType: IRSlotType.DYNAMIC,
+            },
+            negative: { slotType: IRSlotType.DYNAMIC },
+          },
+        },
+      ],
+    })
+  })
+
+  test('slot + v-if / v-else[-if] should not cause error', () => {
+    const { code } = compileWithSlots(
+      `<div>
+        <slot name="foo"></slot>
+        <Foo v-if="true"></Foo>
+        <Bar v-else />
+      </div>`,
+    )
+    expect(code).toMatchSnapshot()
+  })
+
+  test('quote slot name', () => {
+    const { code } = compileWithSlots(
+      `<Comp><template #nav-bar-title-before></template></Comp>`,
+    )
+    expect(code).toMatchSnapshot()
+    expect(code).contains(`"nav-bar-title-before"`)
+  })
+
+  test('nested component slot', () => {
+    const { ir, code } = compileWithSlots(`<A><B/></A>`)
+    expect(code).toMatchSnapshot()
+    expect(ir.block.dynamic.children[0].operation).toMatchObject({
+      type: IRNodeTypes.CREATE_COMPONENT_NODE,
+      tag: 'A',
+      slots: [
+        {
+          slotType: IRSlotType.STATIC,
+          slots: {
+            default: {
+              type: IRNodeTypes.BLOCK,
+              dynamic: {
+                children: [
+                  {
+                    operation: {
+                      type: IRNodeTypes.CREATE_COMPONENT_NODE,
+                      tag: 'B',
+                      slots: [],
+                    },
+                  },
+                ],
+              },
+            },
+          },
+        },
+      ],
+    })
+  })
+
+  describe('errors', () => {
+    test('error on extraneous children w/ named default slot', () => {
+      const onError = vi.fn()
+      const source = `<Comp><template #default>foo</template>bar</Comp>`
+      compileWithSlots(source, { onError })
+      const index = source.indexOf('bar')
+      expect(onError.mock.calls[0][0]).toMatchObject({
+        code: ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
+        loc: {
+          start: {
+            offset: index,
+            line: 1,
+            column: index + 1,
+          },
+          end: {
+            offset: index + 3,
+            line: 1,
+            column: index + 4,
+          },
+        },
+      })
+    })
+
+    test('error on duplicated slot names', () => {
+      const onError = vi.fn()
+      const source = `<Comp><template #foo></template><template #foo></template></Comp>`
+      compileWithSlots(source, { onError })
+      const index = source.lastIndexOf('#foo')
+      expect(onError.mock.calls[0][0]).toMatchObject({
+        code: ErrorCodes.X_V_SLOT_DUPLICATE_SLOT_NAMES,
+        loc: {
+          start: {
+            offset: index,
+            line: 1,
+            column: index + 1,
+          },
+          end: {
+            offset: index + 4,
+            line: 1,
+            column: index + 5,
+          },
+        },
+      })
+    })
+
+    test('error on invalid mixed slot usage', () => {
+      const onError = vi.fn()
+      const source = `<Comp v-slot="foo"><template #foo></template></Comp>`
+      compileWithSlots(source, { onError })
+      const index = source.lastIndexOf('v-slot="foo"')
+      expect(onError.mock.calls[0][0]).toMatchObject({
+        code: ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE,
+        loc: {
+          start: {
+            offset: index,
+            line: 1,
+            column: index + 1,
+          },
+          end: {
+            offset: index + 12,
+            line: 1,
+            column: index + 13,
+          },
+        },
+      })
+    })
+
+    test('error on v-slot usage on plain elements', () => {
+      const onError = vi.fn()
+      const source = `<div v-slot/>`
+      compileWithSlots(source, { onError })
+      const index = source.indexOf('v-slot')
+      expect(onError.mock.calls[0][0]).toMatchObject({
+        code: ErrorCodes.X_V_SLOT_MISPLACED,
+        loc: {
+          start: {
+            offset: index,
+            line: 1,
+            column: index + 1,
+          },
+          end: {
+            offset: index + 6,
+            line: 1,
+            column: index + 7,
+          },
+        },
+      })
+    })
+  })
+
+  describe(`with whitespace: 'preserve'`, () => {
+    test('named default slot + implicit whitespace content', () => {
+      const source = `
+      <Comp>
+        <template #header> Header </template>
+        <template #default> Default </template>
+      </Comp>
+      `
+      const { code } = compileWithSlots(source, {
+        whitespace: 'preserve',
+      })
+
+      expect(
+        `Extraneous children found when component already has explicitly named default slot.`,
+      ).not.toHaveBeenWarned()
+      expect(code).toMatchSnapshot()
+    })
+
+    test('implicit default slot', () => {
+      const source = `
+      <Comp>
+        <template #header> Header </template>
+        <p/>
+      </Comp>
+      `
+      const { code } = compileWithSlots(source, {
+        whitespace: 'preserve',
+      })
+
+      expect(
+        `Extraneous children found when component already has explicitly named default slot.`,
+      ).not.toHaveBeenWarned()
+      expect(code).toMatchSnapshot()
+    })
+
+    test('should not generate whitespace only default slot', () => {
+      const source = `
+      <Comp>
+        <template #header> Header </template>
+        <template #footer> Footer </template>
+      </Comp>
+      `
+      const { code, ir } = compileWithSlots(source, {
+        whitespace: 'preserve',
+      })
+
+      const slots = (ir.block.dynamic.children[0].operation as any).slots[0]
+        .slots
+      // should be: header, footer (no default)
+      expect(Object.keys(slots).length).toBe(2)
+      expect(!!slots['default']).toBe(false)
+
+      expect(code).toMatchSnapshot()
+    })
+  })
+})
diff --git a/packages/compiler-vapor/__tests__/transforms/vText.spec.ts b/packages/compiler-vapor/__tests__/transforms/vText.spec.ts
new file mode 100644
index 00000000000..4f074fee87e
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/transforms/vText.spec.ts
@@ -0,0 +1,111 @@
+import { BindingTypes, DOMErrorCodes, NodeTypes } from '@vue/compiler-dom'
+import {
+  IRNodeTypes,
+  transformChildren,
+  transformElement,
+  transformVText,
+} from '../../src'
+import { makeCompile } from './_utils'
+
+const compileWithVText = makeCompile({
+  nodeTransforms: [transformElement, transformChildren],
+  directiveTransforms: {
+    text: transformVText,
+  },
+})
+
+describe('v-text', () => {
+  test('should convert v-text to setText', () => {
+    const { code, ir, helpers } = compileWithVText(`<div v-text="str"></div>`, {
+      bindingMetadata: {
+        str: BindingTypes.SETUP_REF,
+      },
+    })
+
+    expect(helpers).contains('setText')
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.GET_TEXT_CHILD,
+        parent: 0,
+      },
+    ])
+
+    expect(ir.block.effect).toMatchObject([
+      {
+        expressions: [
+          {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: 'str',
+            isStatic: false,
+          },
+        ],
+        operations: [
+          {
+            type: IRNodeTypes.SET_TEXT,
+            element: 0,
+            values: [
+              {
+                type: NodeTypes.SIMPLE_EXPRESSION,
+                content: 'str',
+                isStatic: false,
+              },
+            ],
+          },
+        ],
+      },
+    ])
+
+    expect(code).matchSnapshot()
+  })
+
+  test('should raise error and ignore children when v-text is present', () => {
+    const onError = vi.fn()
+    const { code, ir } = compileWithVText(`<div v-text="test">hello</div>`, {
+      onError,
+    })
+    expect(onError.mock.calls).toMatchObject([
+      [{ code: DOMErrorCodes.X_V_TEXT_WITH_CHILDREN }],
+    ])
+
+    // children should have been removed
+    expect(ir.template).toEqual(['<div> </div>'])
+
+    expect(ir.block.effect).toMatchObject([
+      {
+        expressions: [
+          {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: 'test',
+            isStatic: false,
+          },
+        ],
+        operations: [
+          {
+            type: IRNodeTypes.SET_TEXT,
+            element: 0,
+            values: [
+              {
+                type: NodeTypes.SIMPLE_EXPRESSION,
+                content: 'test',
+                isStatic: false,
+              },
+            ],
+          },
+        ],
+      },
+    ])
+
+    expect(code).matchSnapshot()
+    // children should have been removed
+    expect(code).contains('template("<div> </div>", true)')
+  })
+
+  test('should raise error if has no expression', () => {
+    const onError = vi.fn()
+    const { code } = compileWithVText(`<div v-text></div>`, { onError })
+    expect(code).matchSnapshot()
+    expect(onError.mock.calls).toMatchObject([
+      [{ code: DOMErrorCodes.X_V_TEXT_NO_EXPRESSION }],
+    ])
+  })
+})
diff --git a/packages/compiler-vapor/package.json b/packages/compiler-vapor/package.json
new file mode 100644
index 00000000000..81a37cde0a2
--- /dev/null
+++ b/packages/compiler-vapor/package.json
@@ -0,0 +1,51 @@
+{
+  "name": "@vue/compiler-vapor",
+  "version": "3.6.0-alpha.2",
+  "description": "@vue/compiler-vapor",
+  "main": "dist/compiler-vapor.cjs.js",
+  "module": "dist/compiler-vapor.esm-bundler.js",
+  "types": "dist/compiler-vapor.d.ts",
+  "files": [
+    "dist"
+  ],
+  "sideEffects": false,
+  "exports": {
+    ".": {
+      "types": "./dist/compiler-vapor.d.ts",
+      "node": "./dist/compiler-vapor.cjs.js",
+      "module": "./dist/compiler-vapor.esm-browser.js",
+      "import": "./dist/compiler-vapor.esm-browser.js",
+      "require": "./dist/compiler-vapor.cjs.js"
+    },
+    "./*": "./*"
+  },
+  "buildOptions": {
+    "formats": [
+      "cjs",
+      "esm-browser"
+    ],
+    "prod": false,
+    "enableNonBrowserBranches": true
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/vuejs/core.git",
+    "directory": "packages/compiler-vapor"
+  },
+  "keywords": [
+    "vue"
+  ],
+  "author": "Evan You",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/vuejs/core/issues"
+  },
+  "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-vapor#readme",
+  "dependencies": {
+    "@babel/parser": "catalog:",
+    "@vue/compiler-dom": "workspace:*",
+    "@vue/shared": "workspace:*",
+    "estree-walker": "catalog:",
+    "source-map-js": "catalog:"
+  }
+}
diff --git a/packages/compiler-vapor/src/compile.ts b/packages/compiler-vapor/src/compile.ts
new file mode 100644
index 00000000000..c39037a47d8
--- /dev/null
+++ b/packages/compiler-vapor/src/compile.ts
@@ -0,0 +1,99 @@
+import {
+  type CompilerOptions as BaseCompilerOptions,
+  type RootNode,
+  parse,
+} from '@vue/compiler-dom'
+import { extend, isString } from '@vue/shared'
+import {
+  type DirectiveTransform,
+  type NodeTransform,
+  transform,
+} from './transform'
+import { type VaporCodegenResult, generate } from './generate'
+import { transformChildren } from './transforms/transformChildren'
+import { transformVOnce } from './transforms/vOnce'
+import { transformElement } from './transforms/transformElement'
+import { transformVHtml } from './transforms/vHtml'
+import { transformVText } from './transforms/vText'
+import { transformVBind } from './transforms/vBind'
+import { transformVOn } from './transforms/vOn'
+import { transformVShow } from './transforms/vShow'
+import { transformTemplateRef } from './transforms/transformTemplateRef'
+import { transformText } from './transforms/transformText'
+import { transformVModel } from './transforms/vModel'
+import { transformVIf } from './transforms/vIf'
+import { transformVFor } from './transforms/vFor'
+import { transformComment } from './transforms/transformComment'
+import { transformSlotOutlet } from './transforms/transformSlotOutlet'
+import { transformVSlot } from './transforms/vSlot'
+import type { HackOptions } from './ir'
+
+export { wrapTemplate } from './transforms/utils'
+
+// code/AST -> IR (transform) -> JS (generate)
+export function compile(
+  source: string | RootNode,
+  options: CompilerOptions = {},
+): VaporCodegenResult {
+  const resolvedOptions = extend({}, options)
+  const ast = isString(source) ? parse(source, resolvedOptions) : source
+  const [nodeTransforms, directiveTransforms] = getBaseTransformPreset()
+
+  if (options.isTS) {
+    const { expressionPlugins } = options
+    if (!expressionPlugins || !expressionPlugins.includes('typescript')) {
+      resolvedOptions.expressionPlugins = [
+        ...(expressionPlugins || []),
+        'typescript',
+      ]
+    }
+  }
+
+  const ir = transform(
+    ast,
+    extend({}, resolvedOptions, {
+      nodeTransforms: [
+        ...nodeTransforms,
+        ...(options.nodeTransforms || []), // user transforms
+      ],
+      directiveTransforms: extend(
+        {},
+        directiveTransforms,
+        options.directiveTransforms || {}, // user transforms
+      ),
+    }),
+  )
+
+  return generate(ir, resolvedOptions)
+}
+
+export type CompilerOptions = HackOptions<BaseCompilerOptions>
+export type TransformPreset = [
+  NodeTransform[],
+  Record<string, DirectiveTransform>,
+]
+
+export function getBaseTransformPreset(): TransformPreset {
+  return [
+    [
+      transformVOnce,
+      transformVIf,
+      transformVFor,
+      transformSlotOutlet,
+      transformTemplateRef,
+      transformElement,
+      transformText,
+      transformVSlot,
+      transformComment,
+      transformChildren,
+    ],
+    {
+      bind: transformVBind,
+      on: transformVOn,
+      html: transformVHtml,
+      text: transformVText,
+      show: transformVShow,
+      model: transformVModel,
+    },
+  ]
+}
diff --git a/packages/compiler-vapor/src/errors.ts b/packages/compiler-vapor/src/errors.ts
new file mode 100644
index 00000000000..3ccd32cba7c
--- /dev/null
+++ b/packages/compiler-vapor/src/errors.ts
@@ -0,0 +1,32 @@
+import {
+  type CompilerError,
+  type SourceLocation,
+  createCompilerError,
+} from '@vue/compiler-dom'
+
+export interface VaporCompilerError extends CompilerError {
+  code: VaporErrorCodes
+}
+
+export function createVaporCompilerError(
+  code: VaporErrorCodes,
+  loc?: SourceLocation,
+) {
+  return createCompilerError(
+    code,
+    loc,
+    VaporErrorMessages,
+  ) as VaporCompilerError
+}
+
+export enum VaporErrorCodes {
+  X_V_PLACEHOLDER = 100,
+  __EXTEND_POINT__,
+}
+
+export const VaporErrorMessages: Record<VaporErrorCodes, string> = {
+  [VaporErrorCodes.X_V_PLACEHOLDER]: `[placeholder]`,
+
+  // just to fulfill types
+  [VaporErrorCodes.__EXTEND_POINT__]: ``,
+}
diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts
new file mode 100644
index 00000000000..193a0f5da77
--- /dev/null
+++ b/packages/compiler-vapor/src/generate.ts
@@ -0,0 +1,180 @@
+import type {
+  CodegenOptions as BaseCodegenOptions,
+  BaseCodegenResult,
+  SimpleExpressionNode,
+} from '@vue/compiler-dom'
+import type { BlockIRNode, CoreHelper, RootIRNode, VaporHelper } from './ir'
+import { extend, remove } from '@vue/shared'
+import { genBlockContent } from './generators/block'
+import { genTemplates } from './generators/template'
+import {
+  type CodeFragment,
+  INDENT_END,
+  INDENT_START,
+  LF,
+  NEWLINE,
+  buildCodeFragment,
+  codeFragmentToString,
+  genCall,
+} from './generators/utils'
+import { setTemplateRefIdent } from './generators/templateRef'
+
+export type CodegenOptions = Omit<BaseCodegenOptions, 'optimizeImports'>
+
+export class CodegenContext {
+  options: Required<CodegenOptions>
+
+  helpers: Set<string> = new Set<string>([])
+
+  helper = (name: CoreHelper | VaporHelper) => {
+    this.helpers.add(name)
+    return `_${name}`
+  }
+
+  delegates: Set<string> = new Set<string>()
+
+  identifiers: Record<string, (string | SimpleExpressionNode)[]> =
+    Object.create(null)
+
+  seenInlineHandlerNames: Record<string, number> = Object.create(null)
+
+  block: BlockIRNode
+  withId<T>(
+    fn: () => T,
+    map: Record<string, string | SimpleExpressionNode | null>,
+  ): T {
+    const { identifiers } = this
+    const ids = Object.keys(map)
+
+    for (const id of ids) {
+      identifiers[id] ||= []
+      identifiers[id].unshift(map[id] || id)
+    }
+
+    const ret = fn()
+    ids.forEach(id => remove(identifiers[id], map[id] || id))
+
+    return ret
+  }
+
+  enterBlock(block: BlockIRNode) {
+    const parent = this.block
+    this.block = block
+    return (): BlockIRNode => (this.block = parent)
+  }
+
+  scopeLevel: number = 0
+  enterScope(): [level: number, exit: () => number] {
+    return [this.scopeLevel++, () => this.scopeLevel--] as const
+  }
+
+  constructor(
+    public ir: RootIRNode,
+    options: CodegenOptions,
+  ) {
+    const defaultOptions: Required<CodegenOptions> = {
+      mode: 'module',
+      prefixIdentifiers: true,
+      sourceMap: false,
+      filename: `template.vue.html`,
+      scopeId: null,
+      runtimeGlobalName: `Vue`,
+      runtimeModuleName: `vue`,
+      ssrRuntimeModuleName: 'vue/server-renderer',
+      ssr: false,
+      isTS: false,
+      inSSR: false,
+      inline: false,
+      bindingMetadata: {},
+      expressionPlugins: [],
+    }
+    this.options = extend(defaultOptions, options)
+    this.block = ir.block
+  }
+}
+
+export interface VaporCodegenResult extends BaseCodegenResult {
+  ast: RootIRNode
+  helpers: Set<string>
+}
+
+// IR -> JS codegen
+export function generate(
+  ir: RootIRNode,
+  options: CodegenOptions = {},
+): VaporCodegenResult {
+  const [frag, push] = buildCodeFragment()
+  const context = new CodegenContext(ir, options)
+  const { helpers } = context
+  const { inline, bindingMetadata } = options
+  const functionName = 'render'
+
+  const args = ['_ctx']
+  if (bindingMetadata && !inline) {
+    // binding optimization args
+    args.push('$props', '$emit', '$attrs', '$slots')
+  }
+  const signature = (options.isTS ? args.map(arg => `${arg}: any`) : args).join(
+    ', ',
+  )
+
+  if (!inline) {
+    push(NEWLINE, `export function ${functionName}(${signature}) {`)
+  }
+
+  push(INDENT_START)
+  if (ir.hasTemplateRef) {
+    push(
+      NEWLINE,
+      `const ${setTemplateRefIdent} = ${context.helper('createTemplateRefSetter')}()`,
+    )
+  }
+  push(...genBlockContent(ir.block, context, true))
+  push(INDENT_END, NEWLINE)
+
+  if (!inline) {
+    push('}')
+  }
+
+  const delegates = genDelegates(context)
+  const templates = genTemplates(ir.template, ir.rootTemplateIndex, context)
+  const imports = genHelperImports(context)
+  const preamble = imports + templates + delegates
+
+  const newlineCount = [...preamble].filter(c => c === '\n').length
+  if (newlineCount && !inline) {
+    frag.unshift(...new Array<CodeFragment>(newlineCount).fill(LF))
+  }
+
+  let [code, map] = codeFragmentToString(frag, context)
+  if (!inline) {
+    code = preamble + code
+  }
+
+  return {
+    code,
+    ast: ir,
+    preamble,
+    map: map && map.toJSON(),
+    helpers,
+  }
+}
+
+function genDelegates({ delegates, helper }: CodegenContext) {
+  return delegates.size
+    ? genCall(
+        helper('delegateEvents'),
+        ...Array.from(delegates).map(v => `"${v}"`),
+      ).join('') + '\n'
+    : ''
+}
+
+function genHelperImports({ helpers, helper, options }: CodegenContext) {
+  let imports = ''
+  if (helpers.size) {
+    imports += `import { ${[...helpers]
+      .map(h => `${h} as _${h}`)
+      .join(', ')} } from '${options.runtimeModuleName}';\n`
+  }
+  return imports
+}
diff --git a/packages/compiler-vapor/src/generators/block.ts b/packages/compiler-vapor/src/generators/block.ts
new file mode 100644
index 00000000000..30347394756
--- /dev/null
+++ b/packages/compiler-vapor/src/generators/block.ts
@@ -0,0 +1,98 @@
+import type { BlockIRNode, CoreHelper } from '../ir'
+import {
+  type CodeFragment,
+  DELIMITERS_ARRAY,
+  INDENT_END,
+  INDENT_START,
+  NEWLINE,
+  buildCodeFragment,
+  genCall,
+  genMulti,
+} from './utils'
+import type { CodegenContext } from '../generate'
+import { genEffects, genOperations } from './operation'
+import { genChildren, genSelf } from './template'
+import { toValidAssetId } from '@vue/compiler-dom'
+
+export function genBlock(
+  oper: BlockIRNode,
+  context: CodegenContext,
+  args: CodeFragment[] = [],
+  root?: boolean,
+): CodeFragment[] {
+  return [
+    '(',
+    ...args,
+    ') => {',
+    INDENT_START,
+    ...genBlockContent(oper, context, root),
+    INDENT_END,
+    NEWLINE,
+    '}',
+  ]
+}
+
+export function genBlockContent(
+  block: BlockIRNode,
+  context: CodegenContext,
+  root?: boolean,
+  genEffectsExtraFrag?: () => CodeFragment[],
+): CodeFragment[] {
+  const [frag, push] = buildCodeFragment()
+  const { dynamic, effect, operation, returns } = block
+  const resetBlock = context.enterBlock(block)
+
+  if (root) {
+    for (let name of context.ir.component) {
+      const id = toValidAssetId(name, 'component')
+      const maybeSelfReference = name.endsWith('__self')
+      if (maybeSelfReference) name = name.slice(0, -6)
+      push(
+        NEWLINE,
+        `const ${id} = `,
+        ...genCall(
+          context.helper('resolveComponent'),
+          JSON.stringify(name),
+          // pass additional `maybeSelfReference` flag
+          maybeSelfReference ? 'true' : undefined,
+        ),
+      )
+    }
+    genResolveAssets('directive', 'resolveDirective')
+  }
+
+  for (const child of dynamic.children) {
+    push(...genSelf(child, context))
+  }
+  for (const child of dynamic.children) {
+    push(...genChildren(child, context, push, `n${child.id!}`))
+  }
+
+  push(...genOperations(operation, context))
+  push(...genEffects(effect, context, genEffectsExtraFrag))
+
+  push(NEWLINE, `return `)
+
+  const returnNodes = returns.map(n => `n${n}`)
+  const returnsCode: CodeFragment[] =
+    returnNodes.length > 1
+      ? genMulti(DELIMITERS_ARRAY, ...returnNodes)
+      : [returnNodes[0] || 'null']
+  push(...returnsCode)
+
+  resetBlock()
+  return frag
+
+  function genResolveAssets(
+    kind: 'component' | 'directive',
+    helper: CoreHelper,
+  ) {
+    for (const name of context.ir[kind]) {
+      push(
+        NEWLINE,
+        `const ${toValidAssetId(name, kind)} = `,
+        ...genCall(context.helper(helper), JSON.stringify(name)),
+      )
+    }
+  }
+}
diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts
new file mode 100644
index 00000000000..fb76abaa505
--- /dev/null
+++ b/packages/compiler-vapor/src/generators/component.ts
@@ -0,0 +1,435 @@
+import { camelize, extend, isArray } from '@vue/shared'
+import type { CodegenContext } from '../generate'
+import {
+  type CreateComponentIRNode,
+  IRDynamicPropsKind,
+  type IRProp,
+  type IRProps,
+  type IRPropsStatic,
+  type IRSlotDynamic,
+  type IRSlotDynamicBasic,
+  type IRSlotDynamicConditional,
+  type IRSlotDynamicLoop,
+  IRSlotType,
+  type IRSlots,
+  type IRSlotsStatic,
+  type SlotBlockIRNode,
+} from '../ir'
+import {
+  type CodeFragment,
+  DELIMITERS_ARRAY_NEWLINE,
+  DELIMITERS_OBJECT,
+  DELIMITERS_OBJECT_NEWLINE,
+  INDENT_END,
+  INDENT_START,
+  NEWLINE,
+  genCall,
+  genMulti,
+} from './utils'
+import { genExpression, genVarName } from './expression'
+import { genPropKey, genPropValue } from './prop'
+import {
+  type SimpleExpressionNode,
+  createSimpleExpression,
+  isMemberExpression,
+  toValidAssetId,
+  walkIdentifiers,
+} from '@vue/compiler-dom'
+import { genEventHandler } from './event'
+import { genDirectiveModifiers, genDirectivesForElement } from './directive'
+import { genBlock } from './block'
+import { genModelHandler } from './vModel'
+
+export function genCreateComponent(
+  operation: CreateComponentIRNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  const { helper } = context
+
+  const tag = genTag()
+  const { root, props, slots, once } = operation
+  const rawSlots = genRawSlots(slots, context)
+  const [ids, handlers] = processInlineHandlers(props, context)
+  const rawProps = context.withId(() => genRawProps(props, context), ids)
+
+  const inlineHandlers: CodeFragment[] = handlers.reduce<CodeFragment[]>(
+    (acc, { name, value }) => {
+      const handler = genEventHandler(context, value, undefined, false)
+      return [...acc, `const ${name} = `, ...handler, NEWLINE]
+    },
+    [],
+  )
+
+  return [
+    NEWLINE,
+    ...inlineHandlers,
+    `const n${operation.id} = `,
+    ...genCall(
+      operation.dynamic && !operation.dynamic.isStatic
+        ? helper('createDynamicComponent')
+        : operation.asset
+          ? helper('createComponentWithFallback')
+          : helper('createComponent'),
+      tag,
+      rawProps,
+      rawSlots,
+      root ? 'true' : false,
+      once && 'true',
+    ),
+    ...genDirectivesForElement(operation.id, context),
+  ]
+
+  function genTag() {
+    if (operation.dynamic) {
+      if (operation.dynamic.isStatic) {
+        return genCall(
+          helper('resolveDynamicComponent'),
+          genExpression(operation.dynamic, context),
+        )
+      } else {
+        return ['() => (', ...genExpression(operation.dynamic, context), ')']
+      }
+    } else if (operation.asset) {
+      return toValidAssetId(operation.tag, 'component')
+    } else {
+      return genExpression(
+        extend(createSimpleExpression(operation.tag, false), { ast: null }),
+        context,
+      )
+    }
+  }
+}
+
+function getUniqueHandlerName(context: CodegenContext, name: string): string {
+  const { seenInlineHandlerNames } = context
+  name = genVarName(name)
+  const count = seenInlineHandlerNames[name] || 0
+  seenInlineHandlerNames[name] = count + 1
+  return count === 0 ? name : `${name}${count}`
+}
+
+type InlineHandler = {
+  name: string
+  value: SimpleExpressionNode
+}
+
+function processInlineHandlers(
+  props: IRProps[],
+  context: CodegenContext,
+): [Record<string, null>, InlineHandler[]] {
+  const ids: Record<string, null> = Object.create(null)
+  const handlers: InlineHandler[] = []
+  const staticProps = props[0]
+  if (isArray(staticProps)) {
+    for (let i = 0; i < staticProps.length; i++) {
+      const prop = staticProps[i]
+      if (!prop.handler) continue
+      prop.values.forEach((value, i) => {
+        const isMemberExp = isMemberExpression(value, context.options)
+        // cache inline handlers (fn expression or inline statement)
+        if (!isMemberExp) {
+          const name = getUniqueHandlerName(context, `_on_${prop.key.content}`)
+          handlers.push({ name, value })
+          ids[name] = null
+          // replace the original prop value with the handler name
+          prop.values[i] = extend({ ast: null }, createSimpleExpression(name))
+        }
+      })
+    }
+  }
+  return [ids, handlers]
+}
+
+export function genRawProps(
+  props: IRProps[],
+  context: CodegenContext,
+): CodeFragment[] | undefined {
+  const staticProps = props[0]
+  if (isArray(staticProps)) {
+    if (!staticProps.length && props.length === 1) {
+      return
+    }
+    return genStaticProps(
+      staticProps,
+      context,
+      genDynamicProps(props.slice(1), context),
+    )
+  } else if (props.length) {
+    // all dynamic
+    return genStaticProps([], context, genDynamicProps(props, context))
+  }
+}
+
+function genStaticProps(
+  props: IRPropsStatic,
+  context: CodegenContext,
+  dynamicProps?: CodeFragment[],
+): CodeFragment[] {
+  const args = props.map(prop => genProp(prop, context, true))
+  if (dynamicProps) {
+    args.push([`$: `, ...dynamicProps])
+  }
+  return genMulti(
+    args.length > 1 ? DELIMITERS_OBJECT_NEWLINE : DELIMITERS_OBJECT,
+    ...args,
+  )
+}
+
+function genDynamicProps(
+  props: IRProps[],
+  context: CodegenContext,
+): CodeFragment[] | undefined {
+  const { helper } = context
+  const frags: CodeFragment[][] = []
+  for (const p of props) {
+    let expr: CodeFragment[]
+    if (isArray(p)) {
+      if (p.length) {
+        frags.push(genStaticProps(p, context))
+      }
+      continue
+    } else {
+      if (p.kind === IRDynamicPropsKind.ATTRIBUTE)
+        expr = genMulti(DELIMITERS_OBJECT, genProp(p, context))
+      else {
+        expr = genExpression(p.value, context)
+        if (p.handler) expr = genCall(helper('toHandlers'), expr)
+      }
+    }
+    frags.push(['() => (', ...expr, ')'])
+  }
+  if (frags.length) {
+    return genMulti(DELIMITERS_ARRAY_NEWLINE, ...frags)
+  }
+}
+
+function genProp(prop: IRProp, context: CodegenContext, isStatic?: boolean) {
+  const values = genPropValue(prop.values, context)
+  return [
+    ...genPropKey(prop, context),
+    ': ',
+    ...(prop.handler
+      ? genEventHandler(
+          context,
+          prop.values[0],
+          undefined,
+          true /* wrap handlers passed to components */,
+        )
+      : isStatic
+        ? ['() => (', ...values, ')']
+        : values),
+    ...(prop.model
+      ? [...genModelEvent(prop, context), ...genModelModifiers(prop, context)]
+      : []),
+  ]
+}
+
+function genModelEvent(prop: IRProp, context: CodegenContext): CodeFragment[] {
+  const name = prop.key.isStatic
+    ? [JSON.stringify(`onUpdate:${camelize(prop.key.content)}`)]
+    : ['["onUpdate:" + ', ...genExpression(prop.key, context), ']']
+  const handler = genModelHandler(prop.values[0], context)
+
+  return [',', NEWLINE, ...name, ': () => ', ...handler]
+}
+
+function genModelModifiers(
+  prop: IRProp,
+  context: CodegenContext,
+): CodeFragment[] {
+  const { key, modelModifiers } = prop
+  if (!modelModifiers || !modelModifiers.length) return []
+
+  const modifiersKey = key.isStatic
+    ? key.content === 'modelValue'
+      ? [`modelModifiers`]
+      : [`${key.content}Modifiers`]
+    : ['[', ...genExpression(key, context), ' + "Modifiers"]']
+
+  const modifiersVal = genDirectiveModifiers(modelModifiers)
+  return [',', NEWLINE, ...modifiersKey, `: () => ({ ${modifiersVal} })`]
+}
+
+function genRawSlots(slots: IRSlots[], context: CodegenContext) {
+  if (!slots.length) return
+  const staticSlots = slots[0]
+  if (staticSlots.slotType === IRSlotType.STATIC) {
+    // single static slot
+    return genStaticSlots(
+      staticSlots,
+      context,
+      slots.length > 1 ? slots.slice(1) : undefined,
+    )
+  } else {
+    return genStaticSlots(
+      { slotType: IRSlotType.STATIC, slots: {} },
+      context,
+      slots,
+    )
+  }
+}
+
+function genStaticSlots(
+  { slots }: IRSlotsStatic,
+  context: CodegenContext,
+  dynamicSlots?: IRSlots[],
+) {
+  const args = Object.keys(slots).map(name => [
+    `${JSON.stringify(name)}: `,
+    ...genSlotBlockWithProps(slots[name], context),
+  ])
+  if (dynamicSlots) {
+    args.push([`$: `, ...genDynamicSlots(dynamicSlots, context)])
+  }
+  return genMulti(DELIMITERS_OBJECT_NEWLINE, ...args)
+}
+
+function genDynamicSlots(
+  slots: IRSlots[],
+  context: CodegenContext,
+): CodeFragment[] {
+  return genMulti(
+    DELIMITERS_ARRAY_NEWLINE,
+    ...slots.map(slot =>
+      slot.slotType === IRSlotType.STATIC
+        ? genStaticSlots(slot, context)
+        : slot.slotType === IRSlotType.EXPRESSION
+          ? slot.slots.content
+          : genDynamicSlot(slot, context, true),
+    ),
+  )
+}
+
+function genDynamicSlot(
+  slot: IRSlotDynamic,
+  context: CodegenContext,
+  withFunction = false,
+): CodeFragment[] {
+  let frag: CodeFragment[]
+  switch (slot.slotType) {
+    case IRSlotType.DYNAMIC:
+      frag = genBasicDynamicSlot(slot, context)
+      break
+    case IRSlotType.LOOP:
+      frag = genLoopSlot(slot, context)
+      break
+    case IRSlotType.CONDITIONAL:
+      frag = genConditionalSlot(slot, context)
+      break
+  }
+  return withFunction ? ['() => (', ...frag, ')'] : frag
+}
+
+function genBasicDynamicSlot(
+  slot: IRSlotDynamicBasic,
+  context: CodegenContext,
+): CodeFragment[] {
+  const { name, fn } = slot
+  return genMulti(
+    DELIMITERS_OBJECT_NEWLINE,
+    ['name: ', ...genExpression(name, context)],
+    ['fn: ', ...genSlotBlockWithProps(fn, context)],
+  )
+}
+
+function genLoopSlot(
+  slot: IRSlotDynamicLoop,
+  context: CodegenContext,
+): CodeFragment[] {
+  const { name, fn, loop } = slot
+  const { value, key, index, source } = loop
+  const rawValue = value && value.content
+  const rawKey = key && key.content
+  const rawIndex = index && index.content
+
+  const idMap: Record<string, string> = {}
+  if (rawValue) idMap[rawValue] = rawValue
+  if (rawKey) idMap[rawKey] = rawKey
+  if (rawIndex) idMap[rawIndex] = rawIndex
+  const slotExpr = genMulti(
+    DELIMITERS_OBJECT_NEWLINE,
+    ['name: ', ...context.withId(() => genExpression(name, context), idMap)],
+    [
+      'fn: ',
+      ...context.withId(() => genSlotBlockWithProps(fn, context), idMap),
+    ],
+  )
+  return [
+    ...genCall(
+      context.helper('createForSlots'),
+      genExpression(source, context),
+      [
+        ...genMulti(
+          ['(', ')', ', '],
+          rawValue ? rawValue : rawKey || rawIndex ? '_' : undefined,
+          rawKey ? rawKey : rawIndex ? '__' : undefined,
+          rawIndex,
+        ),
+        ' => (',
+        ...slotExpr,
+        ')',
+      ],
+    ),
+  ]
+}
+
+function genConditionalSlot(
+  slot: IRSlotDynamicConditional,
+  context: CodegenContext,
+): CodeFragment[] {
+  const { condition, positive, negative } = slot
+  return [
+    ...genExpression(condition, context),
+    INDENT_START,
+    NEWLINE,
+    '? ',
+    ...genDynamicSlot(positive, context),
+    NEWLINE,
+    ': ',
+    ...(negative ? [...genDynamicSlot(negative, context)] : ['void 0']),
+    INDENT_END,
+  ]
+}
+
+function genSlotBlockWithProps(oper: SlotBlockIRNode, context: CodegenContext) {
+  let isDestructureAssignment = false
+  let rawProps: string | undefined
+  let propsName: string | undefined
+  let exitScope: (() => void) | undefined
+  let depth: number | undefined
+  const { props } = oper
+  const idsOfProps = new Set<string>()
+
+  if (props) {
+    rawProps = props.content
+    if ((isDestructureAssignment = !!props.ast)) {
+      ;[depth, exitScope] = context.enterScope()
+      propsName = `_slotProps${depth}`
+      walkIdentifiers(
+        props.ast,
+        (id, _, __, ___, isLocal) => {
+          if (isLocal) idsOfProps.add(id.name)
+        },
+        true,
+      )
+    } else {
+      idsOfProps.add((propsName = rawProps))
+    }
+  }
+
+  const idMap: Record<string, string | null> = {}
+
+  idsOfProps.forEach(
+    id =>
+      (idMap[id] = isDestructureAssignment
+        ? `${propsName}[${JSON.stringify(id)}]`
+        : null),
+  )
+  const blockFn = context.withId(
+    () => genBlock(oper, context, [propsName]),
+    idMap,
+  )
+  exitScope && exitScope()
+
+  return blockFn
+}
diff --git a/packages/compiler-vapor/src/generators/directive.ts b/packages/compiler-vapor/src/generators/directive.ts
new file mode 100644
index 00000000000..7ed4624c1f8
--- /dev/null
+++ b/packages/compiler-vapor/src/generators/directive.ts
@@ -0,0 +1,111 @@
+import {
+  createSimpleExpression,
+  isSimpleIdentifier,
+  toValidAssetId,
+} from '@vue/compiler-dom'
+import { extend } from '@vue/shared'
+import { genExpression } from './expression'
+import type { CodegenContext } from '../generate'
+import {
+  type CodeFragment,
+  type CodeFragmentDelimiters,
+  DELIMITERS_ARRAY,
+  NEWLINE,
+  genCall,
+  genMulti,
+} from './utils'
+import { type DirectiveIRNode, IRNodeTypes, type OperationNode } from '../ir'
+import { genVShow } from './vShow'
+import { genVModel } from './vModel'
+
+export function genBuiltinDirective(
+  oper: DirectiveIRNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  switch (oper.name) {
+    case 'show':
+      return genVShow(oper, context)
+    case 'model':
+      return genVModel(oper, context)
+    default:
+      return []
+  }
+}
+
+/**
+ * user directives via `withVaporDirectives`
+ * TODO the compiler side is implemented but no runtime support yet
+ * it was removed due to perf issues
+ */
+export function genDirectivesForElement(
+  id: number,
+  context: CodegenContext,
+): CodeFragment[] {
+  const dirs = filterCustomDirectives(id, context.block.operation)
+  return dirs.length ? genCustomDirectives(dirs, context) : []
+}
+
+function genCustomDirectives(
+  opers: DirectiveIRNode[],
+  context: CodegenContext,
+): CodeFragment[] {
+  const { helper } = context
+
+  const element = `n${opers[0].element}`
+  const directiveItems = opers.map(genDirectiveItem)
+  const directives = genMulti(DELIMITERS_ARRAY, ...directiveItems)
+
+  return [
+    NEWLINE,
+    ...genCall(helper('withVaporDirectives'), element, directives),
+  ]
+
+  function genDirectiveItem({
+    dir,
+    name,
+    asset,
+  }: DirectiveIRNode): CodeFragment[] {
+    const directiveVar = asset
+      ? toValidAssetId(name, 'directive')
+      : genExpression(
+          extend(createSimpleExpression(name, false), { ast: null }),
+          context,
+        )
+    const value = dir.exp && ['() => ', ...genExpression(dir.exp, context)]
+    const argument = dir.arg && genExpression(dir.arg, context)
+    const modifiers = !!dir.modifiers.length && [
+      '{ ',
+      genDirectiveModifiers(dir.modifiers.map(m => m.content)),
+      ' }',
+    ]
+
+    return genMulti(
+      DELIMITERS_ARRAY.concat('void 0') as CodeFragmentDelimiters,
+      directiveVar,
+      value,
+      argument,
+      modifiers,
+    )
+  }
+}
+
+export function genDirectiveModifiers(modifiers: string[]): string {
+  return modifiers
+    .map(
+      value =>
+        `${isSimpleIdentifier(value) ? value : JSON.stringify(value)}: true`,
+    )
+    .join(', ')
+}
+
+function filterCustomDirectives(
+  id: number,
+  operations: OperationNode[],
+): DirectiveIRNode[] {
+  return operations.filter(
+    (oper): oper is DirectiveIRNode =>
+      oper.type === IRNodeTypes.DIRECTIVE &&
+      oper.element === id &&
+      !oper.builtin,
+  )
+}
diff --git a/packages/compiler-vapor/src/generators/dom.ts b/packages/compiler-vapor/src/generators/dom.ts
new file mode 100644
index 00000000000..5634f7e4408
--- /dev/null
+++ b/packages/compiler-vapor/src/generators/dom.ts
@@ -0,0 +1,34 @@
+import type { CodegenContext } from '../generate'
+import type { InsertNodeIRNode, PrependNodeIRNode } from '../ir'
+import { type CodeFragment, NEWLINE, genCall } from './utils'
+
+export function genInsertNode(
+  { parent, elements, anchor }: InsertNodeIRNode,
+  { helper }: CodegenContext,
+): CodeFragment[] {
+  let element = elements.map(el => `n${el}`).join(', ')
+  if (elements.length > 1) element = `[${element}]`
+  return [
+    NEWLINE,
+    ...genCall(
+      helper('insert'),
+      element,
+      `n${parent}`,
+      anchor === undefined ? undefined : `n${anchor}`,
+    ),
+  ]
+}
+
+export function genPrependNode(
+  oper: PrependNodeIRNode,
+  { helper }: CodegenContext,
+): CodeFragment[] {
+  return [
+    NEWLINE,
+    ...genCall(
+      helper('prepend'),
+      `n${oper.parent}`,
+      ...oper.elements.map(el => `n${el}`),
+    ),
+  ]
+}
diff --git a/packages/compiler-vapor/src/generators/event.ts b/packages/compiler-vapor/src/generators/event.ts
new file mode 100644
index 00000000000..cfb47b61184
--- /dev/null
+++ b/packages/compiler-vapor/src/generators/event.ts
@@ -0,0 +1,193 @@
+import {
+  BindingTypes,
+  type SimpleExpressionNode,
+  isFnExpression,
+  isMemberExpression,
+} from '@vue/compiler-dom'
+import type { CodegenContext } from '../generate'
+import {
+  IRNodeTypes,
+  type OperationNode,
+  type SetDynamicEventsIRNode,
+  type SetEventIRNode,
+} from '../ir'
+import { genExpression } from './expression'
+import {
+  type CodeFragment,
+  DELIMITERS_OBJECT_NEWLINE,
+  NEWLINE,
+  genCall,
+  genMulti,
+} from './utils'
+
+export function genSetEvent(
+  oper: SetEventIRNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  const { helper } = context
+  const { element, key, keyOverride, value, modifiers, delegate, effect } = oper
+
+  const name = genName()
+  const handler = genEventHandler(context, value, modifiers)
+  const eventOptions = genEventOptions()
+
+  if (delegate) {
+    // key is static
+    context.delegates.add(key.content)
+    // if this is the only delegated event of this name on this element,
+    // we can generate optimized handler attachment code
+    // e.g. n1.$evtclick = () => {}
+    if (!context.block.operation.some(isSameDelegateEvent)) {
+      return [NEWLINE, `n${element}.$evt${key.content} = `, ...handler]
+    }
+  }
+
+  return [
+    NEWLINE,
+    ...genCall(
+      helper(delegate ? 'delegate' : 'on'),
+      `n${element}`,
+      name,
+      handler,
+      eventOptions,
+    ),
+  ]
+
+  function genName(): CodeFragment[] {
+    const expr = genExpression(key, context)
+    if (keyOverride) {
+      // TODO unit test
+      const find = JSON.stringify(keyOverride[0])
+      const replacement = JSON.stringify(keyOverride[1])
+      const wrapped: CodeFragment[] = ['(', ...expr, ')']
+      return [...wrapped, ` === ${find} ? ${replacement} : `, ...wrapped]
+    } else {
+      return genExpression(key, context)
+    }
+  }
+
+  function genEventOptions(): CodeFragment[] | undefined {
+    let { options } = modifiers
+    if (!options.length && !effect) return
+
+    return genMulti(
+      DELIMITERS_OBJECT_NEWLINE,
+      effect && ['effect: true'],
+      ...options.map((option): CodeFragment[] => [`${option}: true`]),
+    )
+  }
+
+  function isSameDelegateEvent(op: OperationNode) {
+    if (
+      op.type === IRNodeTypes.SET_EVENT &&
+      op !== oper &&
+      op.delegate &&
+      op.element === oper.element &&
+      op.key.content === key.content
+    ) {
+      return true
+    }
+  }
+}
+
+export function genSetDynamicEvents(
+  oper: SetDynamicEventsIRNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  const { helper } = context
+  return [
+    NEWLINE,
+    ...genCall(
+      helper('setDynamicEvents'),
+      `n${oper.element}`,
+      genExpression(oper.event, context),
+    ),
+  ]
+}
+
+export function genEventHandler(
+  context: CodegenContext,
+  value: SimpleExpressionNode | undefined,
+  modifiers: {
+    nonKeys: string[]
+    keys: string[]
+  } = { nonKeys: [], keys: [] },
+  // passed as component prop - need additional wrap
+  extraWrap: boolean = false,
+): CodeFragment[] {
+  let handlerExp: CodeFragment[] = [`() => {}`]
+  if (value && value.content.trim()) {
+    // Determine how the handler should be wrapped so it always reference the
+    // latest value when invoked.
+    if (isMemberExpression(value, context.options)) {
+      // e.g. @click="foo.bar"
+      handlerExp = genExpression(value, context)
+      if (!isConstantBinding(value, context) && !extraWrap) {
+        // non constant, wrap with invocation as `e => foo.bar(e)`
+        // when passing as component handler, access is always dynamic so we
+        // can skip this
+        handlerExp = [`e => `, ...handlerExp, `(e)`]
+      }
+    } else if (isFnExpression(value, context.options)) {
+      // Fn expression: @click="e => foo(e)"
+      // no need to wrap in this case
+      handlerExp = genExpression(value, context)
+    } else {
+      // inline statement
+      // @click="foo($event)" ---> $event => foo($event)
+      const referencesEvent = value.content.includes('$event')
+      const hasMultipleStatements = value.content.includes(`;`)
+      const expr = referencesEvent
+        ? context.withId(() => genExpression(value, context), {
+            $event: null,
+          })
+        : genExpression(value, context)
+      handlerExp = [
+        referencesEvent ? '$event => ' : '() => ',
+        hasMultipleStatements ? '{' : '(',
+        ...expr,
+        hasMultipleStatements ? '}' : ')',
+      ]
+    }
+  }
+
+  const { keys, nonKeys } = modifiers
+  if (nonKeys.length)
+    handlerExp = genWithModifiers(context, handlerExp, nonKeys)
+  if (keys.length) handlerExp = genWithKeys(context, handlerExp, keys)
+
+  if (extraWrap) handlerExp.unshift(`() => `)
+  return handlerExp
+}
+
+function genWithModifiers(
+  context: CodegenContext,
+  handler: CodeFragment[],
+  nonKeys: string[],
+): CodeFragment[] {
+  return genCall(
+    context.helper('withModifiers'),
+    handler,
+    JSON.stringify(nonKeys),
+  )
+}
+
+function genWithKeys(
+  context: CodegenContext,
+  handler: CodeFragment[],
+  keys: string[],
+): CodeFragment[] {
+  return genCall(context.helper('withKeys'), handler, JSON.stringify(keys))
+}
+
+function isConstantBinding(
+  value: SimpleExpressionNode,
+  context: CodegenContext,
+) {
+  if (value.ast === null) {
+    const bindingType = context.options.bindingMetadata[value.content]
+    if (bindingType === BindingTypes.SETUP_CONST) {
+      return true
+    }
+  }
+}
diff --git a/packages/compiler-vapor/src/generators/expression.ts b/packages/compiler-vapor/src/generators/expression.ts
new file mode 100644
index 00000000000..c70d4a56613
--- /dev/null
+++ b/packages/compiler-vapor/src/generators/expression.ts
@@ -0,0 +1,690 @@
+import {
+  NOOP,
+  extend,
+  genPropsAccessExp,
+  isGloballyAllowed,
+  isString,
+} from '@vue/shared'
+import {
+  BindingTypes,
+  NewlineType,
+  type SimpleExpressionNode,
+  type SourceLocation,
+  TS_NODE_TYPES,
+  advancePositionWithClone,
+  createSimpleExpression,
+  isInDestructureAssignment,
+  isStaticProperty,
+  walkIdentifiers,
+} from '@vue/compiler-dom'
+import type { Identifier, Node } from '@babel/types'
+import type { CodegenContext } from '../generate'
+import { isConstantExpression } from '../utils'
+import { type CodeFragment, NEWLINE, buildCodeFragment } from './utils'
+import { type ParserOptions, parseExpression } from '@babel/parser'
+
+export function genExpression(
+  node: SimpleExpressionNode,
+  context: CodegenContext,
+  assignment?: string,
+): CodeFragment[] {
+  const { content, ast, isStatic, loc } = node
+
+  if (isStatic) {
+    return [[JSON.stringify(content), NewlineType.None, loc]]
+  }
+
+  if (
+    !node.content.trim() ||
+    // there was a parsing error
+    ast === false ||
+    isConstantExpression(node)
+  ) {
+    return [[content, NewlineType.None, loc], assignment && ` = ${assignment}`]
+  }
+
+  // the expression is a simple identifier
+  if (ast === null) {
+    return genIdentifier(content, context, loc, assignment)
+  }
+
+  const ids: Identifier[] = []
+  const parentStackMap = new Map<Identifier, Node[]>()
+  const parentStack: Node[] = []
+  walkIdentifiers(
+    ast!,
+    id => {
+      ids.push(id)
+      parentStackMap.set(id, parentStack.slice())
+    },
+    false,
+    parentStack,
+  )
+
+  let hasMemberExpression = false
+  if (ids.length) {
+    const [frag, push] = buildCodeFragment()
+    const isTSNode = ast && TS_NODE_TYPES.includes(ast.type)
+    ids
+      .sort((a, b) => a.start! - b.start!)
+      .forEach((id, i) => {
+        // range is offset by -1 due to the wrapping parens when parsed
+        const start = id.start! - 1
+        const end = id.end! - 1
+        const last = ids[i - 1]
+
+        if (!(isTSNode && i === 0)) {
+          const leadingText = content.slice(last ? last.end! - 1 : 0, start)
+          if (leadingText.length) push([leadingText, NewlineType.Unknown])
+        }
+
+        const source = content.slice(start, end)
+        const parentStack = parentStackMap.get(id)!
+        const parent = parentStack[parentStack.length - 1]
+
+        hasMemberExpression ||=
+          parent &&
+          (parent.type === 'MemberExpression' ||
+            parent.type === 'OptionalMemberExpression')
+
+        push(
+          ...genIdentifier(
+            source,
+            context,
+            {
+              start: advancePositionWithClone(node.loc.start, source, start),
+              end: advancePositionWithClone(node.loc.start, source, end),
+              source,
+            },
+            hasMemberExpression ? undefined : assignment,
+            id,
+            parent,
+            parentStack,
+          ),
+        )
+
+        if (i === ids.length - 1 && end < content.length && !isTSNode) {
+          push([content.slice(end), NewlineType.Unknown])
+        }
+      })
+
+    if (assignment && hasMemberExpression) {
+      push(` = ${assignment}`)
+    }
+    return frag
+  } else {
+    return [[content, NewlineType.Unknown, loc]]
+  }
+}
+
+function genIdentifier(
+  raw: string,
+  context: CodegenContext,
+  loc?: SourceLocation,
+  assignment?: string,
+  id?: Identifier,
+  parent?: Node,
+  parentStack?: Node[],
+): CodeFragment[] {
+  const { options, helper, identifiers } = context
+  const { inline, bindingMetadata } = options
+  let name: string | undefined = raw
+
+  const idMap = identifiers[raw]
+  if (idMap && idMap.length) {
+    const replacement = idMap[0]
+    if (isString(replacement)) {
+      if (parent && parent.type === 'ObjectProperty' && parent.shorthand) {
+        return [[`${name}: ${replacement}`, NewlineType.None, loc]]
+      } else {
+        return [[replacement, NewlineType.None, loc]]
+      }
+    } else {
+      // replacement is an expression - process it again
+      return genExpression(replacement, context, assignment)
+    }
+  }
+
+  let prefix: string | undefined
+  if (isStaticProperty(parent) && parent.shorthand) {
+    // property shorthand like { foo }, we need to add the key since
+    // we rewrite the value
+    prefix = `${raw}: `
+  }
+
+  const type = bindingMetadata && bindingMetadata[raw]
+  if (inline) {
+    switch (type) {
+      case BindingTypes.SETUP_LET:
+        name = raw = assignment
+          ? `_isRef(${raw}) ? (${raw}.value = ${assignment}) : (${raw} = ${assignment})`
+          : unref()
+        break
+      case BindingTypes.SETUP_REF:
+        name = raw = withAssignment(`${raw}.value`)
+        break
+      case BindingTypes.SETUP_MAYBE_REF:
+        // ({ x } = y)
+        const isDestructureAssignment =
+          parent && isInDestructureAssignment(parent, parentStack || [])
+        // x = y
+        const isAssignmentLVal =
+          parent && parent.type === 'AssignmentExpression' && parent.left === id
+        // x++
+        const isUpdateArg =
+          parent && parent.type === 'UpdateExpression' && parent.argument === id
+        // const binding that may or may not be ref
+        // if it's not a ref, then assignments don't make sense -
+        // so we ignore the non-ref assignment case and generate code
+        // that assumes the value to be a ref for more efficiency
+        raw =
+          isAssignmentLVal || isUpdateArg || isDestructureAssignment
+            ? (name = `${raw}.value`)
+            : assignment
+              ? `${helper('isRef')}(${raw}) ? (${raw}.value = ${assignment}) : null`
+              : unref()
+        break
+      case BindingTypes.PROPS:
+        raw = genPropsAccessExp(raw)
+        break
+      case BindingTypes.PROPS_ALIASED:
+        raw = genPropsAccessExp(bindingMetadata.__propsAliases![raw])
+        break
+      default:
+        raw = withAssignment(raw)
+    }
+  } else {
+    if (canPrefix(raw)) {
+      if (type === BindingTypes.PROPS_ALIASED) {
+        raw = `$props['${bindingMetadata.__propsAliases![raw]}']`
+      } else {
+        raw = `${type === BindingTypes.PROPS ? '$props' : '_ctx'}.${raw}`
+      }
+    }
+    raw = withAssignment(raw)
+  }
+  return [prefix, [raw, NewlineType.None, loc, name]]
+
+  function withAssignment(s: string) {
+    return assignment ? `${s} = ${assignment}` : s
+  }
+  function unref() {
+    return `${helper('unref')}(${raw})`
+  }
+}
+
+function canPrefix(name: string) {
+  // skip whitelisted globals
+  if (isGloballyAllowed(name)) {
+    return false
+  }
+  if (
+    // special case for webpack compilation
+    name === 'require' ||
+    name === '$props' ||
+    name === '$emit' ||
+    name === '$attrs' ||
+    name === '$slots'
+  )
+    return false
+  return true
+}
+
+type DeclarationResult = {
+  ids: Record<string, string>
+  frag: CodeFragment[]
+  varNames: string[]
+}
+type DeclarationValue = {
+  name: string
+  isIdentifier?: boolean
+  value: SimpleExpressionNode
+  rawName?: string
+  exps?: Set<SimpleExpressionNode>
+  seenCount?: number
+}
+
+export function processExpressions(
+  context: CodegenContext,
+  expressions: SimpleExpressionNode[],
+  shouldDeclare: boolean,
+): DeclarationResult {
+  // analyze variables
+  const {
+    seenVariable,
+    variableToExpMap,
+    expToVariableMap,
+    seenIdentifier,
+    updatedVariable,
+  } = analyzeExpressions(expressions)
+
+  // process repeated identifiers and member expressions
+  // e.g., `foo[baz]` will be transformed into `foo_baz`
+  const varDeclarations = processRepeatedVariables(
+    context,
+    seenVariable,
+    variableToExpMap,
+    expToVariableMap,
+    seenIdentifier,
+    updatedVariable,
+  )
+
+  // process duplicate expressions after identifier and member expression handling.
+  // e.g., `foo + bar` will be transformed into `foo_bar`
+  const expDeclarations = processRepeatedExpressions(
+    context,
+    expressions,
+    varDeclarations,
+    updatedVariable,
+    expToVariableMap,
+  )
+
+  return genDeclarations(
+    [...varDeclarations, ...expDeclarations],
+    context,
+    shouldDeclare,
+  )
+}
+
+function analyzeExpressions(expressions: SimpleExpressionNode[]) {
+  const seenVariable: Record<string, number> = Object.create(null)
+  const variableToExpMap = new Map<string, Set<SimpleExpressionNode>>()
+  const expToVariableMap = new Map<
+    SimpleExpressionNode,
+    Array<{
+      name: string
+      loc?: { start: number; end: number }
+    }>
+  >()
+  const seenIdentifier = new Set<string>()
+  const updatedVariable = new Set<string>()
+
+  const registerVariable = (
+    name: string,
+    exp: SimpleExpressionNode,
+    isIdentifier: boolean,
+    loc?: { start: number; end: number },
+    parentStack: Node[] = [],
+  ) => {
+    if (isIdentifier) seenIdentifier.add(name)
+    seenVariable[name] = (seenVariable[name] || 0) + 1
+    variableToExpMap.set(
+      name,
+      (variableToExpMap.get(name) || new Set()).add(exp),
+    )
+
+    const variables = expToVariableMap.get(exp) || []
+    variables.push({ name, loc })
+    expToVariableMap.set(exp, variables)
+
+    if (
+      parentStack.some(
+        p => p.type === 'UpdateExpression' || p.type === 'AssignmentExpression',
+      )
+    ) {
+      updatedVariable.add(name)
+    }
+  }
+
+  for (const exp of expressions) {
+    if (!exp.ast) {
+      exp.ast === null && registerVariable(exp.content, exp, true)
+      continue
+    }
+
+    walkIdentifiers(exp.ast, (currentNode, parent, parentStack) => {
+      if (parent && isMemberExpression(parent)) {
+        const memberExp = extractMemberExpression(parent, id => {
+          registerVariable(id.name, exp, true, {
+            start: id.start!,
+            end: id.end!,
+          })
+        })
+        registerVariable(
+          memberExp,
+          exp,
+          false,
+          { start: parent.start!, end: parent.end! },
+          parentStack,
+        )
+      } else if (!parentStack.some(isMemberExpression)) {
+        registerVariable(
+          currentNode.name,
+          exp,
+          true,
+          { start: currentNode.start!, end: currentNode.end! },
+          parentStack,
+        )
+      }
+    })
+  }
+
+  return {
+    seenVariable,
+    seenIdentifier,
+    variableToExpMap,
+    expToVariableMap,
+    updatedVariable,
+  }
+}
+
+function processRepeatedVariables(
+  context: CodegenContext,
+  seenVariable: Record<string, number>,
+  variableToExpMap: Map<string, Set<SimpleExpressionNode>>,
+  expToVariableMap: Map<
+    SimpleExpressionNode,
+    Array<{ name: string; loc?: { start: number; end: number } }>
+  >,
+  seenIdentifier: Set<string>,
+  updatedVariable: Set<string>,
+): DeclarationValue[] {
+  const declarations: DeclarationValue[] = []
+  const expToReplacementMap = new Map<
+    SimpleExpressionNode,
+    Array<{
+      name: string
+      locs: { start: number; end: number }[]
+    }>
+  >()
+
+  for (const [name, exps] of variableToExpMap) {
+    if (updatedVariable.has(name)) continue
+    if (seenVariable[name] > 1 && exps.size > 0) {
+      const isIdentifier = seenIdentifier.has(name)
+      const varName = isIdentifier ? name : genVarName(name)
+
+      // replaces all non-identifiers with the new name. if node content
+      // includes only one member expression, it will become an identifier,
+      // e.g., foo[baz] -> foo_baz.
+      // for identifiers, we don't need to replace the content - they will be
+      // replaced during context.withId(..., ids)
+      exps.forEach(node => {
+        if (node.ast && varName !== name) {
+          const replacements = expToReplacementMap.get(node) || []
+          replacements.push({
+            name: varName,
+            locs: expToVariableMap.get(node)!.reduce(
+              (locs, v) => {
+                if (v.name === name && v.loc) locs.push(v.loc)
+                return locs
+              },
+              [] as { start: number; end: number }[],
+            ),
+          })
+          expToReplacementMap.set(node, replacements)
+        }
+      })
+
+      if (
+        !declarations.some(d => d.name === varName) &&
+        (!isIdentifier || shouldDeclareVariable(name, expToVariableMap, exps))
+      ) {
+        declarations.push({
+          name: varName,
+          isIdentifier,
+          value: extend(
+            { ast: isIdentifier ? null : parseExp(context, name) },
+            createSimpleExpression(name),
+          ),
+          rawName: name,
+          exps,
+          seenCount: seenVariable[name],
+        })
+      }
+    }
+  }
+
+  for (const [exp, replacements] of expToReplacementMap) {
+    replacements
+      .flatMap(({ name, locs }) =>
+        locs.map(({ start, end }) => ({ start, end, name })),
+      )
+      .sort((a, b) => b.end - a.end)
+      .forEach(({ start, end, name }) => {
+        exp.content =
+          exp.content.slice(0, start - 1) + name + exp.content.slice(end - 1)
+      })
+
+    // re-parse the expression
+    exp.ast = parseExp(context, exp.content)
+  }
+
+  return declarations
+}
+
+function shouldDeclareVariable(
+  name: string,
+  expToVariableMap: Map<
+    SimpleExpressionNode,
+    Array<{ name: string; loc?: { start: number; end: number } }>
+  >,
+  exps: Set<SimpleExpressionNode>,
+): boolean {
+  const vars = Array.from(exps, exp =>
+    expToVariableMap.get(exp)!.map(v => v.name),
+  )
+  // assume name equals to `foo`
+  // if each expression only references `foo`, declaration is needed
+  // to avoid reactivity tracking
+  // e.g., [[foo],[foo]]
+  if (vars.every(v => v.length === 1)) {
+    return true
+  }
+
+  // if `foo` appears multiple times in one array, declaration is needed
+  // e.g., [[foo,foo]]
+  if (vars.some(v => v.filter(e => e === name).length > 1)) {
+    return true
+  }
+
+  const first = vars[0]
+  // if arrays have different lengths, declaration is needed
+  // e.g., [[foo],[foo,bar]]
+  if (vars.some(v => v.length !== first.length)) {
+    // special case, no declaration needed if one array is a subset of the other
+    // because they will be treated as repeated expressions
+    // e.g., [[foo,bar],[foo,foo,bar]] -> const foo_bar = _ctx.foo + _ctx.bar
+    if (
+      vars.some(
+        v => v.length > first.length && v.every(e => first.includes(e)),
+      ) ||
+      vars.some(v => first.length > v.length && first.every(e => v.includes(e)))
+    ) {
+      return false
+    }
+    return true
+  }
+  // if arrays share common elements, no declaration needed
+  // because they will be treat as repeated expressions
+  // e.g., [[foo,bar],[foo,bar]] -> const foo_bar = _ctx.foo + _ctx.bar
+  if (vars.some(v => v.some(e => first.includes(e)))) {
+    return false
+  }
+
+  return true
+}
+
+function processRepeatedExpressions(
+  context: CodegenContext,
+  expressions: SimpleExpressionNode[],
+  varDeclarations: DeclarationValue[],
+  updatedVariable: Set<string>,
+  expToVariableMap: Map<
+    SimpleExpressionNode,
+    Array<{ name: string; loc?: { start: number; end: number } }>
+  >,
+): DeclarationValue[] {
+  const declarations: DeclarationValue[] = []
+  const seenExp = expressions.reduce(
+    (acc, exp) => {
+      const variables = expToVariableMap.get(exp)!.map(v => v.name)
+      // only handle expressions that are not identifiers
+      if (
+        exp.ast &&
+        exp.ast.type !== 'Identifier' &&
+        !(variables && variables.some(v => updatedVariable.has(v)))
+      ) {
+        acc[exp.content] = (acc[exp.content] || 0) + 1
+      }
+      return acc
+    },
+    Object.create(null) as Record<string, number>,
+  )
+
+  Object.entries(seenExp).forEach(([content, count]) => {
+    if (count > 1) {
+      // foo + baz -> foo_baz
+      const varName = genVarName(content)
+      if (!declarations.some(d => d.name === varName)) {
+        // if foo and baz have no other references, we don't need to declare separate variables
+        // instead of:
+        // const foo = _ctx.foo
+        // const baz = _ctx.baz
+        // const foo_baz = foo + baz
+        // we can generate:
+        // const foo_baz = _ctx.foo + _ctx.baz
+        const delVars: Record<string, string> = {}
+        for (let i = varDeclarations.length - 1; i >= 0; i--) {
+          const item = varDeclarations[i]
+          if (!item.exps || !item.seenCount) continue
+
+          const shouldRemove = [...item.exps].every(
+            node => node.content === content && item.seenCount === count,
+          )
+          if (shouldRemove) {
+            delVars[item.name] = item.rawName!
+            varDeclarations.splice(i, 1)
+          }
+        }
+        const value = extend(
+          {},
+          expressions.find(exp => exp.content === content)!,
+        )
+        Object.keys(delVars).forEach(name => {
+          value.content = value.content.replace(name, delVars[name])
+          if (value.ast) value.ast = parseExp(context, value.content)
+        })
+        declarations.push({
+          name: varName,
+          value: value,
+        })
+      }
+
+      // assume content equals to `foo + baz`
+      expressions.forEach(exp => {
+        // foo + baz -> foo_baz
+        if (exp.content === content) {
+          exp.content = varName
+          // ast is no longer needed since it becomes an identifier.
+          exp.ast = null
+        }
+        // foo + foo + baz -> foo + foo_baz
+        else if (exp.content.includes(content)) {
+          exp.content = exp.content.replace(
+            new RegExp(escapeRegExp(content), 'g'),
+            varName,
+          )
+          // re-parse the expression
+          exp.ast = parseExp(context, exp.content)
+        }
+      })
+    }
+  })
+
+  return declarations
+}
+
+function genDeclarations(
+  declarations: DeclarationValue[],
+  context: CodegenContext,
+  shouldDeclare: boolean,
+): DeclarationResult {
+  const [frag, push] = buildCodeFragment()
+  const ids: Record<string, string> = Object.create(null)
+  const varNames = new Set<string>()
+
+  // process identifiers first as expressions may rely on them
+  declarations.forEach(({ name, isIdentifier, value }) => {
+    if (isIdentifier) {
+      const varName = (ids[name] = `_${name}`)
+      varNames.add(varName)
+      if (shouldDeclare) {
+        push(`const `)
+      }
+      push(`${varName} = `, ...genExpression(value, context), NEWLINE)
+    }
+  })
+
+  // process expressions
+  declarations.forEach(({ name, isIdentifier, value }) => {
+    if (!isIdentifier) {
+      const varName = (ids[name] = `_${name}`)
+      varNames.add(varName)
+      if (shouldDeclare) {
+        push(`const `)
+      }
+      push(
+        `${varName} = `,
+        ...context.withId(() => genExpression(value, context), ids),
+        NEWLINE,
+      )
+    }
+  })
+
+  return { ids, frag, varNames: [...varNames] }
+}
+
+function escapeRegExp(string: string) {
+  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+}
+
+function parseExp(context: CodegenContext, content: string): Node {
+  const plugins = context.options.expressionPlugins
+  const options: ParserOptions = {
+    plugins: plugins ? [...plugins, 'typescript'] : ['typescript'],
+  }
+  return parseExpression(`(${content})`, options)
+}
+
+export function genVarName(exp: string): string {
+  return `${exp
+    .replace(/[^a-zA-Z0-9]/g, '_')
+    .replace(/_+/g, '_')
+    .replace(/_+$/, '')}`
+}
+
+function extractMemberExpression(
+  exp: Node,
+  onIdentifier: (id: Identifier) => void,
+): string {
+  if (!exp) return ''
+  switch (exp.type) {
+    case 'Identifier': // foo[bar]
+      onIdentifier(exp)
+      return exp.name
+    case 'StringLiteral': // foo['bar']
+      return exp.extra ? (exp.extra.raw as string) : exp.value
+    case 'NumericLiteral': // foo[0]
+      return exp.value.toString()
+    case 'BinaryExpression': // foo[bar + 1]
+      return `${extractMemberExpression(exp.left, onIdentifier)} ${exp.operator} ${extractMemberExpression(exp.right, onIdentifier)}`
+    case 'CallExpression': // foo[bar(baz)]
+      return `${extractMemberExpression(exp.callee, onIdentifier)}(${exp.arguments.map(arg => extractMemberExpression(arg, onIdentifier)).join(', ')})`
+    case 'MemberExpression': // foo[bar.baz]
+    case 'OptionalMemberExpression': // foo?.bar
+      const object = extractMemberExpression(exp.object, onIdentifier)
+      const prop = exp.computed
+        ? `[${extractMemberExpression(exp.property, onIdentifier)}]`
+        : `.${extractMemberExpression(exp.property, NOOP)}`
+      return `${object}${prop}`
+    default:
+      return ''
+  }
+}
+
+const isMemberExpression = (node: Node) => {
+  return (
+    node.type === 'MemberExpression' || node.type === 'OptionalMemberExpression'
+  )
+}
diff --git a/packages/compiler-vapor/src/generators/for.ts b/packages/compiler-vapor/src/generators/for.ts
new file mode 100644
index 00000000000..026db797ba6
--- /dev/null
+++ b/packages/compiler-vapor/src/generators/for.ts
@@ -0,0 +1,537 @@
+import {
+  type SimpleExpressionNode,
+  createSimpleExpression,
+  isStaticNode,
+  walkIdentifiers,
+} from '@vue/compiler-dom'
+import { genBlockContent } from './block'
+import { genExpression } from './expression'
+import type { CodegenContext } from '../generate'
+import type { BlockIRNode, ForIRNode, IREffect } from '../ir'
+import {
+  type CodeFragment,
+  INDENT_END,
+  INDENT_START,
+  NEWLINE,
+  genCall,
+  genMulti,
+} from './utils'
+import {
+  type Expression,
+  type Identifier,
+  type Node,
+  isNodesEquivalent,
+} from '@babel/types'
+import { parseExpression } from '@babel/parser'
+import { VaporVForFlags } from '../../../shared/src/vaporFlags'
+import { walk } from 'estree-walker'
+import { genOperation } from './operation'
+import { extend, isGloballyAllowed } from '@vue/shared'
+
+export function genFor(
+  oper: ForIRNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  const { helper } = context
+  const {
+    source,
+    value,
+    key,
+    index,
+    render,
+    keyProp,
+    once,
+    id,
+    component,
+    onlyChild,
+  } = oper
+
+  let rawValue: string | null = null
+  const rawKey = key && key.content
+  const rawIndex = index && index.content
+
+  const sourceExpr = ['() => (', ...genExpression(source, context), ')']
+  const idToPathMap = parseValueDestructure()
+
+  const [depth, exitScope] = context.enterScope()
+  const idMap: Record<string, string | SimpleExpressionNode | null> = {}
+
+  const itemVar = `_for_item${depth}`
+  idMap[itemVar] = null
+
+  idToPathMap.forEach((pathInfo, id) => {
+    let path = `${itemVar}.value${pathInfo ? pathInfo.path : ''}`
+    if (pathInfo) {
+      if (pathInfo.helper) {
+        idMap[pathInfo.helper] = null
+        path = `${pathInfo.helper}(${path}, ${pathInfo.helperArgs})`
+      }
+      if (pathInfo.dynamic) {
+        const node = (idMap[id] = createSimpleExpression(path))
+        const plugins = context.options.expressionPlugins
+        node.ast = parseExpression(`(${path})`, {
+          plugins: plugins ? [...plugins, 'typescript'] : ['typescript'],
+        })
+      } else {
+        idMap[id] = path
+      }
+    } else {
+      idMap[id] = path
+    }
+  })
+
+  const args = [itemVar]
+  if (rawKey) {
+    const keyVar = `_for_key${depth}`
+    args.push(`, ${keyVar}`)
+    idMap[rawKey] = `${keyVar}.value`
+    idMap[keyVar] = null
+  }
+  if (rawIndex) {
+    const indexVar = `_for_index${depth}`
+    args.push(`, ${indexVar}`)
+    idMap[rawIndex] = `${indexVar}.value`
+    idMap[indexVar] = null
+  }
+
+  const { selectorPatterns, keyOnlyBindingPatterns } = matchPatterns(
+    render,
+    keyProp,
+    idMap,
+  )
+  const selectorDeclarations: CodeFragment[] = []
+  const selectorSetup: CodeFragment[] = []
+
+  for (let i = 0; i < selectorPatterns.length; i++) {
+    const { selector } = selectorPatterns[i]
+    const selectorName = `_selector${id}_${i}`
+    selectorDeclarations.push(`let ${selectorName}`, NEWLINE)
+    if (i === 0) {
+      selectorSetup.push(`({ createSelector }) => {`, INDENT_START)
+    }
+    selectorSetup.push(
+      NEWLINE,
+      `${selectorName} = `,
+      ...genCall(`createSelector`, [
+        `() => `,
+        ...genExpression(selector, context),
+      ]),
+    )
+    if (i === selectorPatterns.length - 1) {
+      selectorSetup.push(INDENT_END, NEWLINE, '}')
+    }
+  }
+
+  const blockFn = context.withId(() => {
+    const frag: CodeFragment[] = []
+    frag.push('(', ...args, ') => {', INDENT_START)
+    if (selectorPatterns.length || keyOnlyBindingPatterns.length) {
+      frag.push(
+        ...genBlockContent(render, context, false, () => {
+          const patternFrag: CodeFragment[] = []
+
+          for (let i = 0; i < selectorPatterns.length; i++) {
+            const { effect } = selectorPatterns[i]
+            patternFrag.push(
+              NEWLINE,
+              `_selector${id}_${i}(() => {`,
+              INDENT_START,
+            )
+            for (const oper of effect.operations) {
+              patternFrag.push(...genOperation(oper, context))
+            }
+            patternFrag.push(INDENT_END, NEWLINE, `})`)
+          }
+
+          for (const { effect } of keyOnlyBindingPatterns) {
+            for (const oper of effect.operations) {
+              patternFrag.push(...genOperation(oper, context))
+            }
+          }
+
+          return patternFrag
+        }),
+      )
+    } else {
+      frag.push(...genBlockContent(render, context))
+    }
+    frag.push(INDENT_END, NEWLINE, '}')
+    return frag
+  }, idMap)
+  exitScope()
+
+  let flags = 0
+  if (onlyChild) {
+    flags |= VaporVForFlags.FAST_REMOVE
+  }
+  if (component) {
+    flags |= VaporVForFlags.IS_COMPONENT
+  }
+  if (once) {
+    flags |= VaporVForFlags.ONCE
+  }
+
+  return [
+    NEWLINE,
+    ...selectorDeclarations,
+    `const n${id} = `,
+    ...genCall(
+      [helper('createFor'), 'undefined'],
+      sourceExpr,
+      blockFn,
+      genCallback(keyProp),
+      flags ? String(flags) : undefined,
+      selectorSetup.length ? selectorSetup : undefined,
+      // todo: hydrationNode
+    ),
+  ]
+
+  // construct a id -> accessor path map.
+  // e.g. `{ x: { y: [z] }}` -> `Map{ 'z' => '.x.y[0]' }`
+  function parseValueDestructure() {
+    const map = new Map<
+      string,
+      {
+        path: string
+        dynamic: boolean
+        helper?: string
+        helperArgs?: string
+      } | null
+    >()
+    if (value) {
+      rawValue = value && value.content
+      if (value.ast) {
+        walkIdentifiers(
+          value.ast,
+          (id, _, parentStack, ___, isLocal) => {
+            if (isLocal) {
+              let path = ''
+              let isDynamic = false
+              let helper
+              let helperArgs
+              for (let i = 0; i < parentStack.length; i++) {
+                const parent = parentStack[i]
+                const child = parentStack[i + 1] || id
+
+                if (
+                  parent.type === 'ObjectProperty' &&
+                  parent.value === child
+                ) {
+                  if (parent.key.type === 'StringLiteral') {
+                    path += `[${JSON.stringify(parent.key.value)}]`
+                  } else if (parent.computed) {
+                    isDynamic = true
+                    path += `[${value.content.slice(
+                      parent.key.start! - 1,
+                      parent.key.end! - 1,
+                    )}]`
+                  } else {
+                    // non-computed, can only be identifier
+                    path += `.${(parent.key as Identifier).name}`
+                  }
+                } else if (parent.type === 'ArrayPattern') {
+                  const index = parent.elements.indexOf(child as any)
+                  if (child.type === 'RestElement') {
+                    path += `.slice(${index})`
+                  } else {
+                    path += `[${index}]`
+                  }
+                } else if (
+                  parent.type === 'ObjectPattern' &&
+                  child.type === 'RestElement'
+                ) {
+                  helper = context.helper('getRestElement')
+                  helperArgs =
+                    '[' +
+                    parent.properties
+                      .filter(p => p.type === 'ObjectProperty')
+                      .map(p => {
+                        if (p.key.type === 'StringLiteral') {
+                          return JSON.stringify(p.key.value)
+                        } else if (p.computed) {
+                          isDynamic = true
+                          return value.content.slice(
+                            p.key.start! - 1,
+                            p.key.end! - 1,
+                          )
+                        } else {
+                          return JSON.stringify((p.key as Identifier).name)
+                        }
+                      })
+                      .join(', ') +
+                    ']'
+                }
+
+                // default value
+                if (
+                  child.type === 'AssignmentPattern' &&
+                  (parent.type === 'ObjectProperty' ||
+                    parent.type === 'ArrayPattern')
+                ) {
+                  isDynamic = true
+                  helper = context.helper('getDefaultValue')
+                  helperArgs = value.content.slice(
+                    child.right.start! - 1,
+                    child.right.end! - 1,
+                  )
+                }
+              }
+              map.set(id.name, { path, dynamic: isDynamic, helper, helperArgs })
+            }
+          },
+          true,
+        )
+      } else {
+        map.set(rawValue, null)
+      }
+    }
+    return map
+  }
+
+  function genCallback(expr: SimpleExpressionNode | undefined) {
+    if (!expr) return false
+    const res = context.withId(
+      () => genExpression(expr, context),
+      genSimpleIdMap(),
+    )
+    return [
+      ...genMulti(
+        ['(', ')', ', '],
+        rawValue ? rawValue : rawKey || rawIndex ? '_' : undefined,
+        rawKey ? rawKey : rawIndex ? '__' : undefined,
+        rawIndex,
+      ),
+      ' => (',
+      ...res,
+      ')',
+    ]
+  }
+
+  function genSimpleIdMap() {
+    const idMap: Record<string, null> = {}
+    if (rawKey) idMap[rawKey] = null
+    if (rawIndex) idMap[rawIndex] = null
+    idToPathMap.forEach((_, id) => (idMap[id] = null))
+    return idMap
+  }
+}
+
+function matchPatterns(
+  render: BlockIRNode,
+  keyProp: SimpleExpressionNode | undefined,
+  idMap: Record<string, string | SimpleExpressionNode | null>,
+) {
+  const selectorPatterns: NonNullable<
+    ReturnType<typeof matchSelectorPattern>
+  >[] = []
+  const keyOnlyBindingPatterns: NonNullable<
+    ReturnType<typeof matchKeyOnlyBindingPattern>
+  >[] = []
+
+  render.effect = render.effect.filter(effect => {
+    if (keyProp !== undefined) {
+      const selector = matchSelectorPattern(effect, keyProp.ast, idMap)
+      if (selector) {
+        selectorPatterns.push(selector)
+        return false
+      }
+      const keyOnly = matchKeyOnlyBindingPattern(effect, keyProp.ast)
+      if (keyOnly) {
+        keyOnlyBindingPatterns.push(keyOnly)
+        return false
+      }
+    }
+
+    return true
+  })
+
+  return {
+    keyOnlyBindingPatterns,
+    selectorPatterns,
+  }
+}
+
+function matchKeyOnlyBindingPattern(
+  effect: IREffect,
+  keyAst: any,
+):
+  | {
+      effect: IREffect
+    }
+  | undefined {
+  // TODO: expressions can be multiple?
+  if (effect.expressions.length === 1) {
+    const ast = effect.expressions[0].ast
+    if (typeof ast === 'object' && ast !== null) {
+      if (isKeyOnlyBinding(ast, keyAst)) {
+        return { effect }
+      }
+    }
+  }
+}
+
+function matchSelectorPattern(
+  effect: IREffect,
+  keyAst: any,
+  idMap: Record<string, string | SimpleExpressionNode | null>,
+):
+  | {
+      effect: IREffect
+      selector: SimpleExpressionNode
+    }
+  | undefined {
+  // TODO: expressions can be multiple?
+  if (effect.expressions.length === 1) {
+    const ast = effect.expressions[0].ast
+    if (typeof ast === 'object' && ast) {
+      const matcheds: [key: Expression, selector: Expression][] = []
+
+      walk(ast, {
+        enter(node) {
+          if (
+            typeof node === 'object' &&
+            node &&
+            node.type === 'BinaryExpression' &&
+            node.operator === '===' &&
+            node.left.type !== 'PrivateName'
+          ) {
+            const { left, right } = node
+            for (const [a, b] of [
+              [left, right],
+              [right, left],
+            ]) {
+              const aIsKey = isKeyOnlyBinding(a, keyAst)
+              const bIsKey = isKeyOnlyBinding(b, keyAst)
+              const bVars = analyzeVariableScopes(b, idMap)
+              if (aIsKey && !bIsKey && !bVars.locals.length) {
+                matcheds.push([a, b])
+              }
+            }
+          }
+        },
+      })
+
+      if (matcheds.length === 1) {
+        const [key, selector] = matcheds[0]
+        const content = effect.expressions[0].content
+
+        let hasExtraId = false
+        const parentStackMap = new Map<Identifier, Node[]>()
+        const parentStack: Node[] = []
+        walkIdentifiers(
+          ast,
+          id => {
+            if (id.start !== key.start && id.start !== selector.start) {
+              hasExtraId = true
+            }
+            parentStackMap.set(id, parentStack.slice())
+          },
+          false,
+          parentStack,
+        )
+
+        if (!hasExtraId) {
+          const name = content.slice(selector.start! - 1, selector.end! - 1)
+          return {
+            effect,
+            // @ts-expect-error
+            selector: {
+              content: name,
+              ast: extend({}, selector, {
+                start: 1,
+                end: name.length + 1,
+              }),
+              loc: selector.loc as any,
+              isStatic: false,
+            },
+          }
+        }
+      }
+    }
+
+    const content = effect.expressions[0].content
+    if (
+      typeof ast === 'object' &&
+      ast &&
+      ast.type === 'ConditionalExpression' &&
+      ast.test.type === 'BinaryExpression' &&
+      ast.test.operator === '===' &&
+      ast.test.left.type !== 'PrivateName' &&
+      isStaticNode(ast.consequent) &&
+      isStaticNode(ast.alternate)
+    ) {
+      const left = ast.test.left
+      const right = ast.test.right
+      for (const [a, b] of [
+        [left, right],
+        [right, left],
+      ]) {
+        const aIsKey = isKeyOnlyBinding(a, keyAst)
+        const bIsKey = isKeyOnlyBinding(b, keyAst)
+        const bVars = analyzeVariableScopes(b, idMap)
+        if (aIsKey && !bIsKey && !bVars.locals.length) {
+          return {
+            effect,
+            // @ts-expect-error
+            selector: {
+              content: content.slice(b.start! - 1, b.end! - 1),
+              ast: b,
+              loc: b.loc as any,
+              isStatic: false,
+            },
+          }
+        }
+      }
+    }
+  }
+}
+
+function analyzeVariableScopes(
+  ast: Node,
+  idMap: Record<string, string | SimpleExpressionNode | null>,
+) {
+  let globals: string[] = []
+  let locals: string[] = []
+
+  const ids: Identifier[] = []
+  const parentStackMap = new Map<Identifier, Node[]>()
+  const parentStack: Node[] = []
+  walkIdentifiers(
+    ast,
+    id => {
+      ids.push(id)
+      parentStackMap.set(id, parentStack.slice())
+    },
+    false,
+    parentStack,
+  )
+
+  for (const id of ids) {
+    if (isGloballyAllowed(id.name)) {
+      continue
+    }
+    if (idMap[id.name]) {
+      locals.push(id.name)
+    } else {
+      globals.push(id.name)
+    }
+  }
+
+  return { globals, locals }
+}
+
+function isKeyOnlyBinding(expr: Node, keyAst: any) {
+  let only = true
+  walk(expr, {
+    enter(node) {
+      if (isNodesEquivalent(node, keyAst)) {
+        this.skip()
+        return
+      }
+      if (node.type === 'Identifier') {
+        only = false
+      }
+    },
+  })
+  return only
+}
diff --git a/packages/compiler-vapor/src/generators/html.ts b/packages/compiler-vapor/src/generators/html.ts
new file mode 100644
index 00000000000..72af699dd03
--- /dev/null
+++ b/packages/compiler-vapor/src/generators/html.ts
@@ -0,0 +1,16 @@
+import type { CodegenContext } from '../generate'
+import type { SetHtmlIRNode } from '../ir'
+import { genExpression } from './expression'
+import { type CodeFragment, NEWLINE, genCall } from './utils'
+
+export function genSetHtml(
+  oper: SetHtmlIRNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  const { helper } = context
+  const { value, element } = oper
+  return [
+    NEWLINE,
+    ...genCall(helper('setHtml'), `n${element}`, genExpression(value, context)),
+  ]
+}
diff --git a/packages/compiler-vapor/src/generators/if.ts b/packages/compiler-vapor/src/generators/if.ts
new file mode 100644
index 00000000000..f4a3e599fab
--- /dev/null
+++ b/packages/compiler-vapor/src/generators/if.ts
@@ -0,0 +1,45 @@
+import type { CodegenContext } from '../generate'
+import { IRNodeTypes, type IfIRNode } from '../ir'
+import { genBlock } from './block'
+import { genExpression } from './expression'
+import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils'
+
+export function genIf(
+  oper: IfIRNode,
+  context: CodegenContext,
+  isNested = false,
+): CodeFragment[] {
+  const { helper } = context
+  const { condition, positive, negative, once } = oper
+  const [frag, push] = buildCodeFragment()
+
+  const conditionExpr: CodeFragment[] = [
+    '() => (',
+    ...genExpression(condition, context),
+    ')',
+  ]
+
+  let positiveArg = genBlock(positive, context)
+  let negativeArg: false | CodeFragment[] = false
+
+  if (negative) {
+    if (negative.type === IRNodeTypes.BLOCK) {
+      negativeArg = genBlock(negative, context)
+    } else {
+      negativeArg = ['() => ', ...genIf(negative!, context, true)]
+    }
+  }
+
+  if (!isNested) push(NEWLINE, `const n${oper.id} = `)
+  push(
+    ...genCall(
+      helper('createIf'),
+      conditionExpr,
+      positiveArg,
+      negativeArg,
+      once && 'true',
+    ),
+  )
+
+  return frag
+}
diff --git a/packages/compiler-vapor/src/generators/operation.ts b/packages/compiler-vapor/src/generators/operation.ts
new file mode 100644
index 00000000000..13ce5477cc1
--- /dev/null
+++ b/packages/compiler-vapor/src/generators/operation.ts
@@ -0,0 +1,183 @@
+import {
+  type IREffect,
+  IRNodeTypes,
+  type InsertionStateTypes,
+  type OperationNode,
+  isBlockOperation,
+} from '../ir'
+import type { CodegenContext } from '../generate'
+import { genInsertNode, genPrependNode } from './dom'
+import { genSetDynamicEvents, genSetEvent } from './event'
+import { genFor } from './for'
+import { genSetHtml } from './html'
+import { genIf } from './if'
+import { genDynamicProps, genSetProp } from './prop'
+import { genDeclareOldRef, genSetTemplateRef } from './templateRef'
+import { genGetTextChild, genSetText } from './text'
+import {
+  type CodeFragment,
+  INDENT_END,
+  INDENT_START,
+  NEWLINE,
+  buildCodeFragment,
+  genCall,
+} from './utils'
+import { genCreateComponent } from './component'
+import { genSlotOutlet } from './slotOutlet'
+import { processExpressions } from './expression'
+import { genBuiltinDirective } from './directive'
+
+export function genOperations(
+  opers: OperationNode[],
+  context: CodegenContext,
+): CodeFragment[] {
+  const [frag, push] = buildCodeFragment()
+  for (const operation of opers) {
+    push(...genOperationWithInsertionState(operation, context))
+  }
+  return frag
+}
+
+export function genOperationWithInsertionState(
+  oper: OperationNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  const [frag, push] = buildCodeFragment()
+  if (isBlockOperation(oper) && oper.parent) {
+    push(...genInsertionState(oper, context))
+  }
+  push(...genOperation(oper, context))
+  return frag
+}
+
+export function genOperation(
+  oper: OperationNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  switch (oper.type) {
+    case IRNodeTypes.SET_PROP:
+      return genSetProp(oper, context)
+    case IRNodeTypes.SET_DYNAMIC_PROPS:
+      return genDynamicProps(oper, context)
+    case IRNodeTypes.SET_TEXT:
+      return genSetText(oper, context)
+    case IRNodeTypes.SET_EVENT:
+      return genSetEvent(oper, context)
+    case IRNodeTypes.SET_DYNAMIC_EVENTS:
+      return genSetDynamicEvents(oper, context)
+    case IRNodeTypes.SET_HTML:
+      return genSetHtml(oper, context)
+    case IRNodeTypes.SET_TEMPLATE_REF:
+      return genSetTemplateRef(oper, context)
+    case IRNodeTypes.INSERT_NODE:
+      return genInsertNode(oper, context)
+    case IRNodeTypes.PREPEND_NODE:
+      return genPrependNode(oper, context)
+    case IRNodeTypes.IF:
+      return genIf(oper, context)
+    case IRNodeTypes.FOR:
+      return genFor(oper, context)
+    case IRNodeTypes.CREATE_COMPONENT_NODE:
+      return genCreateComponent(oper, context)
+    case IRNodeTypes.DECLARE_OLD_REF:
+      return genDeclareOldRef(oper)
+    case IRNodeTypes.SLOT_OUTLET_NODE:
+      return genSlotOutlet(oper, context)
+    case IRNodeTypes.DIRECTIVE:
+      return genBuiltinDirective(oper, context)
+    case IRNodeTypes.GET_TEXT_CHILD:
+      return genGetTextChild(oper, context)
+    default:
+      const exhaustiveCheck: never = oper
+      throw new Error(
+        `Unhandled operation type in genOperation: ${exhaustiveCheck}`,
+      )
+  }
+}
+
+export function genEffects(
+  effects: IREffect[],
+  context: CodegenContext,
+  genExtraFrag?: () => CodeFragment[],
+): CodeFragment[] {
+  const { helper } = context
+  const expressions = effects.flatMap(effect => effect.expressions)
+  const [frag, push, unshift] = buildCodeFragment()
+  const shouldDeclare = genExtraFrag === undefined
+  let operationsCount = 0
+  const {
+    ids,
+    frag: declarationFrags,
+    varNames,
+  } = processExpressions(context, expressions, shouldDeclare)
+  push(...declarationFrags)
+  for (let i = 0; i < effects.length; i++) {
+    const effect = effects[i]
+    operationsCount += effect.operations.length
+    const frags = context.withId(() => genEffect(effect, context), ids)
+    i > 0 && push(NEWLINE)
+    if (frag[frag.length - 1] === ')' && frags[0] === '(') {
+      push(';')
+    }
+    push(...frags)
+  }
+
+  const newLineCount = frag.filter(frag => frag === NEWLINE).length
+  if (newLineCount > 1 || operationsCount > 1 || declarationFrags.length > 0) {
+    unshift(`{`, INDENT_START, NEWLINE)
+    push(INDENT_END, NEWLINE, '}')
+    if (!effects.length) {
+      unshift(NEWLINE)
+    }
+  }
+
+  if (effects.length) {
+    unshift(NEWLINE, `${helper('renderEffect')}(() => `)
+    push(`)`)
+  }
+
+  if (!shouldDeclare && varNames.length) {
+    unshift(NEWLINE, `let `, varNames.join(', '))
+  }
+
+  if (genExtraFrag) {
+    push(...context.withId(genExtraFrag, ids))
+  }
+
+  return frag
+}
+
+export function genEffect(
+  { operations }: IREffect,
+  context: CodegenContext,
+): CodeFragment[] {
+  const [frag, push] = buildCodeFragment()
+  const operationsExps = genOperations(operations, context)
+  const newlineCount = operationsExps.filter(frag => frag === NEWLINE).length
+
+  if (newlineCount > 1) {
+    push(...operationsExps)
+  } else {
+    push(...operationsExps.filter(frag => frag !== NEWLINE))
+  }
+
+  return frag
+}
+
+function genInsertionState(
+  operation: InsertionStateTypes,
+  context: CodegenContext,
+): CodeFragment[] {
+  return [
+    NEWLINE,
+    ...genCall(
+      context.helper('setInsertionState'),
+      `n${operation.parent}`,
+      operation.anchor == null
+        ? undefined
+        : operation.anchor === -1 // -1 indicates prepend
+          ? `0` // runtime anchor value for prepend
+          : `n${operation.anchor}`,
+    ),
+  ]
+}
diff --git a/packages/compiler-vapor/src/generators/prop.ts b/packages/compiler-vapor/src/generators/prop.ts
new file mode 100644
index 00000000000..42f063331fc
--- /dev/null
+++ b/packages/compiler-vapor/src/generators/prop.ts
@@ -0,0 +1,225 @@
+import {
+  NewlineType,
+  type SimpleExpressionNode,
+  isSimpleIdentifier,
+} from '@vue/compiler-dom'
+import type { CodegenContext } from '../generate'
+import {
+  IRDynamicPropsKind,
+  type IRProp,
+  type SetDynamicPropsIRNode,
+  type SetPropIRNode,
+  type VaporHelper,
+} from '../ir'
+import { genExpression } from './expression'
+import {
+  type CodeFragment,
+  DELIMITERS_ARRAY,
+  DELIMITERS_OBJECT,
+  NEWLINE,
+  genCall,
+  genMulti,
+} from './utils'
+import {
+  canSetValueDirectly,
+  capitalize,
+  isSVGTag,
+  shouldSetAsAttr,
+  toHandlerKey,
+} from '@vue/shared'
+
+export type HelperConfig = {
+  name: VaporHelper
+  needKey?: boolean
+  acceptRoot?: boolean
+}
+
+// this should be kept in sync with runtime-vapor/src/dom/prop.ts
+const helpers = {
+  setText: { name: 'setText' },
+  setHtml: { name: 'setHtml' },
+  setClass: { name: 'setClass' },
+  setStyle: { name: 'setStyle' },
+  setValue: { name: 'setValue' },
+  setAttr: { name: 'setAttr', needKey: true },
+  setProp: { name: 'setProp', needKey: true },
+  setDOMProp: { name: 'setDOMProp', needKey: true },
+  setDynamicProps: { name: 'setDynamicProps' },
+} as const satisfies Partial<Record<VaporHelper, HelperConfig>>
+
+// only the static key prop will reach here
+export function genSetProp(
+  oper: SetPropIRNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  const { helper } = context
+  const {
+    prop: { key, values, modifier },
+    tag,
+  } = oper
+  const resolvedHelper = getRuntimeHelper(tag, key.content, modifier)
+  const propValue = genPropValue(values, context)
+  return [
+    NEWLINE,
+    ...genCall(
+      [helper(resolvedHelper.name), null],
+      `n${oper.element}`,
+      resolvedHelper.needKey ? genExpression(key, context) : false,
+      propValue,
+    ),
+  ]
+}
+
+// dynamic key props and v-bind="{}" will reach here
+export function genDynamicProps(
+  oper: SetDynamicPropsIRNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  const { helper } = context
+  const values = oper.props.map(props =>
+    Array.isArray(props)
+      ? genLiteralObjectProps(props, context) // static and dynamic arg props
+      : props.kind === IRDynamicPropsKind.ATTRIBUTE
+        ? genLiteralObjectProps([props], context) // dynamic arg props
+        : genExpression(props.value, context),
+  ) // v-bind=""
+  return [
+    NEWLINE,
+    ...genCall(
+      helper('setDynamicProps'),
+      `n${oper.element}`,
+      genMulti(DELIMITERS_ARRAY, ...values),
+      oper.root && 'true',
+    ),
+  ]
+}
+
+function genLiteralObjectProps(
+  props: IRProp[],
+  context: CodegenContext,
+): CodeFragment[] {
+  return genMulti(
+    DELIMITERS_OBJECT,
+    ...props.map(prop => [
+      ...genPropKey(prop, context),
+      `: `,
+      ...genPropValue(prop.values, context),
+    ]),
+  )
+}
+
+export function genPropKey(
+  { key: node, modifier, runtimeCamelize, handler, handlerModifiers }: IRProp,
+  context: CodegenContext,
+): CodeFragment[] {
+  const { helper } = context
+
+  const handlerModifierPostfix = handlerModifiers
+    ? handlerModifiers.map(capitalize).join('')
+    : ''
+  // static arg was transformed by v-bind transformer
+  if (node.isStatic) {
+    // only quote keys if necessary
+    const keyName =
+      (handler ? toHandlerKey(node.content) : node.content) +
+      handlerModifierPostfix
+    return [
+      [
+        isSimpleIdentifier(keyName) ? keyName : JSON.stringify(keyName),
+        NewlineType.None,
+        node.loc,
+      ],
+    ]
+  }
+
+  let key = genExpression(node, context)
+  if (runtimeCamelize) {
+    key = genCall(helper('camelize'), key)
+  }
+  if (handler) {
+    key = genCall(helper('toHandlerKey'), key)
+  }
+  return [
+    '[',
+    modifier && `${JSON.stringify(modifier)} + `,
+    ...key,
+    handlerModifierPostfix
+      ? ` + ${JSON.stringify(handlerModifierPostfix)}`
+      : undefined,
+    ']',
+  ]
+}
+
+export function genPropValue(
+  values: SimpleExpressionNode[],
+  context: CodegenContext,
+): CodeFragment[] {
+  if (values.length === 1) {
+    return genExpression(values[0], context)
+  }
+  return genMulti(
+    DELIMITERS_ARRAY,
+    ...values.map(expr => genExpression(expr, context)),
+  )
+}
+
+function getRuntimeHelper(
+  tag: string,
+  key: string,
+  modifier: '.' | '^' | undefined,
+): HelperConfig {
+  const tagName = tag.toUpperCase()
+  if (modifier) {
+    if (modifier === '.') {
+      return getSpecialHelper(key, tagName) || helpers.setDOMProp
+    } else {
+      return helpers.setAttr
+    }
+  }
+
+  // 1. special handling for value / style / class / textContent /  innerHTML
+  const helper = getSpecialHelper(key, tagName)
+  if (helper) {
+    return helper
+  }
+
+  // 2. Aria DOM properties shared between all Elements in
+  //    https://developer.mozilla.org/en-US/docs/Web/API/Element
+  if (/aria[A-Z]/.test(key)) {
+    return helpers.setDOMProp
+  }
+
+  // 3. SVG: always attribute
+  if (isSVGTag(tag)) {
+    // TODO pass svg flag
+    return helpers.setAttr
+  }
+
+  // 4. respect shouldSetAsAttr used in vdom and setDynamicProp for consistency
+  //    also fast path for presence of hyphen (covers data-* and aria-*)
+  if (shouldSetAsAttr(tagName, key) || key.includes('-')) {
+    return helpers.setAttr
+  }
+
+  // 5. Fallback to setDOMProp, which has a runtime `key in el` check to
+  // ensure behavior consistency with vdom
+  return helpers.setProp
+}
+
+function getSpecialHelper(
+  keyName: string,
+  tagName: string,
+): HelperConfig | undefined {
+  // special case for 'value' property
+  if (keyName === 'value' && canSetValueDirectly(tagName)) {
+    return helpers.setValue
+  } else if (keyName === 'class') {
+    return helpers.setClass
+  } else if (keyName === 'style') {
+    return helpers.setStyle
+  } else if (keyName === 'innerHTML') {
+    return helpers.setHtml
+  } else if (keyName === 'textContent') {
+    return helpers.setText
+  }
+}
diff --git a/packages/compiler-vapor/src/generators/slotOutlet.ts b/packages/compiler-vapor/src/generators/slotOutlet.ts
new file mode 100644
index 00000000000..3221cbbd2c7
--- /dev/null
+++ b/packages/compiler-vapor/src/generators/slotOutlet.ts
@@ -0,0 +1,37 @@
+import type { CodegenContext } from '../generate'
+import type { SlotOutletIRNode } from '../ir'
+import { genBlock } from './block'
+import { genExpression } from './expression'
+import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils'
+import { genRawProps } from './component'
+
+export function genSlotOutlet(
+  oper: SlotOutletIRNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  const { helper } = context
+  const { id, name, fallback } = oper
+  const [frag, push] = buildCodeFragment()
+
+  const nameExpr = name.isStatic
+    ? genExpression(name, context)
+    : ['() => (', ...genExpression(name, context), ')']
+
+  let fallbackArg: CodeFragment[] | undefined
+  if (fallback) {
+    fallbackArg = genBlock(fallback, context)
+  }
+
+  push(
+    NEWLINE,
+    `const n${id} = `,
+    ...genCall(
+      helper('createSlot'),
+      nameExpr,
+      genRawProps(oper.props, context) || 'null',
+      fallbackArg,
+    ),
+  )
+
+  return frag
+}
diff --git a/packages/compiler-vapor/src/generators/template.ts b/packages/compiler-vapor/src/generators/template.ts
new file mode 100644
index 00000000000..5a066b09e9a
--- /dev/null
+++ b/packages/compiler-vapor/src/generators/template.ts
@@ -0,0 +1,118 @@
+import type { CodegenContext } from '../generate'
+import { DynamicFlag, type IRDynamicInfo } from '../ir'
+import { genDirectivesForElement } from './directive'
+import { genOperationWithInsertionState } from './operation'
+import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils'
+
+export function genTemplates(
+  templates: string[],
+  rootIndex: number | undefined,
+  { helper }: CodegenContext,
+): string {
+  return templates
+    .map(
+      (template, i) =>
+        `const t${i} = ${helper('template')}(${JSON.stringify(
+          template,
+        )}${i === rootIndex ? ', true' : ''})\n`,
+    )
+    .join('')
+}
+
+export function genSelf(
+  dynamic: IRDynamicInfo,
+  context: CodegenContext,
+): CodeFragment[] {
+  const [frag, push] = buildCodeFragment()
+  const { id, template, operation } = dynamic
+
+  if (id !== undefined && template !== undefined) {
+    push(NEWLINE, `const n${id} = t${template}()`)
+    push(...genDirectivesForElement(id, context))
+  }
+
+  if (operation) {
+    push(...genOperationWithInsertionState(operation, context))
+  }
+
+  return frag
+}
+
+export function genChildren(
+  dynamic: IRDynamicInfo,
+  context: CodegenContext,
+  pushBlock: (...items: CodeFragment[]) => number,
+  from: string = `n${dynamic.id}`,
+): CodeFragment[] {
+  const { helper } = context
+  const [frag, push] = buildCodeFragment()
+  const { children } = dynamic
+
+  let offset = 0
+  let prev: [variable: string, elementIndex: number] | undefined
+  const childrenToGen: [IRDynamicInfo, string][] = []
+
+  for (const [index, child] of children.entries()) {
+    if (child.flags & DynamicFlag.NON_TEMPLATE) {
+      offset--
+    }
+
+    const id =
+      child.flags & DynamicFlag.REFERENCED
+        ? child.flags & DynamicFlag.INSERT
+          ? child.anchor
+          : child.id
+        : undefined
+
+    if (id === undefined && !child.hasDynamicChild) {
+      push(...genSelf(child, context))
+      continue
+    }
+
+    const elementIndex = Number(index) + offset
+    // p for "placeholder" variables that are meant for possible reuse by
+    // other access paths
+    const variable = id === undefined ? `p${context.block.tempId++}` : `n${id}`
+    pushBlock(NEWLINE, `const ${variable} = `)
+
+    if (prev) {
+      if (elementIndex - prev[1] === 1) {
+        pushBlock(...genCall(helper('next'), prev[0]))
+      } else {
+        pushBlock(...genCall(helper('nthChild'), from, String(elementIndex)))
+      }
+    } else {
+      if (elementIndex === 0) {
+        pushBlock(...genCall(helper('child'), from))
+      } else {
+        // check if there's a node that we can reuse from
+        let init = genCall(helper('child'), from)
+        if (elementIndex === 1) {
+          init = genCall(helper('next'), init)
+        } else if (elementIndex > 1) {
+          init = genCall(helper('nthChild'), from, String(elementIndex))
+        }
+        pushBlock(...init)
+      }
+    }
+
+    if (id === child.anchor) {
+      push(...genSelf(child, context))
+    }
+
+    if (id !== undefined) {
+      push(...genDirectivesForElement(id, context))
+    }
+
+    prev = [variable, elementIndex]
+    childrenToGen.push([child, variable])
+  }
+
+  if (childrenToGen.length) {
+    for (const [child, from] of childrenToGen) {
+      push(...genChildren(child, context, pushBlock, from))
+    }
+  }
+
+  return frag
+}
diff --git a/packages/compiler-vapor/src/generators/templateRef.ts b/packages/compiler-vapor/src/generators/templateRef.ts
new file mode 100644
index 00000000000..af8facc57b1
--- /dev/null
+++ b/packages/compiler-vapor/src/generators/templateRef.ts
@@ -0,0 +1,45 @@
+import { genExpression } from './expression'
+import type { CodegenContext } from '../generate'
+import type { DeclareOldRefIRNode, SetTemplateRefIRNode } from '../ir'
+import { type CodeFragment, NEWLINE, genCall } from './utils'
+import { BindingTypes, type SimpleExpressionNode } from '@vue/compiler-dom'
+
+export const setTemplateRefIdent = `_setTemplateRef`
+
+export function genSetTemplateRef(
+  oper: SetTemplateRefIRNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  return [
+    NEWLINE,
+    oper.effect && `r${oper.element} = `,
+    ...genCall(
+      setTemplateRefIdent, // will be generated in root scope
+      `n${oper.element}`,
+      genRefValue(oper.value, context),
+      oper.effect ? `r${oper.element}` : oper.refFor ? 'void 0' : undefined,
+      oper.refFor && 'true',
+    ),
+  ]
+}
+
+export function genDeclareOldRef(oper: DeclareOldRefIRNode): CodeFragment[] {
+  return [NEWLINE, `let r${oper.id}`]
+}
+
+function genRefValue(value: SimpleExpressionNode, context: CodegenContext) {
+  // in inline mode there is no setupState object, so we can't use string
+  // keys to set the ref. Instead, we need to transform it to pass the
+  // actual ref instead.
+  if (!__BROWSER__ && value && context.options.inline) {
+    const binding = context.options.bindingMetadata[value.content]
+    if (
+      binding === BindingTypes.SETUP_LET ||
+      binding === BindingTypes.SETUP_REF ||
+      binding === BindingTypes.SETUP_MAYBE_REF
+    ) {
+      return [value.content]
+    }
+  }
+  return genExpression(value, context)
+}
diff --git a/packages/compiler-vapor/src/generators/text.ts b/packages/compiler-vapor/src/generators/text.ts
new file mode 100644
index 00000000000..89e3167c664
--- /dev/null
+++ b/packages/compiler-vapor/src/generators/text.ts
@@ -0,0 +1,47 @@
+import type { SimpleExpressionNode } from '@vue/compiler-dom'
+import type { CodegenContext } from '../generate'
+import type { GetTextChildIRNode, SetTextIRNode } from '../ir'
+import { getLiteralExpressionValue } from '../utils'
+import { genExpression } from './expression'
+import { type CodeFragment, NEWLINE, genCall } from './utils'
+
+export function genSetText(
+  oper: SetTextIRNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  const { helper } = context
+  const { element, values, generated, jsx } = oper
+  const texts = combineValues(values, context, jsx)
+  return [
+    NEWLINE,
+    ...genCall(helper('setText'), `${generated ? 'x' : 'n'}${element}`, texts),
+  ]
+}
+
+function combineValues(
+  values: SimpleExpressionNode[],
+  context: CodegenContext,
+  jsx?: boolean,
+): CodeFragment[] {
+  return values.flatMap((value, i) => {
+    let exp = genExpression(value, context)
+    if (!jsx && getLiteralExpressionValue(value) == null) {
+      // dynamic, wrap with toDisplayString
+      exp = genCall(context.helper('toDisplayString'), exp)
+    }
+    if (i > 0) {
+      exp.unshift(jsx ? ', ' : ' + ')
+    }
+    return exp
+  })
+}
+
+export function genGetTextChild(
+  oper: GetTextChildIRNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  return [
+    NEWLINE,
+    `const x${oper.parent} = ${context.helper('child')}(n${oper.parent})`,
+  ]
+}
diff --git a/packages/compiler-vapor/src/generators/utils.ts b/packages/compiler-vapor/src/generators/utils.ts
new file mode 100644
index 00000000000..904b3dc87ca
--- /dev/null
+++ b/packages/compiler-vapor/src/generators/utils.ts
@@ -0,0 +1,204 @@
+import { SourceMapGenerator } from 'source-map-js'
+import {
+  type CodegenSourceMapGenerator,
+  NewlineType,
+  type Position,
+  type SourceLocation,
+  advancePositionWithMutation,
+  locStub,
+} from '@vue/compiler-dom'
+import { isArray, isString } from '@vue/shared'
+import type { CodegenContext } from '../generate'
+
+export const NEWLINE: unique symbol = Symbol(__DEV__ ? `newline` : ``)
+/** increase offset but don't push actual code */
+export const LF: unique symbol = Symbol(__DEV__ ? `line feed` : ``)
+export const INDENT_START: unique symbol = Symbol(__DEV__ ? `indent start` : ``)
+export const INDENT_END: unique symbol = Symbol(__DEV__ ? `indent end` : ``)
+
+type FalsyValue = false | null | undefined
+export type CodeFragment =
+  | typeof NEWLINE
+  | typeof LF
+  | typeof INDENT_START
+  | typeof INDENT_END
+  | string
+  | [code: string, newlineIndex?: number, loc?: SourceLocation, name?: string]
+  | FalsyValue
+export type CodeFragments = Exclude<CodeFragment, any[]> | CodeFragment[]
+
+export function buildCodeFragment(
+  ...frag: CodeFragment[]
+): [
+  CodeFragment[],
+  (...items: CodeFragment[]) => number,
+  (...items: CodeFragment[]) => number,
+] {
+  const push = frag.push.bind(frag)
+  const unshift = frag.unshift.bind(frag)
+  return [frag, push, unshift]
+}
+
+export type CodeFragmentDelimiters = [
+  left: CodeFragments,
+  right: CodeFragments,
+  delimiter: CodeFragments,
+  placeholder?: CodeFragments,
+]
+
+export function genMulti(
+  [left, right, seg, placeholder]: CodeFragmentDelimiters,
+  ...frags: CodeFragments[]
+): CodeFragment[] {
+  if (placeholder) {
+    while (frags.length > 0 && !frags[frags.length - 1]) {
+      frags.pop()
+    }
+    frags = frags.map(frag => frag || placeholder)
+  } else {
+    frags = frags.filter(Boolean)
+  }
+
+  const frag: CodeFragment[] = []
+  push(left)
+  for (let [i, fn] of (
+    frags as Array<Exclude<CodeFragments, FalsyValue>>
+  ).entries()) {
+    push(fn)
+    if (i < frags.length - 1) push(seg)
+  }
+  push(right)
+  return frag
+
+  function push(fn: CodeFragments) {
+    if (!isArray(fn)) fn = [fn]
+    frag.push(...fn)
+  }
+}
+export const DELIMITERS_ARRAY: CodeFragmentDelimiters = ['[', ']', ', ']
+export const DELIMITERS_ARRAY_NEWLINE: CodeFragmentDelimiters = [
+  ['[', INDENT_START, NEWLINE],
+  [INDENT_END, NEWLINE, ']'],
+  [', ', NEWLINE],
+]
+export const DELIMITERS_OBJECT: CodeFragmentDelimiters = ['{ ', ' }', ', ']
+export const DELIMITERS_OBJECT_NEWLINE: CodeFragmentDelimiters = [
+  ['{', INDENT_START, NEWLINE],
+  [INDENT_END, NEWLINE, '}'],
+  [', ', NEWLINE],
+]
+
+export function genCall(
+  name: string | [name: string, placeholder?: CodeFragments],
+  ...frags: CodeFragments[]
+): CodeFragment[] {
+  const hasPlaceholder = isArray(name)
+  const fnName = hasPlaceholder ? name[0] : name
+  const placeholder = hasPlaceholder ? name[1] : 'null'
+  return [fnName, ...genMulti(['(', ')', ', ', placeholder], ...frags)]
+}
+
+export function codeFragmentToString(
+  code: CodeFragment[],
+  context: CodegenContext,
+): [code: string, map: CodegenSourceMapGenerator | undefined] {
+  const {
+    options: { filename, sourceMap },
+  } = context
+
+  let map: CodegenSourceMapGenerator | undefined
+  if (sourceMap) {
+    // lazy require source-map implementation, only in non-browser builds
+    map = new SourceMapGenerator() as unknown as CodegenSourceMapGenerator
+    map.setSourceContent(filename, context.ir.source)
+    map._sources.add(filename)
+  }
+
+  let codegen = ''
+  const pos = { line: 1, column: 1, offset: 0 }
+  let indentLevel = 0
+
+  for (let frag of code) {
+    if (!frag) continue
+
+    if (frag === NEWLINE) {
+      frag = [`\n${`  `.repeat(indentLevel)}`, NewlineType.Start]
+    } else if (frag === INDENT_START) {
+      indentLevel++
+      continue
+    } else if (frag === INDENT_END) {
+      indentLevel--
+      continue
+    } else if (frag === LF) {
+      pos.line++
+      pos.column = 0
+      pos.offset++
+      continue
+    }
+
+    if (isString(frag)) frag = [frag]
+
+    let [code, newlineIndex = NewlineType.None, loc, name] = frag
+    codegen += code
+
+    if (map) {
+      if (loc) addMapping(loc.start, name)
+      if (newlineIndex === NewlineType.Unknown) {
+        // multiple newlines, full iteration
+        advancePositionWithMutation(pos, code)
+      } else {
+        // fast paths
+        pos.offset += code.length
+        if (newlineIndex === NewlineType.None) {
+          // no newlines; fast path to avoid newline detection
+          if (__TEST__ && code.includes('\n')) {
+            throw new Error(
+              `CodegenContext.push() called newlineIndex: none, but contains` +
+                `newlines: ${code.replace(/\n/g, '\\n')}`,
+            )
+          }
+          pos.column += code.length
+        } else {
+          // single newline at known index
+          if (newlineIndex === NewlineType.End) {
+            newlineIndex = code.length - 1
+          }
+          if (
+            __TEST__ &&
+            (code.charAt(newlineIndex) !== '\n' ||
+              code.slice(0, newlineIndex).includes('\n') ||
+              code.slice(newlineIndex + 1).includes('\n'))
+          ) {
+            throw new Error(
+              `CodegenContext.push() called with newlineIndex: ${newlineIndex} ` +
+                `but does not conform: ${code.replace(/\n/g, '\\n')}`,
+            )
+          }
+          pos.line++
+          pos.column = code.length - newlineIndex
+        }
+      }
+      if (loc && loc !== locStub) {
+        addMapping(loc.end)
+      }
+    }
+  }
+
+  return [codegen, map]
+
+  function addMapping(loc: Position, name: string | null = null) {
+    // we use the private property to directly add the mapping
+    // because the addMapping() implementation in source-map-js has a bunch of
+    // unnecessary arg and validation checks that are pure overhead in our case.
+    const { _names, _mappings } = map!
+    if (name !== null && !_names.has(name)) _names.add(name)
+    _mappings.add({
+      originalLine: loc.line,
+      originalColumn: loc.column - 1, // source-map column is 0 based
+      generatedLine: pos.line,
+      generatedColumn: pos.column - 1,
+      source: filename,
+      name,
+    })
+  }
+}
diff --git a/packages/compiler-vapor/src/generators/vModel.ts b/packages/compiler-vapor/src/generators/vModel.ts
new file mode 100644
index 00000000000..a7082d91464
--- /dev/null
+++ b/packages/compiler-vapor/src/generators/vModel.ts
@@ -0,0 +1,52 @@
+import type { CodegenContext } from '../generate'
+import type { DirectiveIRNode } from '../ir'
+import { type CodeFragment, NEWLINE, genCall } from './utils'
+import { genExpression } from './expression'
+import type { SimpleExpressionNode } from '@vue/compiler-dom'
+
+const helperMap = {
+  text: 'applyTextModel',
+  radio: 'applyRadioModel',
+  checkbox: 'applyCheckboxModel',
+  select: 'applySelectModel',
+  dynamic: 'applyDynamicModel',
+} as const
+
+// This is only for built-in v-model on native elements.
+export function genVModel(
+  oper: DirectiveIRNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  const {
+    modelType,
+    element,
+    dir: { exp, modifiers },
+  } = oper
+
+  return [
+    NEWLINE,
+    ...genCall(
+      context.helper(helperMap[modelType!]),
+      `n${element}`,
+      // getter
+      [`() => (`, ...genExpression(exp!, context), `)`],
+      // setter
+      genModelHandler(exp!, context),
+      // modifiers
+      modifiers.length
+        ? `{ ${modifiers.map(e => e.content + ': true').join(',')} }`
+        : undefined,
+    ),
+  ]
+}
+
+export function genModelHandler(
+  exp: SimpleExpressionNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  return [
+    `${context.options.isTS ? `(_value: any)` : `_value`} => (`,
+    ...genExpression(exp, context, '_value'),
+    ')',
+  ]
+}
diff --git a/packages/compiler-vapor/src/generators/vShow.ts b/packages/compiler-vapor/src/generators/vShow.ts
new file mode 100644
index 00000000000..9a6ccefcded
--- /dev/null
+++ b/packages/compiler-vapor/src/generators/vShow.ts
@@ -0,0 +1,18 @@
+import type { CodegenContext } from '../generate'
+import type { DirectiveIRNode } from '../ir'
+import { genExpression } from './expression'
+import { type CodeFragment, NEWLINE, genCall } from './utils'
+
+export function genVShow(
+  oper: DirectiveIRNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  return [
+    NEWLINE,
+    ...genCall(context.helper('applyVShow'), `n${oper.element}`, [
+      `() => (`,
+      ...genExpression(oper.dir.exp!, context),
+      `)`,
+    ]),
+  ]
+}
diff --git a/packages/compiler-vapor/src/index.ts b/packages/compiler-vapor/src/index.ts
new file mode 100644
index 00000000000..6eda1021060
--- /dev/null
+++ b/packages/compiler-vapor/src/index.ts
@@ -0,0 +1,52 @@
+export { parse } from '@vue/compiler-dom'
+export {
+  transform,
+  createStructuralDirectiveTransform,
+  type TransformContext,
+  type NodeTransform,
+  type StructuralDirectiveTransform,
+  type DirectiveTransform,
+} from './transform'
+export {
+  generate,
+  CodegenContext,
+  type CodegenOptions,
+  type VaporCodegenResult,
+} from './generate'
+export {
+  genCall,
+  genMulti,
+  buildCodeFragment,
+  codeFragmentToString,
+  type CodeFragment,
+} from './generators/utils'
+export {
+  wrapTemplate,
+  compile,
+  type CompilerOptions,
+  type TransformPreset,
+} from './compile'
+export * from './ir'
+export {
+  VaporErrorCodes,
+  VaporErrorMessages,
+  createVaporCompilerError,
+  type VaporCompilerError,
+} from './errors'
+
+export { transformElement } from './transforms/transformElement'
+export { transformChildren } from './transforms/transformChildren'
+export { transformTemplateRef } from './transforms/transformTemplateRef'
+export { transformText } from './transforms/transformText'
+export { transformVBind } from './transforms/vBind'
+export { transformVHtml } from './transforms/vHtml'
+export { transformVOn } from './transforms/vOn'
+export { transformVOnce } from './transforms/vOnce'
+export { transformVShow } from './transforms/vShow'
+export { transformVText } from './transforms/vText'
+export { transformVIf } from './transforms/vIf'
+export { transformVFor } from './transforms/vFor'
+export { transformVModel } from './transforms/vModel'
+export { transformComment } from './transforms/transformComment'
+export { transformSlotOutlet } from './transforms/transformSlotOutlet'
+export { transformVSlot } from './transforms/vSlot'
diff --git a/packages/compiler-vapor/src/ir/component.ts b/packages/compiler-vapor/src/ir/component.ts
new file mode 100644
index 00000000000..884636cdfa8
--- /dev/null
+++ b/packages/compiler-vapor/src/ir/component.ts
@@ -0,0 +1,71 @@
+import type { SimpleExpressionNode } from '@vue/compiler-dom'
+import type { DirectiveTransformResult } from '../transform'
+import type { BlockIRNode, IRFor } from './index'
+
+// props
+export interface IRProp extends Omit<DirectiveTransformResult, 'value'> {
+  values: SimpleExpressionNode[]
+}
+
+export enum IRDynamicPropsKind {
+  EXPRESSION, // v-bind="value"
+  ATTRIBUTE, // v-bind:[foo]="value"
+}
+
+export type IRPropsStatic = IRProp[]
+export interface IRPropsDynamicExpression {
+  kind: IRDynamicPropsKind.EXPRESSION
+  value: SimpleExpressionNode
+  handler?: boolean
+}
+export interface IRPropsDynamicAttribute extends IRProp {
+  kind: IRDynamicPropsKind.ATTRIBUTE
+}
+export type IRProps =
+  | IRPropsStatic
+  | IRPropsDynamicAttribute
+  | IRPropsDynamicExpression
+
+// slots
+export interface SlotBlockIRNode extends BlockIRNode {
+  props?: SimpleExpressionNode
+}
+
+export enum IRSlotType {
+  STATIC,
+  DYNAMIC,
+  LOOP,
+  CONDITIONAL,
+  EXPRESSION, // JSX only
+}
+export type IRSlotsStatic = {
+  slotType: IRSlotType.STATIC
+  slots: Record<string, SlotBlockIRNode>
+}
+export interface IRSlotDynamicBasic {
+  slotType: IRSlotType.DYNAMIC
+  name: SimpleExpressionNode
+  fn: SlotBlockIRNode
+}
+export interface IRSlotDynamicLoop {
+  slotType: IRSlotType.LOOP
+  name: SimpleExpressionNode
+  fn: SlotBlockIRNode
+  loop: IRFor
+}
+export interface IRSlotDynamicConditional {
+  slotType: IRSlotType.CONDITIONAL
+  condition: SimpleExpressionNode
+  positive: IRSlotDynamicBasic
+  negative?: IRSlotDynamicBasic | IRSlotDynamicConditional
+}
+export interface IRSlotsExpression {
+  slotType: IRSlotType.EXPRESSION
+  slots: SimpleExpressionNode
+}
+
+export type IRSlotDynamic =
+  | IRSlotDynamicBasic
+  | IRSlotDynamicLoop
+  | IRSlotDynamicConditional
+export type IRSlots = IRSlotsStatic | IRSlotDynamic | IRSlotsExpression
diff --git a/packages/compiler-vapor/src/ir/index.ts b/packages/compiler-vapor/src/ir/index.ts
new file mode 100644
index 00000000000..18f0139ab56
--- /dev/null
+++ b/packages/compiler-vapor/src/ir/index.ts
@@ -0,0 +1,305 @@
+import type {
+  CompoundExpressionNode,
+  DirectiveNode,
+  RootNode,
+  SimpleExpressionNode,
+  TemplateChildNode,
+} from '@vue/compiler-dom'
+import type { Prettify } from '@vue/shared'
+import type { DirectiveTransform, NodeTransform } from '../transform'
+import type { IRProp, IRProps, IRSlots } from './component'
+
+export * from './component'
+
+export enum IRNodeTypes {
+  ROOT,
+  BLOCK,
+
+  SET_PROP,
+  SET_DYNAMIC_PROPS,
+  SET_TEXT,
+  SET_EVENT,
+  SET_DYNAMIC_EVENTS,
+  SET_HTML,
+  SET_TEMPLATE_REF,
+
+  INSERT_NODE,
+  PREPEND_NODE,
+  CREATE_COMPONENT_NODE,
+  SLOT_OUTLET_NODE,
+
+  DIRECTIVE,
+  DECLARE_OLD_REF, // consider make it more general
+
+  IF,
+  FOR,
+
+  GET_TEXT_CHILD,
+}
+
+export interface BaseIRNode {
+  type: IRNodeTypes
+}
+
+export type CoreHelper = keyof typeof import('packages/runtime-dom/src')
+
+export type VaporHelper = keyof typeof import('packages/runtime-vapor/src')
+
+export interface BlockIRNode extends BaseIRNode {
+  type: IRNodeTypes.BLOCK
+  node: RootNode | TemplateChildNode
+  dynamic: IRDynamicInfo
+  tempId: number
+  effect: IREffect[]
+  operation: OperationNode[]
+  returns: number[]
+}
+
+export interface RootIRNode {
+  type: IRNodeTypes.ROOT
+  node: RootNode
+  source: string
+  template: string[]
+  rootTemplateIndex?: number
+  component: Set<string>
+  directive: Set<string>
+  block: BlockIRNode
+  hasTemplateRef: boolean
+}
+
+export interface IfIRNode extends BaseIRNode {
+  type: IRNodeTypes.IF
+  id: number
+  condition: SimpleExpressionNode
+  positive: BlockIRNode
+  negative?: BlockIRNode | IfIRNode
+  once?: boolean
+  parent?: number
+  anchor?: number
+}
+
+export interface IRFor {
+  source: SimpleExpressionNode
+  value?: SimpleExpressionNode
+  key?: SimpleExpressionNode
+  index?: SimpleExpressionNode
+}
+
+export interface ForIRNode extends BaseIRNode, IRFor {
+  type: IRNodeTypes.FOR
+  id: number
+  keyProp?: SimpleExpressionNode
+  render: BlockIRNode
+  once: boolean
+  component: boolean
+  onlyChild: boolean
+  parent?: number
+  anchor?: number
+}
+
+export interface SetPropIRNode extends BaseIRNode {
+  type: IRNodeTypes.SET_PROP
+  element: number
+  prop: IRProp
+  root: boolean
+  tag: string
+}
+
+export interface SetDynamicPropsIRNode extends BaseIRNode {
+  type: IRNodeTypes.SET_DYNAMIC_PROPS
+  element: number
+  props: IRProps[]
+  root: boolean
+}
+
+export interface SetDynamicEventsIRNode extends BaseIRNode {
+  type: IRNodeTypes.SET_DYNAMIC_EVENTS
+  element: number
+  event: SimpleExpressionNode
+}
+
+export interface SetTextIRNode extends BaseIRNode {
+  type: IRNodeTypes.SET_TEXT
+  element: number
+  values: SimpleExpressionNode[]
+  generated?: boolean // whether this is a generated empty text node by `processTextLikeContainer`
+  jsx?: boolean
+}
+
+export type KeyOverride = [find: string, replacement: string]
+export interface SetEventIRNode extends BaseIRNode {
+  type: IRNodeTypes.SET_EVENT
+  element: number
+  key: SimpleExpressionNode
+  value?: SimpleExpressionNode
+  modifiers: {
+    // modifiers for addEventListener() options, e.g. .passive & .capture
+    options: string[]
+    // modifiers that needs runtime guards, withKeys
+    keys: string[]
+    // modifiers that needs runtime guards, withModifiers
+    nonKeys: string[]
+  }
+  keyOverride?: KeyOverride
+  delegate: boolean
+  /** Whether it's in effect */
+  effect: boolean
+}
+
+export interface SetHtmlIRNode extends BaseIRNode {
+  type: IRNodeTypes.SET_HTML
+  element: number
+  value: SimpleExpressionNode
+}
+
+export interface SetTemplateRefIRNode extends BaseIRNode {
+  type: IRNodeTypes.SET_TEMPLATE_REF
+  element: number
+  value: SimpleExpressionNode
+  refFor: boolean
+  effect: boolean
+}
+
+export interface InsertNodeIRNode extends BaseIRNode {
+  type: IRNodeTypes.INSERT_NODE
+  elements: number[]
+  parent: number
+  anchor?: number
+}
+
+export interface PrependNodeIRNode extends BaseIRNode {
+  type: IRNodeTypes.PREPEND_NODE
+  elements: number[]
+  parent: number
+}
+
+export interface DirectiveIRNode extends BaseIRNode {
+  type: IRNodeTypes.DIRECTIVE
+  element: number
+  dir: VaporDirectiveNode
+  name: string
+  builtin?: boolean
+  asset?: boolean
+  modelType?: 'text' | 'dynamic' | 'radio' | 'checkbox' | 'select'
+}
+
+export interface CreateComponentIRNode extends BaseIRNode {
+  type: IRNodeTypes.CREATE_COMPONENT_NODE
+  id: number
+  tag: string
+  props: IRProps[]
+  slots: IRSlots[]
+  asset: boolean
+  root: boolean
+  once: boolean
+  dynamic?: SimpleExpressionNode
+  parent?: number
+  anchor?: number
+}
+
+export interface DeclareOldRefIRNode extends BaseIRNode {
+  type: IRNodeTypes.DECLARE_OLD_REF
+  id: number
+}
+
+export interface SlotOutletIRNode extends BaseIRNode {
+  type: IRNodeTypes.SLOT_OUTLET_NODE
+  id: number
+  name: SimpleExpressionNode
+  props: IRProps[]
+  fallback?: BlockIRNode
+  parent?: number
+  anchor?: number
+}
+
+export interface GetTextChildIRNode extends BaseIRNode {
+  type: IRNodeTypes.GET_TEXT_CHILD
+  parent: number
+}
+
+export type IRNode = OperationNode | RootIRNode
+export type OperationNode =
+  | SetPropIRNode
+  | SetDynamicPropsIRNode
+  | SetTextIRNode
+  | SetEventIRNode
+  | SetDynamicEventsIRNode
+  | SetHtmlIRNode
+  | SetTemplateRefIRNode
+  | InsertNodeIRNode
+  | PrependNodeIRNode
+  | DirectiveIRNode
+  | IfIRNode
+  | ForIRNode
+  | CreateComponentIRNode
+  | DeclareOldRefIRNode
+  | SlotOutletIRNode
+  | GetTextChildIRNode
+
+export enum DynamicFlag {
+  NONE = 0,
+  /**
+   * This node is referenced and needs to be saved as a variable.
+   */
+  REFERENCED = 1,
+  /**
+   * This node is not generated from template, but is generated dynamically.
+   */
+  NON_TEMPLATE = 1 << 1,
+  /**
+   * This node needs to be inserted back into the template.
+   */
+  INSERT = 1 << 2,
+}
+
+export interface IRDynamicInfo {
+  id?: number
+  flags: DynamicFlag
+  anchor?: number
+  children: IRDynamicInfo[]
+  template?: number
+  hasDynamicChild?: boolean
+  operation?: OperationNode
+}
+
+export interface IREffect {
+  expressions: SimpleExpressionNode[]
+  operations: OperationNode[]
+}
+
+type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> &
+  Pick<U, Extract<keyof U, keyof T>>
+
+export type HackOptions<T> = Prettify<
+  Overwrite<
+    T,
+    {
+      nodeTransforms?: NodeTransform[]
+      directiveTransforms?: Record<string, DirectiveTransform | undefined>
+    }
+  >
+>
+
+export type VaporDirectiveNode = Overwrite<
+  DirectiveNode,
+  {
+    exp: Exclude<DirectiveNode['exp'], CompoundExpressionNode>
+    arg: Exclude<DirectiveNode['arg'], CompoundExpressionNode>
+  }
+>
+
+export type InsertionStateTypes =
+  | IfIRNode
+  | ForIRNode
+  | SlotOutletIRNode
+  | CreateComponentIRNode
+
+export function isBlockOperation(op: OperationNode): op is InsertionStateTypes {
+  const type = op.type
+  return (
+    type === IRNodeTypes.CREATE_COMPONENT_NODE ||
+    type === IRNodeTypes.SLOT_OUTLET_NODE ||
+    type === IRNodeTypes.IF ||
+    type === IRNodeTypes.FOR
+  )
+}
diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts
new file mode 100644
index 00000000000..946c89b734a
--- /dev/null
+++ b/packages/compiler-vapor/src/transform.ts
@@ -0,0 +1,298 @@
+import {
+  type AllNode,
+  type TransformOptions as BaseTransformOptions,
+  type CommentNode,
+  type CompilerCompatOptions,
+  type ElementNode,
+  ElementTypes,
+  NodeTypes,
+  type RootNode,
+  type SimpleExpressionNode,
+  type TemplateChildNode,
+  defaultOnError,
+  defaultOnWarn,
+  getSelfName,
+  isVSlot,
+} from '@vue/compiler-dom'
+import { EMPTY_OBJ, NOOP, extend, isArray, isString } from '@vue/shared'
+import {
+  type BlockIRNode,
+  DynamicFlag,
+  type HackOptions,
+  type IRDynamicInfo,
+  IRNodeTypes,
+  type IRSlots,
+  type OperationNode,
+  type RootIRNode,
+  type VaporDirectiveNode,
+} from './ir'
+import { isConstantExpression, isStaticExpression } from './utils'
+import { newBlock, newDynamic } from './transforms/utils'
+
+export type NodeTransform = (
+  node: RootNode | TemplateChildNode,
+  context: TransformContext<RootNode | TemplateChildNode>,
+) => void | (() => void) | (() => void)[]
+
+export type DirectiveTransform = (
+  dir: VaporDirectiveNode,
+  node: ElementNode,
+  context: TransformContext<ElementNode>,
+) => DirectiveTransformResult | void
+
+export interface DirectiveTransformResult {
+  key: SimpleExpressionNode
+  value: SimpleExpressionNode
+  modifier?: '.' | '^'
+  runtimeCamelize?: boolean
+  handler?: boolean
+  handlerModifiers?: string[]
+  model?: boolean
+  modelModifiers?: string[]
+}
+
+// A structural directive transform is technically also a NodeTransform;
+// Only v-if and v-for fall into this category.
+export type StructuralDirectiveTransform = (
+  node: ElementNode,
+  dir: VaporDirectiveNode,
+  context: TransformContext<ElementNode>,
+) => void | (() => void)
+
+export type TransformOptions = HackOptions<BaseTransformOptions>
+
+export class TransformContext<T extends AllNode = AllNode> {
+  selfName: string | null = null
+  parent: TransformContext<RootNode | ElementNode> | null = null
+  root: TransformContext<RootNode>
+  index: number = 0
+
+  block: BlockIRNode = this.ir.block
+  options: Required<
+    Omit<TransformOptions, 'filename' | keyof CompilerCompatOptions>
+  >
+
+  template: string = ''
+  childrenTemplate: (string | null)[] = []
+  dynamic: IRDynamicInfo = this.ir.block.dynamic
+
+  inVOnce: boolean = false
+  inVFor: number = 0
+
+  comment: CommentNode[] = []
+  component: Set<string> = this.ir.component
+  directive: Set<string> = this.ir.directive
+
+  slots: IRSlots[] = []
+
+  private globalId = 0
+
+  constructor(
+    public ir: RootIRNode,
+    public node: T,
+    options: TransformOptions = {},
+  ) {
+    this.options = extend({}, defaultOptions, options)
+    this.root = this as TransformContext<RootNode>
+    if (options.filename) this.selfName = getSelfName(options.filename)
+  }
+
+  enterBlock(ir: BlockIRNode, isVFor: boolean = false): () => void {
+    const { block, template, dynamic, childrenTemplate, slots } = this
+    this.block = ir
+    this.dynamic = ir.dynamic
+    this.template = ''
+    this.childrenTemplate = []
+    this.slots = []
+    isVFor && this.inVFor++
+    return () => {
+      // exit
+      this.registerTemplate()
+      this.block = block
+      this.template = template
+      this.dynamic = dynamic
+      this.childrenTemplate = childrenTemplate
+      this.slots = slots
+      isVFor && this.inVFor--
+    }
+  }
+
+  increaseId = (): number => this.globalId++
+  reference(): number {
+    if (this.dynamic.id !== undefined) return this.dynamic.id
+    this.dynamic.flags |= DynamicFlag.REFERENCED
+    return (this.dynamic.id = this.increaseId())
+  }
+
+  pushTemplate(content: string): number {
+    const existing = this.ir.template.findIndex(
+      template => template === content,
+    )
+    if (existing !== -1) return existing
+    this.ir.template.push(content)
+    return this.ir.template.length - 1
+  }
+  registerTemplate(): number {
+    if (!this.template) return -1
+    const id = this.pushTemplate(this.template)
+    return (this.dynamic.template = id)
+  }
+
+  registerEffect(
+    expressions: SimpleExpressionNode[],
+    operation: OperationNode | OperationNode[],
+    getIndex = (): number => this.block.effect.length,
+  ): void {
+    const operations = [operation].flat()
+    expressions = expressions.filter(exp => !isConstantExpression(exp))
+    if (
+      this.inVOnce ||
+      expressions.length === 0 ||
+      expressions.every(e =>
+        isStaticExpression(e, this.root.options.bindingMetadata),
+      )
+    ) {
+      return this.registerOperation(...operations)
+    }
+
+    this.block.effect.splice(getIndex(), 0, {
+      expressions,
+      operations,
+    })
+  }
+
+  registerOperation(...node: OperationNode[]): void {
+    this.block.operation.push(...node)
+  }
+
+  create<T extends TemplateChildNode>(
+    node: T,
+    index: number,
+  ): TransformContext<T> {
+    return Object.assign(Object.create(TransformContext.prototype), this, {
+      node,
+      parent: this as any,
+      index,
+
+      template: '',
+      childrenTemplate: [],
+      dynamic: newDynamic(),
+    } satisfies Partial<TransformContext<T>>)
+  }
+}
+
+const defaultOptions = {
+  filename: '',
+  prefixIdentifiers: true,
+  hoistStatic: false,
+  hmr: false,
+  cacheHandlers: false,
+  nodeTransforms: [],
+  directiveTransforms: {},
+  transformHoist: null,
+  isBuiltInComponent: NOOP,
+  isCustomElement: NOOP,
+  expressionPlugins: [],
+  scopeId: null,
+  slotted: true,
+  ssr: false,
+  inSSR: false,
+  ssrCssVars: ``,
+  bindingMetadata: EMPTY_OBJ,
+  inline: false,
+  isTS: false,
+  onError: defaultOnError,
+  onWarn: defaultOnWarn,
+}
+
+// AST -> IR
+export function transform(
+  node: RootNode,
+  options: TransformOptions = {},
+): RootIRNode {
+  const ir: RootIRNode = {
+    type: IRNodeTypes.ROOT,
+    node,
+    source: node.source,
+    template: [],
+    component: new Set(),
+    directive: new Set(),
+    block: newBlock(node),
+    hasTemplateRef: false,
+  }
+
+  const context = new TransformContext(ir, node, options)
+
+  transformNode(context)
+
+  return ir
+}
+
+export function transformNode(
+  context: TransformContext<RootNode | TemplateChildNode>,
+): void {
+  let { node } = context
+
+  // apply transform plugins
+  const { nodeTransforms } = context.options
+  const exitFns = []
+  for (const nodeTransform of nodeTransforms) {
+    const onExit = nodeTransform(node, context)
+    if (onExit) {
+      if (isArray(onExit)) {
+        exitFns.push(...onExit)
+      } else {
+        exitFns.push(onExit)
+      }
+    }
+    if (!context.node) {
+      // node was removed
+      return
+    } else {
+      // node may have been replaced
+      node = context.node
+    }
+  }
+
+  // exit transforms
+  context.node = node
+  let i = exitFns.length
+  while (i--) {
+    exitFns[i]()
+  }
+
+  if (context.node.type === NodeTypes.ROOT) {
+    context.registerTemplate()
+  }
+}
+
+export function createStructuralDirectiveTransform(
+  name: string | string[],
+  fn: StructuralDirectiveTransform,
+): NodeTransform {
+  const matches = (n: string) =>
+    isString(name) ? n === name : name.includes(n)
+
+  return (node, context) => {
+    if (node.type === NodeTypes.ELEMENT) {
+      const { props } = node
+      // structural directive transforms are not concerned with slots
+      // as they are handled separately in vSlot.ts
+      if (node.tagType === ElementTypes.TEMPLATE && props.some(isVSlot)) {
+        return
+      }
+      const exitFns = []
+      for (const prop of props) {
+        if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
+          const onExit = fn(
+            node,
+            prop as VaporDirectiveNode,
+            context as TransformContext<ElementNode>,
+          )
+          if (onExit) exitFns.push(onExit)
+        }
+      }
+      return exitFns
+    }
+  }
+}
diff --git a/packages/compiler-vapor/src/transforms/transformChildren.ts b/packages/compiler-vapor/src/transforms/transformChildren.ts
new file mode 100644
index 00000000000..790cd9d6fb1
--- /dev/null
+++ b/packages/compiler-vapor/src/transforms/transformChildren.ts
@@ -0,0 +1,111 @@
+import { type ElementNode, ElementTypes, NodeTypes } from '@vue/compiler-dom'
+import {
+  type NodeTransform,
+  type TransformContext,
+  transformNode,
+} from '../transform'
+import {
+  DynamicFlag,
+  type IRDynamicInfo,
+  IRNodeTypes,
+  isBlockOperation,
+} from '../ir'
+
+export const transformChildren: NodeTransform = (node, context) => {
+  const isFragment =
+    node.type === NodeTypes.ROOT ||
+    (node.type === NodeTypes.ELEMENT &&
+      (node.tagType === ElementTypes.TEMPLATE ||
+        node.tagType === ElementTypes.COMPONENT))
+
+  if (!isFragment && node.type !== NodeTypes.ELEMENT) return
+
+  for (const [i, child] of node.children.entries()) {
+    const childContext = context.create(child, i)
+    transformNode(childContext)
+
+    const childDynamic = childContext.dynamic
+
+    if (isFragment) {
+      childContext.reference()
+      childContext.registerTemplate()
+
+      if (
+        !(childDynamic.flags & DynamicFlag.NON_TEMPLATE) ||
+        childDynamic.flags & DynamicFlag.INSERT
+      ) {
+        context.block.returns.push(childContext.dynamic.id!)
+      }
+    } else {
+      context.childrenTemplate.push(childContext.template)
+    }
+
+    if (
+      childDynamic.hasDynamicChild ||
+      childDynamic.id !== undefined ||
+      childDynamic.flags & DynamicFlag.NON_TEMPLATE ||
+      childDynamic.flags & DynamicFlag.INSERT
+    ) {
+      context.dynamic.hasDynamicChild = true
+    }
+
+    context.dynamic.children[i] = childDynamic
+  }
+
+  if (!isFragment) {
+    processDynamicChildren(context as TransformContext<ElementNode>)
+  }
+}
+
+function processDynamicChildren(context: TransformContext<ElementNode>) {
+  let prevDynamics: IRDynamicInfo[] = []
+  let hasStaticTemplate = false
+  const children = context.dynamic.children
+
+  for (const [index, child] of children.entries()) {
+    if (child.flags & DynamicFlag.INSERT) {
+      prevDynamics.push(child)
+    }
+
+    if (!(child.flags & DynamicFlag.NON_TEMPLATE)) {
+      if (prevDynamics.length) {
+        if (hasStaticTemplate) {
+          context.childrenTemplate[index - prevDynamics.length] = `<!>`
+          prevDynamics[0].flags -= DynamicFlag.NON_TEMPLATE
+          const anchor = (prevDynamics[0].anchor = context.increaseId())
+          registerInsertion(prevDynamics, context, anchor)
+        } else {
+          registerInsertion(prevDynamics, context, -1 /* prepend */)
+        }
+        prevDynamics = []
+      }
+      hasStaticTemplate = true
+    }
+  }
+
+  if (prevDynamics.length) {
+    registerInsertion(prevDynamics, context)
+  }
+}
+
+function registerInsertion(
+  dynamics: IRDynamicInfo[],
+  context: TransformContext,
+  anchor?: number,
+) {
+  for (const child of dynamics) {
+    if (child.template != null) {
+      // template node due to invalid nesting - generate actual insertion
+      context.registerOperation({
+        type: IRNodeTypes.INSERT_NODE,
+        elements: dynamics.map(child => child.id!),
+        parent: context.reference(),
+        anchor,
+      })
+    } else if (child.operation && isBlockOperation(child.operation)) {
+      // block types
+      child.operation.parent = context.reference()
+      child.operation.anchor = anchor
+    }
+  }
+}
diff --git a/packages/compiler-vapor/src/transforms/transformComment.ts b/packages/compiler-vapor/src/transforms/transformComment.ts
new file mode 100644
index 00000000000..23c4069551f
--- /dev/null
+++ b/packages/compiler-vapor/src/transforms/transformComment.ts
@@ -0,0 +1,56 @@
+import {
+  type CommentNode,
+  type ElementNode,
+  NodeTypes,
+  type TemplateChildNode,
+} from '@vue/compiler-dom'
+import type { NodeTransform, TransformContext } from '../transform'
+import { DynamicFlag } from '../ir'
+
+export const transformComment: NodeTransform = (node, context) => {
+  if (node.type !== NodeTypes.COMMENT) return
+
+  if (getSiblingIf(context as TransformContext<CommentNode>)) {
+    context.comment.push(node)
+    context.dynamic.flags |= DynamicFlag.NON_TEMPLATE
+  } else {
+    context.template += `<!--${node.content}-->`
+  }
+}
+
+export function getSiblingIf(
+  context: TransformContext<TemplateChildNode>,
+  reverse?: boolean,
+): ElementNode | undefined {
+  const parent = context.parent
+  if (!parent) return
+
+  const siblings = parent.node.children
+  let sibling: TemplateChildNode | undefined
+  let i = siblings.indexOf(context.node)
+  while (reverse ? --i >= 0 : ++i < siblings.length) {
+    sibling = siblings[i]
+    if (!isCommentLike(sibling)) {
+      break
+    }
+  }
+
+  if (
+    sibling &&
+    sibling.type === NodeTypes.ELEMENT &&
+    sibling.props.some(
+      ({ type, name }) =>
+        type === NodeTypes.DIRECTIVE &&
+        ['else-if', reverse ? 'if' : 'else'].includes(name),
+    )
+  ) {
+    return sibling
+  }
+}
+
+function isCommentLike(node: TemplateChildNode) {
+  return (
+    node.type === NodeTypes.COMMENT ||
+    (node.type === NodeTypes.TEXT && !node.content.trim().length)
+  )
+}
diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts
new file mode 100644
index 00000000000..05153e729af
--- /dev/null
+++ b/packages/compiler-vapor/src/transforms/transformElement.ts
@@ -0,0 +1,467 @@
+import { isValidHTMLNesting } from '@vue/compiler-dom'
+import {
+  type AttributeNode,
+  type ComponentNode,
+  type ElementNode,
+  ElementTypes,
+  ErrorCodes,
+  NodeTypes,
+  type PlainElementNode,
+  type SimpleExpressionNode,
+  createCompilerError,
+  createSimpleExpression,
+  isStaticArgOf,
+} from '@vue/compiler-dom'
+import {
+  camelize,
+  capitalize,
+  extend,
+  isBuiltInDirective,
+  isVoidTag,
+  makeMap,
+} from '@vue/shared'
+import type {
+  DirectiveTransformResult,
+  NodeTransform,
+  TransformContext,
+} from '../transform'
+import {
+  DynamicFlag,
+  IRDynamicPropsKind,
+  IRNodeTypes,
+  type IRProp,
+  type IRProps,
+  type IRPropsDynamicAttribute,
+  type IRPropsStatic,
+  type VaporDirectiveNode,
+} from '../ir'
+import { EMPTY_EXPRESSION } from './utils'
+import { findProp } from '../utils'
+
+export const isReservedProp: (key: string) => boolean = /*#__PURE__*/ makeMap(
+  // the leading comma is intentional so empty string "" is also included
+  ',key,ref,ref_for,ref_key,',
+)
+
+export const transformElement: NodeTransform = (node, context) => {
+  let effectIndex = context.block.effect.length
+  const getEffectIndex = () => effectIndex++
+  return function postTransformElement() {
+    ;({ node } = context)
+    if (
+      !(
+        node.type === NodeTypes.ELEMENT &&
+        (node.tagType === ElementTypes.ELEMENT ||
+          node.tagType === ElementTypes.COMPONENT)
+      )
+    )
+      return
+
+    const isComponent = node.tagType === ElementTypes.COMPONENT
+    const isDynamicComponent = isComponentTag(node.tag)
+    const propsResult = buildProps(
+      node,
+      context as TransformContext<ElementNode>,
+      isComponent,
+      isDynamicComponent,
+      getEffectIndex,
+    )
+
+    let { parent } = context
+    while (
+      parent &&
+      parent.parent &&
+      parent.node.type === NodeTypes.ELEMENT &&
+      parent.node.tagType === ElementTypes.TEMPLATE
+    ) {
+      parent = parent.parent
+    }
+    const singleRoot =
+      context.root === parent &&
+      parent.node.children.filter(child => child.type !== NodeTypes.COMMENT)
+        .length === 1
+
+    if (isComponent) {
+      transformComponentElement(
+        node as ComponentNode,
+        propsResult,
+        singleRoot,
+        context,
+        isDynamicComponent,
+      )
+    } else {
+      transformNativeElement(
+        node as PlainElementNode,
+        propsResult,
+        singleRoot,
+        context,
+        getEffectIndex,
+      )
+    }
+  }
+}
+
+function transformComponentElement(
+  node: ComponentNode,
+  propsResult: PropsResult,
+  singleRoot: boolean,
+  context: TransformContext,
+  isDynamicComponent: boolean,
+) {
+  const dynamicComponent = isDynamicComponent
+    ? resolveDynamicComponent(node)
+    : undefined
+
+  let { tag } = node
+  let asset = true
+
+  if (!dynamicComponent) {
+    const fromSetup = resolveSetupReference(tag, context)
+    if (fromSetup) {
+      tag = fromSetup
+      asset = false
+    }
+
+    const dotIndex = tag.indexOf('.')
+    if (dotIndex > 0) {
+      const ns = resolveSetupReference(tag.slice(0, dotIndex), context)
+      if (ns) {
+        tag = ns + tag.slice(dotIndex)
+        asset = false
+      }
+    }
+
+    if (asset) {
+      // self referencing component (inferred from filename)
+      if (context.selfName && capitalize(camelize(tag)) === context.selfName) {
+        // generators/block.ts has special check for __self postfix when generating
+        // component imports, which will pass additional `maybeSelfReference` flag
+        // to `resolveComponent`.
+        tag += `__self`
+      }
+      context.component.add(tag)
+    }
+  }
+
+  context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
+  context.dynamic.operation = {
+    type: IRNodeTypes.CREATE_COMPONENT_NODE,
+    id: context.reference(),
+    tag,
+    props: propsResult[0] ? propsResult[1] : [propsResult[1]],
+    asset,
+    root: singleRoot && !context.inVFor,
+    slots: [...context.slots],
+    once: context.inVOnce,
+    dynamic: dynamicComponent,
+  }
+  context.slots = []
+}
+
+function resolveDynamicComponent(node: ComponentNode) {
+  const isProp = findProp(node, 'is', false, true /* allow empty */)
+  if (!isProp) return
+
+  if (isProp.type === NodeTypes.ATTRIBUTE) {
+    return isProp.value && createSimpleExpression(isProp.value.content, true)
+  } else {
+    return (
+      isProp.exp ||
+      // #10469 handle :is shorthand
+      extend(createSimpleExpression(`is`, false, isProp.arg!.loc), {
+        ast: null,
+      })
+    )
+  }
+}
+
+function resolveSetupReference(name: string, context: TransformContext) {
+  const bindings = context.options.bindingMetadata
+  if (!bindings || bindings.__isScriptSetup === false) {
+    return
+  }
+
+  const camelName = camelize(name)
+  const PascalName = capitalize(camelName)
+  return bindings[name]
+    ? name
+    : bindings[camelName]
+      ? camelName
+      : bindings[PascalName]
+        ? PascalName
+        : undefined
+}
+
+function transformNativeElement(
+  node: PlainElementNode,
+  propsResult: PropsResult,
+  singleRoot: boolean,
+  context: TransformContext,
+  getEffectIndex: () => number,
+) {
+  const { tag } = node
+  const { scopeId } = context.options
+
+  let template = ''
+
+  template += `<${tag}`
+  if (scopeId) template += ` ${scopeId}`
+
+  const dynamicProps: string[] = []
+  if (propsResult[0] /* dynamic props */) {
+    const [, dynamicArgs, expressions] = propsResult
+    context.registerEffect(
+      expressions,
+      {
+        type: IRNodeTypes.SET_DYNAMIC_PROPS,
+        element: context.reference(),
+        props: dynamicArgs,
+        root: singleRoot,
+      },
+      getEffectIndex,
+    )
+  } else {
+    for (const prop of propsResult[1]) {
+      const { key, values } = prop
+      if (key.isStatic && values.length === 1 && values[0].isStatic) {
+        template += ` ${key.content}`
+        if (values[0].content) template += `="${values[0].content}"`
+      } else {
+        dynamicProps.push(key.content)
+        context.registerEffect(
+          values,
+          {
+            type: IRNodeTypes.SET_PROP,
+            element: context.reference(),
+            prop,
+            root: singleRoot,
+            tag,
+          },
+          getEffectIndex,
+        )
+      }
+    }
+  }
+
+  template += `>` + context.childrenTemplate.join('')
+  // TODO remove unnecessary close tag, e.g. if it's the last element of the template
+  if (!isVoidTag(tag)) {
+    template += `</${tag}>`
+  }
+
+  if (singleRoot) {
+    context.ir.rootTemplateIndex = context.ir.template.length
+  }
+
+  if (
+    context.parent &&
+    context.parent.node.type === NodeTypes.ELEMENT &&
+    !isValidHTMLNesting(context.parent.node.tag, tag)
+  ) {
+    context.reference()
+    context.dynamic.template = context.pushTemplate(template)
+    context.dynamic.flags |= DynamicFlag.INSERT | DynamicFlag.NON_TEMPLATE
+  } else {
+    context.template += template
+  }
+}
+
+export type PropsResult =
+  | [dynamic: true, props: IRProps[], expressions: SimpleExpressionNode[]]
+  | [dynamic: false, props: IRPropsStatic]
+
+export function buildProps(
+  node: ElementNode,
+  context: TransformContext<ElementNode>,
+  isComponent: boolean,
+  isDynamicComponent?: boolean,
+  getEffectIndex?: () => number,
+): PropsResult {
+  const props = node.props as (VaporDirectiveNode | AttributeNode)[]
+  if (props.length === 0) return [false, []]
+
+  const dynamicArgs: IRProps[] = []
+  const dynamicExpr: SimpleExpressionNode[] = []
+  let results: DirectiveTransformResult[] = []
+
+  function pushMergeArg() {
+    if (results.length) {
+      dynamicArgs.push(dedupeProperties(results))
+      results = []
+    }
+  }
+
+  for (const prop of props) {
+    if (prop.type === NodeTypes.DIRECTIVE && !prop.arg) {
+      if (prop.name === 'bind') {
+        // v-bind="obj"
+        if (prop.exp) {
+          dynamicExpr.push(prop.exp)
+          pushMergeArg()
+          dynamicArgs.push({
+            kind: IRDynamicPropsKind.EXPRESSION,
+            value: prop.exp,
+          })
+        } else {
+          context.options.onError(
+            createCompilerError(ErrorCodes.X_V_BIND_NO_EXPRESSION, prop.loc),
+          )
+        }
+        continue
+      } else if (prop.name === 'on') {
+        // v-on="obj"
+        if (prop.exp) {
+          if (isComponent) {
+            dynamicExpr.push(prop.exp)
+            pushMergeArg()
+            dynamicArgs.push({
+              kind: IRDynamicPropsKind.EXPRESSION,
+              value: prop.exp,
+              handler: true,
+            })
+          } else {
+            context.registerEffect(
+              [prop.exp],
+              {
+                type: IRNodeTypes.SET_DYNAMIC_EVENTS,
+                element: context.reference(),
+                event: prop.exp,
+              },
+              getEffectIndex,
+            )
+          }
+        } else {
+          context.options.onError(
+            createCompilerError(ErrorCodes.X_V_ON_NO_EXPRESSION, prop.loc),
+          )
+        }
+        continue
+      }
+    }
+
+    // exclude `is` prop for <component>
+    if (
+      (isDynamicComponent &&
+        prop.type === NodeTypes.ATTRIBUTE &&
+        prop.name === 'is') ||
+      (prop.type === NodeTypes.DIRECTIVE &&
+        prop.name === 'bind' &&
+        isStaticArgOf(prop.arg, 'is'))
+    ) {
+      continue
+    }
+
+    const result = transformProp(prop, node, context)
+    if (result) {
+      dynamicExpr.push(result.key, result.value)
+      if (isComponent && !result.key.isStatic) {
+        // v-bind:[name]="value" or v-on:[name]="value"
+        pushMergeArg()
+        dynamicArgs.push(
+          extend(resolveDirectiveResult(result), {
+            kind: IRDynamicPropsKind.ATTRIBUTE,
+          }) as IRPropsDynamicAttribute,
+        )
+      } else {
+        // other static props
+        results.push(result)
+      }
+    }
+  }
+
+  // has dynamic key or v-bind="{}"
+  if (dynamicArgs.length || results.some(({ key }) => !key.isStatic)) {
+    // take rest of props as dynamic props
+    pushMergeArg()
+    return [true, dynamicArgs, dynamicExpr]
+  }
+
+  const irProps = dedupeProperties(results)
+  return [false, irProps]
+}
+
+function transformProp(
+  prop: VaporDirectiveNode | AttributeNode,
+  node: ElementNode,
+  context: TransformContext<ElementNode>,
+): DirectiveTransformResult | void {
+  let { name } = prop
+
+  if (prop.type === NodeTypes.ATTRIBUTE) {
+    if (isReservedProp(name)) return
+    return {
+      key: createSimpleExpression(prop.name, true, prop.nameLoc),
+      value: prop.value
+        ? createSimpleExpression(prop.value.content, true, prop.value.loc)
+        : EMPTY_EXPRESSION,
+    }
+  }
+
+  const directiveTransform = context.options.directiveTransforms[name]
+  if (directiveTransform) {
+    return directiveTransform(prop, node, context)
+  }
+
+  if (!isBuiltInDirective(name)) {
+    const fromSetup = resolveSetupReference(`v-${name}`, context)
+    if (fromSetup) {
+      name = fromSetup
+    } else {
+      context.directive.add(name)
+    }
+
+    context.registerOperation({
+      type: IRNodeTypes.DIRECTIVE,
+      element: context.reference(),
+      dir: prop,
+      name,
+      asset: !fromSetup,
+    })
+  }
+}
+
+// Dedupe props in an object literal.
+// Literal duplicated attributes would have been warned during the parse phase,
+// however, it's possible to encounter duplicated `onXXX` handlers with different
+// modifiers. We also need to merge static and dynamic class / style attributes.
+function dedupeProperties(results: DirectiveTransformResult[]): IRProp[] {
+  const knownProps: Map<string, IRProp> = new Map()
+  const deduped: IRProp[] = []
+
+  for (const result of results) {
+    const prop = resolveDirectiveResult(result)
+    // dynamic keys are always allowed
+    if (!prop.key.isStatic) {
+      deduped.push(prop)
+      continue
+    }
+    const name = prop.key.content
+    const existing = knownProps.get(name)
+    if (existing) {
+      if (name === 'style' || name === 'class') {
+        mergePropValues(existing, prop)
+      }
+      // unexpected duplicate, should have emitted error during parse
+    } else {
+      knownProps.set(name, prop)
+      deduped.push(prop)
+    }
+  }
+  return deduped
+}
+
+function resolveDirectiveResult(prop: DirectiveTransformResult): IRProp {
+  return extend({}, prop, {
+    value: undefined,
+    values: [prop.value],
+  })
+}
+
+function mergePropValues(existing: IRProp, incoming: IRProp) {
+  const newValues = incoming.values
+  existing.values.push(...newValues)
+}
+
+function isComponentTag(tag: string) {
+  return tag === 'component' || tag === 'Component'
+}
diff --git a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts
new file mode 100644
index 00000000000..e76fcdde65a
--- /dev/null
+++ b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts
@@ -0,0 +1,133 @@
+import {
+  type AttributeNode,
+  type ElementNode,
+  ElementTypes,
+  ErrorCodes,
+  NodeTypes,
+  type SimpleExpressionNode,
+  createCompilerError,
+  createSimpleExpression,
+  isStaticArgOf,
+  isStaticExp,
+} from '@vue/compiler-dom'
+import type { NodeTransform, TransformContext } from '../transform'
+import {
+  type BlockIRNode,
+  type DirectiveIRNode,
+  DynamicFlag,
+  IRNodeTypes,
+  type IRProps,
+  type VaporDirectiveNode,
+} from '../ir'
+import { camelize, extend } from '@vue/shared'
+import { newBlock } from './utils'
+import { buildProps } from './transformElement'
+
+export const transformSlotOutlet: NodeTransform = (node, context) => {
+  if (node.type !== NodeTypes.ELEMENT || node.tag !== 'slot') {
+    return
+  }
+  const id = context.reference()
+  context.dynamic.flags |= DynamicFlag.INSERT | DynamicFlag.NON_TEMPLATE
+  const [fallback, exitBlock] = createFallback(
+    node,
+    context as TransformContext<ElementNode>,
+  )
+
+  let slotName: SimpleExpressionNode | undefined
+  const slotProps: (AttributeNode | VaporDirectiveNode)[] = []
+  for (const prop of node.props as (AttributeNode | VaporDirectiveNode)[]) {
+    if (prop.type === NodeTypes.ATTRIBUTE) {
+      if (prop.value) {
+        if (prop.name === 'name') {
+          slotName = createSimpleExpression(prop.value.content, true, prop.loc)
+        } else {
+          slotProps.push(extend({}, prop, { name: camelize(prop.name) }))
+        }
+      }
+    } else if (prop.name === 'bind' && isStaticArgOf(prop.arg, 'name')) {
+      if (prop.exp) {
+        slotName = prop.exp!
+      } else {
+        // v-bind shorthand syntax
+        slotName = createSimpleExpression(
+          camelize(prop.arg!.content),
+          false,
+          prop.arg!.loc,
+        )
+        slotName.ast = null
+      }
+    } else {
+      let slotProp = prop
+      if (
+        slotProp.name === 'bind' &&
+        slotProp.arg &&
+        isStaticExp(slotProp.arg)
+      ) {
+        slotProp = extend({}, prop, {
+          arg: extend({}, slotProp.arg, {
+            content: camelize(slotProp.arg!.content),
+          }),
+        })
+      }
+      slotProps.push(slotProp)
+    }
+  }
+
+  slotName ||= createSimpleExpression('default', true)
+  let irProps: IRProps[] = []
+  if (slotProps.length) {
+    const [isDynamic, props] = buildProps(
+      extend({}, node, { props: slotProps }),
+      context as TransformContext<ElementNode>,
+      true,
+    )
+    irProps = isDynamic ? props : [props]
+
+    const runtimeDirective = context.block.operation.find(
+      (oper): oper is DirectiveIRNode =>
+        oper.type === IRNodeTypes.DIRECTIVE && oper.element === id,
+    )
+    if (runtimeDirective) {
+      context.options.onError(
+        createCompilerError(
+          ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
+          runtimeDirective.dir.loc,
+        ),
+      )
+    }
+  }
+
+  return () => {
+    exitBlock && exitBlock()
+    context.dynamic.operation = {
+      type: IRNodeTypes.SLOT_OUTLET_NODE,
+      id,
+      name: slotName,
+      props: irProps,
+      fallback,
+    }
+  }
+}
+
+function createFallback(
+  node: ElementNode,
+  context: TransformContext<ElementNode>,
+): [block?: BlockIRNode, exit?: () => void] {
+  if (!node.children.length) {
+    return []
+  }
+
+  context.node = node = extend({}, node, {
+    type: NodeTypes.ELEMENT,
+    tag: 'template',
+    props: [],
+    tagType: ElementTypes.TEMPLATE,
+    children: node.children,
+  })
+
+  const fallback = newBlock(node)
+  const exitBlock = context.enterBlock(fallback)
+  context.reference()
+  return [fallback, exitBlock]
+}
diff --git a/packages/compiler-vapor/src/transforms/transformTemplateRef.ts b/packages/compiler-vapor/src/transforms/transformTemplateRef.ts
new file mode 100644
index 00000000000..9fa7c09bf58
--- /dev/null
+++ b/packages/compiler-vapor/src/transforms/transformTemplateRef.ts
@@ -0,0 +1,45 @@
+import {
+  NodeTypes,
+  type SimpleExpressionNode,
+  createSimpleExpression,
+} from '@vue/compiler-dom'
+import type { NodeTransform } from '../transform'
+import { IRNodeTypes } from '../ir'
+import { normalizeBindShorthand } from './vBind'
+import { findProp, isConstantExpression } from '../utils'
+import { EMPTY_EXPRESSION } from './utils'
+
+export const transformTemplateRef: NodeTransform = (node, context) => {
+  if (node.type !== NodeTypes.ELEMENT) return
+
+  const dir = findProp(node, 'ref', false, true)
+  if (!dir) return
+
+  context.ir.hasTemplateRef = true
+
+  let value: SimpleExpressionNode
+  if (dir.type === NodeTypes.DIRECTIVE) {
+    value = dir.exp || normalizeBindShorthand(dir.arg!, context)
+  } else {
+    value = dir.value
+      ? createSimpleExpression(dir.value.content, true, dir.value.loc)
+      : EMPTY_EXPRESSION
+  }
+
+  return () => {
+    const id = context.reference()
+    const effect = !isConstantExpression(value)
+    effect &&
+      context.registerOperation({
+        type: IRNodeTypes.DECLARE_OLD_REF,
+        id,
+      })
+    context.registerEffect([value], {
+      type: IRNodeTypes.SET_TEMPLATE_REF,
+      element: id,
+      value,
+      refFor: !!context.inVFor,
+      effect,
+    })
+  }
+}
diff --git a/packages/compiler-vapor/src/transforms/transformText.ts b/packages/compiler-vapor/src/transforms/transformText.ts
new file mode 100644
index 00000000000..e9c273b85c7
--- /dev/null
+++ b/packages/compiler-vapor/src/transforms/transformText.ts
@@ -0,0 +1,183 @@
+import {
+  type ElementNode,
+  ElementTypes,
+  type InterpolationNode,
+  NodeTypes,
+  type RootNode,
+  type SimpleExpressionNode,
+  type TemplateChildNode,
+  type TextNode,
+  createSimpleExpression,
+} from '@vue/compiler-dom'
+import type { NodeTransform, TransformContext } from '../transform'
+import { DynamicFlag, IRNodeTypes } from '../ir'
+import {
+  getLiteralExpressionValue,
+  isConstantExpression,
+  isStaticExpression,
+} from '../utils'
+
+type TextLike = TextNode | InterpolationNode
+const seen = new WeakMap<
+  TransformContext<RootNode>,
+  WeakSet<TemplateChildNode | RootNode>
+>()
+
+export function markNonTemplate(
+  node: TemplateChildNode,
+  context: TransformContext,
+): void {
+  seen.get(context.root)!.add(node)
+}
+
+export const transformText: NodeTransform = (node, context) => {
+  if (!seen.has(context.root)) seen.set(context.root, new WeakSet())
+  if (seen.get(context.root)!.has(node)) {
+    context.dynamic.flags |= DynamicFlag.NON_TEMPLATE
+    return
+  }
+
+  const isFragment =
+    node.type === NodeTypes.ROOT ||
+    (node.type === NodeTypes.ELEMENT &&
+      (node.tagType === ElementTypes.TEMPLATE ||
+        node.tagType === ElementTypes.COMPONENT))
+
+  if (
+    (isFragment ||
+      (node.type === NodeTypes.ELEMENT &&
+        node.tagType === ElementTypes.ELEMENT)) &&
+    node.children.length
+  ) {
+    let hasInterp = false
+    let isAllTextLike = true
+    for (const c of node.children) {
+      if (c.type === NodeTypes.INTERPOLATION) {
+        hasInterp = true
+      } else if (c.type !== NodeTypes.TEXT) {
+        isAllTextLike = false
+      }
+    }
+    // all text like with interpolation
+    if (!isFragment && isAllTextLike && hasInterp) {
+      processTextContainer(
+        node.children as TextLike[],
+        context as TransformContext<ElementNode>,
+      )
+    } else if (hasInterp) {
+      // check if there's any text before interpolation, it needs to be merged
+      for (let i = 0; i < node.children.length; i++) {
+        const c = node.children[i]
+        const prev = node.children[i - 1]
+        if (
+          c.type === NodeTypes.INTERPOLATION &&
+          prev &&
+          prev.type === NodeTypes.TEXT
+        ) {
+          // mark leading text node for skipping
+          markNonTemplate(prev, context)
+        }
+      }
+    }
+  } else if (node.type === NodeTypes.INTERPOLATION) {
+    processInterpolation(context as TransformContext<InterpolationNode>)
+  } else if (node.type === NodeTypes.TEXT) {
+    context.template += node.content
+  }
+}
+
+function processInterpolation(context: TransformContext<InterpolationNode>) {
+  const parentNode = context.parent!.node
+  const children = parentNode.children
+  const nexts = children.slice(context.index)
+  const idx = nexts.findIndex(n => !isTextLike(n))
+  const nodes = (idx > -1 ? nexts.slice(0, idx) : nexts) as Array<TextLike>
+
+  // merge leading text
+  const prev = children[context.index - 1]
+  if (prev && prev.type === NodeTypes.TEXT) {
+    nodes.unshift(prev)
+  }
+  const values = processTextLikeChildren(nodes, context)
+
+  if (values.length === 0 && parentNode.type !== NodeTypes.ROOT) {
+    return
+  }
+
+  context.template += ' '
+  const id = context.reference()
+
+  if (values.length === 0) {
+    return
+  }
+
+  const nonConstantExps = values.filter(v => !isConstantExpression(v))
+  const isStatic =
+    !nonConstantExps.length ||
+    nonConstantExps.every(e =>
+      isStaticExpression(e, context.options.bindingMetadata),
+    ) ||
+    context.inVOnce
+
+  if (isStatic) {
+    context.registerOperation({
+      type: IRNodeTypes.SET_TEXT,
+      element: id,
+      values,
+    })
+  } else {
+    context.registerEffect(values, {
+      type: IRNodeTypes.SET_TEXT,
+      element: id,
+      values,
+    })
+  }
+}
+
+function processTextContainer(
+  children: TextLike[],
+  context: TransformContext<ElementNode>,
+) {
+  const values = processTextLikeChildren(children, context)
+
+  const literals = values.map(getLiteralExpressionValue)
+
+  if (literals.every(l => l != null)) {
+    context.childrenTemplate = literals.map(l => String(l))
+  } else {
+    context.childrenTemplate = [' ']
+    context.registerOperation({
+      type: IRNodeTypes.GET_TEXT_CHILD,
+      parent: context.reference(),
+    })
+    context.registerEffect(values, {
+      type: IRNodeTypes.SET_TEXT,
+      element: context.reference(),
+      values,
+      // indicates this node is generated, so prefix should be "x" instead of "n"
+      generated: true,
+    })
+  }
+}
+
+function processTextLikeChildren(nodes: TextLike[], context: TransformContext) {
+  const exps: SimpleExpressionNode[] = []
+  for (const node of nodes) {
+    let exp: SimpleExpressionNode
+    markNonTemplate(node, context)
+
+    if (node.type === NodeTypes.TEXT) {
+      exp = createSimpleExpression(node.content, true, node.loc)
+    } else {
+      exp = node.content as SimpleExpressionNode
+    }
+
+    if (exp.content) exps.push(exp)
+  }
+
+  return exps
+}
+
+function isTextLike(node: TemplateChildNode): node is TextLike {
+  return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT
+}
diff --git a/packages/compiler-vapor/src/transforms/utils.ts b/packages/compiler-vapor/src/transforms/utils.ts
new file mode 100644
index 00000000000..f7d0594fe58
--- /dev/null
+++ b/packages/compiler-vapor/src/transforms/utils.ts
@@ -0,0 +1,62 @@
+import {
+  type AttributeNode,
+  type DirectiveNode,
+  type ElementNode,
+  ElementTypes,
+  NodeTypes,
+  type SimpleExpressionNode,
+  type TemplateChildNode,
+  type TemplateNode,
+  createSimpleExpression,
+} from '@vue/compiler-dom'
+import { extend } from '@vue/shared'
+import {
+  type BlockIRNode,
+  DynamicFlag,
+  type IRDynamicInfo,
+  IRNodeTypes,
+} from '../ir'
+
+export const newDynamic = (): IRDynamicInfo => ({
+  flags: DynamicFlag.REFERENCED,
+  children: [],
+})
+
+export const newBlock = (node: BlockIRNode['node']): BlockIRNode => ({
+  type: IRNodeTypes.BLOCK,
+  node,
+  dynamic: newDynamic(),
+  effect: [],
+  operation: [],
+  returns: [],
+  tempId: 0,
+})
+
+export function wrapTemplate(node: ElementNode, dirs: string[]): TemplateNode {
+  if (node.tagType === ElementTypes.TEMPLATE) {
+    return node
+  }
+
+  const reserved: Array<AttributeNode | DirectiveNode> = []
+  const pass: Array<AttributeNode | DirectiveNode> = []
+  node.props.forEach(prop => {
+    if (prop.type === NodeTypes.DIRECTIVE && dirs.includes(prop.name)) {
+      reserved.push(prop)
+    } else {
+      pass.push(prop)
+    }
+  })
+
+  return extend({}, node, {
+    type: NodeTypes.ELEMENT,
+    tag: 'template',
+    props: reserved,
+    tagType: ElementTypes.TEMPLATE,
+    children: [extend({}, node, { props: pass } as TemplateChildNode)],
+  } as Partial<TemplateNode>)
+}
+
+export const EMPTY_EXPRESSION: SimpleExpressionNode = createSimpleExpression(
+  '',
+  true,
+)
diff --git a/packages/compiler-vapor/src/transforms/vBind.ts b/packages/compiler-vapor/src/transforms/vBind.ts
new file mode 100644
index 00000000000..dc56eb34b4e
--- /dev/null
+++ b/packages/compiler-vapor/src/transforms/vBind.ts
@@ -0,0 +1,74 @@
+import {
+  ErrorCodes,
+  NodeTypes,
+  type SimpleExpressionNode,
+  createCompilerError,
+  createSimpleExpression,
+} from '@vue/compiler-dom'
+import { camelize, extend } from '@vue/shared'
+import type { DirectiveTransform, TransformContext } from '../transform'
+import { resolveExpression } from '../utils'
+import { isReservedProp } from './transformElement'
+
+// same-name shorthand - :arg is expanded to :arg="arg"
+export function normalizeBindShorthand(
+  arg: SimpleExpressionNode,
+  context: TransformContext,
+): SimpleExpressionNode {
+  if (arg.type !== NodeTypes.SIMPLE_EXPRESSION || !arg.isStatic) {
+    // only simple expression is allowed for same-name shorthand
+    context.options.onError(
+      createCompilerError(
+        ErrorCodes.X_V_BIND_INVALID_SAME_NAME_ARGUMENT,
+        arg.loc,
+      ),
+    )
+    return createSimpleExpression('', true, arg.loc)
+  }
+
+  const propName = camelize(arg.content)
+  const exp = createSimpleExpression(propName, false, arg.loc)
+  exp.ast = null
+  return exp
+}
+
+export const transformVBind: DirectiveTransform = (dir, node, context) => {
+  const { loc, modifiers } = dir
+  let { exp } = dir
+  let arg = dir.arg!
+  const modifiersString = modifiers.map(s => s.content)
+
+  if (!exp) exp = normalizeBindShorthand(arg, context)
+  if (!exp.content.trim()) {
+    context.options.onError(
+      createCompilerError(ErrorCodes.X_V_BIND_NO_EXPRESSION, loc),
+    )
+    exp = createSimpleExpression('', true, loc)
+  }
+
+  exp = resolveExpression(exp)
+  arg = resolveExpression(arg)
+
+  if (arg.isStatic && isReservedProp(arg.content)) return
+
+  let camel = false
+  if (modifiersString.includes('camel')) {
+    if (arg.isStatic) {
+      arg = extend({}, arg, { content: camelize(arg.content) })
+    } else {
+      camel = true
+    }
+  }
+
+  return {
+    key: arg,
+    value: exp,
+    loc,
+    runtimeCamelize: camel,
+    modifier: modifiersString.includes('prop')
+      ? '.'
+      : modifiersString.includes('attr')
+        ? '^'
+        : undefined,
+  }
+}
diff --git a/packages/compiler-vapor/src/transforms/vFor.ts b/packages/compiler-vapor/src/transforms/vFor.ts
new file mode 100644
index 00000000000..d4cf42dbd55
--- /dev/null
+++ b/packages/compiler-vapor/src/transforms/vFor.ts
@@ -0,0 +1,89 @@
+import {
+  type ElementNode,
+  ElementTypes,
+  ErrorCodes,
+  type SimpleExpressionNode,
+  createCompilerError,
+} from '@vue/compiler-dom'
+import {
+  type NodeTransform,
+  type TransformContext,
+  createStructuralDirectiveTransform,
+} from '../transform'
+import {
+  type BlockIRNode,
+  DynamicFlag,
+  IRNodeTypes,
+  type VaporDirectiveNode,
+} from '../ir'
+import { findProp, isStaticExpression, propToExpression } from '../utils'
+import { newBlock, wrapTemplate } from './utils'
+
+export const transformVFor: NodeTransform = createStructuralDirectiveTransform(
+  'for',
+  processFor,
+)
+
+export function processFor(
+  node: ElementNode,
+  dir: VaporDirectiveNode,
+  context: TransformContext<ElementNode>,
+) {
+  if (!dir.exp) {
+    context.options.onError(
+      createCompilerError(ErrorCodes.X_V_FOR_NO_EXPRESSION, dir.loc),
+    )
+    return
+  }
+  const parseResult = dir.forParseResult
+  if (!parseResult) {
+    context.options.onError(
+      createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, dir.loc),
+    )
+    return
+  }
+
+  const { source, value, key, index } = parseResult
+
+  const keyProp = findProp(node, 'key')
+  const keyProperty = keyProp && propToExpression(keyProp)
+  const isComponent = node.tagType === ElementTypes.COMPONENT
+  context.node = node = wrapTemplate(node, ['for'])
+  context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
+  const id = context.reference()
+  const render: BlockIRNode = newBlock(node)
+  const exitBlock = context.enterBlock(render, true)
+  context.reference()
+
+  return (): void => {
+    exitBlock()
+
+    const { parent } = context
+
+    // if v-for is the only child of a parent element, it can go the fast path
+    // when the entire list is emptied
+    const isOnlyChild =
+      parent &&
+      parent.block.node !== parent.node &&
+      parent.node.children.length === 1
+
+    context.dynamic.operation = {
+      type: IRNodeTypes.FOR,
+      id,
+      source: source as SimpleExpressionNode,
+      value: value as SimpleExpressionNode | undefined,
+      key: key as SimpleExpressionNode | undefined,
+      index: index as SimpleExpressionNode | undefined,
+      keyProp: keyProperty,
+      render,
+      once:
+        context.inVOnce ||
+        isStaticExpression(
+          source as SimpleExpressionNode,
+          context.options.bindingMetadata,
+        ),
+      component: isComponent,
+      onlyChild: !!isOnlyChild,
+    }
+  }
+}
diff --git a/packages/compiler-vapor/src/transforms/vHtml.ts b/packages/compiler-vapor/src/transforms/vHtml.ts
new file mode 100644
index 00000000000..6b9a269e495
--- /dev/null
+++ b/packages/compiler-vapor/src/transforms/vHtml.ts
@@ -0,0 +1,26 @@
+import { IRNodeTypes } from '../ir'
+import type { DirectiveTransform } from '../transform'
+import { DOMErrorCodes, createDOMCompilerError } from '@vue/compiler-dom'
+import { EMPTY_EXPRESSION } from './utils'
+
+export const transformVHtml: DirectiveTransform = (dir, node, context) => {
+  let { exp, loc } = dir
+  if (!exp) {
+    context.options.onError(
+      createDOMCompilerError(DOMErrorCodes.X_V_HTML_NO_EXPRESSION, loc),
+    )
+    exp = EMPTY_EXPRESSION
+  }
+  if (node.children.length) {
+    context.options.onError(
+      createDOMCompilerError(DOMErrorCodes.X_V_HTML_WITH_CHILDREN, loc),
+    )
+    context.childrenTemplate.length = 0
+  }
+
+  context.registerEffect([exp], {
+    type: IRNodeTypes.SET_HTML,
+    element: context.reference(),
+    value: exp,
+  })
+}
diff --git a/packages/compiler-vapor/src/transforms/vIf.ts b/packages/compiler-vapor/src/transforms/vIf.ts
new file mode 100644
index 00000000000..bae9f1aa23f
--- /dev/null
+++ b/packages/compiler-vapor/src/transforms/vIf.ts
@@ -0,0 +1,139 @@
+import {
+  type ElementNode,
+  ErrorCodes,
+  createCompilerError,
+  createSimpleExpression,
+} from '@vue/compiler-dom'
+import {
+  type NodeTransform,
+  type TransformContext,
+  createStructuralDirectiveTransform,
+} from '../transform'
+import {
+  type BlockIRNode,
+  DynamicFlag,
+  IRNodeTypes,
+  type VaporDirectiveNode,
+} from '../ir'
+import { extend } from '@vue/shared'
+import { newBlock, wrapTemplate } from './utils'
+import { getSiblingIf } from './transformComment'
+import { isStaticExpression } from '../utils'
+
+export const transformVIf: NodeTransform = createStructuralDirectiveTransform(
+  ['if', 'else', 'else-if'],
+  processIf,
+)
+
+export function processIf(
+  node: ElementNode,
+  dir: VaporDirectiveNode,
+  context: TransformContext<ElementNode>,
+): (() => void) | undefined {
+  if (dir.name !== 'else' && (!dir.exp || !dir.exp.content.trim())) {
+    const loc = dir.exp ? dir.exp.loc : node.loc
+    context.options.onError(
+      createCompilerError(ErrorCodes.X_V_IF_NO_EXPRESSION, dir.loc),
+    )
+    dir.exp = createSimpleExpression(`true`, false, loc)
+  }
+
+  context.dynamic.flags |= DynamicFlag.NON_TEMPLATE
+  if (dir.name === 'if') {
+    const id = context.reference()
+    context.dynamic.flags |= DynamicFlag.INSERT
+    const [branch, onExit] = createIfBranch(node, context)
+
+    return () => {
+      onExit()
+      context.dynamic.operation = {
+        type: IRNodeTypes.IF,
+        id,
+        condition: dir.exp!,
+        positive: branch,
+        once:
+          context.inVOnce ||
+          isStaticExpression(dir.exp!, context.options.bindingMetadata),
+      }
+    }
+  } else {
+    // check the adjacent v-if
+    const siblingIf = getSiblingIf(context, true)
+
+    const siblings = context.parent && context.parent.dynamic.children
+    let lastIfNode
+    if (siblings) {
+      let i = siblings.length
+      while (i--) {
+        if (
+          siblings[i].operation &&
+          siblings[i].operation!.type === IRNodeTypes.IF
+        ) {
+          lastIfNode = siblings[i].operation
+          break
+        }
+      }
+    }
+
+    if (
+      // check if v-if is the sibling node
+      !siblingIf ||
+      // check if IfNode is the last operation and get the root IfNode
+      !lastIfNode ||
+      lastIfNode.type !== IRNodeTypes.IF
+    ) {
+      context.options.onError(
+        createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc),
+      )
+      return
+    }
+
+    while (lastIfNode.negative && lastIfNode.negative.type === IRNodeTypes.IF) {
+      lastIfNode = lastIfNode.negative
+    }
+
+    // Check if v-else was followed by v-else-if
+    if (dir.name === 'else-if' && lastIfNode.negative) {
+      context.options.onError(
+        createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc),
+      )
+    }
+
+    // TODO ignore comments if the v-if is direct child of <transition> (PR #3622)
+    if (__DEV__ && context.root.comment.length) {
+      node = wrapTemplate(node, ['else-if', 'else'])
+      context.node = node = extend({}, node, {
+        children: [...context.comment, ...node.children],
+      })
+    }
+    context.root.comment = []
+
+    const [branch, onExit] = createIfBranch(node, context)
+
+    if (dir.name === 'else') {
+      lastIfNode.negative = branch
+    } else {
+      lastIfNode.negative = {
+        type: IRNodeTypes.IF,
+        id: -1,
+        condition: dir.exp!,
+        positive: branch,
+        once: context.inVOnce,
+      }
+    }
+
+    return () => onExit()
+  }
+}
+
+export function createIfBranch(
+  node: ElementNode,
+  context: TransformContext<ElementNode>,
+): [BlockIRNode, () => void] {
+  context.node = node = wrapTemplate(node, ['if', 'else-if', 'else'])
+
+  const branch: BlockIRNode = newBlock(node)
+  const exitBlock = context.enterBlock(branch)
+  context.reference()
+  return [branch, exitBlock]
+}
diff --git a/packages/compiler-vapor/src/transforms/vModel.ts b/packages/compiler-vapor/src/transforms/vModel.ts
new file mode 100644
index 00000000000..e92f1bcebff
--- /dev/null
+++ b/packages/compiler-vapor/src/transforms/vModel.ts
@@ -0,0 +1,163 @@
+import {
+  BindingTypes,
+  DOMErrorCodes,
+  ElementTypes,
+  ErrorCodes,
+  NodeTypes,
+  createCompilerError,
+  createDOMCompilerError,
+  createSimpleExpression,
+  findDir,
+  findProp,
+  hasDynamicKeyVBind,
+  isMemberExpression,
+  isStaticArgOf,
+} from '@vue/compiler-dom'
+import type { DirectiveTransform } from '../transform'
+import { type DirectiveIRNode, IRNodeTypes } from '../ir'
+
+export const transformVModel: DirectiveTransform = (dir, node, context) => {
+  const { exp, arg } = dir
+  if (!exp) {
+    context.options.onError(
+      createCompilerError(ErrorCodes.X_V_MODEL_NO_EXPRESSION, dir.loc),
+    )
+    return
+  }
+
+  // we assume v-model directives are always parsed
+  // (not artificially created by a transform)
+  const rawExp = exp.loc.source
+
+  // in SFC <script setup> inline mode, the exp may have been transformed into
+  // _unref(exp)
+  const bindingType = context.options.bindingMetadata[rawExp]
+
+  // check props
+  if (
+    bindingType === BindingTypes.PROPS ||
+    bindingType === BindingTypes.PROPS_ALIASED
+  ) {
+    context.options.onError(
+      createCompilerError(ErrorCodes.X_V_MODEL_ON_PROPS, exp.loc),
+    )
+    return
+  }
+
+  const expString = exp.content
+  const maybeRef =
+    context.options.inline &&
+    (bindingType === BindingTypes.SETUP_LET ||
+      bindingType === BindingTypes.SETUP_REF ||
+      bindingType === BindingTypes.SETUP_MAYBE_REF)
+  if (
+    !expString.trim() ||
+    (!isMemberExpression(exp, context.options) && !maybeRef)
+  ) {
+    context.options.onError(
+      createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION, exp.loc),
+    )
+    return
+  }
+
+  const isComponent = node.tagType === ElementTypes.COMPONENT
+  if (isComponent) {
+    return {
+      key: arg ? arg : createSimpleExpression('modelValue', true),
+      value: exp,
+      model: true,
+      modelModifiers: dir.modifiers.map(m => m.content),
+    }
+  }
+
+  if (dir.arg)
+    context.options.onError(
+      createDOMCompilerError(
+        DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT,
+        dir.arg.loc,
+      ),
+    )
+  const { tag } = node
+  const isCustomElement = context.options.isCustomElement(tag)
+  let modelType: DirectiveIRNode['modelType'] | undefined = 'text'
+  // TODO let runtimeDirective: VaporHelper | undefined = 'vModelText'
+  if (
+    tag === 'input' ||
+    tag === 'textarea' ||
+    tag === 'select' ||
+    isCustomElement
+  ) {
+    if (tag === 'input' || isCustomElement) {
+      const type = findProp(node, 'type')
+      if (type) {
+        if (type.type === NodeTypes.DIRECTIVE) {
+          // :type="foo"
+          modelType = 'dynamic'
+        } else if (type.value) {
+          switch (type.value.content) {
+            case 'radio':
+              modelType = 'radio'
+              break
+            case 'checkbox':
+              modelType = 'checkbox'
+              break
+            case 'file':
+              modelType = undefined
+              context.options.onError(
+                createDOMCompilerError(
+                  DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT,
+                  dir.loc,
+                ),
+              )
+              break
+            default:
+              // text type
+              __DEV__ && checkDuplicatedValue()
+              break
+          }
+        }
+      } else if (hasDynamicKeyVBind(node)) {
+        // element has bindings with dynamic keys, which can possibly contain
+        // "type".
+        modelType = 'dynamic'
+      } else {
+        // text type
+        __DEV__ && checkDuplicatedValue()
+      }
+    } else if (tag === 'select') {
+      modelType = 'select'
+    } else {
+      // textarea
+      __DEV__ && checkDuplicatedValue()
+    }
+  } else {
+    context.options.onError(
+      createDOMCompilerError(
+        DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT,
+        dir.loc,
+      ),
+    )
+  }
+
+  if (modelType)
+    context.registerOperation({
+      type: IRNodeTypes.DIRECTIVE,
+      element: context.reference(),
+      dir,
+      name: 'model',
+      modelType,
+      builtin: true,
+    })
+
+  function checkDuplicatedValue() {
+    const value = findDir(node, 'bind')
+    if (value && isStaticArgOf(value.arg, 'value')) {
+      context.options.onError(
+        createDOMCompilerError(
+          DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE,
+          value.loc,
+        ),
+      )
+    }
+  }
+}
diff --git a/packages/compiler-vapor/src/transforms/vOn.ts b/packages/compiler-vapor/src/transforms/vOn.ts
new file mode 100644
index 00000000000..fcbfc265d43
--- /dev/null
+++ b/packages/compiler-vapor/src/transforms/vOn.ts
@@ -0,0 +1,95 @@
+import {
+  ElementTypes,
+  ErrorCodes,
+  createCompilerError,
+} from '@vue/compiler-dom'
+import type { DirectiveTransform } from '../transform'
+import { IRNodeTypes, type KeyOverride, type SetEventIRNode } from '../ir'
+import { resolveModifiers } from '@vue/compiler-dom'
+import { extend, makeMap } from '@vue/shared'
+import { resolveExpression } from '../utils'
+import { EMPTY_EXPRESSION } from './utils'
+
+const delegatedEvents = /*#__PURE__*/ makeMap(
+  'beforeinput,click,dblclick,contextmenu,focusin,focusout,input,keydown,' +
+    'keyup,mousedown,mousemove,mouseout,mouseover,mouseup,pointerdown,' +
+    'pointermove,pointerout,pointerover,pointerup,touchend,touchmove,' +
+    'touchstart',
+)
+
+export const transformVOn: DirectiveTransform = (dir, node, context) => {
+  let { arg, exp, loc, modifiers } = dir
+  const isComponent = node.tagType === ElementTypes.COMPONENT
+  const isSlotOutlet = node.tag === 'slot'
+
+  if (!exp && !modifiers.length) {
+    context.options.onError(
+      createCompilerError(ErrorCodes.X_V_ON_NO_EXPRESSION, loc),
+    )
+  }
+  arg = resolveExpression(arg!)
+
+  const { keyModifiers, nonKeyModifiers, eventOptionModifiers } =
+    resolveModifiers(
+      arg.isStatic ? `on${arg.content}` : arg,
+      modifiers,
+      null,
+      loc,
+    )
+
+  let keyOverride: KeyOverride | undefined
+  const isStaticClick = arg.isStatic && arg.content.toLowerCase() === 'click'
+
+  // normalize click.right and click.middle since they don't actually fire
+  if (nonKeyModifiers.includes('middle')) {
+    if (keyOverride) {
+      // TODO error here
+    }
+    if (isStaticClick) {
+      arg = extend({}, arg, { content: 'mouseup' })
+    } else if (!arg.isStatic) {
+      keyOverride = ['click', 'mouseup']
+    }
+  }
+  if (nonKeyModifiers.includes('right')) {
+    if (isStaticClick) {
+      arg = extend({}, arg, { content: 'contextmenu' })
+    } else if (!arg.isStatic) {
+      keyOverride = ['click', 'contextmenu']
+    }
+  }
+
+  if (isComponent || isSlotOutlet) {
+    const handler = exp || EMPTY_EXPRESSION
+    return {
+      key: arg,
+      value: handler,
+      handler: true,
+      handlerModifiers: eventOptionModifiers,
+    }
+  }
+
+  // Only delegate if:
+  // - no dynamic event name
+  // - no event option modifiers (passive, capture, once)
+  // - is a delegatable event
+  const delegate =
+    arg.isStatic && !eventOptionModifiers.length && delegatedEvents(arg.content)
+
+  const operation: SetEventIRNode = {
+    type: IRNodeTypes.SET_EVENT,
+    element: context.reference(),
+    key: arg,
+    value: exp,
+    modifiers: {
+      keys: keyModifiers,
+      nonKeys: nonKeyModifiers,
+      options: eventOptionModifiers,
+    },
+    keyOverride,
+    delegate,
+    effect: !arg.isStatic,
+  }
+
+  context.registerEffect([arg], operation)
+}
diff --git a/packages/compiler-vapor/src/transforms/vOnce.ts b/packages/compiler-vapor/src/transforms/vOnce.ts
new file mode 100644
index 00000000000..3ccfb232059
--- /dev/null
+++ b/packages/compiler-vapor/src/transforms/vOnce.ts
@@ -0,0 +1,12 @@
+import { NodeTypes, findDir } from '@vue/compiler-dom'
+import type { NodeTransform } from '../transform'
+
+export const transformVOnce: NodeTransform = (node, context) => {
+  if (
+    // !context.inSSR &&
+    node.type === NodeTypes.ELEMENT &&
+    findDir(node, 'once', true)
+  ) {
+    context.inVOnce = true
+  }
+}
diff --git a/packages/compiler-vapor/src/transforms/vShow.ts b/packages/compiler-vapor/src/transforms/vShow.ts
new file mode 100644
index 00000000000..f1135d6b0a5
--- /dev/null
+++ b/packages/compiler-vapor/src/transforms/vShow.ts
@@ -0,0 +1,37 @@
+import {
+  DOMErrorCodes,
+  ElementTypes,
+  ErrorCodes,
+  createCompilerError,
+  createDOMCompilerError,
+} from '@vue/compiler-dom'
+import type { DirectiveTransform } from '../transform'
+import { IRNodeTypes } from '../ir'
+
+export const transformVShow: DirectiveTransform = (dir, node, context) => {
+  const { exp, loc } = dir
+  if (!exp) {
+    context.options.onError(
+      createDOMCompilerError(DOMErrorCodes.X_V_SHOW_NO_EXPRESSION, loc),
+    )
+    return
+  }
+
+  if (node.tagType === ElementTypes.SLOT) {
+    context.options.onError(
+      createCompilerError(
+        ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
+        loc,
+      ),
+    )
+    return
+  }
+
+  context.registerOperation({
+    type: IRNodeTypes.DIRECTIVE,
+    element: context.reference(),
+    dir,
+    name: 'show',
+    builtin: true,
+  })
+}
diff --git a/packages/compiler-vapor/src/transforms/vSlot.ts b/packages/compiler-vapor/src/transforms/vSlot.ts
new file mode 100644
index 00000000000..3e78913a23e
--- /dev/null
+++ b/packages/compiler-vapor/src/transforms/vSlot.ts
@@ -0,0 +1,257 @@
+import {
+  type ElementNode,
+  ElementTypes,
+  ErrorCodes,
+  NodeTypes,
+  type SimpleExpressionNode,
+  type TemplateChildNode,
+  createCompilerError,
+  isTemplateNode,
+  isVSlot,
+} from '@vue/compiler-dom'
+import type { NodeTransform, TransformContext } from '../transform'
+import { newBlock } from './utils'
+import {
+  DynamicFlag,
+  type IRFor,
+  type IRSlotDynamic,
+  type IRSlotDynamicBasic,
+  type IRSlotDynamicConditional,
+  IRSlotType,
+  type IRSlots,
+  type IRSlotsStatic,
+  type SlotBlockIRNode,
+  type VaporDirectiveNode,
+} from '../ir'
+import { findDir, resolveExpression } from '../utils'
+import { markNonTemplate } from './transformText'
+
+export const transformVSlot: NodeTransform = (node, context) => {
+  if (node.type !== NodeTypes.ELEMENT) return
+
+  const dir = findDir(node, 'slot', true)
+  const { tagType, children } = node
+  const { parent } = context
+
+  const isComponent = tagType === ElementTypes.COMPONENT
+  const isSlotTemplate =
+    isTemplateNode(node) &&
+    parent &&
+    parent.node.type === NodeTypes.ELEMENT &&
+    parent.node.tagType === ElementTypes.COMPONENT
+
+  if (isComponent && children.length) {
+    return transformComponentSlot(
+      node,
+      dir,
+      context as TransformContext<ElementNode>,
+    )
+  } else if (isSlotTemplate && dir) {
+    return transformTemplateSlot(
+      node,
+      dir,
+      context as TransformContext<ElementNode>,
+    )
+  } else if (!isComponent && dir) {
+    context.options.onError(
+      createCompilerError(ErrorCodes.X_V_SLOT_MISPLACED, dir.loc),
+    )
+  }
+}
+
+// <Foo v-slot:default>
+function transformComponentSlot(
+  node: ElementNode,
+  dir: VaporDirectiveNode | undefined,
+  context: TransformContext<ElementNode>,
+) {
+  const { children } = node
+  const arg = dir && dir.arg
+
+  // whitespace: 'preserve'
+  const emptyTextNodes: TemplateChildNode[] = []
+  const nonSlotTemplateChildren = children.filter(n => {
+    if (isNonWhitespaceContent(n)) {
+      return !(n.type === NodeTypes.ELEMENT && n.props.some(isVSlot))
+    } else {
+      emptyTextNodes.push(n)
+    }
+  })
+  if (!nonSlotTemplateChildren.length) {
+    emptyTextNodes.forEach(n => {
+      markNonTemplate(n, context)
+    })
+  }
+
+  const [block, onExit] = createSlotBlock(node, dir, context)
+
+  const { slots } = context
+
+  return () => {
+    onExit()
+
+    const hasOtherSlots = !!slots.length
+    if (dir && hasOtherSlots) {
+      // already has on-component slot - this is incorrect usage.
+      context.options.onError(
+        createCompilerError(ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE, dir.loc),
+      )
+      return
+    }
+
+    if (nonSlotTemplateChildren.length) {
+      if (hasStaticSlot(slots, 'default')) {
+        context.options.onError(
+          createCompilerError(
+            ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
+            nonSlotTemplateChildren[0].loc,
+          ),
+        )
+      } else {
+        registerSlot(slots, arg, block)
+        context.slots = slots
+      }
+    } else if (hasOtherSlots) {
+      context.slots = slots
+    }
+  }
+}
+
+// <template #foo>
+function transformTemplateSlot(
+  node: ElementNode,
+  dir: VaporDirectiveNode,
+  context: TransformContext<ElementNode>,
+) {
+  context.dynamic.flags |= DynamicFlag.NON_TEMPLATE
+
+  const arg = dir.arg && resolveExpression(dir.arg)
+  const vFor = findDir(node, 'for')
+  const vIf = findDir(node, 'if')
+  const vElse = findDir(node, /^else(-if)?$/, true /* allowEmpty */)
+  const { slots } = context
+  const [block, onExit] = createSlotBlock(node, dir, context)
+
+  if (!vFor && !vIf && !vElse) {
+    const slotName = arg ? arg.isStatic && arg.content : 'default'
+    if (slotName && hasStaticSlot(slots, slotName)) {
+      context.options.onError(
+        createCompilerError(ErrorCodes.X_V_SLOT_DUPLICATE_SLOT_NAMES, dir.loc),
+      )
+    } else {
+      registerSlot(slots, arg, block)
+    }
+  } else if (vIf) {
+    registerDynamicSlot(slots, {
+      slotType: IRSlotType.CONDITIONAL,
+      condition: vIf.exp!,
+      positive: {
+        slotType: IRSlotType.DYNAMIC,
+        name: arg!,
+        fn: block,
+      },
+    })
+  } else if (vElse) {
+    const vIfSlot = slots[slots.length - 1] as IRSlotDynamic
+    if (vIfSlot.slotType === IRSlotType.CONDITIONAL) {
+      let ifNode = vIfSlot
+      while (
+        ifNode.negative &&
+        ifNode.negative.slotType === IRSlotType.CONDITIONAL
+      )
+        ifNode = ifNode.negative
+      const negative: IRSlotDynamicBasic | IRSlotDynamicConditional = vElse.exp
+        ? {
+            slotType: IRSlotType.CONDITIONAL,
+            condition: vElse.exp,
+            positive: {
+              slotType: IRSlotType.DYNAMIC,
+              name: arg!,
+              fn: block,
+            },
+          }
+        : {
+            slotType: IRSlotType.DYNAMIC,
+            name: arg!,
+            fn: block,
+          }
+      ifNode.negative = negative
+    } else {
+      context.options.onError(
+        createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, vElse.loc),
+      )
+    }
+  } else if (vFor) {
+    if (vFor.forParseResult) {
+      registerDynamicSlot(slots, {
+        slotType: IRSlotType.LOOP,
+        name: arg!,
+        fn: block,
+        loop: vFor.forParseResult as IRFor,
+      })
+    } else {
+      context.options.onError(
+        createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, vFor.loc),
+      )
+    }
+  }
+
+  return onExit
+}
+
+function ensureStaticSlots(slots: IRSlots[]): IRSlotsStatic['slots'] {
+  let lastSlots = slots[slots.length - 1]
+  if (!slots.length || lastSlots.slotType !== IRSlotType.STATIC) {
+    slots.push(
+      (lastSlots = {
+        slotType: IRSlotType.STATIC,
+        slots: {},
+      }),
+    )
+  }
+  return lastSlots.slots
+}
+
+function registerSlot(
+  slots: IRSlots[],
+  name: SimpleExpressionNode | undefined,
+  block: SlotBlockIRNode,
+) {
+  const isStatic = !name || name.isStatic
+  if (isStatic) {
+    const staticSlots = ensureStaticSlots(slots)
+    staticSlots[name ? name.content : 'default'] = block
+  } else {
+    slots.push({
+      slotType: IRSlotType.DYNAMIC,
+      name: name!,
+      fn: block,
+    })
+  }
+}
+
+function registerDynamicSlot(allSlots: IRSlots[], dynamic: IRSlotDynamic) {
+  allSlots.push(dynamic)
+}
+
+function hasStaticSlot(slots: IRSlots[], name: string) {
+  return slots.some(slot => {
+    if (slot.slotType === IRSlotType.STATIC) return !!slot.slots[name]
+  })
+}
+
+function createSlotBlock(
+  slotNode: ElementNode,
+  dir: VaporDirectiveNode | undefined,
+  context: TransformContext<ElementNode>,
+): [SlotBlockIRNode, () => void] {
+  const block: SlotBlockIRNode = newBlock(slotNode)
+  block.props = dir && dir.exp
+  const exitBlock = context.enterBlock(block)
+  return [block, exitBlock]
+}
+
+function isNonWhitespaceContent(node: TemplateChildNode): boolean {
+  if (node.type !== NodeTypes.TEXT) return true
+  return !!node.content.trim()
+}
diff --git a/packages/compiler-vapor/src/transforms/vText.ts b/packages/compiler-vapor/src/transforms/vText.ts
new file mode 100644
index 00000000000..0832398e12a
--- /dev/null
+++ b/packages/compiler-vapor/src/transforms/vText.ts
@@ -0,0 +1,44 @@
+import { DOMErrorCodes, createDOMCompilerError } from '@vue/compiler-dom'
+import { IRNodeTypes } from '../ir'
+import { EMPTY_EXPRESSION } from './utils'
+import type { DirectiveTransform } from '../transform'
+import { getLiteralExpressionValue } from '../utils'
+import { isVoidTag } from '../../../shared/src'
+
+export const transformVText: DirectiveTransform = (dir, node, context) => {
+  let { exp, loc } = dir
+  if (!exp) {
+    context.options.onError(
+      createDOMCompilerError(DOMErrorCodes.X_V_TEXT_NO_EXPRESSION, loc),
+    )
+    exp = EMPTY_EXPRESSION
+  }
+  if (node.children.length) {
+    context.options.onError(
+      createDOMCompilerError(DOMErrorCodes.X_V_TEXT_WITH_CHILDREN, loc),
+    )
+    context.childrenTemplate.length = 0
+  }
+
+  // v-text on void tags do nothing
+  if (isVoidTag(context.node.tag)) {
+    return
+  }
+
+  const literal = getLiteralExpressionValue(exp)
+  if (literal != null) {
+    context.childrenTemplate = [String(literal)]
+  } else {
+    context.childrenTemplate = [' ']
+    context.registerOperation({
+      type: IRNodeTypes.GET_TEXT_CHILD,
+      parent: context.reference(),
+    })
+    context.registerEffect([exp], {
+      type: IRNodeTypes.SET_TEXT,
+      element: context.reference(),
+      values: [exp],
+      generated: true,
+    })
+  }
+}
diff --git a/packages/compiler-vapor/src/utils.ts b/packages/compiler-vapor/src/utils.ts
new file mode 100644
index 00000000000..728281914fd
--- /dev/null
+++ b/packages/compiler-vapor/src/utils.ts
@@ -0,0 +1,90 @@
+import type { BigIntLiteral, NumericLiteral, StringLiteral } from '@babel/types'
+import { isGloballyAllowed } from '@vue/shared'
+import {
+  type AttributeNode,
+  type BindingMetadata,
+  BindingTypes,
+  type ElementNode,
+  NodeTypes,
+  type SimpleExpressionNode,
+  findDir as _findDir,
+  findProp as _findProp,
+  createSimpleExpression,
+  isConstantNode,
+  isLiteralWhitelisted,
+} from '@vue/compiler-dom'
+import type { VaporDirectiveNode } from './ir'
+import { EMPTY_EXPRESSION } from './transforms/utils'
+
+export const findProp = _findProp as (
+  node: ElementNode,
+  name: string,
+  dynamicOnly?: boolean,
+  allowEmpty?: boolean,
+) => AttributeNode | VaporDirectiveNode | undefined
+
+/** find directive */
+export const findDir = _findDir as (
+  node: ElementNode,
+  name: string | RegExp,
+  allowEmpty?: boolean,
+) => VaporDirectiveNode | undefined
+
+export function propToExpression(
+  prop: AttributeNode | VaporDirectiveNode,
+): SimpleExpressionNode | undefined {
+  return prop.type === NodeTypes.ATTRIBUTE
+    ? prop.value
+      ? createSimpleExpression(prop.value.content, true, prop.value.loc)
+      : EMPTY_EXPRESSION
+    : prop.exp
+}
+
+export function isConstantExpression(exp: SimpleExpressionNode): boolean {
+  return (
+    isLiteralWhitelisted(exp.content) ||
+    isGloballyAllowed(exp.content) ||
+    getLiteralExpressionValue(exp) !== null
+  )
+}
+
+export function isStaticExpression(
+  node: SimpleExpressionNode,
+  bindings: BindingMetadata,
+): boolean {
+  if (node.ast) {
+    return isConstantNode(node.ast, bindings)
+  } else if (node.ast === null) {
+    const type = bindings[node.content]
+    return type === BindingTypes.LITERAL_CONST
+  }
+  return false
+}
+
+export function resolveExpression(
+  exp: SimpleExpressionNode,
+): SimpleExpressionNode {
+  if (!exp.isStatic) {
+    const value = getLiteralExpressionValue(exp)
+    if (value !== null) {
+      return createSimpleExpression('' + value, true, exp.loc)
+    }
+  }
+  return exp
+}
+
+export function getLiteralExpressionValue(
+  exp: SimpleExpressionNode,
+): number | string | boolean | null {
+  if (exp.ast) {
+    if (exp.ast.type === 'StringLiteral') {
+      return (exp.ast as StringLiteral | NumericLiteral | BigIntLiteral).value
+    } else if (
+      exp.ast.type === 'TemplateLiteral' &&
+      exp.ast.expressions.length === 0
+    ) {
+      return exp.ast.quasis[0].value.cooked!
+    }
+  }
+  return exp.isStatic ? exp.content : null
+}
diff --git a/packages/global.d.ts b/packages/global.d.ts
index 8b627d2e556..e6acf0d1be8 100644
--- a/packages/global.d.ts
+++ b/packages/global.d.ts
@@ -9,6 +9,7 @@ declare var __CJS__: boolean
 declare var __SSR__: boolean
 declare var __VERSION__: string
 declare var __COMPAT__: boolean
+declare var __BENCHMARK__: boolean
 
 // Feature flags
 declare var __FEATURE_OPTIONS_API__: boolean
diff --git a/packages/reactivity/__tests__/computed.spec.ts b/packages/reactivity/__tests__/computed.spec.ts
index c7963499f85..84a2ccb2edc 100644
--- a/packages/reactivity/__tests__/computed.spec.ts
+++ b/packages/reactivity/__tests__/computed.spec.ts
@@ -25,8 +25,9 @@ import {
   toRaw,
   triggerRef,
 } from '../src'
-import { EffectFlags, pauseTracking, resetTracking } from '../src/effect'
 import type { ComputedRef, ComputedRefImpl } from '../src/computed'
+import { pauseTracking, resetTracking } from '../src/effect'
+import { ReactiveFlags } from '../src/system'
 
 describe('reactivity/computed', () => {
   it('should return updated value', () => {
@@ -409,9 +410,9 @@ describe('reactivity/computed', () => {
     a.value++
     e.value
 
-    expect(e.deps!.dep).toBe(b.dep)
-    expect(e.deps!.nextDep!.dep).toBe(d.dep)
-    expect(e.deps!.nextDep!.nextDep!.dep).toBe(c.dep)
+    expect(e.deps!.dep).toBe(b)
+    expect(e.deps!.nextDep!.dep).toBe(d)
+    expect(e.deps!.nextDep!.nextDep!.dep).toBe(c)
     expect(cSpy).toHaveBeenCalledTimes(2)
 
     a.value++
@@ -466,8 +467,8 @@ describe('reactivity/computed', () => {
     const c2 = computed(() => c1.value) as unknown as ComputedRefImpl
 
     c2.value
-    expect(c1.flags & EffectFlags.DIRTY).toBeFalsy()
-    expect(c2.flags & EffectFlags.DIRTY).toBeFalsy()
+    expect(c1.flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)).toBe(0)
+    expect(c2.flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)).toBe(0)
   })
 
   it('should chained computeds dirtyLevel update with first computed effect', () => {
diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts
index 242fc707153..5d7a1e39cef 100644
--- a/packages/reactivity/__tests__/effect.spec.ts
+++ b/packages/reactivity/__tests__/effect.spec.ts
@@ -1,3 +1,14 @@
+import {
+  computed,
+  h,
+  nextTick,
+  nodeOps,
+  ref,
+  render,
+  serializeInner,
+} from '@vue/runtime-test'
+import { ITERATE_KEY, getDepFromReactive } from '../src/dep'
+import { onEffectCleanup, pauseTracking, resetTracking } from '../src/effect'
 import {
   type DebuggerEvent,
   type ReactiveEffectRunner,
@@ -11,23 +22,7 @@ import {
   stop,
   toRaw,
 } from '../src/index'
-import { type Dep, ITERATE_KEY, getDepFromReactive } from '../src/dep'
-import {
-  computed,
-  h,
-  nextTick,
-  nodeOps,
-  ref,
-  render,
-  serializeInner,
-} from '@vue/runtime-test'
-import {
-  endBatch,
-  onEffectCleanup,
-  pauseTracking,
-  resetTracking,
-  startBatch,
-} from '../src/effect'
+import { type ReactiveNode, endBatch, startBatch } from '../src/system'
 
 describe('reactivity/effect', () => {
   it('should run the passed function once (wrapped by a effect)', () => {
@@ -1183,12 +1178,12 @@ describe('reactivity/effect', () => {
   })
 
   describe('dep unsubscribe', () => {
-    function getSubCount(dep: Dep | undefined) {
+    function getSubCount(dep: ReactiveNode | undefined) {
       let count = 0
       let sub = dep!.subs
       while (sub) {
         count++
-        sub = sub.prevSub
+        sub = sub.nextSub
       }
       return count
     }
diff --git a/packages/reactivity/__tests__/effectScope.spec.ts b/packages/reactivity/__tests__/effectScope.spec.ts
index debbdafb1e7..93ee648e2df 100644
--- a/packages/reactivity/__tests__/effectScope.spec.ts
+++ b/packages/reactivity/__tests__/effectScope.spec.ts
@@ -2,6 +2,7 @@ import { nextTick, watch, watchEffect } from '@vue/runtime-core'
 import {
   type ComputedRef,
   EffectScope,
+  ReactiveEffect,
   computed,
   effect,
   effectScope,
@@ -9,6 +10,7 @@ import {
   onScopeDispose,
   reactive,
   ref,
+  setCurrentScope,
 } from '../src'
 
 describe('reactivity/effect/scope', () => {
@@ -20,7 +22,7 @@ describe('reactivity/effect/scope', () => {
 
   it('should accept zero argument', () => {
     const scope = effectScope()
-    expect(scope.effects.length).toBe(0)
+    expect(getEffectsCount(scope)).toBe(0)
   })
 
   it('should return run value', () => {
@@ -29,7 +31,8 @@ describe('reactivity/effect/scope', () => {
 
   it('should work w/ active property', () => {
     const scope = effectScope()
-    scope.run(() => 1)
+    const src = computed(() => 1)
+    scope.run(() => src.value)
     expect(scope.active).toBe(true)
     scope.stop()
     expect(scope.active).toBe(false)
@@ -47,7 +50,7 @@ describe('reactivity/effect/scope', () => {
       expect(dummy).toBe(7)
     })
 
-    expect(scope.effects.length).toBe(1)
+    expect(getEffectsCount(scope)).toBe(1)
   })
 
   it('stop', () => {
@@ -60,7 +63,7 @@ describe('reactivity/effect/scope', () => {
       effect(() => (doubled = counter.num * 2))
     })
 
-    expect(scope.effects.length).toBe(2)
+    expect(getEffectsCount(scope)).toBe(2)
 
     expect(dummy).toBe(0)
     counter.num = 7
@@ -87,9 +90,8 @@ describe('reactivity/effect/scope', () => {
       })
     })
 
-    expect(scope.effects.length).toBe(1)
-    expect(scope.scopes!.length).toBe(1)
-    expect(scope.scopes![0]).toBeInstanceOf(EffectScope)
+    expect(getEffectsCount(scope)).toBe(1)
+    expect(scope.deps?.nextDep?.dep).toBeInstanceOf(EffectScope)
 
     expect(dummy).toBe(0)
     counter.num = 7
@@ -117,7 +119,7 @@ describe('reactivity/effect/scope', () => {
       })
     })
 
-    expect(scope.effects.length).toBe(1)
+    expect(getEffectsCount(scope)).toBe(1)
 
     expect(dummy).toBe(0)
     counter.num = 7
@@ -142,13 +144,13 @@ describe('reactivity/effect/scope', () => {
       effect(() => (dummy = counter.num))
     })
 
-    expect(scope.effects.length).toBe(1)
+    expect(getEffectsCount(scope)).toBe(1)
 
     scope.run(() => {
       effect(() => (doubled = counter.num * 2))
     })
 
-    expect(scope.effects.length).toBe(2)
+    expect(getEffectsCount(scope)).toBe(2)
 
     counter.num = 7
     expect(dummy).toBe(7)
@@ -166,21 +168,21 @@ describe('reactivity/effect/scope', () => {
       effect(() => (dummy = counter.num))
     })
 
-    expect(scope.effects.length).toBe(1)
+    expect(getEffectsCount(scope)).toBe(1)
 
     scope.stop()
 
+    expect(getEffectsCount(scope)).toBe(0)
+
     scope.run(() => {
       effect(() => (doubled = counter.num * 2))
     })
 
-    expect('[Vue warn] cannot run an inactive effect scope.').toHaveBeenWarned()
-
-    expect(scope.effects.length).toBe(0)
+    expect(getEffectsCount(scope)).toBe(1)
 
     counter.num = 7
     expect(dummy).toBe(0)
-    expect(doubled).toBe(undefined)
+    expect(doubled).toBe(14)
   })
 
   it('should fire onScopeDispose hook', () => {
@@ -224,9 +226,9 @@ describe('reactivity/effect/scope', () => {
   it('should dereference child scope from parent scope after stopping child scope (no memleaks)', () => {
     const parent = effectScope()
     const child = parent.run(() => effectScope())!
-    expect(parent.scopes!.includes(child)).toBe(true)
+    expect(parent.deps?.dep).toBe(child)
     child.stop()
-    expect(parent.scopes!.includes(child)).toBe(false)
+    expect(parent.deps).toBeUndefined()
   })
 
   it('test with higher level APIs', async () => {
@@ -290,8 +292,7 @@ describe('reactivity/effect/scope', () => {
 
     parentScope.run(() => {
       const childScope = effectScope(true)
-      childScope.on()
-      childScope.off()
+      setCurrentScope(setCurrentScope(childScope))
       expect(getCurrentScope()).toBe(parentScope)
     })
   })
@@ -359,7 +360,17 @@ describe('reactivity/effect/scope', () => {
     expect(watcherCalls).toBe(3)
     expect(cleanupCalls).toBe(1)
 
-    expect(scope.effects.length).toBe(0)
-    expect(scope.cleanups.length).toBe(0)
+    expect(getEffectsCount(scope)).toBe(0)
+    expect(scope.cleanupsLength).toBe(0)
   })
 })
+
+function getEffectsCount(scope: EffectScope): number {
+  let n = 0
+  for (let dep = scope.deps; dep !== undefined; dep = dep.nextDep) {
+    if (dep.dep instanceof ReactiveEffect) {
+      n++
+    }
+  }
+  return n
+}
diff --git a/packages/reactivity/__tests__/gc.spec.ts b/packages/reactivity/__tests__/gc.spec.ts
index a609958409f..6e7a8f7bcd8 100644
--- a/packages/reactivity/__tests__/gc.spec.ts
+++ b/packages/reactivity/__tests__/gc.spec.ts
@@ -2,6 +2,7 @@ import {
   type ComputedRef,
   computed,
   effect,
+  effectScope,
   reactive,
   shallowRef as ref,
   toRaw,
@@ -79,4 +80,36 @@ describe.skipIf(!global.gc)('reactivity/gc', () => {
     src.foo++
     expect(spy).toHaveBeenCalledTimes(2)
   })
+
+  it('should release computed that untrack by effect', async () => {
+    const src = ref(0)
+    // @ts-expect-error ES2021 API
+    const c = new WeakRef(computed(() => src.value))
+    const scope = effectScope()
+
+    scope.run(() => {
+      effect(() => c.deref().value)
+    })
+
+    expect(c.deref()).toBeDefined()
+    scope.stop()
+    await gc()
+    expect(c.deref()).toBeUndefined()
+  })
+
+  it('should release computed that untrack by effectScope', async () => {
+    const src = ref(0)
+    // @ts-expect-error ES2021 API
+    const c = new WeakRef(computed(() => src.value))
+    const scope = effectScope()
+
+    scope.run(() => {
+      c.deref().value
+    })
+
+    expect(c.deref()).toBeDefined()
+    scope.stop()
+    await gc()
+    expect(c.deref()).toBeUndefined()
+  })
 })
diff --git a/packages/reactivity/__tests__/ref.spec.ts b/packages/reactivity/__tests__/ref.spec.ts
index 7976a5373ba..b6fd23ed39b 100644
--- a/packages/reactivity/__tests__/ref.spec.ts
+++ b/packages/reactivity/__tests__/ref.spec.ts
@@ -386,16 +386,6 @@ describe('reactivity/ref', () => {
     expect(dummyY).toBe(5)
   })
 
-  test('toRefs should warn on plain object', () => {
-    toRefs({})
-    expect(`toRefs() expects a reactive object`).toHaveBeenWarned()
-  })
-
-  test('toRefs should warn on plain array', () => {
-    toRefs([])
-    expect(`toRefs() expects a reactive object`).toHaveBeenWarned()
-  })
-
   test('toRefs reactive array', () => {
     const arr = reactive(['a', 'b', 'c'])
     const refs = toRefs(arr)
diff --git a/packages/reactivity/__tests__/watch.spec.ts b/packages/reactivity/__tests__/watch.spec.ts
index 9bec54e5f68..e8686700aae 100644
--- a/packages/reactivity/__tests__/watch.spec.ts
+++ b/packages/reactivity/__tests__/watch.spec.ts
@@ -3,40 +3,12 @@ import {
   type Ref,
   WatchErrorCodes,
   type WatchOptions,
-  type WatchScheduler,
   computed,
   onWatcherCleanup,
   ref,
   watch,
 } from '../src'
 
-const queue: (() => void)[] = []
-
-// a simple scheduler for testing purposes
-let isFlushPending = false
-const resolvedPromise = /*@__PURE__*/ Promise.resolve() as Promise<any>
-const nextTick = (fn?: () => any) =>
-  fn ? resolvedPromise.then(fn) : resolvedPromise
-
-const scheduler: WatchScheduler = (job, isFirstRun) => {
-  if (isFirstRun) {
-    job()
-  } else {
-    queue.push(job)
-    flushJobs()
-  }
-}
-
-const flushJobs = () => {
-  if (isFlushPending) return
-  isFlushPending = true
-  resolvedPromise.then(() => {
-    queue.forEach(job => job())
-    queue.length = 0
-    isFlushPending = false
-  })
-}
-
 describe('watch', () => {
   test('effect', () => {
     let dummy: any
@@ -147,54 +119,6 @@ describe('watch', () => {
     expect(dummy).toBe(30)
   })
 
-  test('nested calls to baseWatch and onWatcherCleanup', async () => {
-    let calls: string[] = []
-    let source: Ref<number>
-    let copyist: Ref<number>
-    const scope = new EffectScope()
-
-    scope.run(() => {
-      source = ref(0)
-      copyist = ref(0)
-      // sync by default
-      watch(
-        () => {
-          const current = (copyist.value = source.value)
-          onWatcherCleanup(() => calls.push(`sync ${current}`))
-        },
-        null,
-        {},
-      )
-      // with scheduler
-      watch(
-        () => {
-          const current = copyist.value
-          onWatcherCleanup(() => calls.push(`post ${current}`))
-        },
-        null,
-        { scheduler },
-      )
-    })
-
-    await nextTick()
-    expect(calls).toEqual([])
-
-    scope.run(() => source.value++)
-    expect(calls).toEqual(['sync 0'])
-    await nextTick()
-    expect(calls).toEqual(['sync 0', 'post 0'])
-    calls.length = 0
-
-    scope.run(() => source.value++)
-    expect(calls).toEqual(['sync 1'])
-    await nextTick()
-    expect(calls).toEqual(['sync 1', 'post 1'])
-    calls.length = 0
-
-    scope.stop()
-    expect(calls).toEqual(['sync 2', 'post 2'])
-  })
-
   test('once option should be ignored by simple watch', async () => {
     let dummy: any
     const source = ref(0)
diff --git a/packages/reactivity/package.json b/packages/reactivity/package.json
index d861a5fb1ac..76c9a08b609 100644
--- a/packages/reactivity/package.json
+++ b/packages/reactivity/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/reactivity",
-  "version": "3.5.17",
+  "version": "3.6.0-alpha.2",
   "description": "@vue/reactivity",
   "main": "index.js",
   "module": "dist/reactivity.esm-bundler.js",
diff --git a/packages/reactivity/src/arrayInstrumentations.ts b/packages/reactivity/src/arrayInstrumentations.ts
index e031df4fe10..5e35230efdf 100644
--- a/packages/reactivity/src/arrayInstrumentations.ts
+++ b/packages/reactivity/src/arrayInstrumentations.ts
@@ -1,8 +1,8 @@
+import { isArray } from '@vue/shared'
 import { TrackOpTypes } from './constants'
-import { endBatch, pauseTracking, resetTracking, startBatch } from './effect'
-import { isProxy, isShallow, toRaw, toReactive } from './reactive'
 import { ARRAY_ITERATE_KEY, track } from './dep'
-import { isArray } from '@vue/shared'
+import { isProxy, isShallow, toRaw, toReactive } from './reactive'
+import { endBatch, setActiveSub, startBatch } from './system'
 
 /**
  * Track array iteration and return:
@@ -319,10 +319,10 @@ function noTracking(
   method: keyof Array<any>,
   args: unknown[] = [],
 ) {
-  pauseTracking()
   startBatch()
+  const prevSub = setActiveSub()
   const res = (toRaw(self) as any)[method].apply(self, args)
+  setActiveSub(prevSub)
   endBatch()
-  resetTracking()
   return res
 }
diff --git a/packages/reactivity/src/baseHandlers.ts b/packages/reactivity/src/baseHandlers.ts
index faec3012f40..3b2dcfdec90 100644
--- a/packages/reactivity/src/baseHandlers.ts
+++ b/packages/reactivity/src/baseHandlers.ts
@@ -96,15 +96,20 @@ class BaseReactiveHandler implements ProxyHandler<Target> {
       }
     }
 
+    const wasRef = isRef(target)
     const res = Reflect.get(
       target,
       key,
       // if this is a proxy wrapping a ref, return methods using the raw ref
       // as receiver so that we don't have to call `toRaw` on the ref in all
       // its class methods
-      isRef(target) ? target : receiver,
+      wasRef ? target : receiver,
     )
 
+    if (wasRef && key !== 'value') {
+      return res
+    }
+
     if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
       return res
     }
diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts
index ea798e201d4..cb367a274b3 100644
--- a/packages/reactivity/src/computed.ts
+++ b/packages/reactivity/src/computed.ts
@@ -1,17 +1,21 @@
-import { isFunction } from '@vue/shared'
+import { hasChanged, isFunction } from '@vue/shared'
+import { ReactiveFlags, TrackOpTypes } from './constants'
+import { onTrack, setupOnTrigger } from './debug'
+import type { DebuggerEvent, DebuggerOptions } from './effect'
+import { activeEffectScope } from './effectScope'
+import type { Ref } from './ref'
 import {
-  type DebuggerEvent,
-  type DebuggerOptions,
-  EffectFlags,
-  type Subscriber,
+  type Link,
+  type ReactiveNode,
+  ReactiveFlags as SystemReactiveFlags,
   activeSub,
-  batch,
-  refreshComputed,
-} from './effect'
-import type { Ref } from './ref'
+  checkDirty,
+  endTracking,
+  link,
+  shallowPropagate,
+  startTracking,
+} from './system'
 import { warn } from './warning'
-import { Dep, type Link, globalVersion } from './dep'
-import { ReactiveFlags, TrackOpTypes } from './constants'
 
 declare const ComputedRefSymbol: unique symbol
 declare const WritableComputedRefSymbol: unique symbol
@@ -44,15 +48,19 @@ export interface WritableComputedOptions<T, S = T> {
  * @private exported by @vue/reactivity for Vue core use, but not exported from
  * the main vue package
  */
-export class ComputedRefImpl<T = any> implements Subscriber {
-  /**
-   * @internal
-   */
-  _value: any = undefined
+export class ComputedRefImpl<T = any> implements ReactiveNode {
   /**
    * @internal
    */
-  readonly dep: Dep = new Dep(this)
+  _value: T | undefined = undefined
+
+  subs: Link | undefined = undefined
+  subsTail: Link | undefined = undefined
+  deps: Link | undefined = undefined
+  depsTail: Link | undefined = undefined
+  flags: SystemReactiveFlags =
+    SystemReactiveFlags.Mutable | SystemReactiveFlags.Dirty
+
   /**
    * @internal
    */
@@ -63,85 +71,86 @@ export class ComputedRefImpl<T = any> implements Subscriber {
    */
   readonly __v_isReadonly: boolean
   // TODO isolatedDeclarations ReactiveFlags.IS_READONLY
-  // A computed is also a subscriber that tracks other deps
-  /**
-   * @internal
-   */
-  deps?: Link = undefined
-  /**
-   * @internal
-   */
-  depsTail?: Link = undefined
-  /**
-   * @internal
-   */
-  flags: EffectFlags = EffectFlags.DIRTY
-  /**
-   * @internal
-   */
-  globalVersion: number = globalVersion - 1
+
+  // for backwards compat
+  get effect(): this {
+    return this
+  }
+  // for backwards compat
+  get dep(): ReactiveNode {
+    return this
+  }
   /**
    * @internal
+   * for backwards compat
    */
-  isSSR: boolean
+  get _dirty(): boolean {
+    const flags = this.flags
+    if (flags & SystemReactiveFlags.Dirty) {
+      return true
+    }
+    if (flags & SystemReactiveFlags.Pending) {
+      if (checkDirty(this.deps!, this)) {
+        this.flags = flags | SystemReactiveFlags.Dirty
+        return true
+      } else {
+        this.flags = flags & ~SystemReactiveFlags.Pending
+      }
+    }
+    return false
+  }
   /**
    * @internal
+   * for backwards compat
    */
-  next?: Subscriber = undefined
+  set _dirty(v: boolean) {
+    if (v) {
+      this.flags |= SystemReactiveFlags.Dirty
+    } else {
+      this.flags &= ~(SystemReactiveFlags.Dirty | SystemReactiveFlags.Pending)
+    }
+  }
 
-  // for backwards compat
-  effect: this = this
   // dev only
   onTrack?: (event: DebuggerEvent) => void
   // dev only
   onTrigger?: (event: DebuggerEvent) => void
 
-  /**
-   * Dev only
-   * @internal
-   */
-  _warnRecursive?: boolean
-
   constructor(
     public fn: ComputedGetter<T>,
     private readonly setter: ComputedSetter<T> | undefined,
-    isSSR: boolean,
   ) {
     this[ReactiveFlags.IS_READONLY] = !setter
-    this.isSSR = isSSR
   }
 
-  /**
-   * @internal
-   */
-  notify(): true | void {
-    this.flags |= EffectFlags.DIRTY
+  get value(): T {
+    const flags = this.flags
     if (
-      !(this.flags & EffectFlags.NOTIFIED) &&
-      // avoid infinite self recursion
-      activeSub !== this
+      flags & SystemReactiveFlags.Dirty ||
+      (flags & SystemReactiveFlags.Pending && checkDirty(this.deps!, this))
     ) {
-      batch(this, true)
-      return true
-    } else if (__DEV__) {
-      // TODO warn
+      if (this.update()) {
+        const subs = this.subs
+        if (subs !== undefined) {
+          shallowPropagate(subs)
+        }
+      }
+    } else if (flags & SystemReactiveFlags.Pending) {
+      this.flags = flags & ~SystemReactiveFlags.Pending
     }
-  }
-
-  get value(): T {
-    const link = __DEV__
-      ? this.dep.track({
+    if (activeSub !== undefined) {
+      if (__DEV__) {
+        onTrack(activeSub!, {
           target: this,
           type: TrackOpTypes.GET,
           key: 'value',
         })
-      : this.dep.track()
-    refreshComputed(this)
-    // sync version after evaluation
-    if (link) {
-      link.version = this.dep.version
+      }
+      link(this, activeSub)
+    } else if (activeEffectScope !== undefined) {
+      link(this, activeEffectScope)
     }
-    return this._value
+    return this._value!
   }
 
   set value(newValue) {
@@ -151,6 +160,25 @@ export class ComputedRefImpl<T = any> implements Subscriber {
       warn('Write operation failed: computed value is readonly')
     }
   }
+
+  update(): boolean {
+    const prevSub = startTracking(this)
+    try {
+      const oldValue = this._value
+      const newValue = this.fn(oldValue)
+      if (hasChanged(oldValue, newValue)) {
+        this._value = newValue
+        return true
+      }
+      return false
+    } finally {
+      endTracking(this, prevSub)
+    }
+  }
+}
+
+if (__DEV__) {
+  setupOnTrigger(ComputedRefImpl)
 }
 
 /**
@@ -209,7 +237,7 @@ export function computed<T>(
     setter = getterOrOptions.set
   }
 
-  const cRef = new ComputedRefImpl(getter, setter, isSSR)
+  const cRef = new ComputedRefImpl(getter, setter)
 
   if (__DEV__ && debugOptions && !isSSR) {
     cRef.onTrack = debugOptions.onTrack
diff --git a/packages/reactivity/src/debug.ts b/packages/reactivity/src/debug.ts
new file mode 100644
index 00000000000..ba323d8993c
--- /dev/null
+++ b/packages/reactivity/src/debug.ts
@@ -0,0 +1,83 @@
+import { extend } from '@vue/shared'
+import type { DebuggerEventExtraInfo, ReactiveEffectOptions } from './effect'
+import { type Link, ReactiveFlags, type ReactiveNode } from './system'
+
+export const triggerEventInfos: DebuggerEventExtraInfo[] = []
+
+export function onTrack(
+  sub: Link['sub'],
+  debugInfo: DebuggerEventExtraInfo,
+): void {
+  if (!__DEV__) {
+    throw new Error(
+      `Internal error: onTrack should be called only in development.`,
+    )
+  }
+  if ((sub as ReactiveEffectOptions).onTrack) {
+    ;(sub as ReactiveEffectOptions).onTrack!(
+      extend(
+        {
+          effect: sub,
+        },
+        debugInfo,
+      ),
+    )
+  }
+}
+
+export function onTrigger(sub: Link['sub']): void {
+  if (!__DEV__) {
+    throw new Error(
+      `Internal error: onTrigger should be called only in development.`,
+    )
+  }
+  if ((sub as ReactiveEffectOptions).onTrigger) {
+    const debugInfo = triggerEventInfos[triggerEventInfos.length - 1]
+    ;(sub as ReactiveEffectOptions).onTrigger!(
+      extend(
+        {
+          effect: sub,
+        },
+        debugInfo,
+      ),
+    )
+  }
+}
+
+export function setupOnTrigger(target: { new (...args: any[]): any }): void {
+  if (!__DEV__) {
+    throw new Error(
+      `Internal error: setupOnTrigger should be called only in development.`,
+    )
+  }
+  Object.defineProperty(target.prototype, 'onTrigger', {
+    get() {
+      return this._onTrigger
+    },
+    set(val) {
+      if (val && !this._onTrigger) setupFlagsHandler(this)
+      this._onTrigger = val
+    },
+  })
+}
+
+function setupFlagsHandler(target: ReactiveNode): void {
+  ;(target as any)._flags = target.flags
+  Object.defineProperty(target, 'flags', {
+    get() {
+      return (target as any)._flags
+    },
+    set(value) {
+      if (
+        !(
+          (target as any)._flags &
+          (ReactiveFlags.Dirty | ReactiveFlags.Pending)
+        ) &&
+        !!(value & (ReactiveFlags.Dirty | ReactiveFlags.Pending))
+      ) {
+        onTrigger(this)
+      }
+      ;(target as any)._flags = value
+    },
+  })
+}
diff --git a/packages/reactivity/src/dep.ts b/packages/reactivity/src/dep.ts
index 8ad8a389baf..a24000329c1 100644
--- a/packages/reactivity/src/dep.ts
+++ b/packages/reactivity/src/dep.ts
@@ -1,233 +1,37 @@
-import { extend, isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
-import type { ComputedRefImpl } from './computed'
+import { isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
 import { type TrackOpTypes, TriggerOpTypes } from './constants'
+import { onTrack, triggerEventInfos } from './debug'
 import {
-  type DebuggerEventExtraInfo,
-  EffectFlags,
-  type Subscriber,
+  type Link,
+  ReactiveFlags,
+  type ReactiveNode,
   activeSub,
   endBatch,
-  shouldTrack,
+  link,
+  propagate,
+  shallowPropagate,
   startBatch,
-} from './effect'
+} from './system'
 
-/**
- * Incremented every time a reactive change happens
- * This is used to give computed a fast path to avoid re-compute when nothing
- * has changed.
- */
-export let globalVersion = 0
-
-/**
- * Represents a link between a source (Dep) and a subscriber (Effect or Computed).
- * Deps and subs have a many-to-many relationship - each link between a
- * dep and a sub is represented by a Link instance.
- *
- * A Link is also a node in two doubly-linked lists - one for the associated
- * sub to track all its deps, and one for the associated dep to track all its
- * subs.
- *
- * @internal
- */
-export class Link {
-  /**
-   * - Before each effect run, all previous dep links' version are reset to -1
-   * - During the run, a link's version is synced with the source dep on access
-   * - After the run, links with version -1 (that were never used) are cleaned
-   *   up
-   */
-  version: number
-
-  /**
-   * Pointers for doubly-linked lists
-   */
-  nextDep?: Link
-  prevDep?: Link
-  nextSub?: Link
-  prevSub?: Link
-  prevActiveLink?: Link
+class Dep implements ReactiveNode {
+  _subs: Link | undefined = undefined
+  subsTail: Link | undefined = undefined
+  flags: ReactiveFlags = ReactiveFlags.None
 
   constructor(
-    public sub: Subscriber,
-    public dep: Dep,
-  ) {
-    this.version = dep.version
-    this.nextDep =
-      this.prevDep =
-      this.nextSub =
-      this.prevSub =
-      this.prevActiveLink =
-        undefined
-  }
-}
-
-/**
- * @internal
- */
-export class Dep {
-  version = 0
-  /**
-   * Link between this dep and the current active effect
-   */
-  activeLink?: Link = undefined
-
-  /**
-   * Doubly linked list representing the subscribing effects (tail)
-   */
-  subs?: Link = undefined
-
-  /**
-   * Doubly linked list representing the subscribing effects (head)
-   * DEV only, for invoking onTrigger hooks in correct order
-   */
-  subsHead?: Link
-
-  /**
-   * For object property deps cleanup
-   */
-  map?: KeyToDepMap = undefined
-  key?: unknown = undefined
-
-  /**
-   * Subscriber counter
-   */
-  sc: number = 0
-
-  /**
-   * @internal
-   */
-  readonly __v_skip = true
-  // TODO isolatedDeclarations ReactiveFlags.SKIP
-
-  constructor(public computed?: ComputedRefImpl | undefined) {
-    if (__DEV__) {
-      this.subsHead = undefined
-    }
-  }
-
-  track(debugInfo?: DebuggerEventExtraInfo): Link | undefined {
-    if (!activeSub || !shouldTrack || activeSub === this.computed) {
-      return
-    }
-
-    let link = this.activeLink
-    if (link === undefined || link.sub !== activeSub) {
-      link = this.activeLink = new Link(activeSub, this)
-
-      // add the link to the activeEffect as a dep (as tail)
-      if (!activeSub.deps) {
-        activeSub.deps = activeSub.depsTail = link
-      } else {
-        link.prevDep = activeSub.depsTail
-        activeSub.depsTail!.nextDep = link
-        activeSub.depsTail = link
-      }
-
-      addSub(link)
-    } else if (link.version === -1) {
-      // reused from last run - already a sub, just sync version
-      link.version = this.version
-
-      // If this dep has a next, it means it's not at the tail - move it to the
-      // tail. This ensures the effect's dep list is in the order they are
-      // accessed during evaluation.
-      if (link.nextDep) {
-        const next = link.nextDep
-        next.prevDep = link.prevDep
-        if (link.prevDep) {
-          link.prevDep.nextDep = next
-        }
-
-        link.prevDep = activeSub.depsTail
-        link.nextDep = undefined
-        activeSub.depsTail!.nextDep = link
-        activeSub.depsTail = link
-
-        // this was the head - point to the new head
-        if (activeSub.deps === link) {
-          activeSub.deps = next
-        }
-      }
-    }
-
-    if (__DEV__ && activeSub.onTrack) {
-      activeSub.onTrack(
-        extend(
-          {
-            effect: activeSub,
-          },
-          debugInfo,
-        ),
-      )
-    }
+    private map: KeyToDepMap,
+    private key: unknown,
+  ) {}
 
-    return link
+  get subs(): Link | undefined {
+    return this._subs
   }
 
-  trigger(debugInfo?: DebuggerEventExtraInfo): void {
-    this.version++
-    globalVersion++
-    this.notify(debugInfo)
-  }
-
-  notify(debugInfo?: DebuggerEventExtraInfo): void {
-    startBatch()
-    try {
-      if (__DEV__) {
-        // subs are notified and batched in reverse-order and then invoked in
-        // original order at the end of the batch, but onTrigger hooks should
-        // be invoked in original order here.
-        for (let head = this.subsHead; head; head = head.nextSub) {
-          if (head.sub.onTrigger && !(head.sub.flags & EffectFlags.NOTIFIED)) {
-            head.sub.onTrigger(
-              extend(
-                {
-                  effect: head.sub,
-                },
-                debugInfo,
-              ),
-            )
-          }
-        }
-      }
-      for (let link = this.subs; link; link = link.prevSub) {
-        if (link.sub.notify()) {
-          // if notify() returns `true`, this is a computed. Also call notify
-          // on its dep - it's called here instead of inside computed's notify
-          // in order to reduce call stack depth.
-          ;(link.sub as ComputedRefImpl).dep.notify()
-        }
-      }
-    } finally {
-      endBatch()
-    }
-  }
-}
-
-function addSub(link: Link) {
-  link.dep.sc++
-  if (link.sub.flags & EffectFlags.TRACKING) {
-    const computed = link.dep.computed
-    // computed getting its first subscriber
-    // enable tracking + lazily subscribe to all its deps
-    if (computed && !link.dep.subs) {
-      computed.flags |= EffectFlags.TRACKING | EffectFlags.DIRTY
-      for (let l = computed.deps; l; l = l.nextDep) {
-        addSub(l)
-      }
-    }
-
-    const currentTail = link.dep.subs
-    if (currentTail !== link) {
-      link.prevSub = currentTail
-      if (currentTail) currentTail.nextSub = link
-    }
-
-    if (__DEV__ && link.dep.subsHead === undefined) {
-      link.dep.subsHead = link
+  set subs(value: Link | undefined) {
+    this._subs = value
+    if (value === undefined) {
+      this.map.delete(this.key)
     }
-
-    link.dep.subs = link
   }
 }
 
@@ -260,26 +64,23 @@ export const ARRAY_ITERATE_KEY: unique symbol = Symbol(
  * @param key - Identifier of the reactive property to track.
  */
 export function track(target: object, type: TrackOpTypes, key: unknown): void {
-  if (shouldTrack && activeSub) {
+  if (activeSub !== undefined) {
     let depsMap = targetMap.get(target)
     if (!depsMap) {
       targetMap.set(target, (depsMap = new Map()))
     }
     let dep = depsMap.get(key)
     if (!dep) {
-      depsMap.set(key, (dep = new Dep()))
-      dep.map = depsMap
-      dep.key = key
+      depsMap.set(key, (dep = new Dep(depsMap, key)))
     }
     if (__DEV__) {
-      dep.track({
+      onTrack(activeSub!, {
         target,
         type,
         key,
       })
-    } else {
-      dep.track()
     }
+    link(dep, activeSub!)
   }
 }
 
@@ -302,14 +103,13 @@ export function trigger(
   const depsMap = targetMap.get(target)
   if (!depsMap) {
     // never been tracked
-    globalVersion++
     return
   }
 
-  const run = (dep: Dep | undefined) => {
-    if (dep) {
+  const run = (dep: ReactiveNode | undefined) => {
+    if (dep !== undefined && dep.subs !== undefined) {
       if (__DEV__) {
-        dep.trigger({
+        triggerEventInfos.push({
           target,
           type,
           key,
@@ -317,8 +117,11 @@ export function trigger(
           oldValue,
           oldTarget,
         })
-      } else {
-        dep.trigger()
+      }
+      propagate(dep.subs)
+      shallowPropagate(dep.subs)
+      if (__DEV__) {
+        triggerEventInfos.pop()
       }
     }
   }
@@ -391,7 +194,7 @@ export function trigger(
 export function getDepFromReactive(
   object: any,
   key: string | number | symbol,
-): Dep | undefined {
+): ReactiveNode | undefined {
   const depMap = targetMap.get(object)
   return depMap && depMap.get(key)
 }
diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts
index 84cf184c154..d48900f18cc 100644
--- a/packages/reactivity/src/effect.ts
+++ b/packages/reactivity/src/effect.ts
@@ -1,14 +1,25 @@
-import { extend, hasChanged } from '@vue/shared'
-import type { ComputedRefImpl } from './computed'
+import { extend } from '@vue/shared'
 import type { TrackOpTypes, TriggerOpTypes } from './constants'
-import { type Link, globalVersion } from './dep'
+import { setupOnTrigger } from './debug'
 import { activeEffectScope } from './effectScope'
+import {
+  type Link,
+  ReactiveFlags,
+  type ReactiveNode,
+  activeSub,
+  checkDirty,
+  endTracking,
+  link,
+  setActiveSub,
+  startTracking,
+  unlink,
+} from './system'
 import { warn } from './warning'
 
 export type EffectScheduler = (...args: any[]) => any
 
 export type DebuggerEvent = {
-  effect: Subscriber
+  effect: ReactiveNode
 } & DebuggerEventExtraInfo
 
 export type DebuggerEventExtraInfo = {
@@ -27,7 +38,6 @@ export interface DebuggerOptions {
 
 export interface ReactiveEffectOptions extends DebuggerOptions {
   scheduler?: EffectScheduler
-  allowRecurse?: boolean
   onStop?: () => void
 }
 
@@ -36,87 +46,52 @@ export interface ReactiveEffectRunner<T = any> {
   effect: ReactiveEffect
 }
 
-export let activeSub: Subscriber | undefined
-
 export enum EffectFlags {
   /**
    * ReactiveEffect only
    */
-  ACTIVE = 1 << 0,
-  RUNNING = 1 << 1,
-  TRACKING = 1 << 2,
-  NOTIFIED = 1 << 3,
-  DIRTY = 1 << 4,
-  ALLOW_RECURSE = 1 << 5,
-  PAUSED = 1 << 6,
-  EVALUATED = 1 << 7,
+  ALLOW_RECURSE = 1 << 7,
+  PAUSED = 1 << 8,
+  STOP = 1 << 10,
 }
 
-/**
- * Subscriber is a type that tracks (or subscribes to) a list of deps.
- */
-export interface Subscriber extends DebuggerOptions {
-  /**
-   * Head of the doubly linked list representing the deps
-   * @internal
-   */
-  deps?: Link
-  /**
-   * Tail of the same list
-   * @internal
-   */
-  depsTail?: Link
-  /**
-   * @internal
-   */
-  flags: EffectFlags
-  /**
-   * @internal
-   */
-  next?: Subscriber
-  /**
-   * returning `true` indicates it's a computed that needs to call notify
-   * on its dep too
-   * @internal
-   */
-  notify(): true | void
-}
-
-const pausedQueueEffects = new WeakSet<ReactiveEffect>()
-
 export class ReactiveEffect<T = any>
-  implements Subscriber, ReactiveEffectOptions
+  implements ReactiveEffectOptions, ReactiveNode
 {
+  deps: Link | undefined = undefined
+  depsTail: Link | undefined = undefined
+  subs: Link | undefined = undefined
+  subsTail: Link | undefined = undefined
+  flags: number = ReactiveFlags.Watching | ReactiveFlags.Dirty
+
   /**
    * @internal
    */
-  deps?: Link = undefined
-  /**
-   * @internal
-   */
-  depsTail?: Link = undefined
-  /**
-   * @internal
-   */
-  flags: EffectFlags = EffectFlags.ACTIVE | EffectFlags.TRACKING
-  /**
-   * @internal
-   */
-  next?: Subscriber = undefined
+  cleanups: (() => void)[] = []
   /**
    * @internal
    */
-  cleanup?: () => void = undefined
+  cleanupsLength = 0
 
-  scheduler?: EffectScheduler = undefined
-  onStop?: () => void
+  // dev only
   onTrack?: (event: DebuggerEvent) => void
+  // dev only
   onTrigger?: (event: DebuggerEvent) => void
 
-  constructor(public fn: () => T) {
-    if (activeEffectScope && activeEffectScope.active) {
-      activeEffectScope.effects.push(this)
+  // @ts-expect-error
+  fn(): T {}
+
+  constructor(fn?: () => T) {
+    if (fn !== undefined) {
+      this.fn = fn
     }
+    if (activeEffectScope) {
+      link(this, activeEffectScope)
+    }
+  }
+
+  get active(): boolean {
+    return !(this.flags & EffectFlags.STOP)
   }
 
   pause(): void {
@@ -124,350 +99,74 @@ export class ReactiveEffect<T = any>
   }
 
   resume(): void {
-    if (this.flags & EffectFlags.PAUSED) {
-      this.flags &= ~EffectFlags.PAUSED
-      if (pausedQueueEffects.has(this)) {
-        pausedQueueEffects.delete(this)
-        this.trigger()
-      }
+    const flags = (this.flags &= ~EffectFlags.PAUSED)
+    if (flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)) {
+      this.notify()
     }
   }
 
-  /**
-   * @internal
-   */
   notify(): void {
-    if (
-      this.flags & EffectFlags.RUNNING &&
-      !(this.flags & EffectFlags.ALLOW_RECURSE)
-    ) {
-      return
-    }
-    if (!(this.flags & EffectFlags.NOTIFIED)) {
-      batch(this)
+    if (!(this.flags & EffectFlags.PAUSED) && this.dirty) {
+      this.run()
     }
   }
 
   run(): T {
-    // TODO cleanupEffect
-
-    if (!(this.flags & EffectFlags.ACTIVE)) {
-      // stopped during cleanup
+    if (!this.active) {
       return this.fn()
     }
-
-    this.flags |= EffectFlags.RUNNING
-    cleanupEffect(this)
-    prepareDeps(this)
-    const prevEffect = activeSub
-    const prevShouldTrack = shouldTrack
-    activeSub = this
-    shouldTrack = true
-
+    cleanup(this)
+    const prevSub = startTracking(this)
     try {
       return this.fn()
     } finally {
-      if (__DEV__ && activeSub !== this) {
-        warn(
-          'Active effect was not restored correctly - ' +
-            'this is likely a Vue internal bug.',
-        )
+      endTracking(this, prevSub)
+      const flags = this.flags
+      if (
+        (flags & (ReactiveFlags.Recursed | EffectFlags.ALLOW_RECURSE)) ===
+        (ReactiveFlags.Recursed | EffectFlags.ALLOW_RECURSE)
+      ) {
+        this.flags = flags & ~ReactiveFlags.Recursed
+        this.notify()
       }
-      cleanupDeps(this)
-      activeSub = prevEffect
-      shouldTrack = prevShouldTrack
-      this.flags &= ~EffectFlags.RUNNING
     }
   }
 
   stop(): void {
-    if (this.flags & EffectFlags.ACTIVE) {
-      for (let link = this.deps; link; link = link.nextDep) {
-        removeSub(link)
-      }
-      this.deps = this.depsTail = undefined
-      cleanupEffect(this)
-      this.onStop && this.onStop()
-      this.flags &= ~EffectFlags.ACTIVE
+    if (!this.active) {
+      return
     }
-  }
-
-  trigger(): void {
-    if (this.flags & EffectFlags.PAUSED) {
-      pausedQueueEffects.add(this)
-    } else if (this.scheduler) {
-      this.scheduler()
-    } else {
-      this.runIfDirty()
+    this.flags = EffectFlags.STOP
+    let dep = this.deps
+    while (dep !== undefined) {
+      dep = unlink(dep, this)
     }
-  }
-
-  /**
-   * @internal
-   */
-  runIfDirty(): void {
-    if (isDirty(this)) {
-      this.run()
+    const sub = this.subs
+    if (sub !== undefined) {
+      unlink(sub)
     }
+    cleanup(this)
   }
 
   get dirty(): boolean {
-    return isDirty(this)
-  }
-}
-
-/**
- * For debugging
- */
-// function printDeps(sub: Subscriber) {
-//   let d = sub.deps
-//   let ds = []
-//   while (d) {
-//     ds.push(d)
-//     d = d.nextDep
-//   }
-//   return ds.map(d => ({
-//     id: d.id,
-//     prev: d.prevDep?.id,
-//     next: d.nextDep?.id,
-//   }))
-// }
-
-let batchDepth = 0
-let batchedSub: Subscriber | undefined
-let batchedComputed: Subscriber | undefined
-
-export function batch(sub: Subscriber, isComputed = false): void {
-  sub.flags |= EffectFlags.NOTIFIED
-  if (isComputed) {
-    sub.next = batchedComputed
-    batchedComputed = sub
-    return
-  }
-  sub.next = batchedSub
-  batchedSub = sub
-}
-
-/**
- * @internal
- */
-export function startBatch(): void {
-  batchDepth++
-}
-
-/**
- * Run batched effects when all batches have ended
- * @internal
- */
-export function endBatch(): void {
-  if (--batchDepth > 0) {
-    return
-  }
-
-  if (batchedComputed) {
-    let e: Subscriber | undefined = batchedComputed
-    batchedComputed = undefined
-    while (e) {
-      const next: Subscriber | undefined = e.next
-      e.next = undefined
-      e.flags &= ~EffectFlags.NOTIFIED
-      e = next
-    }
-  }
-
-  let error: unknown
-  while (batchedSub) {
-    let e: Subscriber | undefined = batchedSub
-    batchedSub = undefined
-    while (e) {
-      const next: Subscriber | undefined = e.next
-      e.next = undefined
-      e.flags &= ~EffectFlags.NOTIFIED
-      if (e.flags & EffectFlags.ACTIVE) {
-        try {
-          // ACTIVE flag is effect-only
-          ;(e as ReactiveEffect).trigger()
-        } catch (err) {
-          if (!error) error = err
-        }
-      }
-      e = next
-    }
-  }
-
-  if (error) throw error
-}
-
-function prepareDeps(sub: Subscriber) {
-  // Prepare deps for tracking, starting from the head
-  for (let link = sub.deps; link; link = link.nextDep) {
-    // set all previous deps' (if any) version to -1 so that we can track
-    // which ones are unused after the run
-    link.version = -1
-    // store previous active sub if link was being used in another context
-    link.prevActiveLink = link.dep.activeLink
-    link.dep.activeLink = link
-  }
-}
-
-function cleanupDeps(sub: Subscriber) {
-  // Cleanup unsued deps
-  let head
-  let tail = sub.depsTail
-  let link = tail
-  while (link) {
-    const prev = link.prevDep
-    if (link.version === -1) {
-      if (link === tail) tail = prev
-      // unused - remove it from the dep's subscribing effect list
-      removeSub(link)
-      // also remove it from this effect's dep list
-      removeDep(link)
-    } else {
-      // The new head is the last node seen which wasn't removed
-      // from the doubly-linked list
-      head = link
-    }
-
-    // restore previous active link if any
-    link.dep.activeLink = link.prevActiveLink
-    link.prevActiveLink = undefined
-    link = prev
-  }
-  // set the new head & tail
-  sub.deps = head
-  sub.depsTail = tail
-}
-
-function isDirty(sub: Subscriber): boolean {
-  for (let link = sub.deps; link; link = link.nextDep) {
-    if (
-      link.dep.version !== link.version ||
-      (link.dep.computed &&
-        (refreshComputed(link.dep.computed) ||
-          link.dep.version !== link.version))
-    ) {
+    const flags = this.flags
+    if (flags & ReactiveFlags.Dirty) {
       return true
     }
-  }
-  // @ts-expect-error only for backwards compatibility where libs manually set
-  // this flag - e.g. Pinia's testing module
-  if (sub._dirty) {
-    return true
-  }
-  return false
-}
-
-/**
- * Returning false indicates the refresh failed
- * @internal
- */
-export function refreshComputed(computed: ComputedRefImpl): undefined {
-  if (
-    computed.flags & EffectFlags.TRACKING &&
-    !(computed.flags & EffectFlags.DIRTY)
-  ) {
-    return
-  }
-  computed.flags &= ~EffectFlags.DIRTY
-
-  // Global version fast path when no reactive changes has happened since
-  // last refresh.
-  if (computed.globalVersion === globalVersion) {
-    return
-  }
-  computed.globalVersion = globalVersion
-
-  // In SSR there will be no render effect, so the computed has no subscriber
-  // and therefore tracks no deps, thus we cannot rely on the dirty check.
-  // Instead, computed always re-evaluate and relies on the globalVersion
-  // fast path above for caching.
-  // #12337 if computed has no deps (does not rely on any reactive data) and evaluated,
-  // there is no need to re-evaluate.
-  if (
-    !computed.isSSR &&
-    computed.flags & EffectFlags.EVALUATED &&
-    ((!computed.deps && !(computed as any)._dirty) || !isDirty(computed))
-  ) {
-    return
-  }
-  computed.flags |= EffectFlags.RUNNING
-
-  const dep = computed.dep
-  const prevSub = activeSub
-  const prevShouldTrack = shouldTrack
-  activeSub = computed
-  shouldTrack = true
-
-  try {
-    prepareDeps(computed)
-    const value = computed.fn(computed._value)
-    if (dep.version === 0 || hasChanged(value, computed._value)) {
-      computed.flags |= EffectFlags.EVALUATED
-      computed._value = value
-      dep.version++
-    }
-  } catch (err) {
-    dep.version++
-    throw err
-  } finally {
-    activeSub = prevSub
-    shouldTrack = prevShouldTrack
-    cleanupDeps(computed)
-    computed.flags &= ~EffectFlags.RUNNING
-  }
-}
-
-function removeSub(link: Link, soft = false) {
-  const { dep, prevSub, nextSub } = link
-  if (prevSub) {
-    prevSub.nextSub = nextSub
-    link.prevSub = undefined
-  }
-  if (nextSub) {
-    nextSub.prevSub = prevSub
-    link.nextSub = undefined
-  }
-  if (__DEV__ && dep.subsHead === link) {
-    // was previous head, point new head to next
-    dep.subsHead = nextSub
-  }
-
-  if (dep.subs === link) {
-    // was previous tail, point new tail to prev
-    dep.subs = prevSub
-
-    if (!prevSub && 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) {
-        // here we are only "soft" unsubscribing because the computed still keeps
-        // referencing the deps and the dep should not decrease its sub count
-        removeSub(l, true)
+    if (flags & ReactiveFlags.Pending) {
+      if (checkDirty(this.deps!, this)) {
+        this.flags = flags | ReactiveFlags.Dirty
+        return true
+      } else {
+        this.flags = flags & ~ReactiveFlags.Pending
       }
     }
-  }
-
-  if (!soft && !--dep.sc && dep.map) {
-    // #11979
-    // property dep no longer has effect subscribers, delete it
-    // this mostly is for the case where an object is kept in memory but only a
-    // subset of its properties is tracked at one time
-    dep.map.delete(dep.key)
+    return false
   }
 }
 
-function removeDep(link: Link) {
-  const { prevDep, nextDep } = link
-  if (prevDep) {
-    prevDep.nextDep = nextDep
-    link.prevDep = undefined
-  }
-  if (nextDep) {
-    nextDep.prevDep = prevDep
-    link.nextDep = undefined
-  }
+if (__DEV__) {
+  setupOnTrigger(ReactiveEffect)
 }
 
 export interface ReactiveEffectRunner<T = any> {
@@ -485,6 +184,23 @@ export function effect<T = any>(
 
   const e = new ReactiveEffect(fn)
   if (options) {
+    const { onStop, scheduler } = options
+    if (onStop) {
+      options.onStop = undefined
+      const stop = e.stop.bind(e)
+      e.stop = () => {
+        stop()
+        onStop()
+      }
+    }
+    if (scheduler) {
+      options.scheduler = undefined
+      e.notify = () => {
+        if (!(e.flags & EffectFlags.PAUSED)) {
+          scheduler()
+        }
+      }
+    }
     extend(e, options)
   }
   try {
@@ -507,34 +223,65 @@ export function stop(runner: ReactiveEffectRunner): void {
   runner.effect.stop()
 }
 
-/**
- * @internal
- */
-export let shouldTrack = true
-const trackStack: boolean[] = []
+const resetTrackingStack: (ReactiveNode | undefined)[] = []
 
 /**
  * Temporarily pauses tracking.
  */
 export function pauseTracking(): void {
-  trackStack.push(shouldTrack)
-  shouldTrack = false
+  resetTrackingStack.push(activeSub)
+  setActiveSub()
 }
 
 /**
  * Re-enables effect tracking (if it was paused).
  */
 export function enableTracking(): void {
-  trackStack.push(shouldTrack)
-  shouldTrack = true
+  const isPaused = activeSub === undefined
+  if (!isPaused) {
+    // Add the current active effect to the trackResetStack so it can be
+    // restored by calling resetTracking.
+    resetTrackingStack.push(activeSub)
+  } else {
+    // Add a placeholder to the trackResetStack so we can it can be popped
+    // to restore the previous active effect.
+    resetTrackingStack.push(undefined)
+    for (let i = resetTrackingStack.length - 1; i >= 0; i--) {
+      if (resetTrackingStack[i] !== undefined) {
+        setActiveSub(resetTrackingStack[i])
+        break
+      }
+    }
+  }
 }
 
 /**
  * Resets the previous global effect tracking state.
  */
 export function resetTracking(): void {
-  const last = trackStack.pop()
-  shouldTrack = last === undefined ? true : last
+  if (__DEV__ && resetTrackingStack.length === 0) {
+    warn(
+      `resetTracking() was called when there was no active tracking ` +
+        `to reset.`,
+    )
+  }
+  if (resetTrackingStack.length) {
+    setActiveSub(resetTrackingStack.pop()!)
+  } else {
+    setActiveSub()
+  }
+}
+
+export function cleanup(
+  sub: ReactiveNode & { cleanups: (() => void)[]; cleanupsLength: number },
+): void {
+  const l = sub.cleanupsLength
+  if (l) {
+    for (let i = 0; i < l; i++) {
+      sub.cleanups[i]()
+    }
+    sub.cleanupsLength = 0
+  }
 }
 
 /**
@@ -551,7 +298,7 @@ export function resetTracking(): void {
  */
 export function onEffectCleanup(fn: () => void, failSilently = false): void {
   if (activeSub instanceof ReactiveEffect) {
-    activeSub.cleanup = fn
+    activeSub.cleanups[activeSub.cleanupsLength++] = () => cleanupEffect(fn)
   } else if (__DEV__ && !failSilently) {
     warn(
       `onEffectCleanup() was called when there was no active effect` +
@@ -560,17 +307,12 @@ export function onEffectCleanup(fn: () => void, failSilently = false): void {
   }
 }
 
-function cleanupEffect(e: ReactiveEffect) {
-  const { cleanup } = e
-  e.cleanup = undefined
-  if (cleanup) {
-    // run cleanup without active effect
-    const prevSub = activeSub
-    activeSub = undefined
-    try {
-      cleanup()
-    } finally {
-      activeSub = prevSub
-    }
+function cleanupEffect(fn: () => void) {
+  // run cleanup without active effect
+  const prevSub = setActiveSub()
+  try {
+    fn()
+  } finally {
+    setActiveSub(prevSub)
   }
 }
diff --git a/packages/reactivity/src/effectScope.ts b/packages/reactivity/src/effectScope.ts
index 92ad92c1249..36c9b85e8d7 100644
--- a/packages/reactivity/src/effectScope.ts
+++ b/packages/reactivity/src/effectScope.ts
@@ -1,71 +1,44 @@
-import type { ReactiveEffect } from './effect'
+import { EffectFlags, cleanup } from './effect'
+import { type Link, type ReactiveNode, link, unlink } from './system'
 import { warn } from './warning'
 
 export let activeEffectScope: EffectScope | undefined
 
-export class EffectScope {
-  /**
-   * @internal
-   */
-  private _active = true
-  /**
-   * @internal track `on` calls, allow `on` call multiple times
-   */
-  private _on = 0
-  /**
-   * @internal
-   */
-  effects: ReactiveEffect[] = []
-  /**
-   * @internal
-   */
-  cleanups: (() => void)[] = []
-
-  private _isPaused = false
+export class EffectScope implements ReactiveNode {
+  deps: Link | undefined = undefined
+  depsTail: Link | undefined = undefined
+  subs: Link | undefined = undefined
+  subsTail: Link | undefined = undefined
+  flags: number = 0
 
   /**
-   * only assigned by undetached scope
-   * @internal
-   */
-  parent: EffectScope | undefined
-  /**
-   * record undetached scopes
    * @internal
    */
-  scopes: EffectScope[] | undefined
+  cleanups: (() => void)[] = []
   /**
-   * track a child scope's index in its parent's scopes array for optimized
-   * removal
    * @internal
    */
-  private index: number | undefined
+  cleanupsLength = 0
 
-  constructor(public detached = false) {
-    this.parent = activeEffectScope
+  constructor(detached = false) {
     if (!detached && activeEffectScope) {
-      this.index =
-        (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
-          this,
-        ) - 1
+      link(this, activeEffectScope)
     }
   }
 
   get active(): boolean {
-    return this._active
+    return !(this.flags & EffectFlags.STOP)
   }
 
   pause(): void {
-    if (this._active) {
-      this._isPaused = true
-      let i, l
-      if (this.scopes) {
-        for (i = 0, l = this.scopes.length; i < l; i++) {
-          this.scopes[i].pause()
+    if (!(this.flags & EffectFlags.PAUSED)) {
+      this.flags |= EffectFlags.PAUSED
+      for (let link = this.deps; link !== undefined; link = link.nextDep) {
+        const dep = link.dep
+        if ('pause' in dep) {
+          dep.pause()
         }
       }
-      for (i = 0, l = this.effects.length; i < l; i++) {
-        this.effects[i].pause()
-      }
     }
   }
 
@@ -73,91 +46,48 @@ export class EffectScope {
    * Resumes the effect scope, including all child scopes and effects.
    */
   resume(): void {
-    if (this._active) {
-      if (this._isPaused) {
-        this._isPaused = false
-        let i, l
-        if (this.scopes) {
-          for (i = 0, l = this.scopes.length; i < l; i++) {
-            this.scopes[i].resume()
-          }
-        }
-        for (i = 0, l = this.effects.length; i < l; i++) {
-          this.effects[i].resume()
+    const flags = this.flags
+    if (flags & EffectFlags.PAUSED) {
+      this.flags = flags & ~EffectFlags.PAUSED
+      for (let link = this.deps; link !== undefined; link = link.nextDep) {
+        const dep = link.dep
+        if ('resume' in dep) {
+          dep.resume()
         }
       }
     }
   }
 
   run<T>(fn: () => T): T | undefined {
-    if (this._active) {
-      const currentEffectScope = activeEffectScope
-      try {
-        activeEffectScope = this
-        return fn()
-      } finally {
-        activeEffectScope = currentEffectScope
-      }
-    } else if (__DEV__) {
-      warn(`cannot run an inactive effect scope.`)
-    }
-  }
-
-  prevScope: EffectScope | undefined
-  /**
-   * This should only be called on non-detached scopes
-   * @internal
-   */
-  on(): void {
-    if (++this._on === 1) {
-      this.prevScope = activeEffectScope
+    const prevScope = activeEffectScope
+    try {
       activeEffectScope = this
+      return fn()
+    } finally {
+      activeEffectScope = prevScope
     }
   }
 
-  /**
-   * This should only be called on non-detached scopes
-   * @internal
-   */
-  off(): void {
-    if (this._on > 0 && --this._on === 0) {
-      activeEffectScope = this.prevScope
-      this.prevScope = undefined
+  stop(): void {
+    if (!this.active) {
+      return
     }
-  }
-
-  stop(fromParent?: boolean): void {
-    if (this._active) {
-      this._active = false
-      let i, l
-      for (i = 0, l = this.effects.length; i < l; i++) {
-        this.effects[i].stop()
-      }
-      this.effects.length = 0
-
-      for (i = 0, l = this.cleanups.length; i < l; i++) {
-        this.cleanups[i]()
+    this.flags = EffectFlags.STOP
+    let dep = this.deps
+    while (dep !== undefined) {
+      const node = dep.dep
+      if ('stop' in node) {
+        dep = dep.nextDep
+        node.stop()
+      } else {
+        dep = unlink(dep, this)
       }
-      this.cleanups.length = 0
-
-      if (this.scopes) {
-        for (i = 0, l = this.scopes.length; i < l; i++) {
-          this.scopes[i].stop(true)
-        }
-        this.scopes.length = 0
-      }
-
-      // nested scope, dereference from parent to avoid memory leaks
-      if (!this.detached && this.parent && !fromParent) {
-        // optimized O(1) removal
-        const last = this.parent.scopes!.pop()
-        if (last && last !== this) {
-          this.parent.scopes![this.index!] = last
-          last.index = this.index!
-        }
-      }
-      this.parent = undefined
     }
+    const sub = this.subs
+    if (sub !== undefined) {
+      unlink(sub)
+    }
+    cleanup(this)
   }
 }
 
@@ -183,6 +113,14 @@ export function getCurrentScope(): EffectScope | undefined {
   return activeEffectScope
 }
 
+export function setCurrentScope(scope?: EffectScope): EffectScope | undefined {
+  try {
+    return activeEffectScope
+  } finally {
+    activeEffectScope = scope
+  }
+}
+
 /**
  * Registers a dispose callback on the current active effect scope. The
  * callback will be invoked when the associated effect scope is stopped.
@@ -191,8 +129,8 @@ export function getCurrentScope(): EffectScope | undefined {
  * @see {@link https://vuejs.org/api/reactivity-advanced.html#onscopedispose}
  */
 export function onScopeDispose(fn: () => void, failSilently = false): void {
-  if (activeEffectScope) {
-    activeEffectScope.cleanups.push(fn)
+  if (activeEffectScope !== undefined) {
+    activeEffectScope.cleanups[activeEffectScope.cleanupsLength++] = fn
   } else if (__DEV__ && !failSilently) {
     warn(
       `onScopeDispose() is called when there is no active effect scope` +
diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts
index f0445e87da0..ef643940b00 100644
--- a/packages/reactivity/src/index.ts
+++ b/packages/reactivity/src/index.ts
@@ -76,6 +76,10 @@ export {
   effectScope,
   EffectScope,
   getCurrentScope,
+  /**
+   * @internal
+   */
+  setCurrentScope,
   onScopeDispose,
 } from './effectScope'
 export { reactiveReadArray, shallowReadArray } from './arrayInstrumentations'
@@ -86,8 +90,11 @@ export {
   traverse,
   onWatcherCleanup,
   WatchErrorCodes,
+  /**
+   * @internal
+   */
+  WatcherEffect,
   type WatchOptions,
-  type WatchScheduler,
   type WatchStopHandle,
   type WatchHandle,
   type WatchEffect,
@@ -95,3 +102,7 @@ export {
   type WatchCallback,
   type OnCleanup,
 } from './watch'
+/**
+ * @internal
+ */
+export { setActiveSub } from './system'
diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts
index 59b713dd862..92a4ba7f1de 100644
--- a/packages/reactivity/src/ref.ts
+++ b/packages/reactivity/src/ref.ts
@@ -5,20 +5,30 @@ import {
   isFunction,
   isObject,
 } from '@vue/shared'
-import { Dep, getDepFromReactive } from './dep'
+import type { ComputedRef, WritableComputedRef } from './computed'
+import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
+import { onTrack, triggerEventInfos } from './debug'
+import { getDepFromReactive } from './dep'
 import {
   type Builtin,
   type ShallowReactiveMarker,
-  isProxy,
   isReactive,
   isReadonly,
   isShallow,
   toRaw,
   toReactive,
 } from './reactive'
-import type { ComputedRef, WritableComputedRef } from './computed'
-import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
-import { warn } from './warning'
+import {
+  type Link,
+  type ReactiveNode,
+  ReactiveFlags as _ReactiveFlags,
+  activeSub,
+  batchDepth,
+  flush,
+  link,
+  propagate,
+  shallowPropagate,
+} from './system'
 
 declare const RefSymbol: unique symbol
 export declare const RawSymbol: unique symbol
@@ -57,7 +67,7 @@ export function ref<T>(
 ): [T] extends [Ref] ? IfAny<T, Ref<T>, T> : Ref<UnwrapRef<T>, UnwrapRef<T> | T>
 export function ref<T = any>(): Ref<T | undefined>
 export function ref(value?: unknown) {
-  return createRef(value, false)
+  return createRef(value, toReactive)
 }
 
 declare const ShallowRefMarker: unique symbol
@@ -92,43 +102,58 @@ export function shallowRef<T>(
   : ShallowRef<T>
 export function shallowRef<T = any>(): ShallowRef<T | undefined>
 export function shallowRef(value?: unknown) {
-  return createRef(value, true)
+  return createRef(value)
 }
 
-function createRef(rawValue: unknown, shallow: boolean) {
+function createRef(rawValue: unknown, wrap?: <T>(v: T) => T) {
   if (isRef(rawValue)) {
     return rawValue
   }
-  return new RefImpl(rawValue, shallow)
+  return new RefImpl(rawValue, wrap)
 }
 
 /**
  * @internal
  */
-class RefImpl<T = any> {
+class RefImpl<T = any> implements ReactiveNode {
+  subs: Link | undefined = undefined
+  subsTail: Link | undefined = undefined
+  flags: _ReactiveFlags = _ReactiveFlags.Mutable
+
   _value: T
+  _wrap?: <T>(v: T) => T
+  private _oldValue: T
   private _rawValue: T
 
-  dep: Dep = new Dep()
-
-  public readonly [ReactiveFlags.IS_REF] = true
-  public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false
+  /**
+   * @internal
+   */
+  readonly __v_isRef = true
+  // TODO isolatedDeclarations ReactiveFlags.IS_REF
+  /**
+   * @internal
+   */
+  readonly __v_isShallow: boolean = false
+  // TODO isolatedDeclarations ReactiveFlags.IS_SHALLOW
+
+  constructor(value: T, wrap: (<T>(v: T) => T) | undefined) {
+    this._oldValue = this._rawValue = wrap ? toRaw(value) : value
+    this._value = wrap ? wrap(value) : value
+    this._wrap = wrap
+    this[ReactiveFlags.IS_SHALLOW] = !wrap
+  }
 
-  constructor(value: T, isShallow: boolean) {
-    this._rawValue = isShallow ? value : toRaw(value)
-    this._value = isShallow ? value : toReactive(value)
-    this[ReactiveFlags.IS_SHALLOW] = isShallow
+  get dep(): this {
+    return this
   }
 
-  get value() {
-    if (__DEV__) {
-      this.dep.track({
-        target: this,
-        type: TrackOpTypes.GET,
-        key: 'value',
-      })
-    } else {
-      this.dep.track()
+  get value(): T {
+    trackRef(this)
+    if (this.flags & _ReactiveFlags.Dirty && this.update()) {
+      const subs = this.subs
+      if (subs !== undefined) {
+        shallowPropagate(subs)
+      }
     }
     return this._value
   }
@@ -141,21 +166,36 @@ class RefImpl<T = any> {
       isReadonly(newValue)
     newValue = useDirectValue ? newValue : toRaw(newValue)
     if (hasChanged(newValue, oldValue)) {
+      this.flags |= _ReactiveFlags.Dirty
       this._rawValue = newValue
-      this._value = useDirectValue ? newValue : toReactive(newValue)
-      if (__DEV__) {
-        this.dep.trigger({
-          target: this,
-          type: TriggerOpTypes.SET,
-          key: 'value',
-          newValue,
-          oldValue,
-        })
-      } else {
-        this.dep.trigger()
+      this._value =
+        !useDirectValue && this._wrap ? this._wrap(newValue) : newValue
+      const subs = this.subs
+      if (subs !== undefined) {
+        if (__DEV__) {
+          triggerEventInfos.push({
+            target: this,
+            type: TriggerOpTypes.SET,
+            key: 'value',
+            newValue,
+            oldValue,
+          })
+        }
+        propagate(subs)
+        if (!batchDepth) {
+          flush()
+        }
+        if (__DEV__) {
+          triggerEventInfos.pop()
+        }
       }
     }
   }
+
+  update(): boolean {
+    this.flags &= ~_ReactiveFlags.Dirty
+    return hasChanged(this._oldValue, (this._oldValue = this._rawValue))
+  }
 }
 
 /**
@@ -185,17 +225,26 @@ class RefImpl<T = any> {
  */
 export function triggerRef(ref: Ref): void {
   // ref may be an instance of ObjectRefImpl
-  if ((ref as unknown as RefImpl).dep) {
+  const dep = (ref as unknown as RefImpl).dep
+  if (dep !== undefined && dep.subs !== undefined) {
+    propagate(dep.subs)
+    shallowPropagate(dep.subs)
+    if (!batchDepth) {
+      flush()
+    }
+  }
+}
+
+function trackRef(dep: ReactiveNode) {
+  if (activeSub !== undefined) {
     if (__DEV__) {
-      ;(ref as unknown as RefImpl).dep.trigger({
-        target: ref,
-        type: TriggerOpTypes.SET,
+      onTrack(activeSub!, {
+        target: dep,
+        type: TrackOpTypes.GET,
         key: 'value',
-        newValue: (ref as unknown as RefImpl)._value,
       })
-    } else {
-      ;(ref as unknown as RefImpl).dep.trigger()
     }
+    link(dep, activeSub!)
   }
 }
 
@@ -287,8 +336,10 @@ export type CustomRefFactory<T> = (
   set: (value: T) => void
 }
 
-class CustomRefImpl<T> {
-  public dep: Dep
+class CustomRefImpl<T> implements ReactiveNode {
+  subs: Link | undefined = undefined
+  subsTail: Link | undefined = undefined
+  flags: _ReactiveFlags = _ReactiveFlags.None
 
   private readonly _get: ReturnType<CustomRefFactory<T>>['get']
   private readonly _set: ReturnType<CustomRefFactory<T>>['set']
@@ -298,12 +349,18 @@ class CustomRefImpl<T> {
   public _value: T = undefined!
 
   constructor(factory: CustomRefFactory<T>) {
-    const dep = (this.dep = new Dep())
-    const { get, set } = factory(dep.track.bind(dep), dep.trigger.bind(dep))
+    const { get, set } = factory(
+      () => trackRef(this),
+      () => triggerRef(this as unknown as Ref),
+    )
     this._get = get
     this._set = set
   }
 
+  get dep() {
+    return this
+  }
+
   get value() {
     return (this._value = this._get())
   }
@@ -337,9 +394,6 @@ export type ToRefs<T = any> = {
  * @see {@link https://vuejs.org/api/reactivity-utilities.html#torefs}
  */
 export function toRefs<T extends object>(object: T): ToRefs<T> {
-  if (__DEV__ && !isProxy(object)) {
-    warn(`toRefs() expects a reactive object but received a plain one.`)
-  }
   const ret: any = isArray(object) ? new Array(object.length) : {}
   for (const key in object) {
     ret[key] = propertyToRef(object, key)
@@ -366,7 +420,7 @@ class ObjectRefImpl<T extends object, K extends keyof T> {
     this._object[this._key] = newVal
   }
 
-  get dep(): Dep | undefined {
+  get dep(): ReactiveNode | undefined {
     return getDepFromReactive(toRaw(this._object), this._key)
   }
 }
diff --git a/packages/reactivity/src/system.ts b/packages/reactivity/src/system.ts
new file mode 100644
index 00000000000..93adad33532
--- /dev/null
+++ b/packages/reactivity/src/system.ts
@@ -0,0 +1,360 @@
+/* eslint-disable */
+// Ported from https://github.com/stackblitz/alien-signals/blob/v2.0.4/src/system.ts
+import type { ComputedRefImpl as Computed } from './computed.js'
+import type { ReactiveEffect as Effect } from './effect.js'
+import type { EffectScope } from './effectScope.js'
+import { warn } from './warning.js'
+
+export interface ReactiveNode {
+  deps?: Link
+  depsTail?: Link
+  subs?: Link
+  subsTail?: Link
+  flags: ReactiveFlags
+}
+
+export interface Link {
+  dep: ReactiveNode | Computed | Effect | EffectScope
+  sub: ReactiveNode | Computed | Effect | EffectScope
+  prevSub: Link | undefined
+  nextSub: Link | undefined
+  prevDep: Link | undefined
+  nextDep: Link | undefined
+}
+
+interface Stack<T> {
+  value: T
+  prev: Stack<T> | undefined
+}
+
+export const enum ReactiveFlags {
+  None = 0,
+  Mutable = 1 << 0,
+  Watching = 1 << 1,
+  RecursedCheck = 1 << 2,
+  Recursed = 1 << 3,
+  Dirty = 1 << 4,
+  Pending = 1 << 5,
+}
+
+const notifyBuffer: (Effect | undefined)[] = []
+
+export let batchDepth = 0
+export let activeSub: ReactiveNode | undefined = undefined
+
+let notifyIndex = 0
+let notifyBufferLength = 0
+
+export function setActiveSub(sub?: ReactiveNode): ReactiveNode | undefined {
+  try {
+    return activeSub
+  } finally {
+    activeSub = sub
+  }
+}
+
+export function startBatch(): void {
+  ++batchDepth
+}
+
+export function endBatch(): void {
+  if (!--batchDepth && notifyBufferLength) {
+    flush()
+  }
+}
+
+export function link(dep: ReactiveNode, sub: ReactiveNode): void {
+  const prevDep = sub.depsTail
+  if (prevDep !== undefined && prevDep.dep === dep) {
+    return
+  }
+  let nextDep: Link | undefined = undefined
+  const recursedCheck = sub.flags & ReactiveFlags.RecursedCheck
+  if (recursedCheck) {
+    nextDep = prevDep !== undefined ? prevDep.nextDep : sub.deps
+    if (nextDep !== undefined && nextDep.dep === dep) {
+      sub.depsTail = nextDep
+      return
+    }
+  }
+  // TODO: maybe can find a good way to check duplicate link
+  const prevSub = dep.subsTail
+  const newLink =
+    (sub.depsTail =
+    dep.subsTail =
+      {
+        dep,
+        sub,
+        prevDep,
+        nextDep,
+        prevSub,
+        nextSub: undefined,
+      })
+  if (nextDep !== undefined) {
+    nextDep.prevDep = newLink
+  }
+  if (prevDep !== undefined) {
+    prevDep.nextDep = newLink
+  } else {
+    sub.deps = newLink
+  }
+  if (prevSub !== undefined) {
+    prevSub.nextSub = newLink
+  } else {
+    dep.subs = newLink
+  }
+}
+
+export function unlink(
+  link: Link,
+  sub: ReactiveNode = link.sub,
+): Link | undefined {
+  const dep = link.dep
+  const prevDep = link.prevDep
+  const nextDep = link.nextDep
+  const nextSub = link.nextSub
+  const prevSub = link.prevSub
+  if (nextDep !== undefined) {
+    nextDep.prevDep = prevDep
+  } else {
+    sub.depsTail = prevDep
+  }
+  if (prevDep !== undefined) {
+    prevDep.nextDep = nextDep
+  } else {
+    sub.deps = nextDep
+  }
+  if (nextSub !== undefined) {
+    nextSub.prevSub = prevSub
+  } else {
+    dep.subsTail = prevSub
+  }
+  if (prevSub !== undefined) {
+    prevSub.nextSub = nextSub
+  } else if ((dep.subs = nextSub) === undefined) {
+    let toRemove = dep.deps
+    if (toRemove !== undefined) {
+      do {
+        toRemove = unlink(toRemove, dep)
+      } while (toRemove !== undefined)
+      dep.flags |= ReactiveFlags.Dirty
+    }
+  }
+  return nextDep
+}
+
+export function propagate(link: Link): void {
+  let next = link.nextSub
+  let stack: Stack<Link | undefined> | undefined
+
+  top: do {
+    const sub = link.sub
+
+    let flags = sub.flags
+
+    if (flags & (ReactiveFlags.Mutable | ReactiveFlags.Watching)) {
+      if (
+        !(
+          flags &
+          (ReactiveFlags.RecursedCheck |
+            ReactiveFlags.Recursed |
+            ReactiveFlags.Dirty |
+            ReactiveFlags.Pending)
+        )
+      ) {
+        sub.flags = flags | ReactiveFlags.Pending
+      } else if (
+        !(flags & (ReactiveFlags.RecursedCheck | ReactiveFlags.Recursed))
+      ) {
+        flags = ReactiveFlags.None
+      } else if (!(flags & ReactiveFlags.RecursedCheck)) {
+        sub.flags = (flags & ~ReactiveFlags.Recursed) | ReactiveFlags.Pending
+      } else if (
+        !(flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)) &&
+        isValidLink(link, sub)
+      ) {
+        sub.flags = flags | ReactiveFlags.Recursed | ReactiveFlags.Pending
+        flags &= ReactiveFlags.Mutable
+      } else {
+        flags = ReactiveFlags.None
+      }
+
+      if (flags & ReactiveFlags.Watching) {
+        notifyBuffer[notifyBufferLength++] = sub as Effect
+      }
+
+      if (flags & ReactiveFlags.Mutable) {
+        const subSubs = sub.subs
+        if (subSubs !== undefined) {
+          link = subSubs
+          if (subSubs.nextSub !== undefined) {
+            stack = { value: next, prev: stack }
+            next = link.nextSub
+          }
+          continue
+        }
+      }
+    }
+
+    if ((link = next!) !== undefined) {
+      next = link.nextSub
+      continue
+    }
+
+    while (stack !== undefined) {
+      link = stack.value!
+      stack = stack.prev
+      if (link !== undefined) {
+        next = link.nextSub
+        continue top
+      }
+    }
+
+    break
+  } while (true)
+}
+
+export function startTracking(sub: ReactiveNode): ReactiveNode | undefined {
+  sub.depsTail = undefined
+  sub.flags =
+    (sub.flags &
+      ~(ReactiveFlags.Recursed | ReactiveFlags.Dirty | ReactiveFlags.Pending)) |
+    ReactiveFlags.RecursedCheck
+  return setActiveSub(sub)
+}
+
+export function endTracking(
+  sub: ReactiveNode,
+  prevSub: ReactiveNode | undefined,
+): void {
+  if (__DEV__ && activeSub !== sub) {
+    warn(
+      'Active effect was not restored correctly - ' +
+        'this is likely a Vue internal bug.',
+    )
+  }
+  activeSub = prevSub
+
+  const depsTail = sub.depsTail
+  let toRemove = depsTail !== undefined ? depsTail.nextDep : sub.deps
+  while (toRemove !== undefined) {
+    toRemove = unlink(toRemove, sub)
+  }
+  sub.flags &= ~ReactiveFlags.RecursedCheck
+}
+
+export function flush(): void {
+  while (notifyIndex < notifyBufferLength) {
+    const effect = notifyBuffer[notifyIndex]!
+    notifyBuffer[notifyIndex++] = undefined
+    effect.notify()
+  }
+  notifyIndex = 0
+  notifyBufferLength = 0
+}
+
+export function checkDirty(link: Link, sub: ReactiveNode): boolean {
+  let stack: Stack<Link> | undefined
+  let checkDepth = 0
+
+  top: do {
+    const dep = link.dep
+    const depFlags = dep.flags
+
+    let dirty = false
+
+    if (sub.flags & ReactiveFlags.Dirty) {
+      dirty = true
+    } else if (
+      (depFlags & (ReactiveFlags.Mutable | ReactiveFlags.Dirty)) ===
+      (ReactiveFlags.Mutable | ReactiveFlags.Dirty)
+    ) {
+      if ((dep as Computed).update()) {
+        const subs = dep.subs!
+        if (subs.nextSub !== undefined) {
+          shallowPropagate(subs)
+        }
+        dirty = true
+      }
+    } else if (
+      (depFlags & (ReactiveFlags.Mutable | ReactiveFlags.Pending)) ===
+      (ReactiveFlags.Mutable | ReactiveFlags.Pending)
+    ) {
+      if (link.nextSub !== undefined || link.prevSub !== undefined) {
+        stack = { value: link, prev: stack }
+      }
+      link = dep.deps!
+      sub = dep
+      ++checkDepth
+      continue
+    }
+
+    if (!dirty && link.nextDep !== undefined) {
+      link = link.nextDep
+      continue
+    }
+
+    while (checkDepth) {
+      --checkDepth
+      const firstSub = sub.subs!
+      const hasMultipleSubs = firstSub.nextSub !== undefined
+      if (hasMultipleSubs) {
+        link = stack!.value
+        stack = stack!.prev
+      } else {
+        link = firstSub
+      }
+      if (dirty) {
+        if ((sub as Computed).update()) {
+          if (hasMultipleSubs) {
+            shallowPropagate(firstSub)
+          }
+          sub = link.sub
+          continue
+        }
+      } else {
+        sub.flags &= ~ReactiveFlags.Pending
+      }
+      sub = link.sub
+      if (link.nextDep !== undefined) {
+        link = link.nextDep
+        continue top
+      }
+      dirty = false
+    }
+
+    return dirty
+  } while (true)
+}
+
+export function shallowPropagate(link: Link): void {
+  do {
+    const sub = link.sub
+    const nextSub = link.nextSub
+    const subFlags = sub.flags
+    if (
+      (subFlags & (ReactiveFlags.Pending | ReactiveFlags.Dirty)) ===
+      ReactiveFlags.Pending
+    ) {
+      sub.flags = subFlags | ReactiveFlags.Dirty
+    }
+    link = nextSub!
+  } while (link !== undefined)
+}
+
+function isValidLink(checkLink: Link, sub: ReactiveNode): boolean {
+  const depsTail = sub.depsTail
+  if (depsTail !== undefined) {
+    let link = sub.deps!
+    do {
+      if (link === checkLink) {
+        return true
+      }
+      if (link === depsTail) {
+        break
+      }
+      link = link.nextDep!
+    } while (link !== undefined)
+  }
+  return false
+}
diff --git a/packages/reactivity/src/watch.ts b/packages/reactivity/src/watch.ts
index 648e6481b18..532169a2dde 100644
--- a/packages/reactivity/src/watch.ts
+++ b/packages/reactivity/src/watch.ts
@@ -8,22 +8,14 @@ import {
   isObject,
   isPlainObject,
   isSet,
-  remove,
 } from '@vue/shared'
-import { warn } from './warning'
 import type { ComputedRef } from './computed'
 import { ReactiveFlags } from './constants'
-import {
-  type DebuggerOptions,
-  EffectFlags,
-  type EffectScheduler,
-  ReactiveEffect,
-  pauseTracking,
-  resetTracking,
-} from './effect'
+import { type DebuggerOptions, ReactiveEffect, cleanup } from './effect'
 import { isReactive, isShallow } from './reactive'
 import { type Ref, isRef } from './ref'
-import { getCurrentScope } from './effectScope'
+import { setActiveSub } from './system'
+import { warn } from './warning'
 
 // These errors were transferred from `packages/runtime-core/src/errorHandling.ts`
 // to @vue/reactivity to allow co-location with the moved base watch logic, hence
@@ -50,12 +42,7 @@ export interface WatchOptions<Immediate = boolean> extends DebuggerOptions {
   immediate?: Immediate
   deep?: boolean | number
   once?: boolean
-  scheduler?: WatchScheduler
   onWarn?: (msg: string, ...args: any[]) => void
-  /**
-   * @internal
-   */
-  augmentJob?: (job: (...args: any[]) => void) => void
   /**
    * @internal
    */
@@ -77,10 +64,7 @@ export interface WatchHandle extends WatchStopHandle {
 // initial value for watchers to trigger on undefined initial values
 const INITIAL_WATCHER_VALUE = {}
 
-export type WatchScheduler = (job: () => void, isFirstRun: boolean) => void
-
-const cleanupMap: WeakMap<ReactiveEffect, (() => void)[]> = new WeakMap()
-let activeWatcher: ReactiveEffect | undefined = undefined
+let activeWatcher: WatcherEffect | undefined = undefined
 
 /**
  * Returns the current active effect if there is one.
@@ -103,12 +87,16 @@ export function getCurrentWatcher(): ReactiveEffect<any> | undefined {
 export function onWatcherCleanup(
   cleanupFn: () => void,
   failSilently = false,
-  owner: ReactiveEffect | undefined = activeWatcher,
+  owner: WatcherEffect | undefined = activeWatcher,
 ): void {
   if (owner) {
-    let cleanups = cleanupMap.get(owner)
-    if (!cleanups) cleanupMap.set(owner, (cleanups = []))
-    cleanups.push(cleanupFn)
+    const { call } = owner.options
+    if (call) {
+      owner.cleanups[owner.cleanupsLength++] = () =>
+        call(cleanupFn, WatchErrorCodes.WATCH_CLEANUP)
+    } else {
+      owner.cleanups[owner.cleanupsLength++] = cleanupFn
+    }
   } else if (__DEV__ && !failSilently) {
     warn(
       `onWatcherCleanup() was called when there was no active watcher` +
@@ -117,215 +105,187 @@ export function onWatcherCleanup(
   }
 }
 
-export function watch(
-  source: WatchSource | WatchSource[] | WatchEffect | object,
-  cb?: WatchCallback | null,
-  options: WatchOptions = EMPTY_OBJ,
-): WatchHandle {
-  const { immediate, deep, once, scheduler, augmentJob, call } = options
-
-  const warnInvalidSource = (s: unknown) => {
-    ;(options.onWarn || warn)(
-      `Invalid watch source: `,
-      s,
-      `A watch source can only be a getter/effect function, a ref, ` +
-        `a reactive object, or an array of these types.`,
-    )
-  }
-
-  const reactiveGetter = (source: object) => {
-    // traverse will happen in wrapped getter below
-    if (deep) return source
-    // for `deep: false | 0` or shallow reactive, only traverse root-level properties
-    if (isShallow(source) || deep === false || deep === 0)
-      return traverse(source, 1)
-    // for `deep: undefined` on a reactive object, deeply traverse all properties
-    return traverse(source)
-  }
-
-  let effect: ReactiveEffect
-  let getter: () => any
-  let cleanup: (() => void) | undefined
-  let boundCleanup: typeof onWatcherCleanup
-  let forceTrigger = false
-  let isMultiSource = false
-
-  if (isRef(source)) {
-    getter = () => source.value
-    forceTrigger = isShallow(source)
-  } else if (isReactive(source)) {
-    getter = () => reactiveGetter(source)
-    forceTrigger = true
-  } else if (isArray(source)) {
-    isMultiSource = true
-    forceTrigger = source.some(s => isReactive(s) || isShallow(s))
-    getter = () =>
-      source.map(s => {
-        if (isRef(s)) {
-          return s.value
-        } else if (isReactive(s)) {
-          return reactiveGetter(s)
-        } else if (isFunction(s)) {
-          return call ? call(s, WatchErrorCodes.WATCH_GETTER) : s()
-        } else {
-          __DEV__ && warnInvalidSource(s)
-        }
-      })
-  } else if (isFunction(source)) {
-    if (cb) {
-      // getter with cb
-      getter = call
-        ? () => call(source, WatchErrorCodes.WATCH_GETTER)
-        : (source as () => any)
-    } else {
-      // no cb -> simple effect
-      getter = () => {
-        if (cleanup) {
-          pauseTracking()
+export class WatcherEffect extends ReactiveEffect {
+  forceTrigger: boolean
+  isMultiSource: boolean
+  oldValue: any
+  boundCleanup: typeof onWatcherCleanup = fn =>
+    onWatcherCleanup(fn, false, this)
+
+  constructor(
+    source: WatchSource | WatchSource[] | WatchEffect | object,
+    public cb?: WatchCallback<any, any> | null | undefined,
+    public options: WatchOptions = EMPTY_OBJ,
+  ) {
+    const { deep, once, call, onWarn } = options
+
+    let getter: () => any
+    let forceTrigger = false
+    let isMultiSource = false
+
+    if (isRef(source)) {
+      getter = () => source.value
+      forceTrigger = isShallow(source)
+    } else if (isReactive(source)) {
+      getter = () => reactiveGetter(source, deep)
+      forceTrigger = true
+    } else if (isArray(source)) {
+      isMultiSource = true
+      forceTrigger = source.some(s => isReactive(s) || isShallow(s))
+      getter = () =>
+        source.map(s => {
+          if (isRef(s)) {
+            return s.value
+          } else if (isReactive(s)) {
+            return reactiveGetter(s, deep)
+          } else if (isFunction(s)) {
+            return call ? call(s, WatchErrorCodes.WATCH_GETTER) : s()
+          } else {
+            __DEV__ && warnInvalidSource(s, onWarn)
+          }
+        })
+    } else if (isFunction(source)) {
+      if (cb) {
+        // getter with cb
+        getter = call
+          ? () => call(source, WatchErrorCodes.WATCH_GETTER)
+          : (source as () => any)
+      } else {
+        // no cb -> simple effect
+        getter = () => {
+          if (this.cleanupsLength) {
+            const prevSub = setActiveSub()
+            try {
+              cleanup(this)
+            } finally {
+              setActiveSub(prevSub)
+            }
+          }
+          const currentEffect = activeWatcher
+          activeWatcher = this
           try {
-            cleanup()
+            return call
+              ? call(source, WatchErrorCodes.WATCH_CALLBACK, [
+                  this.boundCleanup,
+                ])
+              : source(this.boundCleanup)
           } finally {
-            resetTracking()
+            activeWatcher = currentEffect
           }
         }
-        const currentEffect = activeWatcher
-        activeWatcher = effect
-        try {
-          return call
-            ? call(source, WatchErrorCodes.WATCH_CALLBACK, [boundCleanup])
-            : source(boundCleanup)
-        } finally {
-          activeWatcher = currentEffect
-        }
       }
+    } else {
+      getter = NOOP
+      __DEV__ && warnInvalidSource(source, onWarn)
     }
-  } else {
-    getter = NOOP
-    __DEV__ && warnInvalidSource(source)
-  }
 
-  if (cb && deep) {
-    const baseGetter = getter
-    const depth = deep === true ? Infinity : deep
-    getter = () => traverse(baseGetter(), depth)
-  }
+    if (cb && deep) {
+      const baseGetter = getter
+      const depth = deep === true ? Infinity : deep
+      getter = () => traverse(baseGetter(), depth)
+    }
 
-  const scope = getCurrentScope()
-  const watchHandle: WatchHandle = () => {
-    effect.stop()
-    if (scope && scope.active) {
-      remove(scope.effects, effect)
+    super(getter)
+    this.forceTrigger = forceTrigger
+    this.isMultiSource = isMultiSource
+
+    if (once && cb) {
+      const _cb = cb
+      cb = (...args) => {
+        _cb(...args)
+        this.stop()
+      }
     }
-  }
 
-  if (once && cb) {
-    const _cb = cb
-    cb = (...args) => {
-      _cb(...args)
-      watchHandle()
+    this.cb = cb
+
+    this.oldValue = isMultiSource
+      ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
+      : INITIAL_WATCHER_VALUE
+
+    if (__DEV__) {
+      this.onTrack = options.onTrack
+      this.onTrigger = options.onTrigger
     }
   }
 
-  let oldValue: any = isMultiSource
-    ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
-    : INITIAL_WATCHER_VALUE
-
-  const job = (immediateFirstRun?: boolean) => {
-    if (
-      !(effect.flags & EffectFlags.ACTIVE) ||
-      (!effect.dirty && !immediateFirstRun)
-    ) {
+  run(initialRun = false): void {
+    const oldValue = this.oldValue
+    const newValue = (this.oldValue = super.run())
+    if (!this.cb) {
       return
     }
-    if (cb) {
-      // watch(source, cb)
-      const newValue = effect.run()
-      if (
-        deep ||
-        forceTrigger ||
-        (isMultiSource
-          ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
-          : hasChanged(newValue, oldValue))
-      ) {
-        // cleanup before running cb again
-        if (cleanup) {
-          cleanup()
-        }
-        const currentWatcher = activeWatcher
-        activeWatcher = effect
-        try {
-          const args = [
-            newValue,
-            // pass undefined as the old value when it's changed for the first time
-            oldValue === INITIAL_WATCHER_VALUE
-              ? undefined
-              : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
-                ? []
-                : oldValue,
-            boundCleanup,
-          ]
-          oldValue = newValue
-          call
-            ? call(cb!, WatchErrorCodes.WATCH_CALLBACK, args)
-            : // @ts-expect-error
-              cb!(...args)
-        } finally {
-          activeWatcher = currentWatcher
-        }
+    const { immediate, deep, call } = this.options
+    if (initialRun && !immediate) {
+      return
+    }
+    if (
+      deep ||
+      this.forceTrigger ||
+      (this.isMultiSource
+        ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
+        : hasChanged(newValue, oldValue))
+    ) {
+      // cleanup before running cb again
+      cleanup(this)
+      const currentWatcher = activeWatcher
+      activeWatcher = this
+      try {
+        const args = [
+          newValue,
+          // pass undefined as the old value when it's changed for the first time
+          oldValue === INITIAL_WATCHER_VALUE
+            ? undefined
+            : this.isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
+              ? []
+              : oldValue,
+          this.boundCleanup,
+        ]
+        call
+          ? call(this.cb, WatchErrorCodes.WATCH_CALLBACK, args)
+          : // @ts-expect-error
+            this.cb(...args)
+      } finally {
+        activeWatcher = currentWatcher
       }
-    } else {
-      // watchEffect
-      effect.run()
     }
   }
+}
 
-  if (augmentJob) {
-    augmentJob(job)
-  }
-
-  effect = new ReactiveEffect(getter)
-
-  effect.scheduler = scheduler
-    ? () => scheduler(job, false)
-    : (job as EffectScheduler)
-
-  boundCleanup = fn => onWatcherCleanup(fn, false, effect)
+function reactiveGetter(source: object, deep: WatchOptions['deep']): unknown {
+  // traverse will happen in wrapped getter below
+  if (deep) return source
+  // for `deep: false | 0` or shallow reactive, only traverse root-level properties
+  if (isShallow(source) || deep === false || deep === 0)
+    return traverse(source, 1)
+  // for `deep: undefined` on a reactive object, deeply traverse all properties
+  return traverse(source)
+}
 
-  cleanup = effect.onStop = () => {
-    const cleanups = cleanupMap.get(effect)
-    if (cleanups) {
-      if (call) {
-        call(cleanups, WatchErrorCodes.WATCH_CLEANUP)
-      } else {
-        for (const cleanup of cleanups) cleanup()
-      }
-      cleanupMap.delete(effect)
-    }
-  }
+function warnInvalidSource(s: object, onWarn: WatchOptions['onWarn']): void {
+  ;(onWarn || warn)(
+    `Invalid watch source: `,
+    s,
+    `A watch source can only be a getter/effect function, a ref, ` +
+      `a reactive object, or an array of these types.`,
+  )
+}
 
-  if (__DEV__) {
-    effect.onTrack = options.onTrack
-    effect.onTrigger = options.onTrigger
-  }
+export function watch(
+  source: WatchSource | WatchSource[] | WatchEffect | object,
+  cb?: WatchCallback | null,
+  options: WatchOptions = EMPTY_OBJ,
+): WatchHandle {
+  const effect = new WatcherEffect(source, cb, options)
 
-  // initial run
-  if (cb) {
-    if (immediate) {
-      job(true)
-    } else {
-      oldValue = effect.run()
-    }
-  } else if (scheduler) {
-    scheduler(job.bind(null, true), true)
-  } else {
-    effect.run()
-  }
+  effect.run(true)
 
-  watchHandle.pause = effect.pause.bind(effect)
-  watchHandle.resume = effect.resume.bind(effect)
-  watchHandle.stop = watchHandle
+  const stop = effect.stop.bind(effect) as WatchHandle
+  stop.pause = effect.pause.bind(effect)
+  stop.resume = effect.resume.bind(effect)
+  stop.stop = stop
 
-  return watchHandle
+  return stop
 }
 
 export function traverse(
diff --git a/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts b/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts
index 30c8951f405..aa37bc4f49f 100644
--- a/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts
+++ b/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts
@@ -1,3 +1,8 @@
+import {
+  type ComputedRefImpl,
+  type ReactiveEffectRunner,
+  effect,
+} from '@vue/reactivity'
 import {
   type ComponentInternalInstance,
   type SetupContext,
@@ -25,8 +30,6 @@ import {
   withAsyncContext,
   withDefaults,
 } from '../src/apiSetupHelpers'
-import type { ComputedRefImpl } from '../../reactivity/src/computed'
-import { EffectFlags, type ReactiveEffectRunner, effect } from '@vue/reactivity'
 
 describe('SFC <script setup> helpers', () => {
   test('should warn runtime usage', () => {
@@ -450,12 +453,12 @@ describe('SFC <script setup> helpers', () => {
       app.mount(root)
 
       await ready
-      expect(e!.effect.flags & EffectFlags.ACTIVE).toBeTruthy()
-      expect(c!.flags & EffectFlags.TRACKING).toBeTruthy()
+      expect(e!.effect.active).toBeTruthy()
+      expect(c!.flags & 2 /* SubscriberFlags.Tracking */).toBe(0)
 
       app.unmount()
-      expect(e!.effect.flags & EffectFlags.ACTIVE).toBeFalsy()
-      expect(c!.flags & EffectFlags.TRACKING).toBeFalsy()
+      expect(e!.effect.active).toBeFalsy()
+      expect(c!.flags & 2 /* SubscriberFlags.Tracking */).toBe(0)
     })
   })
 })
diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts
index ff06fbea774..80a8b434502 100644
--- a/packages/runtime-core/__tests__/apiWatch.spec.ts
+++ b/packages/runtime-core/__tests__/apiWatch.spec.ts
@@ -25,7 +25,9 @@ import {
 } from '@vue/runtime-test'
 import {
   type DebuggerEvent,
+  type EffectScope,
   ITERATE_KEY,
+  ReactiveEffect,
   type Ref,
   type ShallowRef,
   TrackOpTypes,
@@ -503,6 +505,52 @@ describe('api: watch', () => {
     expect(cleanupWatch).toHaveBeenCalledTimes(2)
   })
 
+  it('nested calls to baseWatch and onWatcherCleanup', async () => {
+    let calls: string[] = []
+    let source: Ref<number>
+    let copyist: Ref<number>
+    const scope = effectScope()
+
+    scope.run(() => {
+      source = ref(0)
+      copyist = ref(0)
+      // sync flush
+      watchEffect(
+        () => {
+          const current = (copyist.value = source.value)
+          onWatcherCleanup(() => calls.push(`sync ${current}`))
+        },
+        { flush: 'sync' },
+      )
+      // post flush
+      watchEffect(
+        () => {
+          const current = copyist.value
+          onWatcherCleanup(() => calls.push(`post ${current}`))
+        },
+        { flush: 'post' },
+      )
+    })
+
+    await nextTick()
+    expect(calls).toEqual([])
+
+    scope.run(() => source.value++)
+    expect(calls).toEqual(['sync 0'])
+    await nextTick()
+    expect(calls).toEqual(['sync 0', 'post 0'])
+    calls.length = 0
+
+    scope.run(() => source.value++)
+    expect(calls).toEqual(['sync 1'])
+    await nextTick()
+    expect(calls).toEqual(['sync 1', 'post 1'])
+    calls.length = 0
+
+    scope.stop()
+    expect(calls).toEqual(['sync 2', 'post 2'])
+  })
+
   it('flush timing: pre (default)', async () => {
     const count = ref(0)
     const count2 = ref(0)
@@ -1332,16 +1380,15 @@ describe('api: watch', () => {
     render(h(Comp), nodeOps.createElement('div'))
 
     expect(instance!).toBeDefined()
-    expect(instance!.scope.effects).toBeInstanceOf(Array)
     // includes the component's own render effect AND the watcher effect
-    expect(instance!.scope.effects.length).toBe(2)
+    expect(getEffectsCount(instance!.scope)).toBe(2)
 
     _show!.value = false
 
     await nextTick()
     await nextTick()
 
-    expect(instance!.scope.effects.length).toBe(0)
+    expect(getEffectsCount(instance!.scope)).toBe(0)
   })
 
   test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => {
@@ -1489,7 +1536,7 @@ describe('api: watch', () => {
     createApp(Comp).mount(root)
     // should not record watcher in detached scope and only the instance's
     // own update effect
-    expect(instance!.scope.effects.length).toBe(1)
+    expect(getEffectsCount(instance!.scope)).toBe(1)
   })
 
   test('watchEffect should keep running if created in a detached scope', async () => {
@@ -1796,9 +1843,9 @@ describe('api: watch', () => {
     }
     const root = nodeOps.createElement('div')
     createApp(Comp).mount(root)
-    expect(instance!.scope.effects.length).toBe(2)
+    expect(getEffectsCount(instance!.scope)).toBe(2)
     unwatch!()
-    expect(instance!.scope.effects.length).toBe(1)
+    expect(getEffectsCount(instance!.scope)).toBe(1)
 
     const scope = effectScope()
     scope.run(() => {
@@ -1806,14 +1853,14 @@ describe('api: watch', () => {
         console.log(num.value)
       })
     })
-    expect(scope.effects.length).toBe(1)
+    expect(getEffectsCount(scope)).toBe(1)
     unwatch!()
-    expect(scope.effects.length).toBe(0)
+    expect(getEffectsCount(scope)).toBe(0)
 
     scope.run(() => {
       watch(num, () => {}, { once: true, immediate: true })
     })
-    expect(scope.effects.length).toBe(0)
+    expect(getEffectsCount(scope)).toBe(0)
   })
 
   // simplified case of VueUse syncRef
@@ -2011,3 +2058,13 @@ describe('api: watch', () => {
     expect(onCleanup).toBeCalledTimes(0)
   })
 })
+
+function getEffectsCount(scope: EffectScope): number {
+  let n = 0
+  for (let dep = scope.deps; dep !== undefined; dep = dep.nextDep) {
+    if (dep.dep instanceof ReactiveEffect) {
+      n++
+    }
+  }
+  return n
+}
diff --git a/packages/runtime-core/__tests__/componentProps.spec.ts b/packages/runtime-core/__tests__/componentProps.spec.ts
index b8eb0e47208..ffae1085cd8 100644
--- a/packages/runtime-core/__tests__/componentProps.spec.ts
+++ b/packages/runtime-core/__tests__/componentProps.spec.ts
@@ -826,7 +826,7 @@ describe('component props', () => {
     )
   })
 
-  // #691ef
+  // #6915
   test('should not mutate original props long-form definition object', () => {
     const props = {
       msg: {
diff --git a/packages/runtime-core/__tests__/directives.spec.ts b/packages/runtime-core/__tests__/directives.spec.ts
index 8d9982548b5..96ca009e1a0 100644
--- a/packages/runtime-core/__tests__/directives.spec.ts
+++ b/packages/runtime-core/__tests__/directives.spec.ts
@@ -11,7 +11,7 @@ import {
   withDirectives,
 } from '@vue/runtime-test'
 import {
-  type ComponentInternalInstance,
+  type GenericComponentInstance,
   currentInstance,
 } from '../src/component'
 
@@ -111,7 +111,7 @@ describe('directives', () => {
       unmounted,
     }
 
-    let _instance: ComponentInternalInstance | null = null
+    let _instance: GenericComponentInstance | null = null
     let _vnode: VNode | null = null
     let _prevVnode: VNode | null = null
     const Comp = {
@@ -171,7 +171,7 @@ describe('directives', () => {
       expect(prevVNode).toBe(_prevVnode)
     }) as DirectiveHook)
 
-    let _instance: ComponentInternalInstance | null = null
+    let _instance: GenericComponentInstance | null = null
     let _vnode: VNode | null = null
     let _prevVnode: VNode | null = null
     const Comp = {
@@ -300,7 +300,7 @@ describe('directives', () => {
       unmounted,
     }
 
-    let _instance: ComponentInternalInstance | null = null
+    let _instance: GenericComponentInstance | null = null
     let _vnode: VNode | null = null
     let _prevVnode: VNode | null = null
 
diff --git a/packages/runtime-core/__tests__/errorHandling.spec.ts b/packages/runtime-core/__tests__/errorHandling.spec.ts
index 0cd3efa588c..e31a7539d75 100644
--- a/packages/runtime-core/__tests__/errorHandling.spec.ts
+++ b/packages/runtime-core/__tests__/errorHandling.spec.ts
@@ -531,6 +531,9 @@ describe('error handling', () => {
       caughtError = caught
     }
     expect(fn).toHaveBeenCalledWith(err, 'setup function')
+    expect(
+      `Active effect was not restored correctly - this is likely a Vue internal bug.`,
+    ).toHaveBeenWarned()
     expect(
       `Unhandled error during execution of setup function`,
     ).toHaveBeenWarned()
diff --git a/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts b/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts
index 45d00579fee..73e67aab2a0 100644
--- a/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts
+++ b/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts
@@ -573,7 +573,7 @@ describe('attribute fallthrough', () => {
     const Child = {
       props: [],
       render() {
-        return openBlock(), createBlock('div')
+        return (openBlock(), createBlock('div'))
       },
     }
 
diff --git a/packages/runtime-core/__tests__/rendererComponent.spec.ts b/packages/runtime-core/__tests__/rendererComponent.spec.ts
index fefc4137034..fa3c192e885 100644
--- a/packages/runtime-core/__tests__/rendererComponent.spec.ts
+++ b/packages/runtime-core/__tests__/rendererComponent.spec.ts
@@ -46,7 +46,7 @@ describe('renderer: component', () => {
     expect(parentVnode!.el).toBe(childVnode2!.el)
   })
 
-  it('should create an Component with props', () => {
+  it('should create a component with props', () => {
     const Comp = {
       render: () => {
         return h('div')
@@ -57,7 +57,7 @@ describe('renderer: component', () => {
     expect(serializeInner(root)).toBe(`<div id="foo" class="bar"></div>`)
   })
 
-  it('should create an Component with direct text children', () => {
+  it('should create a component with direct text children', () => {
     const Comp = {
       render: () => {
         return h('div', 'test')
diff --git a/packages/runtime-core/__tests__/rendererFragment.spec.ts b/packages/runtime-core/__tests__/rendererFragment.spec.ts
index 81cf7b8df31..3394a41b2a0 100644
--- a/packages/runtime-core/__tests__/rendererFragment.spec.ts
+++ b/packages/runtime-core/__tests__/rendererFragment.spec.ts
@@ -416,7 +416,7 @@ describe('renderer: fragment', () => {
     const root = nodeOps.createElement('div')
 
     const renderFn = () => {
-      return openBlock(true), createBlock(Fragment, null)
+      return (openBlock(true), createBlock(Fragment, null))
     }
 
     render(renderFn(), root)
diff --git a/packages/runtime-core/__tests__/scheduler.spec.ts b/packages/runtime-core/__tests__/scheduler.spec.ts
index 3e454aec023..90b27eaf4e3 100644
--- a/packages/runtime-core/__tests__/scheduler.spec.ts
+++ b/packages/runtime-core/__tests__/scheduler.spec.ts
@@ -49,8 +49,8 @@ describe('scheduler', () => {
       const job1 = () => {
         calls.push('job1')
 
-        queueJob(job2)
-        queueJob(job3)
+        queueJob(job2, 10)
+        queueJob(job3, 1)
       }
 
       const job2 = () => {
@@ -58,12 +58,10 @@ describe('scheduler', () => {
         queueJob(job4)
         queueJob(job5)
       }
-      job2.id = 10
 
       const job3 = () => {
         calls.push('job3')
       }
-      job3.id = 1
 
       const job4 = () => {
         calls.push('job4')
@@ -125,9 +123,8 @@ describe('scheduler', () => {
         calls.push('cb1')
         queueJob(job1)
       }
-      cb1.flags! |= SchedulerJobFlags.PRE
 
-      queueJob(cb1)
+      queueJob(cb1, undefined, true)
       await nextTick()
       expect(calls).toEqual(['cb1', 'job1'])
     })
@@ -137,30 +134,23 @@ describe('scheduler', () => {
       const job1 = () => {
         calls.push('job1')
       }
-      job1.id = 1
 
       const cb1: SchedulerJob = () => {
         calls.push('cb1')
-        queueJob(job1)
+        queueJob(job1, 1)
         // cb2 should execute before the job
-        queueJob(cb2)
-        queueJob(cb3)
+        queueJob(cb2, 1, true)
+        queueJob(cb3, 1, true)
       }
-      cb1.flags! |= SchedulerJobFlags.PRE
 
       const cb2: SchedulerJob = () => {
         calls.push('cb2')
       }
-      cb2.flags! |= SchedulerJobFlags.PRE
-      cb2.id = 1
-
       const cb3: SchedulerJob = () => {
         calls.push('cb3')
       }
-      cb3.flags! |= SchedulerJobFlags.PRE
-      cb3.id = 1
 
-      queueJob(cb1)
+      queueJob(cb1, undefined, true)
       await nextTick()
       expect(calls).toEqual(['cb1', 'cb2', 'cb3', 'job1'])
     })
@@ -170,41 +160,30 @@ describe('scheduler', () => {
       const job1: SchedulerJob = () => {
         calls.push('job1')
       }
-      job1.id = 1
-      job1.flags! |= SchedulerJobFlags.PRE
       const job2: SchedulerJob = () => {
         calls.push('job2')
-        queueJob(job5)
-        queueJob(job6)
+        queueJob(job5, 2)
+        queueJob(job6, 2, true)
       }
-      job2.id = 2
-      job2.flags! |= SchedulerJobFlags.PRE
       const job3: SchedulerJob = () => {
         calls.push('job3')
       }
-      job3.id = 2
-      job3.flags! |= SchedulerJobFlags.PRE
       const job4: SchedulerJob = () => {
         calls.push('job4')
       }
-      job4.id = 3
-      job4.flags! |= SchedulerJobFlags.PRE
       const job5: SchedulerJob = () => {
         calls.push('job5')
       }
-      job5.id = 2
       const job6: SchedulerJob = () => {
         calls.push('job6')
       }
-      job6.id = 2
-      job6.flags! |= SchedulerJobFlags.PRE
 
       // We need several jobs to test this properly, otherwise
       // findInsertionIndex can yield the correct index by chance
-      queueJob(job4)
-      queueJob(job2)
-      queueJob(job3)
-      queueJob(job1)
+      queueJob(job4, 3, true)
+      queueJob(job2, 2, true)
+      queueJob(job3, 2, true)
+      queueJob(job1, 1, true)
 
       await nextTick()
       expect(calls).toEqual(['job1', 'job2', 'job3', 'job6', 'job5', 'job4'])
@@ -217,8 +196,8 @@ describe('scheduler', () => {
         // when updating the props of a child component. This is handled
         // directly inside `updateComponentPreRender` to avoid non atomic
         // cb triggers (#1763)
-        queueJob(cb1)
-        queueJob(cb2)
+        queueJob(cb1, undefined, true)
+        queueJob(cb2, undefined, true)
         flushPreFlushCbs()
         calls.push('job1')
       }
@@ -227,11 +206,9 @@ describe('scheduler', () => {
         // a cb triggers its parent job, which should be skipped
         queueJob(job1)
       }
-      cb1.flags! |= SchedulerJobFlags.PRE
       const cb2: SchedulerJob = () => {
         calls.push('cb2')
       }
-      cb2.flags! |= SchedulerJobFlags.PRE
 
       queueJob(job1)
       await nextTick()
@@ -242,29 +219,24 @@ describe('scheduler', () => {
       const calls: string[] = []
       const job1: SchedulerJob = () => {
         calls.push('job1')
-        queueJob(job3)
-        queueJob(job4)
+        queueJob(job3, undefined, true)
+        queueJob(job4, undefined, true)
       }
       // job1 has no id
-      job1.flags! |= SchedulerJobFlags.PRE
       const job2: SchedulerJob = () => {
         calls.push('job2')
       }
-      job2.id = 1
-      job2.flags! |= SchedulerJobFlags.PRE
       const job3: SchedulerJob = () => {
         calls.push('job3')
       }
       // job3 has no id
-      job3.flags! |= SchedulerJobFlags.PRE
       const job4: SchedulerJob = () => {
         calls.push('job4')
       }
       // job4 has no id
-      job4.flags! |= SchedulerJobFlags.PRE
 
-      queueJob(job1)
-      queueJob(job2)
+      queueJob(job1, undefined, true)
+      queueJob(job2, 1, true)
       await nextTick()
       expect(calls).toEqual(['job1', 'job3', 'job4', 'job2'])
     })
@@ -273,9 +245,8 @@ describe('scheduler', () => {
     it('queue preFlushCb inside postFlushCb', async () => {
       const spy = vi.fn()
       const cb: SchedulerJob = () => spy()
-      cb.flags! |= SchedulerJobFlags.PRE
       queuePostFlushCb(() => {
-        queueJob(cb)
+        queueJob(cb, undefined, true)
       })
       await nextTick()
       expect(spy).toHaveBeenCalled()
@@ -448,16 +419,13 @@ describe('scheduler', () => {
       const job1: SchedulerJob = () => {
         calls.push('job1')
       }
-      job1.id = 1
-
       const job2: SchedulerJob = () => {
         calls.push('job2')
       }
-      job2.id = 2
 
       queuePostFlushCb(() => {
-        queueJob(job2)
-        queueJob(job1)
+        queueJob(job2, 2)
+        queueJob(job1, 1)
       })
 
       await nextTick()
@@ -471,21 +439,16 @@ describe('scheduler', () => {
     const job1 = () => calls.push('job1')
     // job1 has no id
     const job2 = () => calls.push('job2')
-    job2.id = 2
     const job3 = () => calls.push('job3')
-    job3.id = 1
     const job4: SchedulerJob = () => calls.push('job4')
-    job4.id = 2
-    job4.flags! |= SchedulerJobFlags.PRE
     const job5: SchedulerJob = () => calls.push('job5')
     // job5 has no id
-    job5.flags! |= SchedulerJobFlags.PRE
 
     queueJob(job1)
-    queueJob(job2)
-    queueJob(job3)
-    queueJob(job4)
-    queueJob(job5)
+    queueJob(job2, 2)
+    queueJob(job3, 1)
+    queueJob(job4, 2, true)
+    queueJob(job5, undefined, true)
     await nextTick()
     expect(calls).toEqual(['job5', 'job3', 'job4', 'job2', 'job1'])
   })
@@ -495,13 +458,11 @@ describe('scheduler', () => {
     const cb1 = () => calls.push('cb1')
     // cb1 has no id
     const cb2 = () => calls.push('cb2')
-    cb2.id = 2
     const cb3 = () => calls.push('cb3')
-    cb3.id = 1
 
     queuePostFlushCb(cb1)
-    queuePostFlushCb(cb2)
-    queuePostFlushCb(cb3)
+    queuePostFlushCb(cb2, 2)
+    queuePostFlushCb(cb3, 1)
     await nextTick()
     expect(calls).toEqual(['cb3', 'cb2', 'cb1'])
   })
@@ -550,13 +511,10 @@ describe('scheduler', () => {
         throw err
       }
     })
-    job1.id = 1
-
     const job2: SchedulerJob = vi.fn()
-    job2.id = 2
 
-    queueJob(job1)
-    queueJob(job2)
+    queueJob(job1, 1)
+    queueJob(job2, 2)
 
     try {
       await nextTick()
@@ -570,8 +528,8 @@ describe('scheduler', () => {
     expect(job1).toHaveBeenCalledTimes(1)
     expect(job2).toHaveBeenCalledTimes(0)
 
-    queueJob(job1)
-    queueJob(job2)
+    queueJob(job1, 1)
+    queueJob(job2, 2)
 
     await nextTick()
 
@@ -622,11 +580,10 @@ describe('scheduler', () => {
 
   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)
+    queueJob(job, 1)
+    queueJob(job, 1)
 
     await nextTick()
 
@@ -638,15 +595,14 @@ describe('scheduler', () => {
 
     const job: SchedulerJob = vi.fn(() => {
       if (recurse) {
-        queueJob(job)
-        queueJob(job)
+        queueJob(job, 1)
+        queueJob(job, 1)
         recurse = false
       }
     })
-    job.id = 1
     job.flags = SchedulerJobFlags.ALLOW_RECURSE
 
-    queueJob(job)
+    queueJob(job, 1)
 
     await nextTick()
 
@@ -659,22 +615,19 @@ describe('scheduler', () => {
     const job1: SchedulerJob = () => {
       if (recurse) {
         // job2 is already queued, so this shouldn't do anything
-        queueJob(job2)
+        queueJob(job2, 2)
         recurse = false
       }
     }
-    job1.id = 1
-
     const job2: SchedulerJob = vi.fn(() => {
       if (recurse) {
-        queueJob(job1)
-        queueJob(job2)
+        queueJob(job1, 1)
+        queueJob(job2, 2)
       }
     })
-    job2.id = 2
     job2.flags = SchedulerJobFlags.ALLOW_RECURSE
 
-    queueJob(job2)
+    queueJob(job2, 2)
 
     await nextTick()
 
@@ -685,40 +638,35 @@ describe('scheduler', () => {
     let recurse = true
 
     const job1: SchedulerJob = vi.fn(() => {
-      queueJob(job3)
-      queueJob(job3)
+      queueJob(job3, 3, true)
+      queueJob(job3, 3, true)
       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)
+        queueJob(job2, 2, true)
 
         // job3 is already queued, so this shouldn't do anything
-        queueJob(job3)
+        queueJob(job3, 3, true)
         recurse = false
       }
     })
-    job2.id = 2
-    job2.flags = SchedulerJobFlags.PRE
 
     const job3: SchedulerJob = vi.fn(() => {
       if (recurse) {
-        queueJob(job2)
-        queueJob(job3)
+        queueJob(job2, 2, true)
+        queueJob(job3, 3, true)
 
         // The jobs are already queued, so these should have no effect
-        queueJob(job2)
-        queueJob(job3)
+        queueJob(job2, 2, true)
+        queueJob(job3, 3, true)
       }
     })
-    job3.id = 3
-    job3.flags = SchedulerJobFlags.ALLOW_RECURSE | SchedulerJobFlags.PRE
+    job3.flags = SchedulerJobFlags.ALLOW_RECURSE
 
-    queueJob(job1)
+    queueJob(job1, 1, true)
 
     await nextTick()
 
@@ -775,8 +723,7 @@ describe('scheduler', () => {
       spy()
       flushPreFlushCbs()
     }
-    job.flags! |= SchedulerJobFlags.PRE
-    queueJob(job)
+    queueJob(job, undefined, true)
     await nextTick()
     expect(spy).toHaveBeenCalledTimes(1)
   })
@@ -788,18 +735,14 @@ describe('scheduler', () => {
     const job1: SchedulerJob = () => {
       calls.push('job1')
     }
-    job1.id = 1
-    job1.flags! |= SchedulerJobFlags.PRE
 
     const job2: SchedulerJob = () => {
       calls.push('job2')
     }
-    job2.id = 2
-    job2.flags! |= SchedulerJobFlags.PRE
 
     queuePostFlushCb(() => {
-      queueJob(job2)
-      queueJob(job1)
+      queueJob(job2, 2, true)
+      queueJob(job1, 1, true)
 
       // e.g. nested app.mount() call
       flushPreFlushCbs()
@@ -830,18 +773,26 @@ describe('scheduler', () => {
     const cb1 = () => calls.push('cb1')
     // cb1 has no id
     const cb2 = () => calls.push('cb2')
-    cb2.id = -1
     const queueAndFlush = (hook: Function) => {
       queuePostFlushCb(hook)
       flushPostFlushCbs()
     }
 
     queueAndFlush(() => {
-      queuePostFlushCb([cb1, cb2])
+      queuePostFlushCb(cb1)
+      queuePostFlushCb(cb2, -1)
       flushPostFlushCbs()
     })
 
     await nextTick()
     expect(calls).toEqual(['cb2', 'cb1'])
   })
+
+  test('error in postFlush cb should not cause nextTick to stuck in rejected state forever', async () => {
+    queuePostFlushCb(() => {
+      throw 'err'
+    })
+    await expect(nextTick).rejects.toThrow('err')
+    await expect(nextTick()).resolves.toBeUndefined()
+  })
 })
diff --git a/packages/runtime-core/__tests__/vnode.spec.ts b/packages/runtime-core/__tests__/vnode.spec.ts
index a7f6a2d5684..b4d42d47873 100644
--- a/packages/runtime-core/__tests__/vnode.spec.ts
+++ b/packages/runtime-core/__tests__/vnode.spec.ts
@@ -12,8 +12,8 @@ import {
   openBlock,
   transformVNodeArgs,
 } from '../src/vnode'
-import type { Data } from '../src/component'
 import { PatchFlags, ShapeFlags } from '@vue/shared'
+import type { Data } from '../src/component'
 import { h, isReactive, reactive, ref, setBlockTracking, withCtx } from '../src'
 import { createApp, nodeOps, serializeInner } from '@vue/runtime-test'
 import { setCurrentRenderingInstance } from '../src/componentRenderContext'
diff --git a/packages/runtime-core/package.json b/packages/runtime-core/package.json
index 1e60e27cb7a..d379ef7f4b6 100644
--- a/packages/runtime-core/package.json
+++ b/packages/runtime-core/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/runtime-core",
-  "version": "3.5.17",
+  "version": "3.6.0-alpha.2",
   "description": "@vue/runtime-core",
   "main": "index.js",
   "module": "dist/runtime-core.esm-bundler.js",
diff --git a/packages/runtime-core/src/apiAsyncComponent.ts b/packages/runtime-core/src/apiAsyncComponent.ts
index cb675f06e43..5c5c06c4891 100644
--- a/packages/runtime-core/src/apiAsyncComponent.ts
+++ b/packages/runtime-core/src/apiAsyncComponent.ts
@@ -3,6 +3,7 @@ import {
   type ComponentInternalInstance,
   type ComponentOptions,
   type ConcreteComponent,
+  type GenericComponentInstance,
   currentInstance,
   getComponentName,
   isInSSRComponentSetup,
@@ -40,7 +41,7 @@ export interface AsyncComponentOptions<T = any> {
   ) => any
 }
 
-export const isAsyncWrapper = (i: ComponentInternalInstance | VNode): boolean =>
+export const isAsyncWrapper = (i: GenericComponentInstance | VNode): boolean =>
   !!(i.type as ComponentOptions).__asyncLoader
 
 /*! #__NO_SIDE_EFFECTS__ */
@@ -157,7 +158,7 @@ export function defineAsyncComponent<
     },
 
     setup() {
-      const instance = currentInstance!
+      const instance = currentInstance as ComponentInternalInstance
       markAsyncBoundary(instance)
 
       // already resolved
@@ -220,10 +221,14 @@ export function defineAsyncComponent<
       load()
         .then(() => {
           loaded.value = true
-          if (instance.parent && isKeepAlive(instance.parent.vnode)) {
+          if (
+            instance.parent &&
+            instance.parent.vnode &&
+            isKeepAlive(instance.parent.vnode)
+          ) {
             // parent is keep-alive, force update so the loaded component's
             // name is taken into account
-            instance.parent.update()
+            ;(instance.parent as ComponentInternalInstance).update()
           }
         })
         .catch(err => {
diff --git a/packages/runtime-core/src/apiComputed.ts b/packages/runtime-core/src/apiComputed.ts
index 2ebb2d4e63a..39ec10a15f4 100644
--- a/packages/runtime-core/src/apiComputed.ts
+++ b/packages/runtime-core/src/apiComputed.ts
@@ -1,17 +1,10 @@
-import { type ComputedRefImpl, computed as _computed } from '@vue/reactivity'
-import { getCurrentInstance, isInSSRComponentSetup } from './component'
+import { computed as _computed } from '@vue/reactivity'
+import { isInSSRComponentSetup } from './component'
 
 export const computed: typeof _computed = (
   getterOrOptions: any,
   debugOptions?: any,
 ) => {
   // @ts-expect-error
-  const c = _computed(getterOrOptions, debugOptions, isInSSRComponentSetup)
-  if (__DEV__) {
-    const i = getCurrentInstance()
-    if (i && i.appContext.config.warnRecursiveComputed) {
-      ;(c as unknown as ComputedRefImpl<any>)._warnRecursive = true
-    }
-  }
-  return c as any
+  return _computed(getterOrOptions, debugOptions, isInSSRComponentSetup) as any
 }
diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts
index b69a1ccd540..a1409a7fe44 100644
--- a/packages/runtime-core/src/apiCreateApp.ts
+++ b/packages/runtime-core/src/apiCreateApp.ts
@@ -3,7 +3,8 @@ import {
   type ComponentInternalInstance,
   type ConcreteComponent,
   type Data,
-  getComponentPublicInstance,
+  type GenericComponent,
+  type GenericComponentInstance,
   validateComponentName,
 } from './component'
 import type {
@@ -16,11 +17,14 @@ import type {
   ComponentPublicInstance,
 } from './componentPublicInstance'
 import { type Directive, validateDirectiveName } from './directives'
-import type { ElementNamespace, RootRenderFunction } from './renderer'
+import type {
+  ElementNamespace,
+  RootRenderFunction,
+  UnmountComponentFn,
+} from './renderer'
 import type { InjectionKey } from './apiInject'
 import { warn } from './warning'
-import { type VNode, cloneVNode, createVNode } from './vnode'
-import type { RootHydrateFunction } from './hydration'
+import type { VNode } from './vnode'
 import { devtoolsInitApp, devtoolsUnmountApp } from './devtools'
 import { NO, extend, hasOwn, isFunction, isObject } from '@vue/shared'
 import { version } from '.'
@@ -31,6 +35,7 @@ import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
 import type { DefineComponent } from './apiDefineComponent'
 
 export interface App<HostElement = any> {
+  vapor?: boolean
   version: string
   config: AppConfig
 
@@ -95,11 +100,11 @@ export interface App<HostElement = any> {
 
   // internal, but we need to expose these for the server-renderer and devtools
   _uid: number
-  _component: ConcreteComponent
+  _component: GenericComponent
   _props: Data | null
   _container: HostElement | null
   _context: AppContext
-  _instance: ComponentInternalInstance | null
+  _instance: GenericComponentInstance | null
 
   /**
    * @internal custom element vnode
@@ -120,13 +125,11 @@ export interface App<HostElement = any> {
 
 export type OptionMergeFunction = (to: unknown, from: unknown) => any
 
-export interface AppConfig {
-  // @private
-  readonly isNativeTag: (tag: string) => boolean
-
-  performance: boolean
-  optionMergeStrategies: Record<string, OptionMergeFunction>
-  globalProperties: ComponentCustomProperties & Record<string, any>
+/**
+ * Shared app config between vdom and vapor
+ */
+export interface GenericAppConfig {
+  performance?: boolean
   errorHandler?: (
     err: unknown,
     instance: ComponentPublicInstance | null,
@@ -138,6 +141,26 @@ export interface AppConfig {
     trace: string,
   ) => void
 
+  /**
+   * Whether to throw unhandled errors in production.
+   * Default is `false` to avoid crashing on any error (and only logs it)
+   * But in some cases, e.g. SSR, throwing might be more desirable.
+   */
+  throwUnhandledErrorInProduction?: boolean
+
+  /**
+   * Prefix for all useId() calls within this app
+   */
+  idPrefix?: string
+}
+
+export interface AppConfig extends GenericAppConfig {
+  // @private
+  readonly isNativeTag: (tag: string) => boolean
+
+  optionMergeStrategies: Record<string, OptionMergeFunction>
+  globalProperties: ComponentCustomProperties & Record<string, any>
+
   /**
    * Options to pass to `@vue/compiler-dom`.
    * Only supported in runtime compiler build.
@@ -148,33 +171,61 @@ export interface AppConfig {
    * @deprecated use config.compilerOptions.isCustomElement
    */
   isCustomElement?: (tag: string) => boolean
+}
 
-  /**
-   * TODO document for 3.5
-   * Enable warnings for computed getters that recursively trigger itself.
-   */
-  warnRecursiveComputed?: boolean
+/**
+ * The vapor in vdom implementation is in runtime-vapor/src/vdomInterop.ts
+ * @internal
+ */
+export interface VaporInteropInterface {
+  mount(
+    vnode: VNode,
+    container: any,
+    anchor: any,
+    parentComponent: ComponentInternalInstance | null,
+  ): GenericComponentInstance // VaporComponentInstance
+  update(n1: VNode, n2: VNode, shouldUpdate: boolean): void
+  unmount(vnode: VNode, doRemove?: boolean): void
+  move(vnode: VNode, container: any, anchor: any): void
+  slot(n1: VNode | null, n2: VNode, container: any, anchor: any): void
+
+  vdomMount: (component: ConcreteComponent, props?: any, slots?: any) => any
+  vdomUnmount: UnmountComponentFn
+  vdomSlot: (
+    slots: any,
+    name: string | (() => string),
+    props: Record<string, any>,
+    parentComponent: any, // VaporComponentInstance
+    fallback?: any, // VaporSlot
+  ) => any
+}
 
+/**
+ * Minimal app context shared between vdom and vapor
+ */
+export interface GenericAppContext {
+  app: App // for devtools
+  config: GenericAppConfig
+  provides: Record<string | symbol, any>
+  components?: Record<string, Component>
+  directives?: Record<string, Directive>
   /**
-   * Whether to throw unhandled errors in production.
-   * Default is `false` to avoid crashing on any error (and only logs it)
-   * But in some cases, e.g. SSR, throwing might be more desirable.
+   * HMR only
+   * @internal
    */
-  throwUnhandledErrorInProduction?: boolean
+  reload?: () => void
 
   /**
-   * Prefix for all useId() calls within this app
+   * @internal vapor interop only
    */
-  idPrefix?: string
+  vapor?: VaporInteropInterface
 }
 
-export interface AppContext {
-  app: App // for devtools
+export interface AppContext extends GenericAppContext {
   config: AppConfig
-  mixins: ComponentOptions[]
   components: Record<string, Component>
   directives: Record<string, Directive>
-  provides: Record<string | symbol, any>
+  mixins: ComponentOptions[]
 
   /**
    * Cache for merged/normalized component options
@@ -193,11 +244,6 @@ export interface AppContext {
    * @internal
    */
   emitsCache: WeakMap<ConcreteComponent, ObjectEmitsOptions | null>
-  /**
-   * HMR only
-   * @internal
-   */
-  reload?: () => void
   /**
    * v2 compat only
    * @internal
@@ -243,17 +289,33 @@ export function createAppContext(): AppContext {
   }
 }
 
-export type CreateAppFunction<HostElement> = (
-  rootComponent: Component,
+export type CreateAppFunction<HostElement, Comp = Component> = (
+  rootComponent: Comp,
   rootProps?: Data | null,
 ) => App<HostElement>
 
 let uid = 0
 
-export function createAppAPI<HostElement>(
-  render: RootRenderFunction<HostElement>,
-  hydrate?: RootHydrateFunction,
-): CreateAppFunction<HostElement> {
+export type AppMountFn<HostElement> = (
+  app: App,
+  rootContainer: HostElement,
+  isHydrate?: boolean,
+  namespace?: boolean | ElementNamespace,
+) => GenericComponentInstance
+
+export type AppUnmountFn = (app: App) => void
+
+/**
+ * @internal
+ */
+export function createAppAPI<HostElement, Comp = Component>(
+  // render: RootRenderFunction<HostElement>,
+  // hydrate?: RootHydrateFunction,
+  mount: AppMountFn<HostElement>,
+  unmount: AppUnmountFn,
+  getPublicInstance: (instance: GenericComponentInstance) => any,
+  render?: RootRenderFunction,
+): CreateAppFunction<HostElement, Comp> {
   return function createApp(rootComponent, rootProps = null) {
     if (!isFunction(rootComponent)) {
       rootComponent = extend({}, rootComponent)
@@ -356,58 +418,32 @@ export function createAppAPI<HostElement>(
       },
 
       mount(
-        rootContainer: HostElement,
+        rootContainer: HostElement & { __vue_app__?: App },
         isHydrate?: boolean,
         namespace?: boolean | ElementNamespace,
       ): any {
         if (!isMounted) {
           // #5571
-          if (__DEV__ && (rootContainer as any).__vue_app__) {
+          if (__DEV__ && rootContainer.__vue_app__) {
             warn(
               `There is already an app instance mounted on the host container.\n` +
                 ` If you want to mount another app on the same host container,` +
                 ` you need to unmount the previous app by calling \`app.unmount()\` first.`,
             )
           }
-          const vnode = app._ceVNode || createVNode(rootComponent, rootProps)
-          // store app context on the root VNode.
-          // this will be set on the root instance on initial mount.
-          vnode.appContext = context
-
-          if (namespace === true) {
-            namespace = 'svg'
-          } else if (namespace === false) {
-            namespace = undefined
-          }
+          const instance = mount(app, rootContainer, isHydrate, namespace)
 
-          // HMR root reload
-          if (__DEV__) {
-            context.reload = () => {
-              const cloned = cloneVNode(vnode)
-              // avoid hydration for hmr updating
-              cloned.el = null
-              // casting to ElementNamespace because TS doesn't guarantee type narrowing
-              // over function boundaries
-              render(cloned, rootContainer, namespace as ElementNamespace)
-            }
+          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
+            app._instance = instance
+            devtoolsInitApp(app, version)
           }
 
-          if (isHydrate && hydrate) {
-            hydrate(vnode as VNode<Node, Element>, rootContainer as any)
-          } else {
-            render(vnode, rootContainer, namespace)
-          }
           isMounted = true
           app._container = rootContainer
           // for devtools and telemetry
-          ;(rootContainer as any).__vue_app__ = app
-
-          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
-            app._instance = vnode.component
-            devtoolsInitApp(app, version)
-          }
+          rootContainer.__vue_app__ = app
 
-          return getComponentPublicInstance(vnode.component!)
+          return getPublicInstance(instance)
         } else if (__DEV__) {
           warn(
             `App has already been mounted.\n` +
@@ -435,7 +471,7 @@ export function createAppAPI<HostElement>(
             app._instance,
             ErrorCodes.APP_UNMOUNT_CLEANUP,
           )
-          render(null, app._container)
+          unmount(app)
           if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
             app._instance = null
             devtoolsUnmountApp(app)
@@ -479,7 +515,12 @@ export function createAppAPI<HostElement>(
     })
 
     if (__COMPAT__) {
-      installAppCompatProperties(app, context, render)
+      installAppCompatProperties(
+        app,
+        context,
+        // vapor doesn't have compat mode so this is always passed
+        render!,
+      )
     }
 
     return app
diff --git a/packages/runtime-core/src/apiInject.ts b/packages/runtime-core/src/apiInject.ts
index 711c5d84de8..91ffed0e8dc 100644
--- a/packages/runtime-core/src/apiInject.ts
+++ b/packages/runtime-core/src/apiInject.ts
@@ -1,6 +1,5 @@
 import { isFunction } from '@vue/shared'
-import { currentInstance } from './component'
-import { currentRenderingInstance } from './componentRenderContext'
+import { getCurrentGenericInstance } from './component'
 import { currentApp } from './apiCreateApp'
 import { warn } from './warning'
 
@@ -12,6 +11,7 @@ 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__) {
       warn(`provide() can only be used inside setup().`)
@@ -51,7 +51,7 @@ export function inject(
 ) {
   // fallback to `currentRenderingInstance` so that this can be called in
   // a functional component
-  const instance = currentInstance || currentRenderingInstance
+  const instance = getCurrentGenericInstance()
 
   // also support looking up from app-level provides w/ `app.runWithContext()`
   if (instance || currentApp) {
@@ -65,7 +65,7 @@ export function inject(
       ? currentApp._context.provides
       : instance
         ? instance.parent == null || instance.ce
-          ? instance.vnode.appContext && instance.vnode.appContext.provides
+          ? instance.appContext && instance.appContext.provides
           : instance.parent.provides
         : undefined
 
@@ -90,5 +90,5 @@ export function inject(
  * user. One example is `useRoute()` in `vue-router`.
  */
 export function hasInjectionContext(): boolean {
-  return !!(currentInstance || currentRenderingInstance || currentApp)
+  return !!(getCurrentGenericInstance() || currentApp)
 }
diff --git a/packages/runtime-core/src/apiLifecycle.ts b/packages/runtime-core/src/apiLifecycle.ts
index b79a6d38a06..dce4d852def 100644
--- a/packages/runtime-core/src/apiLifecycle.ts
+++ b/packages/runtime-core/src/apiLifecycle.ts
@@ -1,5 +1,5 @@
 import {
-  type ComponentInternalInstance,
+  type GenericComponentInstance,
   currentInstance,
   isInSSRComponentSetup,
   setCurrentInstance,
@@ -8,11 +8,7 @@ import type { ComponentPublicInstance } from './componentPublicInstance'
 import { ErrorTypeStrings, callWithAsyncErrorHandling } from './errorHandling'
 import { warn } from './warning'
 import { toHandlerKey } from '@vue/shared'
-import {
-  type DebuggerEvent,
-  pauseTracking,
-  resetTracking,
-} from '@vue/reactivity'
+import { type DebuggerEvent, setActiveSub } from '@vue/reactivity'
 import { LifecycleHooks } from './enums'
 
 export { onActivated, onDeactivated } from './components/KeepAlive'
@@ -20,7 +16,7 @@ export { onActivated, onDeactivated } from './components/KeepAlive'
 export function injectHook(
   type: LifecycleHooks,
   hook: Function & { __weh?: Function },
-  target: ComponentInternalInstance | null = currentInstance,
+  target: GenericComponentInstance | null = currentInstance,
   prepend: boolean = false,
 ): Function | undefined {
   if (target) {
@@ -33,15 +29,17 @@ export function injectHook(
       (hook.__weh = (...args: unknown[]) => {
         // disable tracking inside all lifecycle hooks
         // since they can potentially be called inside effects.
-        pauseTracking()
+        const prevSub = setActiveSub()
         // Set currentInstance during hook invocation.
         // This assumes the hook does not synchronously trigger other hooks, which
         // can only be false when the user does something really funky.
-        const reset = setCurrentInstance(target)
-        const res = callWithAsyncErrorHandling(hook, target, type, args)
-        reset()
-        resetTracking()
-        return res
+        const prev = setCurrentInstance(target)
+        try {
+          return callWithAsyncErrorHandling(hook, target, type, args)
+        } finally {
+          setCurrentInstance(...prev)
+          setActiveSub(prevSub)
+        }
       })
     if (prepend) {
       hooks.unshift(wrappedHook)
@@ -67,7 +65,7 @@ const createHook =
   <T extends Function = () => any>(lifecycle: LifecycleHooks) =>
   (
     hook: T,
-    target: ComponentInternalInstance | null = currentInstance,
+    target: GenericComponentInstance | null = currentInstance,
   ): void => {
     // post-create lifecycle registrations are noops during SSR (except for serverPrefetch)
     if (
@@ -79,7 +77,7 @@ const createHook =
   }
 type CreateHook<T = any> = (
   hook: T,
-  target?: ComponentInternalInstance | null,
+  target?: GenericComponentInstance | null,
 ) => void
 
 export const onBeforeMount: CreateHook = createHook(LifecycleHooks.BEFORE_MOUNT)
@@ -110,7 +108,7 @@ export type ErrorCapturedHook<TError = unknown> = (
 
 export function onErrorCaptured<TError = Error>(
   hook: ErrorCapturedHook<TError>,
-  target: ComponentInternalInstance | null = currentInstance,
+  target: GenericComponentInstance | null = currentInstance,
 ): void {
   injectHook(LifecycleHooks.ERROR_CAPTURED, hook, target)
 }
diff --git a/packages/runtime-core/src/apiSetupHelpers.ts b/packages/runtime-core/src/apiSetupHelpers.ts
index 2ddaeb509ad..45b1d28f807 100644
--- a/packages/runtime-core/src/apiSetupHelpers.ts
+++ b/packages/runtime-core/src/apiSetupHelpers.ts
@@ -9,11 +9,11 @@ import {
   isPromise,
 } from '@vue/shared'
 import {
+  type ComponentInternalInstance,
   type SetupContext,
   createSetupContext,
-  getCurrentInstance,
+  getCurrentGenericInstance,
   setCurrentInstance,
-  unsetCurrentInstance,
 } from './component'
 import type { EmitFn, EmitsOptions, ObjectEmitsOptions } from './componentEmits'
 import type {
@@ -381,6 +381,7 @@ export function withDefaults<
   return null as any
 }
 
+// TODO return type for Vapor components
 export function useSlots(): SetupContext['slots'] {
   return getContext().slots
 }
@@ -390,11 +391,16 @@ export function useAttrs(): SetupContext['attrs'] {
 }
 
 function getContext(): SetupContext {
-  const i = getCurrentInstance()!
+  const i = getCurrentGenericInstance()!
   if (__DEV__ && !i) {
     warn(`useContext() called without active instance.`)
   }
-  return i.setupContext || (i.setupContext = createSetupContext(i))
+  if (i.vapor) {
+    return i as any // vapor instance act as its own setup context
+  } else {
+    const ii = i as ComponentInternalInstance
+    return ii.setupContext || (ii.setupContext = createSetupContext(ii))
+  }
 }
 
 /**
@@ -496,7 +502,7 @@ export function createPropsRestProxy(
  * @internal
  */
 export function withAsyncContext(getAwaitable: () => any): [any, () => void] {
-  const ctx = getCurrentInstance()!
+  const ctx = getCurrentGenericInstance()!
   if (__DEV__ && !ctx) {
     warn(
       `withAsyncContext called without active current instance. ` +
@@ -504,7 +510,7 @@ export function withAsyncContext(getAwaitable: () => any): [any, () => void] {
     )
   }
   let awaitable = getAwaitable()
-  unsetCurrentInstance()
+  setCurrentInstance(null, undefined)
   if (isPromise(awaitable)) {
     awaitable = awaitable.catch(e => {
       setCurrentInstance(ctx)
diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts
index 8f6168cdf29..7dce012d90b 100644
--- a/packages/runtime-core/src/apiWatch.ts
+++ b/packages/runtime-core/src/apiWatch.ts
@@ -1,17 +1,19 @@
 import {
   type WatchOptions as BaseWatchOptions,
   type DebuggerOptions,
+  EffectFlags,
   type ReactiveMarker,
   type WatchCallback,
   type WatchEffect,
   type WatchHandle,
   type WatchSource,
-  watch as baseWatch,
+  WatcherEffect,
 } from '@vue/reactivity'
 import { type SchedulerJob, SchedulerJobFlags, queueJob } from './scheduler'
 import { EMPTY_OBJ, NOOP, extend, isFunction, isString } from '@vue/shared'
 import {
   type ComponentInternalInstance,
+  type GenericComponentInstance,
   currentInstance,
   isInSSRComponentSetup,
   setCurrentInstance,
@@ -125,7 +127,7 @@ export function watch<
 // implementation
 export function watch<T = any, Immediate extends Readonly<boolean> = false>(
   source: T | WatchSource<T>,
-  cb: any,
+  cb: WatchCallback,
   options?: WatchOptions<Immediate>,
 ): WatchHandle {
   if (__DEV__ && !isFunction(cb)) {
@@ -138,12 +140,57 @@ export function watch<T = any, Immediate extends Readonly<boolean> = false>(
   return doWatch(source as any, cb, options)
 }
 
+class RenderWatcherEffect extends WatcherEffect {
+  job: SchedulerJob
+
+  constructor(
+    instance: GenericComponentInstance | null,
+    source: WatchSource | WatchSource[] | WatchEffect | object,
+    cb: WatchCallback | null,
+    options: BaseWatchOptions,
+    private flush: 'pre' | 'post' | 'sync',
+  ) {
+    super(source, cb, options)
+
+    const job: SchedulerJob = () => {
+      if (this.dirty) {
+        this.run()
+      }
+    }
+    // important: mark the job as a watcher callback so that scheduler knows
+    // it is allowed to self-trigger (#1727)
+    if (cb) {
+      this.flags |= EffectFlags.ALLOW_RECURSE
+      job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
+    }
+    if (instance) {
+      job.i = instance
+    }
+    this.job = job
+  }
+
+  notify(): void {
+    const flags = this.flags
+    if (!(flags & EffectFlags.PAUSED)) {
+      const flush = this.flush
+      const job = this.job
+      if (flush === 'post') {
+        queuePostRenderEffect(job, undefined, job.i ? job.i.suspense : null)
+      } else if (flush === 'pre') {
+        queueJob(job, job.i ? job.i.uid : undefined, true)
+      } else {
+        job()
+      }
+    }
+  }
+}
+
 function doWatch(
   source: WatchSource | WatchSource[] | WatchEffect | object,
   cb: WatchCallback | null,
   options: WatchOptions = EMPTY_OBJ,
 ): WatchHandle {
-  const { immediate, deep, flush, once } = options
+  const { immediate, deep, flush = 'pre', once } = options
 
   if (__DEV__ && !cb) {
     if (immediate !== undefined) {
@@ -190,50 +237,37 @@ function doWatch(
   baseWatchOptions.call = (fn, type, args) =>
     callWithAsyncErrorHandling(fn, instance, type, args)
 
-  // scheduler
-  let isPre = false
-  if (flush === 'post') {
-    baseWatchOptions.scheduler = job => {
-      queuePostRenderEffect(job, instance && instance.suspense)
-    }
-  } else if (flush !== 'sync') {
-    // default: 'pre'
-    isPre = true
-    baseWatchOptions.scheduler = (job, isFirstRun) => {
-      if (isFirstRun) {
-        job()
-      } else {
-        queueJob(job)
-      }
-    }
-  }
+  const effect = new RenderWatcherEffect(
+    instance,
+    source,
+    cb,
+    baseWatchOptions,
+    flush,
+  )
 
-  baseWatchOptions.augmentJob = (job: SchedulerJob) => {
-    // important: mark the job as a watcher callback so that scheduler knows
-    // it is allowed to self-trigger (#1727)
-    if (cb) {
-      job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
-    }
-    if (isPre) {
-      job.flags! |= SchedulerJobFlags.PRE
-      if (instance) {
-        job.id = instance.uid
-        ;(job as SchedulerJob).i = instance
-      }
-    }
+  // initial run
+  if (cb) {
+    effect.run(true)
+  } else if (flush === 'post') {
+    queuePostRenderEffect(effect.job, undefined, instance && instance.suspense)
+  } else {
+    effect.run(true)
   }
 
-  const watchHandle = baseWatch(source, cb, baseWatchOptions)
+  const stop = effect.stop.bind(effect) as WatchHandle
+  stop.pause = effect.pause.bind(effect)
+  stop.resume = effect.resume.bind(effect)
+  stop.stop = stop
 
   if (__SSR__ && isInSSRComponentSetup) {
     if (ssrCleanup) {
-      ssrCleanup.push(watchHandle)
+      ssrCleanup.push(stop)
     } else if (runsImmediately) {
-      watchHandle()
+      stop()
     }
   }
 
-  return watchHandle
+  return stop
 }
 
 // this.$watch
@@ -256,9 +290,9 @@ export function instanceWatch(
     cb = value.handler as Function
     options = value
   }
-  const reset = setCurrentInstance(this)
+  const prev = setCurrentInstance(this)
   const res = doWatch(getter, cb.bind(publicThis), options)
-  reset()
+  setCurrentInstance(...prev)
   return res
 }
 
diff --git a/packages/runtime-core/src/compat/componentAsync.ts b/packages/runtime-core/src/compat/componentAsync.ts
index b6bd6dd4b94..fc6ae3b7939 100644
--- a/packages/runtime-core/src/compat/componentAsync.ts
+++ b/packages/runtime-core/src/compat/componentAsync.ts
@@ -35,7 +35,7 @@ export function convertLegacyAsyncComponent(
   let resolve: (res: LegacyAsyncReturnValue) => void
   let reject: (reason?: any) => void
   const fallbackPromise = new Promise<Component>((r, rj) => {
-    ;(resolve = r), (reject = rj)
+    ;((resolve = r), (reject = rj))
   })
 
   const res = comp(resolve!, reject!)
diff --git a/packages/runtime-core/src/compat/global.ts b/packages/runtime-core/src/compat/global.ts
index edc57436a56..f998260c7dc 100644
--- a/packages/runtime-core/src/compat/global.ts
+++ b/packages/runtime-core/src/compat/global.ts
@@ -462,7 +462,7 @@ function installCompatMount(
    * function simulates that behavior.
    */
   app._createRoot = options => {
-    const component = app._component
+    const component = app._component as Component
     const vnode = createVNode(component, options.propsData || null)
     vnode.appContext = context
 
diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts
index 60552d736d5..67d5a1b8720 100644
--- a/packages/runtime-core/src/component.ts
+++ b/packages/runtime-core/src/component.ts
@@ -5,9 +5,8 @@ import {
   TrackOpTypes,
   isRef,
   markRaw,
-  pauseTracking,
   proxyRefs,
-  resetTracking,
+  setActiveSub,
   shallowReadonly,
   track,
 } from '@vue/reactivity'
@@ -39,6 +38,7 @@ import { ErrorCodes, callWithErrorHandling, handleError } from './errorHandling'
 import {
   type AppConfig,
   type AppContext,
+  type GenericAppContext,
   createAppContext,
 } from './apiCreateApp'
 import { type Directive, validateDirectiveName } from './directives'
@@ -65,17 +65,15 @@ import {
   NOOP,
   ShapeFlags,
   extend,
-  getGlobalThis,
   isArray,
+  isBuiltInTag,
   isFunction,
   isObject,
   isPromise,
-  makeMap,
 } from '@vue/shared'
 import type { SuspenseBoundary } from './components/Suspense'
 import type { CompilerOptions } from '@vue/compiler-core'
 import { markAttrsAccessed } from './componentRenderUtils'
-import { currentRenderingInstance } from './componentRenderContext'
 import { endMeasure, startMeasure } from './profiling'
 import { convertLegacyRenderFn } from './compat/renderFn'
 import {
@@ -95,6 +93,12 @@ import type { DefineComponent } from './apiDefineComponent'
 import { markAsyncBoundary } from './helpers/useId'
 import { isAsyncWrapper } from './apiAsyncComponent'
 import type { RendererElement } from './renderer'
+import {
+  setCurrentInstance,
+  setInSSRSetupState,
+} from './componentCurrentInstance'
+
+export * from './componentCurrentInstance'
 
 export type Data = Record<string, unknown>
 
@@ -189,6 +193,10 @@ export interface AllowedComponentProps {
 // Note: can't mark this whole interface internal because some public interfaces
 // extend it.
 export interface ComponentInternalOptions {
+  /**
+   * indicates vapor component
+   */
+  __vapor?: boolean
   /**
    * @internal
    */
@@ -239,6 +247,17 @@ export interface ClassComponent {
   __vccOpts: ComponentOptions
 }
 
+/**
+ * Type used where a function accepts both vdom and vapor components.
+ */
+export type GenericComponent = (
+  | {
+      name?: string
+    }
+  | ((() => any) & { displayName?: string })
+) &
+  ComponentInternalOptions
+
 /**
  * Concrete component type matches its actual value: it's either an options
  * object, or a function. Use this where the code expects to work with actual
@@ -313,55 +332,81 @@ export type InternalRenderFunction = {
 }
 
 /**
- * We expose a subset of properties on the internal instance as they are
- * useful for advanced external libraries and tools.
+ * Base component instance interface that is shared between vdom mode and vapor
+ * mode, so that we can have a mixed instance tree and reuse core logic that
+ * operate on both.
  */
-export interface ComponentInternalInstance {
+export interface GenericComponentInstance {
+  vapor?: boolean
   uid: number
-  type: ConcreteComponent
-  parent: ComponentInternalInstance | null
-  root: ComponentInternalInstance
-  appContext: AppContext
+  type: GenericComponent
+  root: GenericComponentInstance | null
+  parent: GenericComponentInstance | null
+  appContext: GenericAppContext
   /**
-   * Vnode representing this component in its parent's vdom tree
+   * Object containing values this component provides for its descendants
+   * @internal
    */
-  vnode: VNode
+  provides: Data
   /**
-   * The pending new vnode from parent updates
+   * Tracking reactive effects (e.g. watchers) associated with this component
+   * so that they can be automatically stopped on component unmount
    * @internal
    */
-  next: VNode | null
+  scope: EffectScope
   /**
-   * Root vnode of this component's own vdom tree
+   * render function will have different types between vdom and vapor
    */
-  subTree: VNode
+  render?: Function | null
   /**
-   * Render effect instance
+   * SSR render function
+   * (they are the same between vdom and vapor components.)
+   * @internal
    */
-  effect: ReactiveEffect
+  ssrRender?: Function | null
+
+  // state
+  props: Data
+  attrs: Data
+  refs: Data
+  emit: EmitFn
   /**
-   * Force update render effect
+   * used for keeping track of .once event handlers on components
+   * @internal
    */
-  update: () => void
+  emitted: Record<string, boolean> | null
   /**
-   * Render effect job to be passed to scheduler (checks if dirty)
+   * used for caching the value returned from props default factory functions to
+   * avoid unnecessary watcher trigger
+   * @internal
    */
-  job: SchedulerJob
+  propsDefaults: Data | null
   /**
-   * The render function that returns vdom tree.
+   * used for getting the keys of a component's raw props, vapor only
    * @internal
    */
-  render: InternalRenderFunction | null
+  rawKeys?: () => string[]
+
+  // exposed properties via expose()
+  exposed: Record<string, any> | null
+  exposeProxy: Record<string, any> | null
+
   /**
-   * SSR render function
+   * setup related
    * @internal
    */
-  ssrRender?: Function | null
+  setupState?: Data
   /**
-   * Object containing values this component provides for its descendants
+   * devtools access to additional info
    * @internal
    */
-  provides: Data
+  devtoolsRawSetupState?: any
+
+  // lifecycle
+  isMounted: boolean
+  isUnmounted: boolean
+  isDeactivated: boolean
+
   /**
    * for tracking useId()
    * first element is the current boundary prefix
@@ -369,206 +414,246 @@ export interface ComponentInternalInstance {
    * @internal
    */
   ids: [string, number, number]
+
+  // for vapor the following two are dev only
   /**
-   * Tracking reactive effects (e.g. watchers) associated with this component
-   * so that they can be automatically stopped on component unmount
+   * resolved props options
    * @internal
    */
-  scope: EffectScope
+  propsOptions?: NormalizedPropsOptions
   /**
-   * cache for proxy access type to avoid hasOwnProperty calls
+   * resolved emits options
    * @internal
    */
-  accessCache: Data | null
+  emitsOptions?: ObjectEmitsOptions | null
+
   /**
-   * cache for render function values that rely on _ctx but won't need updates
-   * after initialized (e.g. inline handlers)
+   * Public instance proxy, vdom only
+   */
+  proxy?: any
+  /**
+   * suspense related
    * @internal
    */
-  renderCache: (Function | VNode | undefined)[]
+  suspense: SuspenseBoundary | null
 
+  // lifecycle
   /**
-   * Resolved component registry, only for components with mixins or extends
    * @internal
    */
-  components: Record<string, ConcreteComponent> | null
+  [LifecycleHooks.BEFORE_CREATE]?: LifecycleHook
   /**
-   * Resolved directive registry, only for components with mixins or extends
    * @internal
    */
-  directives: Record<string, Directive> | null
+  [LifecycleHooks.CREATED]?: LifecycleHook
   /**
-   * Resolved filters registry, v2 compat only
    * @internal
    */
-  filters?: Record<string, Function>
+  [LifecycleHooks.BEFORE_MOUNT]?: LifecycleHook
   /**
-   * resolved props options
    * @internal
    */
-  propsOptions: NormalizedPropsOptions
+  [LifecycleHooks.MOUNTED]?: LifecycleHook
   /**
-   * resolved emits options
    * @internal
    */
-  emitsOptions: ObjectEmitsOptions | null
+  [LifecycleHooks.BEFORE_UPDATE]?: LifecycleHook
   /**
-   * resolved inheritAttrs options
    * @internal
    */
-  inheritAttrs?: boolean
+  [LifecycleHooks.UPDATED]?: LifecycleHook
   /**
-   * Custom Element instance (if component is created by defineCustomElement)
    * @internal
    */
-  ce?: ComponentCustomElementInterface
+  [LifecycleHooks.BEFORE_UNMOUNT]?: LifecycleHook
   /**
-   * is custom element? (kept only for compatibility)
    * @internal
    */
-  isCE?: boolean
+  [LifecycleHooks.UNMOUNTED]?: LifecycleHook
   /**
-   * custom element specific HMR method
    * @internal
    */
-  ceReload?: (newStyles?: string[]) => void
-
-  // the rest are only for stateful components ---------------------------------
-
-  // main proxy that serves as the public instance (`this`)
-  proxy: ComponentPublicInstance | null
-
-  // exposed properties via expose()
-  exposed: Record<string, any> | null
-  exposeProxy: Record<string, any> | null
-
+  [LifecycleHooks.RENDER_TRACKED]?: LifecycleHook
   /**
-   * alternative proxy used only for runtime-compiled render functions using
-   * `with` block
    * @internal
    */
-  withProxy: ComponentPublicInstance | null
+  [LifecycleHooks.RENDER_TRIGGERED]?: LifecycleHook
   /**
-   * This is the target for the public instance proxy. It also holds properties
-   * injected by user options (computed, methods etc.) and user-attached
-   * custom properties (via `this.x = ...`)
    * @internal
    */
-  ctx: Data
-
-  // state
-  data: Data
-  props: Data
-  attrs: Data
-  slots: InternalSlots
-  refs: Data
-  emit: EmitFn
-
+  [LifecycleHooks.ACTIVATED]?: LifecycleHook
   /**
-   * used for keeping track of .once event handlers on components
    * @internal
    */
-  emitted: Record<string, boolean> | null
+  [LifecycleHooks.DEACTIVATED]?: LifecycleHook
   /**
-   * used for caching the value returned from props default factory functions to
-   * avoid unnecessary watcher trigger
    * @internal
    */
-  propsDefaults: Data
+  [LifecycleHooks.ERROR_CAPTURED]?: LifecycleHook
   /**
-   * setup related
    * @internal
    */
-  setupState: Data
+  [LifecycleHooks.SERVER_PREFETCH]?: LifecycleHook<() => Promise<unknown>>
   /**
-   * devtools access to additional info
+   * @internal vapor only
+   */
+  hmrRerender?: () => void
+  /**
+   * @internal vapor only
+   */
+  hmrReload?: (newComp: any) => void
+
+  // these only exist on vdom instances
+  vnode?: VNode
+  subTree?: VNode
+
+  /**
+   * Custom Element instance (if component is created by defineCustomElement)
    * @internal
    */
-  devtoolsRawSetupState?: any
+  ce?: ComponentCustomElementInterface
   /**
+   * is custom element? (kept only for compatibility)
    * @internal
    */
-  setupContext: SetupContext | null
-
+  isCE?: boolean
   /**
-   * suspense related
+   * custom element specific HMR method
    * @internal
    */
-  suspense: SuspenseBoundary | null
+  ceReload?: (newStyles?: string[]) => void
+}
+
+/**
+ * We expose a subset of properties on the internal instance as they are
+ * useful for advanced external libraries and tools.
+ */
+export interface ComponentInternalInstance extends GenericComponentInstance {
+  vapor?: never
+  uid: number
+  type: ConcreteComponent
+  parent: GenericComponentInstance | null
+  root: GenericComponentInstance
+  appContext: AppContext
   /**
-   * suspense pending batch id
+   * Vnode representing this component in its parent's vdom tree
+   */
+  vnode: VNode
+  /**
+   * The pending new vnode from parent updates
    * @internal
    */
-  suspenseId: number
+  next: VNode | null
+  /**
+   * Root vnode of this component's own vdom tree
+   */
+  subTree: VNode
+  /**
+   * Render effect instance
+   */
+  effect: ReactiveEffect
   /**
+   * Force update render effect
+   */
+  update: () => void
+  /**
+   * Render effect job to be passed to scheduler (checks if dirty)
+   */
+  job: SchedulerJob
+  /**
+   * The render function that returns vdom tree.
    * @internal
    */
-  asyncDep: Promise<any> | null
+  render: InternalRenderFunction | null
   /**
+   * cache for proxy access type to avoid hasOwnProperty calls
    * @internal
    */
-  asyncResolved: boolean
-
-  // lifecycle
-  isMounted: boolean
-  isUnmounted: boolean
-  isDeactivated: boolean
+  accessCache: Data | null
   /**
+   * cache for render function values that rely on _ctx but won't need updates
+   * after initialized (e.g. inline handlers)
    * @internal
    */
-  [LifecycleHooks.BEFORE_CREATE]: LifecycleHook
+  renderCache: (Function | VNode | undefined)[]
+
   /**
+   * Resolved component registry, only for components with mixins or extends
    * @internal
    */
-  [LifecycleHooks.CREATED]: LifecycleHook
+  components: Record<string, ConcreteComponent> | null
   /**
+   * Resolved directive registry, only for components with mixins or extends
    * @internal
    */
-  [LifecycleHooks.BEFORE_MOUNT]: LifecycleHook
+  directives: Record<string, Directive> | null
   /**
+   * Resolved filters registry, v2 compat only
    * @internal
    */
-  [LifecycleHooks.MOUNTED]: LifecycleHook
+  filters?: Record<string, Function>
   /**
+   * resolved props options
    * @internal
    */
-  [LifecycleHooks.BEFORE_UPDATE]: LifecycleHook
+  propsOptions: NormalizedPropsOptions
   /**
+   * resolved emits options
    * @internal
    */
-  [LifecycleHooks.UPDATED]: LifecycleHook
+  emitsOptions: ObjectEmitsOptions | null
   /**
+   * resolved inheritAttrs options
    * @internal
    */
-  [LifecycleHooks.BEFORE_UNMOUNT]: LifecycleHook
+  inheritAttrs?: boolean
+
+  // the rest are only for stateful components ---------------------------------
   /**
+   * setup related
    * @internal
    */
-  [LifecycleHooks.UNMOUNTED]: LifecycleHook
+  setupState: Data
   /**
    * @internal
    */
-  [LifecycleHooks.RENDER_TRACKED]: LifecycleHook
+  setupContext?: SetupContext | null
+
+  // main proxy that serves as the public instance (`this`)
+  proxy: ComponentPublicInstance | null
+
+  data: Data // options API only
+  emit: EmitFn
+  slots: InternalSlots
+
+  exposeProxy: Record<string, any> | null
+
   /**
+   * alternative proxy used only for runtime-compiled render functions using
+   * `with` block
    * @internal
    */
-  [LifecycleHooks.RENDER_TRIGGERED]: LifecycleHook
+  withProxy: ComponentPublicInstance | null
   /**
+   * This is the target for the public instance proxy. It also holds properties
+   * injected by user options (computed, methods etc.) and user-attached
+   * custom properties (via `this.x = ...`)
    * @internal
    */
-  [LifecycleHooks.ACTIVATED]: LifecycleHook
+  ctx: Data
   /**
+   * suspense pending batch id
    * @internal
    */
-  [LifecycleHooks.DEACTIVATED]: LifecycleHook
+  suspenseId: number
   /**
    * @internal
    */
-  [LifecycleHooks.ERROR_CAPTURED]: LifecycleHook
+  asyncDep: Promise<any> | null
   /**
    * @internal
    */
-  [LifecycleHooks.SERVER_PREFETCH]: LifecycleHook<() => Promise<unknown>>
+  asyncResolved: boolean
 
   /**
    * For caching bound $forceUpdate on public proxy access
@@ -604,6 +689,13 @@ const emptyAppContext = createAppContext()
 
 let uid = 0
 
+/**
+ * @internal for vapor
+ */
+export function nextUid(): number {
+  return uid++
+}
+
 export function createComponentInstance(
   vnode: VNode,
   parent: ComponentInternalInstance | null,
@@ -651,7 +743,7 @@ export function createComponentInstance(
     emitted: null,
 
     // props default value
-    propsDefaults: EMPTY_OBJ,
+    propsDefaults: null,
 
     // inheritAttrs
     inheritAttrs: type.inheritAttrs,
@@ -708,78 +800,9 @@ export function createComponentInstance(
   return instance
 }
 
-export let currentInstance: ComponentInternalInstance | null = null
-
-export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
-  currentInstance || currentRenderingInstance
-
-let internalSetCurrentInstance: (
-  instance: ComponentInternalInstance | null,
-) => void
-let setInSSRSetupState: (state: boolean) => void
-
 /**
- * The following makes getCurrentInstance() usage across multiple copies of Vue
- * work. Some cases of how this can happen are summarized in #7590. In principle
- * the duplication should be avoided, but in practice there are often cases
- * where the user is unable to resolve on their own, especially in complicated
- * SSR setups.
- *
- * Note this fix is technically incomplete, as we still rely on other singletons
- * for effectScope and global reactive dependency maps. However, it does make
- * some of the most common cases work. It also warns if the duplication is
- * found during browser execution.
+ * @internal
  */
-if (__SSR__) {
-  type Setter = (v: any) => void
-  const g = getGlobalThis()
-  const registerGlobalSetter = (key: string, setter: Setter) => {
-    let setters: Setter[]
-    if (!(setters = g[key])) setters = g[key] = []
-    setters.push(setter)
-    return (v: any) => {
-      if (setters.length > 1) setters.forEach(set => set(v))
-      else setters[0](v)
-    }
-  }
-  internalSetCurrentInstance = registerGlobalSetter(
-    `__VUE_INSTANCE_SETTERS__`,
-    v => (currentInstance = v),
-  )
-  // also make `isInSSRComponentSetup` sharable across copies of Vue.
-  // this is needed in the SFC playground when SSRing async components, since
-  // we have to load both the runtime and the server-renderer from CDNs, they
-  // contain duplicated copies of Vue runtime code.
-  setInSSRSetupState = registerGlobalSetter(
-    `__VUE_SSR_SETTERS__`,
-    v => (isInSSRComponentSetup = v),
-  )
-} else {
-  internalSetCurrentInstance = i => {
-    currentInstance = i
-  }
-  setInSSRSetupState = v => {
-    isInSSRComponentSetup = v
-  }
-}
-
-export const setCurrentInstance = (instance: ComponentInternalInstance) => {
-  const prev = currentInstance
-  internalSetCurrentInstance(instance)
-  instance.scope.on()
-  return (): void => {
-    instance.scope.off()
-    internalSetCurrentInstance(prev)
-  }
-}
-
-export const unsetCurrentInstance = (): void => {
-  currentInstance && currentInstance.scope.off()
-  internalSetCurrentInstance(null)
-}
-
-const isBuiltInTag = /*@__PURE__*/ makeMap('slot,component')
-
 export function validateComponentName(
   name: string,
   { isNativeTag }: AppConfig,
@@ -797,8 +820,6 @@ export function isStatefulComponent(
   return instance.vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT
 }
 
-export let isInSSRComponentSetup = false
-
 export function setupComponent(
   instance: ComponentInternalInstance,
   isSSR = false,
@@ -806,10 +827,16 @@ export function setupComponent(
 ): Promise<void> | undefined {
   isSSR && setInSSRSetupState(isSSR)
 
-  const { props, children } = instance.vnode
+  const { props, children, vi } = instance.vnode
   const isStateful = isStatefulComponent(instance)
-  initProps(instance, props, isStateful, isSSR)
-  initSlots(instance, children, optimized || isSSR)
+
+  if (vi) {
+    // Vapor interop override - use Vapor props/attrs proxy
+    vi(instance)
+  } else {
+    initProps(instance, props, isStateful, isSSR)
+    initSlots(instance, children, optimized || isSSR)
+  }
 
   const setupResult = isStateful
     ? setupStatefulComponent(instance, isSSR)
@@ -859,10 +886,10 @@ function setupStatefulComponent(
   // 2. call setup()
   const { setup } = Component
   if (setup) {
-    pauseTracking()
+    const prevSub = setActiveSub()
     const setupContext = (instance.setupContext =
       setup.length > 1 ? createSetupContext(instance) : null)
-    const reset = setCurrentInstance(instance)
+    const prev = setCurrentInstance(instance)
     const setupResult = callWithErrorHandling(
       setup,
       instance,
@@ -873,8 +900,8 @@ function setupStatefulComponent(
       ],
     )
     const isAsyncSetup = isPromise(setupResult)
-    resetTracking()
-    reset()
+    setActiveSub(prevSub)
+    setCurrentInstance(...prev)
 
     if ((isAsyncSetup || instance.sp) && !isAsyncWrapper(instance)) {
       // async setup / serverPrefetch, mark as async boundary for useId()
@@ -882,6 +909,9 @@ function setupStatefulComponent(
     }
 
     if (isAsyncSetup) {
+      const unsetCurrentInstance = (): void => {
+        setCurrentInstance(null, undefined)
+      }
       setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
       if (isSSR) {
         // return the promise so server-renderer can wait on it
@@ -1054,13 +1084,13 @@ export function finishComponentSetup(
 
   // support for 2.x options
   if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
-    const reset = setCurrentInstance(instance)
-    pauseTracking()
+    const prevInstance = setCurrentInstance(instance)
+    const prevSub = setActiveSub()
     try {
       applyOptions(instance)
     } finally {
-      resetTracking()
-      reset()
+      setActiveSub(prevSub)
+      setCurrentInstance(...prevInstance)
     }
   }
 
@@ -1125,30 +1155,6 @@ function getSlotsProxy(instance: ComponentInternalInstance): Slots {
 export function createSetupContext(
   instance: ComponentInternalInstance,
 ): SetupContext {
-  const expose: SetupContext['expose'] = exposed => {
-    if (__DEV__) {
-      if (instance.exposed) {
-        warn(`expose() should be called only once per setup().`)
-      }
-      if (exposed != null) {
-        let exposedType: string = typeof exposed
-        if (exposedType === 'object') {
-          if (isArray(exposed)) {
-            exposedType = 'array'
-          } else if (isRef(exposed)) {
-            exposedType = 'ref'
-          }
-        }
-        if (exposedType !== 'object') {
-          warn(
-            `expose() should be passed a plain object, received ${exposedType}.`,
-          )
-        }
-      }
-    }
-    instance.exposed = exposed || {}
-  }
-
   if (__DEV__) {
     // We use getters in dev in case libs like test-utils overwrite instance
     // properties (overwrites should not be done in prod)
@@ -1167,20 +1173,50 @@ export function createSetupContext(
       get emit() {
         return (event: string, ...args: any[]) => instance.emit(event, ...args)
       },
-      expose,
+      expose: exposed => expose(instance, exposed as any),
     })
   } else {
     return {
       attrs: new Proxy(instance.attrs, attrsProxyHandlers),
       slots: instance.slots,
       emit: instance.emit,
-      expose,
+      expose: exposed => expose(instance, exposed as any),
     }
   }
 }
 
+/**
+ * @internal
+ */
+export function expose(
+  instance: GenericComponentInstance,
+  exposed: Record<string, any>,
+): void {
+  if (__DEV__) {
+    if (instance.exposed) {
+      warn(`expose() should be called only once per setup().`)
+    }
+    if (exposed != null) {
+      let exposedType: string = typeof exposed
+      if (exposedType === 'object') {
+        if (isArray(exposed)) {
+          exposedType = 'array'
+        } else if (isRef(exposed)) {
+          exposedType = 'ref'
+        }
+      }
+      if (exposedType !== 'object') {
+        warn(
+          `expose() should be passed a plain object, received ${exposedType}.`,
+        )
+      }
+    }
+  }
+  instance.exposed = exposed || {}
+}
+
 export function getComponentPublicInstance(
-  instance: ComponentInternalInstance,
+  instance: GenericComponentInstance,
 ): ComponentPublicInstance | ComponentInternalInstance['exposed'] | null {
   if (instance.exposed) {
     return (
@@ -1190,7 +1226,9 @@ export function getComponentPublicInstance(
           if (key in target) {
             return target[key]
           } else if (key in publicPropertiesMap) {
-            return publicPropertiesMap[key](instance)
+            return publicPropertiesMap[key](
+              instance as ComponentInternalInstance,
+            )
           }
         },
         has(target, key: string) {
@@ -1208,7 +1246,7 @@ const classify = (str: string): string =>
   str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '')
 
 export function getComponentName(
-  Component: ConcreteComponent,
+  Component: GenericComponent,
   includeInferred = true,
 ): string | false | undefined {
   return isFunction(Component)
@@ -1217,8 +1255,8 @@ export function getComponentName(
 }
 
 export function formatComponentName(
-  instance: ComponentInternalInstance | null,
-  Component: ConcreteComponent,
+  instance: GenericComponentInstance | null,
+  Component: GenericComponent,
   isRoot = false,
 ): string {
   let name = getComponentName(Component)
@@ -1240,7 +1278,7 @@ export function formatComponentName(
     }
     name =
       inferFromRegistry(
-        instance.components ||
+        (instance as ComponentInternalInstance).components ||
           (instance.parent.type as ComponentOptions).components,
       ) || inferFromRegistry(instance.appContext.components)
   }
diff --git a/packages/runtime-core/src/componentCurrentInstance.ts b/packages/runtime-core/src/componentCurrentInstance.ts
new file mode 100644
index 00000000000..7ac52a2e997
--- /dev/null
+++ b/packages/runtime-core/src/componentCurrentInstance.ts
@@ -0,0 +1,92 @@
+import { getGlobalThis } from '@vue/shared'
+import type {
+  ComponentInternalInstance,
+  GenericComponentInstance,
+} from './component'
+import { currentRenderingInstance } from './componentRenderContext'
+import { type EffectScope, setCurrentScope } from '@vue/reactivity'
+
+/**
+ * @internal
+ */
+export let currentInstance: GenericComponentInstance | null = null
+
+/**
+ * @internal
+ */
+export const getCurrentGenericInstance: () => GenericComponentInstance | null =
+  () => currentInstance || currentRenderingInstance
+
+export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
+  currentInstance && !currentInstance.vapor
+    ? (currentInstance as ComponentInternalInstance)
+    : currentRenderingInstance
+
+export let isInSSRComponentSetup = false
+
+export let setInSSRSetupState: (state: boolean) => void
+
+/**
+ * @internal
+ */
+export let simpleSetCurrentInstance: (
+  instance: GenericComponentInstance | null,
+) => void
+
+/**
+ * The following makes getCurrentInstance() usage across multiple copies of Vue
+ * work. Some cases of how this can happen are summarized in #7590. In principle
+ * the duplication should be avoided, but in practice there are often cases
+ * where the user is unable to resolve on their own, especially in complicated
+ * SSR setups.
+ *
+ * Note this fix is technically incomplete, as we still rely on other singletons
+ * for effectScope and global reactive dependency maps. However, it does make
+ * some of the most common cases work. It also warns if the duplication is
+ * found during browser execution.
+ */
+if (__SSR__) {
+  type Setter = (v: any) => void
+  const g = getGlobalThis()
+  const registerGlobalSetter = (key: string, setter: Setter) => {
+    let setters: Setter[]
+    if (!(setters = g[key])) setters = g[key] = []
+    setters.push(setter)
+    return (v: any) => {
+      if (setters.length > 1) setters.forEach(set => set(v))
+      else setters[0](v)
+    }
+  }
+  simpleSetCurrentInstance = registerGlobalSetter(
+    `__VUE_INSTANCE_SETTERS__`,
+    v => (currentInstance = v),
+  )
+  // also make `isInSSRComponentSetup` sharable across copies of Vue.
+  // this is needed in the SFC playground when SSRing async components, since
+  // we have to load both the runtime and the server-renderer from CDNs, they
+  // contain duplicated copies of Vue runtime code.
+  setInSSRSetupState = registerGlobalSetter(
+    `__VUE_SSR_SETTERS__`,
+    v => (isInSSRComponentSetup = v),
+  )
+} else {
+  simpleSetCurrentInstance = i => {
+    currentInstance = i
+  }
+  setInSSRSetupState = v => {
+    isInSSRComponentSetup = v
+  }
+}
+
+export const setCurrentInstance = (
+  instance: GenericComponentInstance | null,
+  scope: EffectScope | undefined = instance !== null
+    ? instance.scope
+    : undefined,
+): [GenericComponentInstance | null, EffectScope | undefined] => {
+  try {
+    return [currentInstance, setCurrentScope(scope)]
+  } finally {
+    simpleSetCurrentInstance(instance)
+  }
+}
diff --git a/packages/runtime-core/src/componentEmits.ts b/packages/runtime-core/src/componentEmits.ts
index c03bead3a92..f055deae5f9 100644
--- a/packages/runtime-core/src/componentEmits.ts
+++ b/packages/runtime-core/src/componentEmits.ts
@@ -18,6 +18,7 @@ import {
   type ComponentInternalInstance,
   type ComponentOptions,
   type ConcreteComponent,
+  type GenericComponentInstance,
   formatComponentName,
 } from './component'
 import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
@@ -113,14 +114,28 @@ export function emit(
   event: string,
   ...rawArgs: any[]
 ): ComponentPublicInstance | null | undefined {
-  if (instance.isUnmounted) return
-  const props = instance.vnode.props || EMPTY_OBJ
+  return baseEmit(
+    instance,
+    instance.vnode.props || EMPTY_OBJ,
+    defaultPropGetter,
+    event,
+    ...rawArgs,
+  )
+}
 
+/**
+ * @internal for vapor only
+ */
+export function baseEmit(
+  instance: GenericComponentInstance,
+  props: Record<string, any>,
+  getter: (props: Record<string, any>, key: string) => unknown,
+  event: string,
+  ...rawArgs: any[]
+): ComponentPublicInstance | null | undefined {
+  if (instance.isUnmounted) return
   if (__DEV__) {
-    const {
-      emitsOptions,
-      propsOptions: [propsOptions],
-    } = instance
+    const { emitsOptions, propsOptions } = instance
     if (emitsOptions) {
       if (
         !(event in emitsOptions) &&
@@ -130,7 +145,11 @@ export function emit(
             event.startsWith(compatModelEventPrefix))
         )
       ) {
-        if (!propsOptions || !(toHandlerKey(camelize(event)) in propsOptions)) {
+        if (
+          !propsOptions ||
+          !propsOptions[0] ||
+          !(toHandlerKey(camelize(event)) in propsOptions[0])
+        ) {
           warn(
             `Component emitted event "${event}" but it is neither declared in ` +
               `the emits option nor as an "${toHandlerKey(camelize(event))}" prop.`,
@@ -154,11 +173,12 @@ export function emit(
   const isCompatModelListener =
     __COMPAT__ && compatModelEventPrefix + event in props
   const isModelListener = isCompatModelListener || event.startsWith('update:')
+  // for v-model update:xxx events, apply modifiers on args
+  // it's ok to use static get because modelModifiers can only be in the static
+  // part of the props
   const modifiers = isCompatModelListener
     ? props.modelModifiers
-    : isModelListener && getModelModifiers(props, event.slice(7))
-
-  // for v-model update:xxx events, apply modifiers on args
+    : isModelListener && getModelModifiers(props, event.slice(7), getter)
   if (modifiers) {
     if (modifiers.trim) {
       args = rawArgs.map(a => (isString(a) ? a.trim() : a))
@@ -174,7 +194,10 @@ export function emit(
 
   if (__DEV__) {
     const lowerCaseEvent = event.toLowerCase()
-    if (lowerCaseEvent !== event && props[toHandlerKey(lowerCaseEvent)]) {
+    if (
+      lowerCaseEvent !== event &&
+      getter(props, toHandlerKey(lowerCaseEvent))
+    ) {
       warn(
         `Event "${lowerCaseEvent}" is emitted in component ` +
           `${formatComponentName(
@@ -192,25 +215,25 @@ export function emit(
 
   let handlerName
   let handler =
-    props[(handlerName = toHandlerKey(event))] ||
+    getter(props, (handlerName = toHandlerKey(event))) ||
     // also try camelCase event handler (#2249)
-    props[(handlerName = toHandlerKey(camelize(event)))]
+    getter(props, (handlerName = toHandlerKey(camelize(event))))
   // for v-model update:xxx events, also trigger kebab-case equivalent
   // for props passed via kebab-case
   if (!handler && isModelListener) {
-    handler = props[(handlerName = toHandlerKey(hyphenate(event)))]
+    handler = getter(props, (handlerName = toHandlerKey(hyphenate(event))))
   }
 
   if (handler) {
     callWithAsyncErrorHandling(
-      handler,
+      handler as Function | Function[],
       instance,
       ErrorCodes.COMPONENT_EVENT_HANDLER,
       args,
     )
   }
 
-  const onceHandler = props[handlerName + `Once`]
+  const onceHandler = getter(props, handlerName + `Once`)
   if (onceHandler) {
     if (!instance.emitted) {
       instance.emitted = {}
@@ -219,19 +242,30 @@ export function emit(
     }
     instance.emitted[handlerName] = true
     callWithAsyncErrorHandling(
-      onceHandler,
+      onceHandler as Function | Function[],
       instance,
       ErrorCodes.COMPONENT_EVENT_HANDLER,
       args,
     )
   }
 
-  if (__COMPAT__) {
-    compatModelEmit(instance, event, args)
-    return compatInstanceEmit(instance, event, args)
+  if (__COMPAT__ && args) {
+    compatModelEmit(instance as ComponentInternalInstance, event, args)
+    return compatInstanceEmit(
+      instance as ComponentInternalInstance,
+      event,
+      args,
+    )
   }
 }
 
+export function defaultPropGetter(
+  props: Record<string, any>,
+  key: string,
+): unknown {
+  return props[key]
+}
+
 export function normalizeEmitsOptions(
   comp: ConcreteComponent,
   appContext: AppContext,
@@ -286,9 +320,13 @@ export function normalizeEmitsOptions(
   return normalized
 }
 
-// Check if an incoming prop key is a declared emit event listener.
-// e.g. With `emits: { click: null }`, props named `onClick` and `onclick` are
-// both considered matched listeners.
+/**
+ * Check if an incoming prop key is a declared emit event listener.
+ * e.g. With `emits: { click: null }`, props named `onClick` and `onclick` are
+ * both considered matched listeners.
+ *
+ * @internal for vapor only
+ */
 export function isEmitListener(
   options: ObjectEmitsOptions | null,
   key: string,
diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts
index 5db6a0a1760..3422d3a4f4f 100644
--- a/packages/runtime-core/src/componentOptions.ts
+++ b/packages/runtime-core/src/componentOptions.ts
@@ -6,7 +6,7 @@ import {
   type Data,
   type InternalRenderFunction,
   type SetupContext,
-  currentInstance,
+  getCurrentInstance,
 } from './component'
 import {
   type LooseRequired,
@@ -855,10 +855,8 @@ export function createWatcher(
 
   const options: WatchOptions = {}
   if (__COMPAT__) {
-    const instance =
-      currentInstance && getCurrentScope() === currentInstance.scope
-        ? currentInstance
-        : null
+    const cur = getCurrentInstance()
+    const instance = cur && getCurrentScope() === cur.scope ? cur : null
 
     const newValue = getter()
     if (
diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts
index 775eb8b6728..a535927cd07 100644
--- a/packages/runtime-core/src/componentProps.ts
+++ b/packages/runtime-core/src/componentProps.ts
@@ -30,6 +30,7 @@ import {
   type ComponentOptions,
   type ConcreteComponent,
   type Data,
+  type GenericComponentInstance,
   setCurrentInstance,
 } from './component'
 import { isEmitListener } from './componentEmits'
@@ -185,9 +186,12 @@ type NormalizedProp = PropOptions & {
   [BooleanFlags.shouldCastTrue]?: boolean
 }
 
-// normalized value is a tuple of the actual normalized options
-// and an array of prop keys that need value casting (booleans and defaults)
+/**
+ * normalized value is a tuple of the actual normalized options
+ * and an array of prop keys that need value casting (booleans and defaults)
+ */
 export type NormalizedProps = Record<string, NormalizedProp>
+
 export type NormalizedPropsOptions = [NormalizedProps, string[]] | []
 
 export function initProps(
@@ -196,7 +200,7 @@ export function initProps(
   isStateful: number, // result of bitwise flag comparison
   isSSR = false,
 ): void {
-  const props: Data = {}
+  const props: Data = (instance.props = {})
   const attrs: Data = createInternalObject()
 
   instance.propsDefaults = Object.create(null)
@@ -212,7 +216,7 @@ export function initProps(
 
   // validation
   if (__DEV__) {
-    validateProps(rawProps || {}, props, instance)
+    validateProps(rawProps || {}, props, instance.propsOptions[0]!)
   }
 
   if (isStateful) {
@@ -230,7 +234,7 @@ export function initProps(
   instance.attrs = attrs
 }
 
-function isInHmrContext(instance: ComponentInternalInstance | null) {
+function isInHmrContext(instance: GenericComponentInstance | null) {
   while (instance) {
     if (instance.type.__hmrId) return true
     instance = instance.parent
@@ -284,11 +288,10 @@ export function updateProps(
             const camelizedKey = camelize(key)
             props[camelizedKey] = resolvePropValue(
               options,
-              rawCurrentProps,
               camelizedKey,
               value,
               instance,
-              false /* isAbsent */,
+              baseResolveDefault,
             )
           }
         } else {
@@ -333,10 +336,10 @@ export function updateProps(
           ) {
             props[key] = resolvePropValue(
               options,
-              rawCurrentProps,
               key,
               undefined,
               instance,
+              baseResolveDefault,
               true /* isAbsent */,
             )
           }
@@ -367,7 +370,7 @@ export function updateProps(
   }
 
   if (__DEV__) {
-    validateProps(rawProps || {}, props, instance)
+    validateProps(rawProps || {}, props, instance.propsOptions[0]!)
   }
 }
 
@@ -430,16 +433,15 @@ function setFullProps(
   }
 
   if (needCastKeys) {
-    const rawCurrentProps = toRaw(props)
     const castValues = rawCastValues || EMPTY_OBJ
     for (let i = 0; i < needCastKeys.length; i++) {
       const key = needCastKeys[i]
       props[key] = resolvePropValue(
         options!,
-        rawCurrentProps,
         key,
         castValues[key],
         instance,
+        baseResolveDefault,
         !hasOwn(castValues, key),
       )
     }
@@ -448,14 +450,26 @@ function setFullProps(
   return hasAttrsChanged
 }
 
-function resolvePropValue(
+/**
+ * @internal for runtime-vapor
+ */
+export function resolvePropValue<
+  T extends GenericComponentInstance & Pick<ComponentInternalInstance, 'ce'>,
+>(
   options: NormalizedProps,
-  props: Data,
   key: string,
   value: unknown,
-  instance: ComponentInternalInstance,
-  isAbsent: boolean,
-) {
+  instance: T,
+  /**
+   * Allow runtime-specific default resolution logic
+   */
+  resolveDefault: (
+    factory: (props: Data) => unknown,
+    instance: T,
+    key: string,
+  ) => unknown,
+  isAbsent = false,
+): unknown {
   const opt = options[key]
   if (opt != null) {
     const hasDefault = hasOwn(opt, 'default')
@@ -467,19 +481,16 @@ function resolvePropValue(
         !opt.skipFactory &&
         isFunction(defaultValue)
       ) {
-        const { propsDefaults } = instance
-        if (key in propsDefaults) {
-          value = propsDefaults[key]
+        const cachedDefaults =
+          instance.propsDefaults || (instance.propsDefaults = {})
+        if (hasOwn(cachedDefaults, key)) {
+          value = cachedDefaults[key]
         } else {
-          const reset = setCurrentInstance(instance)
-          value = propsDefaults[key] = defaultValue.call(
-            __COMPAT__ &&
-              isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance)
-              ? createPropsDefaultThis(instance, props, key)
-              : null,
-            props,
+          value = cachedDefaults[key] = resolveDefault(
+            defaultValue,
+            instance,
+            key,
           )
-          reset()
         }
       } else {
         value = defaultValue
@@ -504,6 +515,27 @@ function resolvePropValue(
   return value
 }
 
+/**
+ * runtime-dom-specific default resolving logic
+ */
+function baseResolveDefault(
+  factory: (props: Data) => unknown,
+  instance: ComponentInternalInstance,
+  key: string,
+) {
+  let value
+  const prev = setCurrentInstance(instance)
+  const props = toRaw(instance.props)
+  value = factory.call(
+    __COMPAT__ && isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance)
+      ? createPropsDefaultThis(instance, props, key)
+      : null,
+    props,
+  )
+  setCurrentInstance(...prev)
+  return value
+}
+
 const mixinPropsCache = new WeakMap<ConcreteComponent, NormalizedPropsOptions>()
 
 export function normalizePropsOptions(
@@ -552,6 +584,22 @@ export function normalizePropsOptions(
     return EMPTY_ARR as any
   }
 
+  baseNormalizePropsOptions(raw, normalized, needCastKeys)
+  const res: NormalizedPropsOptions = [normalized, needCastKeys]
+  if (isObject(comp)) {
+    cache.set(comp, res)
+  }
+  return res
+}
+
+/**
+ * @internal for runtime-vapor only
+ */
+export function baseNormalizePropsOptions(
+  raw: ComponentPropsOptions | undefined,
+  normalized: NonNullable<NormalizedPropsOptions[0]>,
+  needCastKeys: NonNullable<NormalizedPropsOptions[1]>,
+): void {
   if (isArray(raw)) {
     for (let i = 0; i < raw.length; i++) {
       if (__DEV__ && !isString(raw[i])) {
@@ -606,12 +654,6 @@ export function normalizePropsOptions(
       }
     }
   }
-
-  const res: NormalizedPropsOptions = [normalized, needCastKeys]
-  if (isObject(comp)) {
-    cache.set(comp, res)
-  }
-  return res
 }
 
 function validatePropName(key: string) {
@@ -648,25 +690,26 @@ function getType(ctor: Prop<any> | null): string {
 
 /**
  * dev only
+ * @internal
  */
-function validateProps(
+export function validateProps(
   rawProps: Data,
-  props: Data,
-  instance: ComponentInternalInstance,
-) {
-  const resolvedValues = toRaw(props)
-  const options = instance.propsOptions[0]
+  resolvedProps: Data,
+  options: NormalizedProps,
+): void {
+  resolvedProps = toRaw(resolvedProps)
   const camelizePropsKey = Object.keys(rawProps).map(key => camelize(key))
   for (const key in options) {
-    let opt = options[key]
-    if (opt == null) continue
-    validateProp(
-      key,
-      resolvedValues[key],
-      opt,
-      __DEV__ ? shallowReadonly(resolvedValues) : resolvedValues,
-      !camelizePropsKey.includes(key),
-    )
+    const opt = options[key]
+    if (opt != null) {
+      validateProp(
+        key,
+        resolvedProps[key],
+        opt,
+        resolvedProps,
+        !camelizePropsKey.includes(key),
+      )
+    }
   }
 }
 
@@ -674,16 +717,16 @@ function validateProps(
  * dev only
  */
 function validateProp(
-  name: string,
+  key: string,
   value: unknown,
-  prop: PropOptions,
-  props: Data,
+  propOptions: PropOptions,
+  resolvedProps: Data,
   isAbsent: boolean,
 ) {
-  const { type, required, validator, skipCheck } = prop
+  const { type, required, validator, skipCheck } = propOptions
   // required!
   if (required && isAbsent) {
-    warn('Missing required prop: "' + name + '"')
+    warn('Missing required prop: "' + key + '"')
     return
   }
   // missing but optional
@@ -702,13 +745,16 @@ function validateProp(
       isValid = valid
     }
     if (!isValid) {
-      warn(getInvalidTypeMessage(name, value, expectedTypes))
+      warn(getInvalidTypeMessage(key, value, expectedTypes))
       return
     }
   }
   // custom validator
-  if (validator && !validator(value, props)) {
-    warn('Invalid prop: custom validator check failed for prop "' + name + '".')
+  if (
+    validator &&
+    !validator(value, __DEV__ ? shallowReadonly(resolvedProps) : resolvedProps)
+  ) {
+    warn('Invalid prop: custom validator check failed for prop "' + key + '".')
   }
 }
 
diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts
index e9e7770ebd9..a43c99e2f45 100644
--- a/packages/runtime-core/src/componentPublicInstance.ts
+++ b/packages/runtime-core/src/componentPublicInstance.ts
@@ -2,6 +2,7 @@ import {
   type Component,
   type ComponentInternalInstance,
   type Data,
+  type GenericComponentInstance,
   getComponentPublicInstance,
   isStatefulComponent,
 } from './component'
@@ -355,10 +356,11 @@ export type PublicPropertiesMap = Record<
  * public $parent chains, skip functional ones and go to the parent instead.
  */
 const getPublicInstance = (
-  i: ComponentInternalInstance | null,
+  i: GenericComponentInstance | null,
 ): ComponentPublicInstance | ComponentInternalInstance['exposed'] | null => {
-  if (!i) return null
-  if (isStatefulComponent(i)) return getComponentPublicInstance(i)
+  if (!i || i.vapor) return null
+  if (isStatefulComponent(i as ComponentInternalInstance))
+    return getComponentPublicInstance(i)
   return getPublicInstance(i.parent)
 }
 
diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts
index a1afae6201a..a62b5cf4a82 100644
--- a/packages/runtime-core/src/componentRenderUtils.ts
+++ b/packages/runtime-core/src/componentRenderUtils.ts
@@ -454,13 +454,13 @@ export function updateHOCHostEl(
   { vnode, parent }: ComponentInternalInstance,
   el: typeof vnode.el, // HostNode
 ): void {
-  while (parent) {
-    const root = parent.subTree
+  while (parent && !parent.vapor) {
+    const root = parent.subTree!
     if (root.suspense && root.suspense.activeBranch === vnode) {
       root.el = vnode.el
     }
     if (root === vnode) {
-      ;(vnode = parent.vnode).el = el
+      ;(vnode = parent.vnode!).el = el
       parent = parent.parent
     } else {
       break
diff --git a/packages/runtime-core/src/componentSlots.ts b/packages/runtime-core/src/componentSlots.ts
index 6114f6c86cf..b2394f72c39 100644
--- a/packages/runtime-core/src/componentSlots.ts
+++ b/packages/runtime-core/src/componentSlots.ts
@@ -106,6 +106,7 @@ const normalizeSlot = (
     if (
       __DEV__ &&
       currentInstance &&
+      !currentInstance.vapor &&
       !(ctx === null && currentRenderingInstance) &&
       !(ctx && ctx.root !== currentInstance.root)
     ) {
diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts
index f2b7bdf9738..f4244f360e3 100644
--- a/packages/runtime-core/src/components/KeepAlive.ts
+++ b/packages/runtime-core/src/components/KeepAlive.ts
@@ -2,8 +2,8 @@ import {
   type ComponentInternalInstance,
   type ComponentOptions,
   type ConcreteComponent,
+  type GenericComponentInstance,
   type SetupContext,
-  currentInstance,
   getComponentName,
   getCurrentInstance,
 } from '../component'
@@ -91,13 +91,13 @@ const KeepAliveImpl: ComponentOptions = {
   },
 
   setup(props: KeepAliveProps, { slots }: SetupContext) {
-    const instance = getCurrentInstance()!
+    const keepAliveInstance = getCurrentInstance()!
     // KeepAlive communicates with the instantiated renderer via the
     // ctx where the renderer passes in its internals,
     // and the KeepAlive instance exposes activate/deactivate implementations.
     // The whole point of this is to avoid importing KeepAlive directly in the
     // renderer to facilitate tree-shaking.
-    const sharedContext = instance.ctx as KeepAliveContext
+    const sharedContext = keepAliveInstance.ctx as KeepAliveContext
 
     // if the internal renderer is not registered, it indicates that this is server-side rendering,
     // for KeepAlive, we just need to render its children
@@ -113,10 +113,10 @@ const KeepAliveImpl: ComponentOptions = {
     let current: VNode | null = null
 
     if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
-      ;(instance as any).__v_cache = cache
+      ;(keepAliveInstance as any).__v_cache = cache
     }
 
-    const parentSuspense = instance.suspense
+    const parentSuspense = keepAliveInstance.suspense
 
     const {
       renderer: {
@@ -136,7 +136,14 @@ const KeepAliveImpl: ComponentOptions = {
       optimized,
     ) => {
       const instance = vnode.component!
-      move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
+      move(
+        vnode,
+        container,
+        anchor,
+        MoveType.ENTER,
+        keepAliveInstance,
+        parentSuspense,
+      )
       // in case props have changed
       patch(
         instance.vnode,
@@ -149,16 +156,20 @@ const KeepAliveImpl: ComponentOptions = {
         vnode.slotScopeIds,
         optimized,
       )
-      queuePostRenderEffect(() => {
-        instance.isDeactivated = false
-        if (instance.a) {
-          invokeArrayFns(instance.a)
-        }
-        const vnodeHook = vnode.props && vnode.props.onVnodeMounted
-        if (vnodeHook) {
-          invokeVNodeHook(vnodeHook, instance.parent, vnode)
-        }
-      }, parentSuspense)
+      queuePostRenderEffect(
+        () => {
+          instance.isDeactivated = false
+          if (instance.a) {
+            invokeArrayFns(instance.a)
+          }
+          const vnodeHook = vnode.props && vnode.props.onVnodeMounted
+          if (vnodeHook) {
+            invokeVNodeHook(vnodeHook, instance.parent, vnode)
+          }
+        },
+        undefined,
+        parentSuspense,
+      )
 
       if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
         // Update components tree
@@ -171,17 +182,28 @@ const KeepAliveImpl: ComponentOptions = {
       invalidateMount(instance.m)
       invalidateMount(instance.a)
 
-      move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
-      queuePostRenderEffect(() => {
-        if (instance.da) {
-          invokeArrayFns(instance.da)
-        }
-        const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
-        if (vnodeHook) {
-          invokeVNodeHook(vnodeHook, instance.parent, vnode)
-        }
-        instance.isDeactivated = true
-      }, parentSuspense)
+      move(
+        vnode,
+        storageContainer,
+        null,
+        MoveType.LEAVE,
+        keepAliveInstance,
+        parentSuspense,
+      )
+      queuePostRenderEffect(
+        () => {
+          if (instance.da) {
+            invokeArrayFns(instance.da)
+          }
+          const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
+          if (vnodeHook) {
+            invokeVNodeHook(vnodeHook, instance.parent, vnode)
+          }
+          instance.isDeactivated = true
+        },
+        undefined,
+        parentSuspense,
+      )
 
       if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
         // Update components tree
@@ -197,7 +219,7 @@ const KeepAliveImpl: ComponentOptions = {
     function unmount(vnode: VNode) {
       // reset the shapeFlag so it can be properly unmounted
       resetShapeFlag(vnode)
-      _unmount(vnode, instance, parentSuspense, true)
+      _unmount(vnode, keepAliveInstance, parentSuspense, true)
     }
 
     function pruneCache(filter: (name: string) => boolean) {
@@ -240,12 +262,19 @@ const KeepAliveImpl: ComponentOptions = {
       if (pendingCacheKey != null) {
         // if KeepAlive child is a Suspense, it needs to be cached after Suspense resolves
         // avoid caching vnode that not been mounted
-        if (isSuspense(instance.subTree.type)) {
-          queuePostRenderEffect(() => {
-            cache.set(pendingCacheKey!, getInnerChild(instance.subTree))
-          }, instance.subTree.suspense)
+        if (isSuspense(keepAliveInstance.subTree.type)) {
+          queuePostRenderEffect(
+            () => {
+              cache.set(
+                pendingCacheKey!,
+                getInnerChild(keepAliveInstance.subTree),
+              )
+            },
+            undefined,
+            keepAliveInstance.subTree.suspense,
+          )
         } else {
-          cache.set(pendingCacheKey, getInnerChild(instance.subTree))
+          cache.set(pendingCacheKey, getInnerChild(keepAliveInstance.subTree))
         }
       }
     }
@@ -254,14 +283,14 @@ const KeepAliveImpl: ComponentOptions = {
 
     onBeforeUnmount(() => {
       cache.forEach(cached => {
-        const { subTree, suspense } = instance
+        const { subTree, suspense } = keepAliveInstance
         const vnode = getInnerChild(subTree)
         if (cached.type === vnode.type && cached.key === vnode.key) {
           // current instance will be unmounted as part of keep-alive's unmount
           resetShapeFlag(vnode)
           // but invoke its deactivated hook here
           const da = vnode.component!.da
-          da && queuePostRenderEffect(da, suspense)
+          da && queuePostRenderEffect(da, undefined, suspense)
           return
         }
         unmount(cached)
@@ -416,7 +445,7 @@ export function onDeactivated(
 function registerKeepAliveHook(
   hook: Function & { __wdc?: Function },
   type: LifecycleHooks,
-  target: ComponentInternalInstance | null = currentInstance,
+  target: ComponentInternalInstance | null = getCurrentInstance(),
 ) {
   // cache the deactivate branch check wrapper for injected hooks so the same
   // hook can be properly deduped by the scheduler. "__wdc" stands for "with
@@ -425,7 +454,7 @@ function registerKeepAliveHook(
     hook.__wdc ||
     (hook.__wdc = () => {
       // only fire the hook if the target instance is NOT in a deactivated branch.
-      let current: ComponentInternalInstance | null = target
+      let current: GenericComponentInstance | null = target
       while (current) {
         if (current.isDeactivated) {
           return
@@ -442,7 +471,7 @@ function registerKeepAliveHook(
   // arrays.
   if (target) {
     let current = target.parent
-    while (current && current.parent) {
+    while (current && current.parent && current.parent.vnode) {
       if (isKeepAlive(current.parent.vnode)) {
         injectToKeepAliveRoot(wrappedHook, type, target, current)
       }
@@ -455,7 +484,7 @@ function injectToKeepAliveRoot(
   hook: Function & { __weh?: Function },
   type: LifecycleHooks,
   target: ComponentInternalInstance,
-  keepAliveRoot: ComponentInternalInstance,
+  keepAliveRoot: GenericComponentInstance,
 ) {
   // injectHook wraps the original for error handling, so make sure to remove
   // the wrapped version.
diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts
index 85001f500cf..2539145bd00 100644
--- a/packages/runtime-core/src/components/Suspense.ts
+++ b/packages/runtime-core/src/components/Suspense.ts
@@ -549,6 +549,7 @@ function createSuspenseBoundary(
                 container,
                 anchor === initialAnchor ? next(activeBranch!) : anchor,
                 MoveType.ENTER,
+                parentComponent,
               )
               queuePostFlushCb(effects)
             }
@@ -573,7 +574,13 @@ function createSuspenseBoundary(
         }
         if (!delayEnter) {
           // move content from off-dom container to actual container
-          move(pendingBranch!, container, anchor, MoveType.ENTER)
+          move(
+            pendingBranch!,
+            container,
+            anchor,
+            MoveType.ENTER,
+            parentComponent,
+          )
         }
       }
 
@@ -672,7 +679,7 @@ function createSuspenseBoundary(
 
     move(container, anchor, type) {
       suspense.activeBranch &&
-        move(suspense.activeBranch, container, anchor, type)
+        move(suspense.activeBranch, container, anchor, type, parentComponent)
       suspense.container = container
     },
 
@@ -866,6 +873,7 @@ function normalizeSuspenseSlot(s: any) {
 
 export function queueEffectWithSuspense(
   fn: Function | Function[],
+  id: number | undefined,
   suspense: SuspenseBoundary | null,
 ): void {
   if (suspense && suspense.pendingBranch) {
@@ -875,7 +883,7 @@ export function queueEffectWithSuspense(
       suspense.effects.push(fn)
     }
   } else {
-    queuePostFlushCb(fn)
+    queuePostFlushCb(fn, id)
   }
 }
 
diff --git a/packages/runtime-core/src/components/Teleport.ts b/packages/runtime-core/src/components/Teleport.ts
index c37356a7869..7e3b132902f 100644
--- a/packages/runtime-core/src/components/Teleport.ts
+++ b/packages/runtime-core/src/components/Teleport.ts
@@ -165,29 +165,37 @@ export const TeleportImpl = {
 
       if (isTeleportDeferred(n2.props)) {
         n2.el!.__isMounted = false
-        queuePostRenderEffect(() => {
-          mountToTarget()
-          delete n2.el!.__isMounted
-        }, parentSuspense)
+        queuePostRenderEffect(
+          () => {
+            mountToTarget()
+            delete n2.el!.__isMounted
+          },
+          undefined,
+          parentSuspense,
+        )
       } else {
         mountToTarget()
       }
     } else {
       if (isTeleportDeferred(n2.props) && n1.el!.__isMounted === false) {
-        queuePostRenderEffect(() => {
-          TeleportImpl.process(
-            n1,
-            n2,
-            container,
-            anchor,
-            parentComponent,
-            parentSuspense,
-            namespace,
-            slotScopeIds,
-            optimized,
-            internals,
-          )
-        }, parentSuspense)
+        queuePostRenderEffect(
+          () => {
+            TeleportImpl.process(
+              n1,
+              n2,
+              container,
+              anchor,
+              parentComponent,
+              parentSuspense,
+              namespace,
+              slotScopeIds,
+              optimized,
+              internals,
+            )
+          },
+          undefined,
+          parentSuspense,
+        )
         return
       }
       // update content
@@ -245,6 +253,7 @@ export const TeleportImpl = {
             container,
             mainAnchor,
             internals,
+            parentComponent,
             TeleportMoveTypes.TOGGLE,
           )
         } else {
@@ -268,6 +277,7 @@ export const TeleportImpl = {
               nextTarget,
               null,
               internals,
+              parentComponent,
               TeleportMoveTypes.TARGET_CHANGE,
             )
           } else if (__DEV__) {
@@ -285,6 +295,7 @@ export const TeleportImpl = {
             target,
             targetAnchor,
             internals,
+            parentComponent,
             TeleportMoveTypes.TOGGLE,
           )
         }
@@ -347,6 +358,7 @@ function moveTeleport(
   container: RendererElement,
   parentAnchor: RendererNode | null,
   { o: { insert }, m: move }: RendererInternals,
+  parentComponent: ComponentInternalInstance | null,
   moveType: TeleportMoveTypes = TeleportMoveTypes.REORDER,
 ): void {
   // move target anchor if this is a target change.
@@ -371,6 +383,7 @@ function moveTeleport(
           container,
           parentAnchor,
           MoveType.REORDER,
+          parentComponent,
         )
       }
     }
diff --git a/packages/runtime-core/src/customFormatter.ts b/packages/runtime-core/src/customFormatter.ts
index dfe39bf4387..9338e8a92f8 100644
--- a/packages/runtime-core/src/customFormatter.ts
+++ b/packages/runtime-core/src/customFormatter.ts
@@ -4,8 +4,7 @@ import {
   isReadonly,
   isRef,
   isShallow,
-  pauseTracking,
-  resetTracking,
+  setActiveSub,
   toRaw,
 } from '@vue/reactivity'
 import { EMPTY_OBJ, extend, isArray, isFunction, isObject } from '@vue/shared'
@@ -37,9 +36,9 @@ export function initCustomFormatter(): void {
         return ['div', vueStyle, `VueInstance`]
       } else if (isRef(obj)) {
         // avoid tracking during debugger accessing
-        pauseTracking()
+        const prevSub = setActiveSub()
         const value = obj.value
-        resetTracking()
+        setActiveSub(prevSub)
         return [
           'div',
           {},
diff --git a/packages/runtime-core/src/devtools.ts b/packages/runtime-core/src/devtools.ts
index 9ac4c433ac1..8b300f6955b 100644
--- a/packages/runtime-core/src/devtools.ts
+++ b/packages/runtime-core/src/devtools.ts
@@ -1,7 +1,7 @@
 /* eslint-disable no-restricted-globals */
 import type { App } from './apiCreateApp'
 import { Comment, Fragment, Static, Text } from './vnode'
-import type { ComponentInternalInstance } from './component'
+import type { GenericComponentInstance } from './component'
 
 interface AppRecord {
   id: number
@@ -111,7 +111,7 @@ const _devtoolsComponentRemoved = /*@__PURE__*/ createDevtoolsComponentHook(
 )
 
 export const devtoolsComponentRemoved = (
-  component: ComponentInternalInstance,
+  component: GenericComponentInstance,
 ): void => {
   if (
     devtools &&
@@ -123,13 +123,13 @@ export const devtoolsComponentRemoved = (
   }
 }
 
-type DevtoolsComponentHook = (component: ComponentInternalInstance) => void
+type DevtoolsComponentHook = (component: GenericComponentInstance) => void
 
 /*! #__NO_SIDE_EFFECTS__ */
 function createDevtoolsComponentHook(
   hook: DevtoolsHooks,
 ): DevtoolsComponentHook {
-  return (component: ComponentInternalInstance) => {
+  return (component: GenericComponentInstance) => {
     emit(
       hook,
       component.appContext.app,
@@ -147,20 +147,20 @@ export const devtoolsPerfEnd: DevtoolsPerformanceHook =
   /*@__PURE__*/ createDevtoolsPerformanceHook(DevtoolsHooks.PERFORMANCE_END)
 
 type DevtoolsPerformanceHook = (
-  component: ComponentInternalInstance,
+  component: GenericComponentInstance,
   type: string,
   time: number,
 ) => void
 function createDevtoolsPerformanceHook(
   hook: DevtoolsHooks,
 ): DevtoolsPerformanceHook {
-  return (component: ComponentInternalInstance, type: string, time: number) => {
+  return (component: GenericComponentInstance, type: string, time: number) => {
     emit(hook, component.appContext.app, component.uid, component, type, time)
   }
 }
 
 export function devtoolsComponentEmit(
-  component: ComponentInternalInstance,
+  component: GenericComponentInstance,
   event: string,
   params: any[],
 ): void {
diff --git a/packages/runtime-core/src/directives.ts b/packages/runtime-core/src/directives.ts
index 5897b39df82..5e3902dcb4b 100644
--- a/packages/runtime-core/src/directives.ts
+++ b/packages/runtime-core/src/directives.ts
@@ -23,7 +23,7 @@ import { currentRenderingInstance } from './componentRenderContext'
 import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
 import type { ComponentPublicInstance } from './componentPublicInstance'
 import { mapCompatDirectiveHook } from './compat/customDirective'
-import { pauseTracking, resetTracking, traverse } from '@vue/reactivity'
+import { setActiveSub, traverse } from '@vue/reactivity'
 
 export interface DirectiveBinding<
   Value = any,
@@ -187,14 +187,14 @@ export function invokeDirectiveHook(
     if (hook) {
       // disable tracking inside all lifecycle hooks
       // since they can potentially be called inside effects.
-      pauseTracking()
+      const prevSub = setActiveSub()
       callWithAsyncErrorHandling(hook, instance, ErrorCodes.DIRECTIVE_HOOK, [
         vnode.el,
         binding,
         vnode,
         prevVNode,
       ])
-      resetTracking()
+      setActiveSub(prevSub)
     }
   }
 }
diff --git a/packages/runtime-core/src/errorHandling.ts b/packages/runtime-core/src/errorHandling.ts
index c4bdf0baccd..0090b6c16ad 100644
--- a/packages/runtime-core/src/errorHandling.ts
+++ b/packages/runtime-core/src/errorHandling.ts
@@ -1,6 +1,5 @@
-import { pauseTracking, resetTracking } from '@vue/reactivity'
-import type { VNode } from './vnode'
-import type { ComponentInternalInstance } from './component'
+import { setActiveSub } from '@vue/reactivity'
+import type { GenericComponentInstance } from './component'
 import { popWarningContext, pushWarningContext, warn } from './warning'
 import { EMPTY_OBJ, isArray, isFunction, isPromise } from '@vue/shared'
 import { LifecycleHooks } from './enums'
@@ -69,7 +68,7 @@ export type ErrorTypes = LifecycleHooks | ErrorCodes | WatchErrorCodes
 
 export function callWithErrorHandling(
   fn: Function,
-  instance: ComponentInternalInstance | null | undefined,
+  instance: GenericComponentInstance | null | undefined,
   type: ErrorTypes,
   args?: unknown[],
 ): any {
@@ -82,7 +81,7 @@ export function callWithErrorHandling(
 
 export function callWithAsyncErrorHandling(
   fn: Function | Function[],
-  instance: ComponentInternalInstance | null,
+  instance: GenericComponentInstance | null,
   type: ErrorTypes,
   args?: unknown[],
 ): any {
@@ -111,17 +110,16 @@ export function callWithAsyncErrorHandling(
 
 export function handleError(
   err: unknown,
-  instance: ComponentInternalInstance | null | undefined,
+  instance: GenericComponentInstance | null | undefined,
   type: ErrorTypes,
   throwInDev = true,
 ): void {
-  const contextVNode = instance ? instance.vnode : null
   const { errorHandler, throwUnhandledErrorInProduction } =
     (instance && instance.appContext.config) || EMPTY_OBJ
   if (instance) {
     let cur = instance.parent
     // the exposed instance is the render proxy to keep it consistent with 2.x
-    const exposedInstance = instance.proxy
+    const exposedInstance = instance.proxy || instance
     // in production the hook receives only the error code
     const errorInfo = __DEV__
       ? ErrorTypeStrings[type]
@@ -141,33 +139,33 @@ export function handleError(
     }
     // app-level handling
     if (errorHandler) {
-      pauseTracking()
+      const prevSub = setActiveSub()
       callWithErrorHandling(errorHandler, null, ErrorCodes.APP_ERROR_HANDLER, [
         err,
         exposedInstance,
         errorInfo,
       ])
-      resetTracking()
+      setActiveSub(prevSub)
       return
     }
   }
-  logError(err, type, contextVNode, throwInDev, throwUnhandledErrorInProduction)
+  logError(err, type, instance, throwInDev, throwUnhandledErrorInProduction)
 }
 
 function logError(
   err: unknown,
   type: ErrorTypes,
-  contextVNode: VNode | null,
+  instance: GenericComponentInstance | null | undefined,
   throwInDev = true,
   throwInProd = false,
 ) {
   if (__DEV__) {
     const info = ErrorTypeStrings[type]
-    if (contextVNode) {
-      pushWarningContext(contextVNode)
+    if (instance) {
+      pushWarningContext(instance)
     }
     warn(`Unhandled error${info ? ` during execution of ${info}` : ``}`)
-    if (contextVNode) {
+    if (instance) {
       popWarningContext()
     }
     // crash in dev by default so it's more noticeable
diff --git a/packages/runtime-core/src/featureFlags.ts b/packages/runtime-core/src/featureFlags.ts
index 8092e8d3574..149f67512de 100644
--- a/packages/runtime-core/src/featureFlags.ts
+++ b/packages/runtime-core/src/featureFlags.ts
@@ -1,11 +1,15 @@
 import { getGlobalThis } from '@vue/shared'
 
+let initialized = false
+
 /**
  * This is only called in esm-bundler builds.
  * It is called when a renderer is created, in `baseCreateRenderer` so that
  * importing runtime-core is side-effects free.
  */
 export function initFeatureFlags(): void {
+  if (initialized) return
+
   const needWarn = []
 
   if (typeof __FEATURE_OPTIONS_API__ !== 'boolean') {
@@ -35,4 +39,6 @@ export function initFeatureFlags(): void {
         `For more details, see https://link.vuejs.org/feature-flags.`,
     )
   }
+
+  initialized = true
 }
diff --git a/packages/runtime-core/src/helpers/renderSlot.ts b/packages/runtime-core/src/helpers/renderSlot.ts
index 92f7dab36b6..d5feae617a6 100644
--- a/packages/runtime-core/src/helpers/renderSlot.ts
+++ b/packages/runtime-core/src/helpers/renderSlot.ts
@@ -1,4 +1,3 @@
-import type { Data } from '../component'
 import type { RawSlots, Slots } from '../componentSlots'
 import {
   type ContextualRenderFn,
@@ -9,6 +8,7 @@ import {
   Fragment,
   type VNode,
   type VNodeArrayChildren,
+  VaporSlot,
   createBlock,
   createVNode,
   isVNode,
@@ -17,6 +17,7 @@ import {
 import { PatchFlags, SlotFlags, isSymbol } from '@vue/shared'
 import { warn } from '../warning'
 import { isAsyncWrapper } from '../apiAsyncComponent'
+import type { Data } from '../component'
 
 /**
  * Compiler runtime helper for rendering `<slot/>`
@@ -31,11 +32,21 @@ export function renderSlot(
   fallback?: () => VNodeArrayChildren,
   noSlotted?: boolean,
 ): VNode {
+  let slot = slots[name]
+
+  // vapor slots rendered in vdom
+  if (slot && (slot as any).__vapor) {
+    const ret = (openBlock(), createBlock(VaporSlot, props))
+    ret.vs = { slot, fallback }
+    return ret
+  }
+
   if (
-    currentRenderingInstance!.ce ||
-    (currentRenderingInstance!.parent &&
-      isAsyncWrapper(currentRenderingInstance!.parent) &&
-      currentRenderingInstance!.parent.ce)
+    currentRenderingInstance &&
+    (currentRenderingInstance.ce ||
+      (currentRenderingInstance.parent &&
+        isAsyncWrapper(currentRenderingInstance.parent) &&
+        currentRenderingInstance.parent.ce))
   ) {
     // in custom element mode, render <slot/> as actual slot outlets
     // wrap it with a fragment because in shadowRoot: false mode the slot
@@ -52,8 +63,6 @@ export function renderSlot(
     )
   }
 
-  let slot = slots[name]
-
   if (__DEV__ && slot && slot.length > 1) {
     warn(
       `SSR-optimized slot function detected in a non-SSR-optimized render ` +
diff --git a/packages/runtime-core/src/helpers/resolveAssets.ts b/packages/runtime-core/src/helpers/resolveAssets.ts
index aa6532c2811..910fab33424 100644
--- a/packages/runtime-core/src/helpers/resolveAssets.ts
+++ b/packages/runtime-core/src/helpers/resolveAssets.ts
@@ -1,4 +1,5 @@
 import {
+  type ComponentInternalInstance,
   type ComponentOptions,
   type ConcreteComponent,
   currentInstance,
@@ -103,8 +104,13 @@ function resolveAsset(
     const res =
       // local registration
       // check instance[type] first which is resolved for options API
-      resolve(instance[type] || (Component as ComponentOptions)[type], name) ||
+      resolve(
+        (instance as ComponentInternalInstance)[type] ||
+          (Component as ComponentOptions)[type],
+        name,
+      ) ||
       // global registration
+      // @ts-expect-error filters only exist in compat mode
       resolve(instance.appContext[type], name)
 
     if (!res && maybeSelfReference) {
diff --git a/packages/runtime-core/src/helpers/useId.ts b/packages/runtime-core/src/helpers/useId.ts
index 4b51e0dda7f..0fcc1e435c0 100644
--- a/packages/runtime-core/src/helpers/useId.ts
+++ b/packages/runtime-core/src/helpers/useId.ts
@@ -1,11 +1,11 @@
 import {
-  type ComponentInternalInstance,
-  getCurrentInstance,
+  type GenericComponentInstance,
+  getCurrentGenericInstance,
 } from '../component'
 import { warn } from '../warning'
 
 export function useId(): string {
-  const i = getCurrentInstance()
+  const i = getCurrentGenericInstance()
   if (i) {
     return (i.appContext.config.idPrefix || 'v') + '-' + i.ids[0] + i.ids[1]++
   } else if (__DEV__) {
@@ -23,6 +23,6 @@ export function useId(): string {
  * - components with async setup()
  * - components with serverPrefetch
  */
-export function markAsyncBoundary(instance: ComponentInternalInstance): void {
+export function markAsyncBoundary(instance: GenericComponentInstance): void {
   instance.ids = [instance.ids[0] + instance.ids[2]++ + '-', 0, 0]
 }
diff --git a/packages/runtime-core/src/helpers/useModel.ts b/packages/runtime-core/src/helpers/useModel.ts
index c40938ead3c..e85edc6e9a7 100644
--- a/packages/runtime-core/src/helpers/useModel.ts
+++ b/packages/runtime-core/src/helpers/useModel.ts
@@ -1,10 +1,14 @@
 import { type Ref, customRef, ref } from '@vue/reactivity'
 import { EMPTY_OBJ, camelize, hasChanged, hyphenate } from '@vue/shared'
 import type { DefineModelOptions, ModelRef } from '../apiSetupHelpers'
-import { getCurrentInstance } from '../component'
+import {
+  type ComponentInternalInstance,
+  getCurrentGenericInstance,
+} from '../component'
 import { warn } from '../warning'
 import type { NormalizedProps } from '../componentProps'
 import { watchSyncEffect } from '../apiWatch'
+import { defaultPropGetter } from '../componentEmits'
 
 export function useModel<
   M extends PropertyKey,
@@ -22,20 +26,20 @@ export function useModel(
   name: string,
   options: DefineModelOptions = EMPTY_OBJ,
 ): Ref {
-  const i = getCurrentInstance()!
+  const i = getCurrentGenericInstance()!
   if (__DEV__ && !i) {
     warn(`useModel() called without active instance.`)
     return ref() as any
   }
 
   const camelizedName = camelize(name)
-  if (__DEV__ && !(i.propsOptions[0] as NormalizedProps)[camelizedName]) {
+  if (__DEV__ && !(i.propsOptions![0] as NormalizedProps)[camelizedName]) {
     warn(`useModel() called with prop "${name}" which is not declared.`)
     return ref() as any
   }
 
   const hyphenatedName = hyphenate(name)
-  const modifiers = getModelModifiers(props, camelizedName)
+  const modifiers = getModelModifiers(props, camelizedName, defaultPropGetter)
 
   const res = customRef((track, trigger) => {
     let localValue: any
@@ -64,19 +68,38 @@ export function useModel(
         ) {
           return
         }
-        const rawProps = i.vnode!.props
-        if (
-          !(
-            rawProps &&
-            // check if parent has passed v-model
-            (name in rawProps ||
-              camelizedName in rawProps ||
-              hyphenatedName in rawProps) &&
-            (`onUpdate:${name}` in rawProps ||
-              `onUpdate:${camelizedName}` in rawProps ||
-              `onUpdate:${hyphenatedName}` in rawProps)
-          )
-        ) {
+
+        let rawPropKeys
+        let parentPassedModelValue = false
+        let parentPassedModelUpdater = false
+
+        if (i.rawKeys) {
+          // vapor instance
+          rawPropKeys = i.rawKeys()
+        } else {
+          const rawProps = (i as ComponentInternalInstance).vnode!.props
+          rawPropKeys = rawProps && Object.keys(rawProps)
+        }
+
+        if (rawPropKeys) {
+          for (const key of rawPropKeys) {
+            if (
+              key === name ||
+              key === camelizedName ||
+              key === hyphenatedName
+            ) {
+              parentPassedModelValue = true
+            } else if (
+              key === `onUpdate:${name}` ||
+              key === `onUpdate:${camelizedName}` ||
+              key === `onUpdate:${hyphenatedName}`
+            ) {
+              parentPassedModelUpdater = true
+            }
+          }
+        }
+
+        if (!parentPassedModelValue || !parentPassedModelUpdater) {
           // no v-model, local update
           localValue = value
           trigger()
@@ -120,10 +143,11 @@ export function useModel(
 export const getModelModifiers = (
   props: Record<string, any>,
   modelName: string,
+  getter: (props: Record<string, any>, key: string) => any,
 ): Record<string, boolean> | undefined => {
   return modelName === 'modelValue' || modelName === 'model-value'
-    ? props.modelModifiers
-    : props[`${modelName}Modifiers`] ||
-        props[`${camelize(modelName)}Modifiers`] ||
-        props[`${hyphenate(modelName)}Modifiers`]
+    ? getter(props, 'modelModifiers')
+    : getter(props, `${modelName}Modifiers`) ||
+        getter(props, `${camelize(modelName)}Modifiers`) ||
+        getter(props, `${hyphenate(modelName)}Modifiers`)
 }
diff --git a/packages/runtime-core/src/helpers/useTemplateRef.ts b/packages/runtime-core/src/helpers/useTemplateRef.ts
index f516d14c9bd..8ba4409c52a 100644
--- a/packages/runtime-core/src/helpers/useTemplateRef.ts
+++ b/packages/runtime-core/src/helpers/useTemplateRef.ts
@@ -1,5 +1,5 @@
 import { type ShallowRef, readonly, shallowRef } from '@vue/reactivity'
-import { getCurrentInstance } from '../component'
+import { getCurrentGenericInstance } from '../component'
 import { warn } from '../warning'
 import { EMPTY_OBJ } from '@vue/shared'
 
@@ -9,8 +9,8 @@ export type TemplateRef<T = unknown> = Readonly<ShallowRef<T | null>>
 
 export function useTemplateRef<T = unknown, Keys extends string = string>(
   key: Keys,
-): TemplateRef<T> {
-  const i = getCurrentInstance()
+): Readonly<ShallowRef<T | null>> {
+  const i = getCurrentGenericInstance()
   const r = shallowRef(null)
   if (i) {
     const refs = i.refs === EMPTY_OBJ ? (i.refs = {}) : i.refs
diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts
index 7aedf52dd3e..eab407cb561 100644
--- a/packages/runtime-core/src/hmr.ts
+++ b/packages/runtime-core/src/hmr.ts
@@ -4,10 +4,10 @@ import {
   type ComponentInternalInstance,
   type ComponentOptions,
   type ConcreteComponent,
-  type InternalRenderFunction,
+  type GenericComponentInstance,
   isClassComponent,
 } from './component'
-import { queueJob, queuePostFlushCb } from './scheduler'
+import { nextTick, queueJob, queuePostFlushCb } from './scheduler'
 import { extend, getGlobalThis } from '@vue/shared'
 
 type HMRComponent = ComponentOptions | ClassComponent
@@ -16,8 +16,8 @@ export let isHmrUpdating = false
 
 export const hmrDirtyComponents: Map<
   ConcreteComponent,
-  Set<ComponentInternalInstance>
-> = new Map<ConcreteComponent, Set<ComponentInternalInstance>>()
+  Set<GenericComponentInstance>
+> = new Map<ConcreteComponent, Set<GenericComponentInstance>>()
 
 export interface HMRRuntime {
   createRecord: typeof createRecord
@@ -45,11 +45,11 @@ const map: Map<
     // to apply hot updates to the component even when there are no actively
     // rendered instance.
     initialDef: ComponentOptions
-    instances: Set<ComponentInternalInstance>
+    instances: Set<GenericComponentInstance>
   }
 > = new Map()
 
-export function registerHMR(instance: ComponentInternalInstance): void {
+export function registerHMR(instance: GenericComponentInstance): void {
   const id = instance.type.__hmrId!
   let record = map.get(id)
   if (!record) {
@@ -59,7 +59,7 @@ export function registerHMR(instance: ComponentInternalInstance): void {
   record.instances.add(instance)
 }
 
-export function unregisterHMR(instance: ComponentInternalInstance): void {
+export function unregisterHMR(instance: GenericComponentInstance): void {
   map.get(instance.type.__hmrId!)!.instances.delete(instance)
 }
 
@@ -90,14 +90,21 @@ function rerender(id: string, newRender?: Function): void {
   // Create a snapshot which avoids the set being mutated during updates
   ;[...record.instances].forEach(instance => {
     if (newRender) {
-      instance.render = newRender as InternalRenderFunction
+      instance.render = newRender
       normalizeClassComponent(instance.type as HMRComponent).render = newRender
     }
-    instance.renderCache = []
     // this flag forces child components with slot content to update
     isHmrUpdating = true
-    instance.update()
-    isHmrUpdating = false
+    if (instance.vapor) {
+      instance.hmrRerender!()
+    } else {
+      const i = instance as ComponentInternalInstance
+      i.renderCache = []
+      i.effect.run()
+    }
+    nextTick(() => {
+      isHmrUpdating = false
+    })
   })
 }
 
@@ -112,62 +119,73 @@ function reload(id: string, newComp: HMRComponent): void {
   // create a snapshot which avoids the set being mutated during updates
   const instances = [...record.instances]
 
-  for (let i = 0; i < instances.length; i++) {
-    const instance = instances[i]
-    const oldComp = normalizeClassComponent(instance.type as HMRComponent)
-
-    let dirtyInstances = hmrDirtyComponents.get(oldComp)
-    if (!dirtyInstances) {
-      // 1. Update existing comp definition to match new one
-      if (oldComp !== record.initialDef) {
-        updateComponentDef(oldComp, newComp)
-      }
-      // 2. mark definition dirty. This forces the renderer to replace the
-      // component on patch.
-      hmrDirtyComponents.set(oldComp, (dirtyInstances = new Set()))
+  if (newComp.__vapor) {
+    for (const instance of instances) {
+      instance.hmrReload!(newComp)
     }
-    dirtyInstances.add(instance)
+  } else {
+    for (const instance of instances as ComponentInternalInstance[]) {
+      const oldComp = normalizeClassComponent(instance.type as HMRComponent)
+
+      let dirtyInstances = hmrDirtyComponents.get(oldComp)
+      if (!dirtyInstances) {
+        // 1. Update existing comp definition to match new one
+        if (oldComp !== record.initialDef) {
+          updateComponentDef(oldComp, newComp)
+        }
+        // 2. mark definition dirty. This forces the renderer to replace the
+        // component on patch.
+        hmrDirtyComponents.set(oldComp, (dirtyInstances = new Set()))
+      }
+      dirtyInstances.add(instance)
 
-    // 3. invalidate options resolution cache
-    instance.appContext.propsCache.delete(instance.type as any)
-    instance.appContext.emitsCache.delete(instance.type as any)
-    instance.appContext.optionsCache.delete(instance.type as any)
+      // 3. invalidate options resolution cache
+      instance.appContext.propsCache.delete(instance.type as any)
+      instance.appContext.emitsCache.delete(instance.type as any)
+      instance.appContext.optionsCache.delete(instance.type as any)
 
-    // 4. actually update
-    if (instance.ceReload) {
-      // custom element
-      dirtyInstances.add(instance)
-      instance.ceReload((newComp as any).styles)
-      dirtyInstances.delete(instance)
-    } else if (instance.parent) {
-      // 4. Force the parent instance to re-render. This will cause all updated
-      // components to be unmounted and re-mounted. Queue the update so that we
-      // don't end up forcing the same parent to re-render multiple times.
-      queueJob(() => {
-        isHmrUpdating = true
-        instance.parent!.update()
-        isHmrUpdating = false
-        // #6930, #11248 avoid infinite recursion
+      // 4. actually update
+      if (instance.ceReload) {
+        // custom element
+        dirtyInstances.add(instance)
+        instance.ceReload((newComp as any).styles)
         dirtyInstances.delete(instance)
-      })
-    } else if (instance.appContext.reload) {
-      // root instance mounted via createApp() has a reload method
-      instance.appContext.reload()
-    } else if (typeof window !== 'undefined') {
-      // root instance inside tree created via raw render(). Force reload.
-      window.location.reload()
-    } else {
-      console.warn(
-        '[HMR] Root or manually mounted instance modified. Full reload required.',
-      )
-    }
+      } else if (instance.parent) {
+        // 4. Force the parent instance to re-render. This will cause all updated
+        // components to be unmounted and re-mounted. Queue the update so that we
+        // don't end up forcing the same parent to re-render multiple times.
+        queueJob(() => {
+          isHmrUpdating = true
+          const parent = instance.parent!
+          if (parent.vapor) {
+            parent.hmrRerender!()
+          } else {
+            ;(parent as ComponentInternalInstance).effect.run()
+          }
+          nextTick(() => {
+            isHmrUpdating = false
+          })
+          // #6930, #11248 avoid infinite recursion
+          dirtyInstances.delete(instance)
+        })
+      } else if (instance.appContext.reload) {
+        // root instance mounted via createApp() has a reload method
+        instance.appContext.reload()
+      } else if (typeof window !== 'undefined') {
+        // root instance inside tree created via raw render(). Force reload.
+        window.location.reload()
+      } else {
+        console.warn(
+          '[HMR] Root or manually mounted instance modified. Full reload required.',
+        )
+      }
 
-    // update custom element child style
-    if (instance.root.ce && instance !== instance.root) {
-      instance.root.ce._removeChildStyle(oldComp)
+      // update custom element child style
+      if (instance.root.ce && instance !== instance.root) {
+        instance.root.ce._removeChildStyle(oldComp)
+      }
     }
   }
-
   // 5. make sure to cleanup dirty hmr components after update
   queuePostFlushCb(() => {
     hmrDirtyComponents.clear()
diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts
index bdebed59602..15b3c7512be 100644
--- a/packages/runtime-core/src/hydration.ts
+++ b/packages/runtime-core/src/hydration.ts
@@ -11,7 +11,11 @@ import {
   normalizeVNode,
 } from './vnode'
 import { flushPostFlushCbs } from './scheduler'
-import type { ComponentInternalInstance, ComponentOptions } from './component'
+import type {
+  ComponentInternalInstance,
+  ComponentOptions,
+  ConcreteComponent,
+} from './component'
 import { invokeDirectiveHook } from './directives'
 import { warn } from './warning'
 import {
@@ -275,6 +279,10 @@ export function createHydrationFunctions(
             )
           }
         } else if (shapeFlag & ShapeFlags.COMPONENT) {
+          if ((vnode.type as ConcreteComponent).__vapor) {
+            throw new Error('Vapor component hydration is not supported yet.')
+          }
+
           // when setting up the render effect, if the initial vnode already
           // has .el set, the component will perform hydration instead of mount
           // on its sub-tree.
@@ -540,11 +548,15 @@ export function createHydrationFunctions(
         dirs ||
         needCallTransitionHooks
       ) {
-        queueEffectWithSuspense(() => {
-          vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode)
-          needCallTransitionHooks && transition!.enter(el)
-          dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
-        }, parentSuspense)
+        queueEffectWithSuspense(
+          () => {
+            vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode)
+            needCallTransitionHooks && transition!.enter(el)
+            dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
+          },
+          undefined,
+          parentSuspense,
+        )
       }
     }
 
@@ -771,7 +783,7 @@ export function createHydrationFunctions(
       if (parent.vnode.el === oldNode) {
         parent.vnode.el = parent.subTree.el = newNode
       }
-      parent = parent.parent
+      parent = parent.parent as ComponentInternalInstance
     }
   }
 
@@ -951,7 +963,11 @@ function resolveCssVars(
     }
   }
   if (vnode === root && instance.parent) {
-    resolveCssVars(instance.parent, instance.vnode, expectedMap)
+    resolveCssVars(
+      instance.parent as ComponentInternalInstance,
+      instance.vnode,
+      expectedMap,
+    )
   }
 }
 
diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts
index 9910f82102b..243bde548c5 100644
--- a/packages/runtime-core/src/index.ts
+++ b/packages/runtime-core/src/index.ts
@@ -240,6 +240,7 @@ export type {
   App,
   AppConfig,
   AppContext,
+  GenericAppContext,
   Plugin,
   ObjectPlugin,
   FunctionPlugin,
@@ -328,6 +329,7 @@ export type {
   ObjectDirective,
   FunctionDirective,
   DirectiveArguments,
+  DirectiveModifiers,
 } from './directives'
 export type { SuspenseBoundary } from './components/Suspense'
 export type {
@@ -479,3 +481,84 @@ export const compatUtils = (
 export const DeprecationTypes = (
   __COMPAT__ ? _DeprecationTypes : null
 ) as typeof _DeprecationTypes
+
+// VAPOR -----------------------------------------------------------------------
+
+// **IMPORTANT** These APIs are exposed solely for @vue/runtime-vapor and may
+// change without notice between versions. User code should never rely on them.
+
+/**
+ * these types cannot be marked internal because runtime-vapor's type relies on
+ * them, but they should be considered internal
+ * @private
+ */
+export {
+  type ComponentInternalOptions,
+  type GenericComponentInstance,
+  type LifecycleHook,
+} from './component'
+export { type NormalizedPropsOptions } from './componentProps'
+/**
+ * @internal
+ */
+export { type VaporInteropInterface } from './apiCreateApp'
+/**
+ * @internal
+ */
+export { type RendererInternals, MoveType } from './renderer'
+/**
+ * @internal
+ */
+export {
+  baseNormalizePropsOptions,
+  resolvePropValue,
+  validateProps,
+} from './componentProps'
+/**
+ * @internal
+ */
+export { baseEmit, isEmitListener } from './componentEmits'
+/**
+ * @internal
+ */
+export { type SchedulerJob, queueJob, flushOnAppMount } from './scheduler'
+/**
+ * @internal
+ */
+export { expose, nextUid, validateComponentName } from './component'
+/**
+ * @internal
+ */
+export { pushWarningContext, popWarningContext } from './warning'
+/**
+ * @internal
+ */
+export {
+  createAppAPI,
+  type AppMountFn,
+  type AppUnmountFn,
+} from './apiCreateApp'
+/**
+ * @internal
+ */
+export {
+  currentInstance,
+  setCurrentInstance,
+  simpleSetCurrentInstance,
+} from './componentCurrentInstance'
+/**
+ * @internal
+ */
+export { registerHMR, unregisterHMR } from './hmr'
+/**
+ * @internal
+ */
+export { startMeasure, endMeasure } from './profiling'
+/**
+ * @internal
+ */
+export { initFeatureFlags } from './featureFlags'
+/**
+ * @internal
+ */
+export { createInternalObject } from './internalObject'
diff --git a/packages/runtime-core/src/profiling.ts b/packages/runtime-core/src/profiling.ts
index 1984f5a21f2..c14207f91e4 100644
--- a/packages/runtime-core/src/profiling.ts
+++ b/packages/runtime-core/src/profiling.ts
@@ -1,15 +1,24 @@
 /* eslint-disable no-restricted-globals */
-import {
-  type ComponentInternalInstance,
-  formatComponentName,
-} from './component'
+import { type GenericComponentInstance, formatComponentName } from './component'
 import { devtoolsPerfEnd, devtoolsPerfStart } from './devtools'
 
 let supported: boolean
 let perf: Performance
 
+// To avoid the overhead of repeatedly calling Date.now(), we cache
+// and use the same timestamp for all event listeners attached in the same tick.
+let cachedNow: number = 0
+const p = /*@__PURE__*/ Promise.resolve()
+const getNow = () =>
+  cachedNow ||
+  (p.then(() => (cachedNow = 0)),
+  (cachedNow = isSupported() ? perf.now() : Date.now()))
+
+/**
+ * @internal
+ */
 export function startMeasure(
-  instance: ComponentInternalInstance,
+  instance: GenericComponentInstance,
   type: string,
 ): void {
   if (instance.appContext.config.performance && isSupported()) {
@@ -17,12 +26,15 @@ export function startMeasure(
   }
 
   if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
-    devtoolsPerfStart(instance, type, isSupported() ? perf.now() : Date.now())
+    devtoolsPerfStart(instance, type, getNow())
   }
 }
 
+/**
+ * @internal
+ */
 export function endMeasure(
-  instance: ComponentInternalInstance,
+  instance: GenericComponentInstance,
   type: string,
 ): void {
   if (instance.appContext.config.performance && isSupported()) {
@@ -39,7 +51,7 @@ export function endMeasure(
   }
 
   if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
-    devtoolsPerfEnd(instance, type, isSupported() ? perf.now() : Date.now())
+    devtoolsPerfEnd(instance, type, getNow())
   }
 }
 
diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts
index a57be791a44..30f9e6b2c1f 100644
--- a/packages/runtime-core/src/renderer.ts
+++ b/packages/runtime-core/src/renderer.ts
@@ -7,7 +7,9 @@ import {
   type VNodeArrayChildren,
   type VNodeHook,
   type VNodeProps,
+  VaporSlot,
   cloneIfMounted,
+  cloneVNode,
   createVNode,
   invokeVNodeHook,
   isSameVNodeType,
@@ -16,9 +18,12 @@ import {
 import {
   type ComponentInternalInstance,
   type ComponentOptions,
+  type ConcreteComponent,
   type Data,
+  type GenericComponentInstance,
   type LifecycleHook,
   createComponentInstance,
+  getComponentPublicInstance,
   setupComponent,
 } from './component'
 import {
@@ -35,6 +40,7 @@ import {
   ShapeFlags,
   def,
   getGlobalThis,
+  getSequence,
   invokeArrayFns,
   isArray,
   isReservedProp,
@@ -43,7 +49,7 @@ import {
   type SchedulerJob,
   SchedulerJobFlags,
   type SchedulerJobs,
-  flushPostFlushCbs,
+  flushOnAppMount,
   flushPreFlushCbs,
   queueJob,
   queuePostFlushCb,
@@ -51,13 +57,18 @@ import {
 import {
   EffectFlags,
   ReactiveEffect,
-  pauseTracking,
-  resetTracking,
+  setActiveSub,
+  setCurrentScope,
 } from '@vue/reactivity'
 import { updateProps } from './componentProps'
 import { updateSlots } from './componentSlots'
 import { popWarningContext, pushWarningContext, warn } from './warning'
-import { type CreateAppFunction, createAppAPI } from './apiCreateApp'
+import {
+  type AppMountFn,
+  type AppUnmountFn,
+  type CreateAppFunction,
+  createAppAPI,
+} from './apiCreateApp'
 import { setRef } from './rendererTemplateRef'
 import {
   type SuspenseBoundary,
@@ -86,11 +97,13 @@ import { isAsyncWrapper } from './apiAsyncComponent'
 import { isCompatEnabled } from './compat/compatConfig'
 import { DeprecationTypes } from './compat/compatConfig'
 import type { TransitionHooks } from './components/BaseTransition'
+import type { VaporInteropInterface } from './apiCreateApp'
 import type { VueElement } from '@vue/runtime-dom'
 
 export interface Renderer<HostElement = RendererElement> {
   render: RootRenderFunction<HostElement>
   createApp: CreateAppFunction<HostElement>
+  internals: RendererInternals
 }
 
 export interface HydrationRenderer extends Renderer<Element | ShadowRoot> {
@@ -166,6 +179,7 @@ export interface RendererInternals<
   r: RemoveFn
   m: MoveFn
   mt: MountComponentFn
+  umt: UnmountComponentFn
   mc: MountChildrenFn
   pc: PatchChildrenFn
   pbc: PatchBlockChildrenFn
@@ -227,6 +241,7 @@ type MoveFn = (
   container: RendererElement,
   anchor: RendererNode | null,
   type: MoveType,
+  parentComponent: ComponentInternalInstance | null,
   parentSuspense?: SuspenseBoundary | null,
 ) => void
 
@@ -261,6 +276,12 @@ export type MountComponentFn = (
   optimized: boolean,
 ) => void
 
+export type UnmountComponentFn = (
+  instance: ComponentInternalInstance,
+  parentSuspense: SuspenseBoundary | null,
+  doRemove?: boolean,
+) => void
+
 type ProcessTextOrCommentFn = (
   n1: VNode | null,
   n2: VNode,
@@ -286,12 +307,16 @@ export enum MoveType {
 
 export const queuePostRenderEffect: (
   fn: SchedulerJobs,
+  id: number | undefined,
   suspense: SuspenseBoundary | null,
 ) => void = __FEATURE_SUSPENSE__
   ? __TEST__
     ? // vitest can't seem to handle eager circular dependency
-      (fn: Function | Function[], suspense: SuspenseBoundary | null) =>
-        queueEffectWithSuspense(fn, suspense)
+      (
+        fn: Function | Function[],
+        id: number | undefined,
+        suspense: SuspenseBoundary | null,
+      ) => queueEffectWithSuspense(fn, id, suspense)
     : queueEffectWithSuspense
   : queuePostFlushCb
 
@@ -426,6 +451,9 @@ function baseCreateRenderer(
           optimized,
         )
         break
+      case VaporSlot:
+        getVaporInterface(parentComponent, n2).slot(n1, n2, container, anchor)
+        break
       default:
         if (shapeFlag & ShapeFlags.ELEMENT) {
           processElement(
@@ -720,11 +748,15 @@ function baseCreateRenderer(
       needCallTransitionHooks ||
       dirs
     ) {
-      queuePostRenderEffect(() => {
-        vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
-        needCallTransitionHooks && transition!.enter(el)
-        dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
-      }, parentSuspense)
+      queuePostRenderEffect(
+        () => {
+          vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
+          needCallTransitionHooks && transition!.enter(el)
+          dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
+        },
+        undefined,
+        parentSuspense,
+      )
     }
   }
 
@@ -733,7 +765,7 @@ function baseCreateRenderer(
     vnode: VNode,
     scopeId: string | null,
     slotScopeIds: string[] | null,
-    parentComponent: ComponentInternalInstance | null,
+    parentComponent: GenericComponentInstance | null,
   ) => {
     if (scopeId) {
       hostSetScopeId(el, scopeId)
@@ -743,8 +775,8 @@ function baseCreateRenderer(
         hostSetScopeId(el, slotScopeIds[i])
       }
     }
-    if (parentComponent) {
-      let subTree = parentComponent.subTree
+    let subTree = parentComponent && parentComponent.subTree
+    if (subTree) {
       if (
         __DEV__ &&
         subTree.patchFlag > 0 &&
@@ -758,13 +790,13 @@ function baseCreateRenderer(
         (isSuspense(subTree.type) &&
           (subTree.ssContent === vnode || subTree.ssFallback === vnode))
       ) {
-        const parentVNode = parentComponent.vnode
+        const parentVNode = parentComponent!.vnode!
         setScopeId(
           el,
           parentVNode,
           parentVNode.scopeId,
           parentVNode.slotScopeIds,
-          parentComponent.parent,
+          parentComponent!.parent,
         )
       }
     }
@@ -932,10 +964,14 @@ function baseCreateRenderer(
     }
 
     if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
-      queuePostRenderEffect(() => {
-        vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
-        dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
-      }, parentSuspense)
+      queuePostRenderEffect(
+        () => {
+          vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
+          dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
+        },
+        undefined,
+        parentSuspense,
+      )
     }
   }
 
@@ -1141,7 +1177,23 @@ function baseCreateRenderer(
     optimized: boolean,
   ) => {
     n2.slotScopeIds = slotScopeIds
-    if (n1 == null) {
+
+    if ((n2.type as ConcreteComponent).__vapor) {
+      if (n1 == null) {
+        getVaporInterface(parentComponent, n2).mount(
+          n2,
+          container,
+          anchor,
+          parentComponent,
+        )
+      } else {
+        getVaporInterface(parentComponent, n2).update(
+          n1,
+          n2,
+          shouldUpdateComponent(n1, n2, optimized),
+        )
+      }
+    } else if (n1 == null) {
       if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
         ;(parentComponent!.ctx as KeepAliveContext).activate(
           n2,
@@ -1267,7 +1319,7 @@ function baseCreateRenderer(
         // normal update
         instance.next = n2
         // instance.update is the reactive effect.
-        instance.update()
+        instance.effect.run()
       }
     } else {
       // no update needed. just copy over properties
@@ -1276,16 +1328,56 @@ function baseCreateRenderer(
     }
   }
 
-  const setupRenderEffect: SetupRenderEffectFn = (
-    instance,
-    initialVNode,
-    container,
-    anchor,
-    parentSuspense,
-    namespace: ElementNamespace,
-    optimized,
-  ) => {
-    const componentUpdateFn = () => {
+  class SetupRenderEffect extends ReactiveEffect {
+    job: SchedulerJob
+
+    constructor(
+      private instance: ComponentInternalInstance,
+      private initialVNode: VNode,
+      private container: RendererElement,
+      private anchor: RendererNode | null,
+      private parentSuspense: SuspenseBoundary | null,
+      private namespace: ElementNamespace,
+      private optimized: boolean,
+    ) {
+      const prevScope = setCurrentScope(instance.scope)
+      super()
+      setCurrentScope(prevScope)
+
+      this.job = instance.job = () => {
+        if (this.dirty) {
+          this.run()
+        }
+      }
+      this.job.i = instance
+
+      if (__DEV__) {
+        this.onTrack = instance.rtc
+          ? e => invokeArrayFns(instance.rtc!, e)
+          : void 0
+        this.onTrigger = instance.rtg
+          ? e => invokeArrayFns(instance.rtg!, e)
+          : void 0
+      }
+    }
+
+    notify(): void {
+      if (!(this.flags & EffectFlags.PAUSED)) {
+        const job = this.job
+        queueJob(job, job.i!.uid)
+      }
+    }
+
+    fn() {
+      const {
+        instance,
+        initialVNode,
+        container,
+        anchor,
+        parentSuspense,
+        namespace,
+        optimized,
+      } = this
       if (!instance.isMounted) {
         let vnodeHook: VNodeHook | null | undefined
         const { el, props } = initialVNode
@@ -1352,11 +1444,12 @@ function baseCreateRenderer(
         } else {
           // custom element style injection
           if (
-            root.ce &&
+            (root as ComponentInternalInstance).ce &&
             // @ts-expect-error _def is private
-            (root.ce as VueElement)._def.shadowRoot !== false
+            ((root as ComponentInternalInstance).ce as VueElement)._def
+              .shadowRoot !== false
           ) {
-            root.ce._injectChildStyle(type)
+            ;(root as ComponentInternalInstance).ce!._injectChildStyle(type)
           }
 
           if (__DEV__) {
@@ -1385,7 +1478,7 @@ function baseCreateRenderer(
         }
         // mounted hook
         if (m) {
-          queuePostRenderEffect(m, parentSuspense)
+          queuePostRenderEffect(m, undefined, parentSuspense)
         }
         // onVnodeMounted
         if (
@@ -1395,6 +1488,7 @@ function baseCreateRenderer(
           const scopedInitialVNode = initialVNode
           queuePostRenderEffect(
             () => invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode),
+            undefined,
             parentSuspense,
           )
         }
@@ -1404,6 +1498,7 @@ function baseCreateRenderer(
         ) {
           queuePostRenderEffect(
             () => instance.emit('hook:mounted'),
+            undefined,
             parentSuspense,
           )
         }
@@ -1414,16 +1509,19 @@ function baseCreateRenderer(
         if (
           initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE ||
           (parent &&
+            parent.vnode &&
             isAsyncWrapper(parent.vnode) &&
             parent.vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE)
         ) {
-          instance.a && queuePostRenderEffect(instance.a, parentSuspense)
+          instance.a &&
+            queuePostRenderEffect(instance.a, undefined, parentSuspense)
           if (
             __COMPAT__ &&
             isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
           ) {
             queuePostRenderEffect(
               () => instance.emit('hook:activated'),
+              undefined,
               parentSuspense,
             )
           }
@@ -1435,7 +1533,7 @@ function baseCreateRenderer(
         }
 
         // #2458: deference mount-only object parameters to prevent memleaks
-        initialVNode = container = anchor = null as any
+        this.initialVNode = this.container = this.anchor = null as any
       } else {
         let { next, bu, u, parent, vnode } = instance
 
@@ -1453,7 +1551,7 @@ function baseCreateRenderer(
             nonHydratedAsyncRoot.asyncDep!.then(() => {
               // the instance may be destroyed during the time period
               if (!instance.isUnmounted) {
-                componentUpdateFn()
+                this.fn()
               }
             })
             return
@@ -1531,12 +1629,13 @@ function baseCreateRenderer(
         }
         // updated hook
         if (u) {
-          queuePostRenderEffect(u, parentSuspense)
+          queuePostRenderEffect(u, undefined, parentSuspense)
         }
         // onVnodeUpdated
         if ((vnodeHook = next.props && next.props.onVnodeUpdated)) {
           queuePostRenderEffect(
             () => invokeVNodeHook(vnodeHook!, parent, next!, vnode),
+            undefined,
             parentSuspense,
           )
         }
@@ -1546,6 +1645,7 @@ function baseCreateRenderer(
         ) {
           queuePostRenderEffect(
             () => instance.emit('hook:updated'),
+            undefined,
             parentSuspense,
           )
         }
@@ -1559,32 +1659,34 @@ function baseCreateRenderer(
         }
       }
     }
+  }
 
+  const setupRenderEffect: SetupRenderEffectFn = (
+    instance,
+    initialVNode,
+    container,
+    anchor,
+    parentSuspense,
+    namespace: ElementNamespace,
+    optimized,
+  ) => {
     // create reactive effect for rendering
-    instance.scope.on()
-    const effect = (instance.effect = new ReactiveEffect(componentUpdateFn))
-    instance.scope.off()
-
-    const update = (instance.update = effect.run.bind(effect))
-    const job: SchedulerJob = (instance.job = effect.runIfDirty.bind(effect))
-    job.i = instance
-    job.id = instance.uid
-    effect.scheduler = () => queueJob(job)
+    const effect = (instance.effect = new SetupRenderEffect(
+      instance,
+      initialVNode,
+      container,
+      anchor,
+      parentSuspense,
+      namespace,
+      optimized,
+    ))
+    instance.update = effect.run.bind(effect)
 
     // allowRecurse
     // #1801, #2043 component render effects should allow recursive updates
     toggleRecurse(instance, true)
 
-    if (__DEV__) {
-      effect.onTrack = instance.rtc
-        ? e => invokeArrayFns(instance.rtc!, e)
-        : void 0
-      effect.onTrigger = instance.rtg
-        ? e => invokeArrayFns(instance.rtg!, e)
-        : void 0
-    }
-
-    update()
+    effect.run()
   }
 
   const updateComponentPreRender = (
@@ -1599,11 +1701,11 @@ function baseCreateRenderer(
     updateProps(instance, nextVNode.props, prevProps, optimized)
     updateSlots(instance, nextVNode.children, optimized)
 
-    pauseTracking()
+    const prevSub = setActiveSub()
     // props update may have triggered pre-flush watchers.
     // flush them before the render update.
     flushPreFlushCbs(instance)
-    resetTracking()
+    setActiveSub(prevSub)
   }
 
   const patchChildren: PatchChildrenFn = (
@@ -1999,7 +2101,13 @@ function baseCreateRenderer(
           // There is no stable subsequence (e.g. a reverse)
           // OR current node is not among the stable sequence
           if (j < 0 || i !== increasingNewIndexSequence[j]) {
-            move(nextChild, container, anchor, MoveType.REORDER)
+            move(
+              nextChild,
+              container,
+              anchor,
+              MoveType.REORDER,
+              parentComponent,
+            )
           } else {
             j--
           }
@@ -2013,11 +2121,22 @@ function baseCreateRenderer(
     container,
     anchor,
     moveType,
+    parentComponent,
     parentSuspense = null,
   ) => {
     const { el, type, transition, children, shapeFlag } = vnode
     if (shapeFlag & ShapeFlags.COMPONENT) {
-      move(vnode.component!.subTree, container, anchor, moveType)
+      if ((type as ConcreteComponent).__vapor) {
+        getVaporInterface(parentComponent, vnode).move(vnode, container, anchor)
+      } else {
+        move(
+          vnode.component!.subTree,
+          container,
+          anchor,
+          moveType,
+          parentComponent,
+        )
+      }
       return
     }
 
@@ -2027,14 +2146,26 @@ function baseCreateRenderer(
     }
 
     if (shapeFlag & ShapeFlags.TELEPORT) {
-      ;(type as typeof TeleportImpl).move(vnode, container, anchor, internals)
+      ;(type as typeof TeleportImpl).move(
+        vnode,
+        container,
+        anchor,
+        internals,
+        parentComponent,
+      )
       return
     }
 
     if (type === Fragment) {
       hostInsert(el!, container, anchor)
       for (let i = 0; i < (children as VNode[]).length; i++) {
-        move((children as VNode[])[i], container, anchor, moveType)
+        move(
+          (children as VNode[])[i],
+          container,
+          anchor,
+          moveType,
+          parentComponent,
+        )
       }
       hostInsert(vnode.anchor!, container, anchor)
       return
@@ -2054,7 +2185,11 @@ function baseCreateRenderer(
       if (moveType === MoveType.ENTER) {
         transition!.beforeEnter(el!)
         hostInsert(el!, container, anchor)
-        queuePostRenderEffect(() => transition!.enter(el!), parentSuspense)
+        queuePostRenderEffect(
+          () => transition!.enter(el!),
+          undefined,
+          parentSuspense,
+        )
       } else {
         const { leave, delayLeave, afterLeave } = transition!
         const remove = () => {
@@ -2106,9 +2241,9 @@ function baseCreateRenderer(
 
     // unset ref
     if (ref != null) {
-      pauseTracking()
+      const prevSub = setActiveSub()
       setRef(ref, null, parentSuspense, vnode, true)
-      resetTracking()
+      setActiveSub(prevSub)
     }
 
     // #6593 should clean memo cache when unmount
@@ -2133,7 +2268,12 @@ function baseCreateRenderer(
     }
 
     if (shapeFlag & ShapeFlags.COMPONENT) {
-      unmountComponent(vnode.component!, parentSuspense, doRemove)
+      if ((type as ConcreteComponent).__vapor) {
+        getVaporInterface(parentComponent, vnode).unmount(vnode, doRemove)
+        return
+      } else {
+        unmountComponent(vnode.component!, parentSuspense, doRemove)
+      }
     } else {
       if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
         vnode.suspense!.unmount(parentSuspense, doRemove)
@@ -2181,6 +2321,11 @@ function baseCreateRenderer(
         unmountChildren(children as VNode[], parentComponent, parentSuspense)
       }
 
+      if (type === VaporSlot) {
+        getVaporInterface(parentComponent, vnode).unmount(vnode, doRemove)
+        return
+      }
+
       if (doRemove) {
         remove(vnode)
       }
@@ -2191,11 +2336,15 @@ function baseCreateRenderer(
         (vnodeHook = props && props.onVnodeUnmounted)) ||
       shouldInvokeDirs
     ) {
-      queuePostRenderEffect(() => {
-        vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
-        shouldInvokeDirs &&
-          invokeDirectiveHook(vnode, null, parentComponent, 'unmounted')
-      }, parentSuspense)
+      queuePostRenderEffect(
+        () => {
+          vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
+          shouldInvokeDirs &&
+            invokeDirectiveHook(vnode, null, parentComponent, 'unmounted')
+        },
+        undefined,
+        parentSuspense,
+      )
     }
   }
 
@@ -2263,10 +2412,10 @@ function baseCreateRenderer(
     hostRemove(end)
   }
 
-  const unmountComponent = (
-    instance: ComponentInternalInstance,
-    parentSuspense: SuspenseBoundary | null,
-    doRemove?: boolean,
+  const unmountComponent: UnmountComponentFn = (
+    instance,
+    parentSuspense,
+    doRemove,
   ) => {
     if (__DEV__ && instance.type.__hmrId) {
       unregisterHMR(instance)
@@ -2275,7 +2424,7 @@ function baseCreateRenderer(
     const {
       bum,
       scope,
-      job,
+      effect,
       subTree,
       um,
       m,
@@ -2294,7 +2443,7 @@ function baseCreateRenderer(
     // remove slots content from parent renderCache
     if (parent && isArray(slotCacheKeys)) {
       slotCacheKeys.forEach(v => {
-        parent.renderCache[v] = undefined
+        ;(parent as ComponentInternalInstance).renderCache[v] = undefined
       })
     }
 
@@ -2310,14 +2459,14 @@ function baseCreateRenderer(
 
     // job may be null if a component is unmounted before its async
     // setup has resolved.
-    if (job) {
+    if (effect) {
       // so that scheduler will no longer invoke it
-      job.flags! |= SchedulerJobFlags.DISPOSED
+      effect.stop()
       unmount(subTree, instance, parentSuspense, doRemove)
     }
     // unmounted hook
     if (um) {
-      queuePostRenderEffect(um, parentSuspense)
+      queuePostRenderEffect(um, undefined, parentSuspense)
     }
     if (
       __COMPAT__ &&
@@ -2325,12 +2474,15 @@ function baseCreateRenderer(
     ) {
       queuePostRenderEffect(
         () => instance.emit('hook:destroyed'),
+        undefined,
         parentSuspense,
       )
     }
-    queuePostRenderEffect(() => {
-      instance.isUnmounted = true
-    }, parentSuspense)
+    queuePostRenderEffect(
+      () => (instance.isUnmounted = true),
+      undefined,
+      parentSuspense,
+    )
 
     // A component with async dep inside a pending suspense is unmounted before
     // its async dep resolves. This should remove the dep from the suspense, and
@@ -2370,6 +2522,9 @@ function baseCreateRenderer(
 
   const getNextHostNode: NextFn = vnode => {
     if (vnode.shapeFlag & ShapeFlags.COMPONENT) {
+      if ((vnode.type as ConcreteComponent).__vapor) {
+        return hostNextSibling((vnode.component! as any).block)
+      }
       return getNextHostNode(vnode.component!.subTree)
     }
     if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
@@ -2383,7 +2538,6 @@ function baseCreateRenderer(
     return teleportEnd ? hostNextSibling(teleportEnd) : el
   }
 
-  let isFlushing = false
   const render: RootRenderFunction = (vnode, container, namespace) => {
     if (vnode == null) {
       if (container._vnode) {
@@ -2401,12 +2555,7 @@ function baseCreateRenderer(
       )
     }
     container._vnode = vnode
-    if (!isFlushing) {
-      isFlushing = true
-      flushPreFlushCbs()
-      flushPostFlushCbs()
-      isFlushing = false
-    }
+    flushOnAppMount()
   }
 
   const internals: RendererInternals = {
@@ -2415,6 +2564,7 @@ function baseCreateRenderer(
     m: move,
     r: remove,
     mt: mountComponent,
+    umt: unmountComponent,
     mc: mountChildren,
     pc: patchChildren,
     pbc: patchBlockChildren,
@@ -2430,10 +2580,58 @@ function baseCreateRenderer(
     )
   }
 
+  const mountApp: AppMountFn<Element> = (
+    app,
+    container,
+    isHydrate,
+    namespace,
+  ) => {
+    const vnode = app._ceVNode || createVNode(app._component, app._props)
+    // store app context on the root VNode.
+    // this will be set on the root instance on initial mount.
+    vnode.appContext = app._context
+
+    if (namespace === true) {
+      namespace = 'svg'
+    } else if (namespace === false) {
+      namespace = undefined
+    }
+
+    // HMR root reload
+    if (__DEV__) {
+      app._context.reload = () => {
+        const cloned = cloneVNode(vnode)
+        // avoid hydration for hmr updating
+        cloned.el = null
+        // casting to ElementNamespace because TS doesn't guarantee type narrowing
+        // over function boundaries
+        render(cloned, container, namespace as ElementNamespace)
+      }
+    }
+
+    if (isHydrate && hydrate) {
+      hydrate(vnode as VNode<Node, Element>, container as any)
+    } else {
+      render(vnode, container, namespace)
+    }
+
+    return vnode.component!
+  }
+
+  const unmountApp: AppUnmountFn = app => {
+    render(null, app._container)
+  }
+
   return {
     render,
     hydrate,
-    createApp: createAppAPI(render, hydrate),
+    internals,
+    createApp: createAppAPI(
+      mountApp,
+      unmountApp,
+      getComponentPublicInstance as any,
+      render,
+    ),
   }
 }
 
@@ -2452,15 +2650,17 @@ function resolveChildrenNamespace(
 }
 
 function toggleRecurse(
-  { effect, job }: ComponentInternalInstance,
+  { effect, job, vapor }: ComponentInternalInstance,
   allowed: boolean,
 ) {
-  if (allowed) {
-    effect.flags |= EffectFlags.ALLOW_RECURSE
-    job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
-  } else {
-    effect.flags &= ~EffectFlags.ALLOW_RECURSE
-    job.flags! &= ~SchedulerJobFlags.ALLOW_RECURSE
+  if (!vapor) {
+    if (allowed) {
+      effect.flags |= EffectFlags.ALLOW_RECURSE
+      job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
+    } else {
+      effect.flags &= ~EffectFlags.ALLOW_RECURSE
+      job.flags! &= ~SchedulerJobFlags.ALLOW_RECURSE
+    }
   }
 }
 
@@ -2524,52 +2724,10 @@ export function traverseStaticChildren(
   }
 }
 
-// https://en.wikipedia.org/wiki/Longest_increasing_subsequence
-function getSequence(arr: number[]): number[] {
-  const p = arr.slice()
-  const result = [0]
-  let i, j, u, v, c
-  const len = arr.length
-  for (i = 0; i < len; i++) {
-    const arrI = arr[i]
-    if (arrI !== 0) {
-      j = result[result.length - 1]
-      if (arr[j] < arrI) {
-        p[i] = j
-        result.push(i)
-        continue
-      }
-      u = 0
-      v = result.length - 1
-      while (u < v) {
-        c = (u + v) >> 1
-        if (arr[result[c]] < arrI) {
-          u = c + 1
-        } else {
-          v = c
-        }
-      }
-      if (arrI < arr[result[u]]) {
-        if (u > 0) {
-          p[i] = result[u - 1]
-        }
-        result[u] = i
-      }
-    }
-  }
-  u = result.length
-  v = result[u - 1]
-  while (u-- > 0) {
-    result[u] = v
-    v = p[v]
-  }
-  return result
-}
-
 function locateNonHydratedAsyncRoot(
   instance: ComponentInternalInstance,
 ): ComponentInternalInstance | undefined {
-  const subComponent = instance.subTree.component
+  const subComponent = instance.vapor ? null : instance.subTree.component
   if (subComponent) {
     if (subComponent.asyncDep && !subComponent.asyncResolved) {
       return subComponent
@@ -2579,9 +2737,27 @@ function locateNonHydratedAsyncRoot(
   }
 }
 
-export function invalidateMount(hooks: LifecycleHook): void {
+export function invalidateMount(hooks: LifecycleHook | undefined): void {
   if (hooks) {
     for (let i = 0; i < hooks.length; i++)
       hooks[i].flags! |= SchedulerJobFlags.DISPOSED
   }
 }
+
+function getVaporInterface(
+  instance: ComponentInternalInstance | null,
+  vnode: VNode,
+): VaporInteropInterface {
+  const ctx = instance ? instance.appContext : vnode.appContext
+  const res = ctx && ctx.vapor
+  if (__DEV__ && !res) {
+    warn(
+      `Vapor component found in vdom tree but vapor-in-vdom interop was not installed. ` +
+        `Make sure to install it:\n` +
+        `\`\`\`\nimport { vaporInteropPlugin } from 'vue'\n` +
+        `app.use(vaporInteropPlugin)\n` +
+        `\`\`\``,
+    )
+  }
+  return res!
+}
diff --git a/packages/runtime-core/src/rendererTemplateRef.ts b/packages/runtime-core/src/rendererTemplateRef.ts
index ca21030dc35..31fcf8c2d5b 100644
--- a/packages/runtime-core/src/rendererTemplateRef.ts
+++ b/packages/runtime-core/src/rendererTemplateRef.ts
@@ -13,7 +13,6 @@ import { isAsyncWrapper } from './apiAsyncComponent'
 import { warn } from './warning'
 import { isRef, toRaw } from '@vue/reactivity'
 import { ErrorCodes, callWithErrorHandling } from './errorHandling'
-import type { SchedulerJob } from './scheduler'
 import { queuePostRenderEffect } from './renderer'
 import { type ComponentOptions, getComponentPublicInstance } from './component'
 import { knownTemplateRefs } from './helpers/useTemplateRef'
@@ -153,8 +152,7 @@ export function setRef(
         // #1789: for non-null values, set them after render
         // null values means this is unmount and it should not overwrite another
         // ref with the same key
-        ;(doSet as SchedulerJob).id = -1
-        queuePostRenderEffect(doSet, parentSuspense)
+        queuePostRenderEffect(doSet, -1, parentSuspense)
       } else {
         doSet()
       }
diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts
index b40c31d3952..967e19cd56d 100644
--- a/packages/runtime-core/src/scheduler.ts
+++ b/packages/runtime-core/src/scheduler.ts
@@ -1,10 +1,9 @@
-import { ErrorCodes, callWithErrorHandling, handleError } from './errorHandling'
-import { NOOP, isArray } from '@vue/shared'
-import { type ComponentInternalInstance, getComponentName } from './component'
+import { ErrorCodes, handleError } from './errorHandling'
+import { isArray } from '@vue/shared'
+import { type GenericComponentInstance, getComponentName } from './component'
 
 export enum SchedulerJobFlags {
   QUEUED = 1 << 0,
-  PRE = 1 << 1,
   /**
    * Indicates whether the effect is allowed to recursively trigger itself
    * when managed by the scheduler.
@@ -20,12 +19,12 @@ export enum SchedulerJobFlags {
    * responsibility to perform recursive state mutation that eventually
    * stabilizes (#1727).
    */
-  ALLOW_RECURSE = 1 << 2,
-  DISPOSED = 1 << 3,
+  ALLOW_RECURSE = 1 << 1,
+  DISPOSED = 1 << 2,
 }
 
 export interface SchedulerJob extends Function {
-  id?: number
+  order?: number
   /**
    * flags can technically be undefined, but it can still be used in bitwise
    * operations just like 0.
@@ -35,22 +34,23 @@ export interface SchedulerJob extends Function {
    * Attached by renderer.ts when setting up a component's render effect
    * Used to obtain component information when reporting max recursive updates.
    */
-  i?: ComponentInternalInstance
+  i?: GenericComponentInstance
 }
 
 export type SchedulerJobs = SchedulerJob | SchedulerJob[]
 
-const queue: SchedulerJob[] = []
-let flushIndex = -1
+const jobs: SchedulerJob[] = []
 
-const pendingPostFlushCbs: SchedulerJob[] = []
-let activePostFlushCbs: SchedulerJob[] | null = null
+let postJobs: SchedulerJob[] = []
+let activePostJobs: SchedulerJob[] | null = null
+let currentFlushPromise: Promise<void> | null = null
+let jobsLength = 0
+let flushIndex = 0
 let postFlushIndex = 0
 
 const resolvedPromise = /*@__PURE__*/ Promise.resolve() as Promise<any>
-let currentFlushPromise: Promise<void> | null = null
-
 const RECURSION_LIMIT = 100
+
 type CountMap = Map<SchedulerJob, number>
 
 export function nextTick<T = void, R = void>(
@@ -70,193 +70,236 @@ export function nextTick<T = void, R = void>(
 // A pre watcher will have the same id as its component's update job. The
 // watcher should be inserted immediately before the update job. This allows
 // watchers to be skipped if the component is unmounted by the parent update.
-function findInsertionIndex(id: number) {
-  let start = flushIndex + 1
-  let end = queue.length
-
+function findInsertionIndex(
+  order: number,
+  queue: SchedulerJob[],
+  start: number,
+  end: number,
+) {
   while (start < end) {
     const middle = (start + end) >>> 1
-    const middleJob = queue[middle]
-    const middleJobId = getId(middleJob)
-    if (
-      middleJobId < id ||
-      (middleJobId === id && middleJob.flags! & SchedulerJobFlags.PRE)
-    ) {
+    if (queue[middle].order! <= order) {
       start = middle + 1
     } else {
       end = middle
     }
   }
-
   return start
 }
 
-export function queueJob(job: SchedulerJob): void {
-  if (!(job.flags! & SchedulerJobFlags.QUEUED)) {
-    const jobId = getId(job)
-    const lastJob = queue[queue.length - 1]
+/**
+ * @internal for runtime-vapor only
+ */
+export function queueJob(job: SchedulerJob, id?: number, isPre = false): void {
+  if (
+    queueJobWorker(
+      job,
+      id === undefined ? (isPre ? -2 : Infinity) : isPre ? id * 2 : id * 2 + 1,
+      jobs,
+      jobsLength,
+      flushIndex,
+    )
+  ) {
+    jobsLength++
+    queueFlush()
+  }
+}
+
+function queueJobWorker(
+  job: SchedulerJob,
+  order: number,
+  queue: SchedulerJob[],
+  length: number,
+  flushIndex: number,
+) {
+  const flags = job.flags!
+  if (!(flags & SchedulerJobFlags.QUEUED)) {
+    job.flags! = flags | SchedulerJobFlags.QUEUED
+    job.order = order
     if (
-      !lastJob ||
+      flushIndex === length ||
       // fast path when the job id is larger than the tail
-      (!(job.flags! & SchedulerJobFlags.PRE) && jobId >= getId(lastJob))
+      order >= queue[length - 1].order!
     ) {
-      queue.push(job)
+      queue[length] = job
     } else {
-      queue.splice(findInsertionIndex(jobId), 0, job)
+      queue.splice(findInsertionIndex(order, queue, flushIndex, length), 0, job)
     }
+    return true
+  }
+  return false
+}
 
-    job.flags! |= SchedulerJobFlags.QUEUED
-
-    queueFlush()
+const doFlushJobs = () => {
+  try {
+    flushJobs()
+  } catch (e) {
+    currentFlushPromise = null
+    throw e
   }
 }
 
 function queueFlush() {
   if (!currentFlushPromise) {
-    currentFlushPromise = resolvedPromise.then(flushJobs)
+    currentFlushPromise = resolvedPromise.then(doFlushJobs)
   }
 }
 
-export function queuePostFlushCb(cb: SchedulerJobs): void {
-  if (!isArray(cb)) {
-    if (activePostFlushCbs && cb.id === -1) {
-      activePostFlushCbs.splice(postFlushIndex + 1, 0, cb)
-    } else if (!(cb.flags! & SchedulerJobFlags.QUEUED)) {
-      pendingPostFlushCbs.push(cb)
-      cb.flags! |= SchedulerJobFlags.QUEUED
+export function queuePostFlushCb(
+  jobs: SchedulerJobs,
+  id: number = Infinity,
+): void {
+  if (!isArray(jobs)) {
+    if (activePostJobs && id === -1) {
+      activePostJobs.splice(postFlushIndex, 0, jobs)
+    } else {
+      queueJobWorker(jobs, id, postJobs, postJobs.length, 0)
     }
   } else {
     // if cb is an array, it is a component lifecycle hook which can only be
     // triggered by a job, which is already deduped in the main queue, so
     // we can skip duplicate check here to improve perf
-    pendingPostFlushCbs.push(...cb)
+    for (const job of jobs) {
+      queueJobWorker(job, id, postJobs, postJobs.length, 0)
+    }
   }
   queueFlush()
 }
 
 export function flushPreFlushCbs(
-  instance?: ComponentInternalInstance,
+  instance?: GenericComponentInstance,
   seen?: CountMap,
-  // skip the current job
-  i: number = flushIndex + 1,
 ): void {
   if (__DEV__) {
     seen = seen || new Map()
   }
-  for (; i < queue.length; i++) {
-    const cb = queue[i]
-    if (cb && cb.flags! & SchedulerJobFlags.PRE) {
-      if (instance && cb.id !== instance.uid) {
-        continue
-      }
-      if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
-        continue
-      }
-      queue.splice(i, 1)
-      i--
-      if (cb.flags! & SchedulerJobFlags.ALLOW_RECURSE) {
-        cb.flags! &= ~SchedulerJobFlags.QUEUED
-      }
-      cb()
-      if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
-        cb.flags! &= ~SchedulerJobFlags.QUEUED
-      }
+  for (let i = flushIndex; i < jobsLength; i++) {
+    const cb = jobs[i]
+    if (cb.order! & 1 || cb.order === Infinity) {
+      continue
+    }
+    if (instance && cb.order !== instance.uid * 2) {
+      continue
+    }
+    if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
+      continue
+    }
+    jobs.splice(i, 1)
+    i--
+    jobsLength--
+    if (cb.flags! & SchedulerJobFlags.ALLOW_RECURSE) {
+      cb.flags! &= ~SchedulerJobFlags.QUEUED
+    }
+    cb()
+    if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
+      cb.flags! &= ~SchedulerJobFlags.QUEUED
     }
   }
 }
 
 export function flushPostFlushCbs(seen?: CountMap): void {
-  if (pendingPostFlushCbs.length) {
-    const deduped = [...new Set(pendingPostFlushCbs)].sort(
-      (a, b) => getId(a) - getId(b),
-    )
-    pendingPostFlushCbs.length = 0
-
+  if (postJobs.length) {
     // #1947 already has active queue, nested flushPostFlushCbs call
-    if (activePostFlushCbs) {
-      activePostFlushCbs.push(...deduped)
+    if (activePostJobs) {
+      activePostJobs.push(...postJobs)
+      postJobs.length = 0
       return
     }
 
-    activePostFlushCbs = deduped
+    activePostJobs = postJobs
+    postJobs = []
+
     if (__DEV__) {
       seen = seen || new Map()
     }
 
-    for (
-      postFlushIndex = 0;
-      postFlushIndex < activePostFlushCbs.length;
-      postFlushIndex++
-    ) {
-      const cb = activePostFlushCbs[postFlushIndex]
+    while (postFlushIndex < activePostJobs.length) {
+      const cb = activePostJobs[postFlushIndex++]
       if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
         continue
       }
       if (cb.flags! & SchedulerJobFlags.ALLOW_RECURSE) {
         cb.flags! &= ~SchedulerJobFlags.QUEUED
       }
-      if (!(cb.flags! & SchedulerJobFlags.DISPOSED)) cb()
-      cb.flags! &= ~SchedulerJobFlags.QUEUED
+      if (!(cb.flags! & SchedulerJobFlags.DISPOSED)) {
+        try {
+          cb()
+        } finally {
+          cb.flags! &= ~SchedulerJobFlags.QUEUED
+        }
+      }
     }
-    activePostFlushCbs = null
+
+    activePostJobs = null
     postFlushIndex = 0
   }
 }
 
-const getId = (job: SchedulerJob): number =>
-  job.id == null ? (job.flags! & SchedulerJobFlags.PRE ? -1 : Infinity) : job.id
+let isFlushing = false
+/**
+ * @internal
+ */
+export function flushOnAppMount(): void {
+  if (!isFlushing) {
+    isFlushing = true
+    flushPreFlushCbs()
+    flushPostFlushCbs()
+    isFlushing = false
+  }
+}
 
 function flushJobs(seen?: CountMap) {
   if (__DEV__) {
-    seen = seen || new Map()
+    seen ||= new Map()
   }
 
-  // conditional usage of checkRecursiveUpdate must be determined out of
-  // try ... catch block since Rollup by default de-optimizes treeshaking
-  // inside try-catch. This can leave all warning code unshaked. Although
-  // they would get eventually shaken by a minifier like terser, some minifiers
-  // would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)
-  const check = __DEV__
-    ? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
-    : NOOP
-
   try {
-    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
-      const job = queue[flushIndex]
-      if (job && !(job.flags! & SchedulerJobFlags.DISPOSED)) {
-        if (__DEV__ && check(job)) {
+    while (flushIndex < jobsLength) {
+      const job = jobs[flushIndex]
+      jobs[flushIndex++] = undefined as any
+
+      if (!(job.flags! & SchedulerJobFlags.DISPOSED)) {
+        // conditional usage of checkRecursiveUpdate must be determined out of
+        // try ... catch block since Rollup by default de-optimizes treeshaking
+        // inside try-catch. This can leave all warning code unshaked. Although
+        // they would get eventually shaken by a minifier like terser, some minifiers
+        // would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)
+        if (__DEV__ && checkRecursiveUpdates(seen!, job)) {
           continue
         }
         if (job.flags! & SchedulerJobFlags.ALLOW_RECURSE) {
           job.flags! &= ~SchedulerJobFlags.QUEUED
         }
-        callWithErrorHandling(
-          job,
-          job.i,
-          job.i ? ErrorCodes.COMPONENT_UPDATE : ErrorCodes.SCHEDULER,
-        )
-        if (!(job.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
-          job.flags! &= ~SchedulerJobFlags.QUEUED
+        try {
+          job()
+        } catch (err) {
+          handleError(
+            err,
+            job.i,
+            job.i ? ErrorCodes.COMPONENT_UPDATE : ErrorCodes.SCHEDULER,
+          )
+        } finally {
+          if (!(job.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
+            job.flags! &= ~SchedulerJobFlags.QUEUED
+          }
         }
       }
     }
   } finally {
     // If there was an error we still need to clear the QUEUED flags
-    for (; flushIndex < queue.length; flushIndex++) {
-      const job = queue[flushIndex]
-      if (job) {
-        job.flags! &= ~SchedulerJobFlags.QUEUED
-      }
+    while (flushIndex < jobsLength) {
+      jobs[flushIndex].flags! &= ~SchedulerJobFlags.QUEUED
+      jobs[flushIndex++] = undefined as any
     }
 
-    flushIndex = -1
-    queue.length = 0
+    flushIndex = 0
+    jobsLength = 0
 
     flushPostFlushCbs(seen)
 
     currentFlushPromise = null
     // If new jobs have been added to either queue, keep flushing
-    if (queue.length || pendingPostFlushCbs.length) {
+    if (jobsLength || postJobs.length) {
       flushJobs(seen)
     }
   }
diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts
index a8c5340cd1f..4b31151da22 100644
--- a/packages/runtime-core/src/vnode.ts
+++ b/packages/runtime-core/src/vnode.ts
@@ -18,12 +18,14 @@ import {
   type ComponentInternalInstance,
   type ConcreteComponent,
   type Data,
+  type GenericComponentInstance,
   isClassComponent,
 } from './component'
 import type { RawSlots } from './componentSlots'
 import {
   type ReactiveFlags,
   type Ref,
+  type ShallowRef,
   isProxy,
   isRef,
   toRaw,
@@ -69,6 +71,7 @@ export const Fragment = Symbol.for('v-fgt') as any as {
 export const Text: unique symbol = Symbol.for('v-txt')
 export const Comment: unique symbol = Symbol.for('v-cmt')
 export const Static: unique symbol = Symbol.for('v-stc')
+export const VaporSlot: unique symbol = Symbol.for('v-vps')
 
 export type VNodeTypes =
   | string
@@ -82,6 +85,7 @@ export type VNodeTypes =
   | typeof TeleportImpl
   | typeof Suspense
   | typeof SuspenseImpl
+  | typeof VaporSlot
 
 export type VNodeRef =
   | string
@@ -253,6 +257,22 @@ export interface VNode<
    * @internal custom element interception hook
    */
   ce?: (instance: ComponentInternalInstance) => void
+  /**
+   * @internal VDOM in Vapor interop hook
+   */
+  vi?: (instance: ComponentInternalInstance) => void
+  /**
+   * @internal Vapor slot in VDOM metadata
+   */
+  vs?: {
+    slot: (props: any) => any
+    fallback: (() => VNodeArrayChildren) | undefined
+    ref?: ShallowRef<any>
+  }
+  /**
+   * @internal Vapor slot Block
+   */
+  vb?: any
 }
 
 // Since v-if and v-for are the two possible ways node structure can dynamically
@@ -899,7 +919,7 @@ export function mergeProps(...args: (Data & VNodeProps)[]): Data {
 
 export function invokeVNodeHook(
   hook: VNodeHook,
-  instance: ComponentInternalInstance | null,
+  instance: GenericComponentInstance | null,
   vnode: VNode,
   prevVNode: VNode | null = null,
 ): void {
diff --git a/packages/runtime-core/src/warning.ts b/packages/runtime-core/src/warning.ts
index 788e9372154..ef4057dd372 100644
--- a/packages/runtime-core/src/warning.ts
+++ b/packages/runtime-core/src/warning.ts
@@ -1,31 +1,34 @@
-import type { VNode } from './vnode'
 import {
-  type ComponentInternalInstance,
-  type ConcreteComponent,
   type Data,
+  type GenericComponentInstance,
   formatComponentName,
 } from './component'
 import { isFunction, isString } from '@vue/shared'
-import { isRef, pauseTracking, resetTracking, toRaw } from '@vue/reactivity'
+import { isRef, setActiveSub, toRaw } from '@vue/reactivity'
 import { ErrorCodes, callWithErrorHandling } from './errorHandling'
+import { type VNode, isVNode } from './vnode'
 
-type ComponentVNode = VNode & {
-  type: ConcreteComponent
-}
-
-const stack: VNode[] = []
+const stack: (GenericComponentInstance | VNode)[] = []
 
 type TraceEntry = {
-  vnode: ComponentVNode
+  ctx: GenericComponentInstance | VNode
   recurseCount: number
 }
 
 type ComponentTraceStack = TraceEntry[]
 
-export function pushWarningContext(vnode: VNode): void {
-  stack.push(vnode)
+/**
+ * @internal
+ */
+export function pushWarningContext(
+  ctx: GenericComponentInstance | VNode,
+): void {
+  stack.push(ctx)
 }
 
+/**
+ * @internal
+ */
 export function popWarningContext(): void {
   stack.pop()
 }
@@ -38,9 +41,10 @@ export function warn(msg: string, ...args: any[]): void {
 
   // avoid props formatting or warn handler tracking deps that might be mutated
   // during patch, leading to infinite recursion.
-  pauseTracking()
+  const prevSub = setActiveSub()
 
-  const instance = stack.length ? stack[stack.length - 1].component : null
+  const entry = stack.length ? stack[stack.length - 1] : null
+  const instance = isVNode(entry) ? entry.component : entry
   const appWarnHandler = instance && instance.appContext.config.warnHandler
   const trace = getComponentTrace()
 
@@ -52,10 +56,11 @@ export function warn(msg: string, ...args: any[]): void {
       [
         // eslint-disable-next-line no-restricted-syntax
         msg + args.map(a => a.toString?.() ?? JSON.stringify(a)).join(''),
-        instance && instance.proxy,
+        (instance && instance.proxy) || instance,
         trace
           .map(
-            ({ vnode }) => `at <${formatComponentName(instance, vnode.type)}>`,
+            ({ ctx }) =>
+              `at <${formatComponentName(instance, (ctx as any).type)}>`,
           )
           .join('\n'),
         trace,
@@ -74,13 +79,13 @@ export function warn(msg: string, ...args: any[]): void {
     console.warn(...warnArgs)
   }
 
-  resetTracking()
+  setActiveSub(prevSub)
   isWarning = false
 }
 
 export function getComponentTrace(): ComponentTraceStack {
-  let currentVNode: VNode | null = stack[stack.length - 1]
-  if (!currentVNode) {
+  let currentCtx: TraceEntry['ctx'] | null = stack[stack.length - 1]
+  if (!currentCtx) {
     return []
   }
 
@@ -89,19 +94,23 @@ export function getComponentTrace(): ComponentTraceStack {
   // instance parent pointers.
   const normalizedStack: ComponentTraceStack = []
 
-  while (currentVNode) {
+  while (currentCtx) {
     const last = normalizedStack[0]
-    if (last && last.vnode === currentVNode) {
+    if (last && last.ctx === currentCtx) {
       last.recurseCount++
     } else {
       normalizedStack.push({
-        vnode: currentVNode as ComponentVNode,
+        ctx: currentCtx,
         recurseCount: 0,
       })
     }
-    const parentInstance: ComponentInternalInstance | null =
-      currentVNode.component && currentVNode.component.parent
-    currentVNode = parentInstance && parentInstance.vnode
+    if (isVNode(currentCtx)) {
+      const parent: GenericComponentInstance | null =
+        currentCtx.component && currentCtx.component.parent
+      currentCtx = (parent && parent.vnode) || parent
+    } else {
+      currentCtx = currentCtx.parent
+    }
   }
 
   return normalizedStack
@@ -116,19 +125,14 @@ function formatTrace(trace: ComponentTraceStack): any[] {
   return logs
 }
 
-function formatTraceEntry({ vnode, recurseCount }: TraceEntry): any[] {
+function formatTraceEntry({ ctx, recurseCount }: TraceEntry): any[] {
   const postfix =
     recurseCount > 0 ? `... (${recurseCount} recursive calls)` : ``
-  const isRoot = vnode.component ? vnode.component.parent == null : false
-  const open = ` at <${formatComponentName(
-    vnode.component,
-    vnode.type,
-    isRoot,
-  )}`
+  const instance = isVNode(ctx) ? ctx.component : ctx
+  const isRoot = instance ? instance.parent == null : false
+  const open = ` at <${formatComponentName(instance, (ctx as any).type, isRoot)}`
   const close = `>` + postfix
-  return vnode.props
-    ? [open, ...formatProps(vnode.props), close]
-    : [open + close]
+  return ctx.props ? [open, ...formatProps(ctx.props), close] : [open + close]
 }
 
 function formatProps(props: Data): any[] {
diff --git a/packages/runtime-dom/package.json b/packages/runtime-dom/package.json
index 4d71206e2e5..36fb787fabb 100644
--- a/packages/runtime-dom/package.json
+++ b/packages/runtime-dom/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/runtime-dom",
-  "version": "3.5.17",
+  "version": "3.6.0-alpha.2",
   "description": "@vue/runtime-dom",
   "main": "index.js",
   "module": "dist/runtime-dom.esm-bundler.js",
diff --git a/packages/runtime-dom/src/components/TransitionGroup.ts b/packages/runtime-dom/src/components/TransitionGroup.ts
index 7bb5ce0c6b1..72af535d385 100644
--- a/packages/runtime-dom/src/components/TransitionGroup.ts
+++ b/packages/runtime-dom/src/components/TransitionGroup.ts
@@ -10,6 +10,7 @@ import {
   vtcKey,
 } from './Transition'
 import {
+  type ComponentInternalInstance,
   type ComponentOptions,
   DeprecationTypes,
   Fragment,
@@ -124,7 +125,7 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({
         !rawProps.tag &&
         compatUtils.checkCompatEnabled(
           DeprecationTypes.TRANSITION_GROUP_ROOT,
-          instance.parent,
+          instance.parent as ComponentInternalInstance,
         )
       ) {
         tag = 'span'
diff --git a/packages/runtime-dom/src/directives/vModel.ts b/packages/runtime-dom/src/directives/vModel.ts
index 5057e16d472..ee6cfcbab7a 100644
--- a/packages/runtime-dom/src/directives/vModel.ts
+++ b/packages/runtime-dom/src/directives/vModel.ts
@@ -53,33 +53,12 @@ export const vModelText: ModelDirective<
 > = {
   created(el, { modifiers: { lazy, trim, number } }, vnode) {
     el[assignKey] = getModelAssigner(vnode)
-    const castToNumber =
-      number || (vnode.props && vnode.props.type === 'number')
-    addEventListener(el, lazy ? 'change' : 'input', e => {
-      if ((e.target as any).composing) return
-      let domValue: string | number = el.value
-      if (trim) {
-        domValue = domValue.trim()
-      }
-      if (castToNumber) {
-        domValue = looseToNumber(domValue)
-      }
-      el[assignKey](domValue)
-    })
-    if (trim) {
-      addEventListener(el, 'change', () => {
-        el.value = el.value.trim()
-      })
-    }
-    if (!lazy) {
-      addEventListener(el, 'compositionstart', onCompositionStart)
-      addEventListener(el, 'compositionend', onCompositionEnd)
-      // Safari < 10.2 & UIWebView doesn't fire compositionend when
-      // switching focus before confirming composition choice
-      // this also fixes the issue where some browsers e.g. iOS Chrome
-      // fires "change" instead of "input" on autocomplete.
-      addEventListener(el, 'change', onCompositionEnd)
-    }
+    vModelTextInit(
+      el,
+      trim,
+      number || !!(vnode.props && vnode.props.type === 'number'),
+      lazy,
+    )
   },
   // set value on mounted so it's after min/max for type="range"
   mounted(el, { value }) {
@@ -91,30 +70,81 @@ export const vModelText: ModelDirective<
     vnode,
   ) {
     el[assignKey] = getModelAssigner(vnode)
-    // avoid clearing unresolved text. #2302
-    if ((el as any).composing) return
-    const elValue =
-      (number || el.type === 'number') && !/^0\d/.test(el.value)
-        ? looseToNumber(el.value)
-        : el.value
-    const newValue = value == null ? '' : value
-
-    if (elValue === newValue) {
-      return
+    vModelTextUpdate(el, oldValue, value, trim, number, lazy)
+  },
+}
+
+/**
+ * @internal
+ */
+export const vModelTextInit = (
+  el: HTMLInputElement | HTMLTextAreaElement,
+  trim: boolean | undefined,
+  number: boolean | undefined,
+  lazy: boolean | undefined,
+  set?: (v: any) => void,
+): void => {
+  addEventListener(el, lazy ? 'change' : 'input', e => {
+    if ((e.target as any).composing) return
+    let domValue: string | number = el.value
+    if (trim) {
+      domValue = domValue.trim()
     }
+    if (number || el.type === 'number') {
+      domValue = looseToNumber(domValue)
+    }
+    ;(set || (el as any)[assignKey])(domValue)
+  })
+  if (trim) {
+    addEventListener(el, 'change', () => {
+      el.value = el.value.trim()
+    })
+  }
+  if (!lazy) {
+    addEventListener(el, 'compositionstart', onCompositionStart)
+    addEventListener(el, 'compositionend', onCompositionEnd)
+    // Safari < 10.2 & UIWebView doesn't fire compositionend when
+    // switching focus before confirming composition choice
+    // this also fixes the issue where some browsers e.g. iOS Chrome
+    // fires "change" instead of "input" on autocomplete.
+    addEventListener(el, 'change', onCompositionEnd)
+  }
+}
 
-    if (document.activeElement === el && el.type !== 'range') {
-      // #8546
-      if (lazy && value === oldValue) {
-        return
-      }
-      if (trim && el.value.trim() === newValue) {
-        return
-      }
+/**
+ * @internal
+ */
+export const vModelTextUpdate = (
+  el: HTMLInputElement | HTMLTextAreaElement,
+  oldValue: any,
+  value: any,
+  trim: boolean | undefined,
+  number: boolean | undefined,
+  lazy: boolean | undefined,
+): void => {
+  // avoid clearing unresolved text. #2302
+  if ((el as any).composing) return
+  const elValue =
+    (number || el.type === 'number') && !/^0\d/.test(el.value)
+      ? looseToNumber(el.value)
+      : el.value
+  const newValue = value == null ? '' : value
+
+  if (elValue === newValue) {
+    return
+  }
+
+  if (document.activeElement === el && el.type !== 'range') {
+    // #8546
+    if (lazy && value === oldValue) {
+      return
+    }
+    if (trim && el.value.trim() === newValue) {
+      return
     }
+  }
 
-    el.value = newValue
-  },
+  el.value = newValue
 }
 
 export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
@@ -122,56 +152,82 @@ export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
   deep: true,
   created(el, _, vnode) {
     el[assignKey] = getModelAssigner(vnode)
-    addEventListener(el, 'change', () => {
-      const modelValue = (el as any)._modelValue
-      const elementValue = getValue(el)
-      const checked = el.checked
-      const assign = el[assignKey]
-      if (isArray(modelValue)) {
-        const index = looseIndexOf(modelValue, elementValue)
-        const found = index !== -1
-        if (checked && !found) {
-          assign(modelValue.concat(elementValue))
-        } else if (!checked && found) {
-          const filtered = [...modelValue]
-          filtered.splice(index, 1)
-          assign(filtered)
-        }
-      } else if (isSet(modelValue)) {
-        const cloned = new Set(modelValue)
-        if (checked) {
-          cloned.add(elementValue)
-        } else {
-          cloned.delete(elementValue)
-        }
-        assign(cloned)
-      } else {
-        assign(getCheckboxValue(el, checked))
-      }
-    })
+    vModelCheckboxInit(el)
   },
   // set initial checked on mount to wait for true-value/false-value
-  mounted: setChecked,
+  mounted(el, binding, vnode) {
+    vModelCheckboxUpdate(
+      el,
+      binding.oldValue,
+      binding.value,
+      vnode.props!.value,
+    )
+  },
   beforeUpdate(el, binding, vnode) {
     el[assignKey] = getModelAssigner(vnode)
-    setChecked(el, binding, vnode)
+    vModelCheckboxUpdate(
+      el,
+      binding.oldValue,
+      binding.value,
+      vnode.props!.value,
+    )
   },
 }
 
-function setChecked(
+/**
+ * @internal
+ */
+export const vModelCheckboxInit = (
   el: HTMLInputElement,
-  { value, oldValue }: DirectiveBinding,
-  vnode: VNode,
-) {
+  set?: (v: any) => void,
+): void => {
+  addEventListener(el, 'change', () => {
+    const assign = set || (el as any)[assignKey]
+    const modelValue = (el as any)._modelValue
+    const elementValue = getValue(el)
+    const checked = el.checked
+    if (isArray(modelValue)) {
+      const index = looseIndexOf(modelValue, elementValue)
+      const found = index !== -1
+      if (checked && !found) {
+        assign(modelValue.concat(elementValue))
+      } else if (!checked && found) {
+        const filtered = [...modelValue]
+        filtered.splice(index, 1)
+        assign(filtered)
+      }
+    } else if (isSet(modelValue)) {
+      const cloned = new Set(modelValue)
+      if (checked) {
+        cloned.add(elementValue)
+      } else {
+        cloned.delete(elementValue)
+      }
+      assign(cloned)
+    } else {
+      assign(getCheckboxValue(el, checked))
+    }
+  })
+}
+
+/**
+ * @internal
+ */
+export const vModelCheckboxUpdate = (
+  el: HTMLInputElement,
+  oldValue: any,
+  value: any,
+  rawValue: any = getValue(el),
+): void => {
   // store the v-model value on the element so it can be accessed by the
   // change listener.
   ;(el as any)._modelValue = value
   let checked: boolean
 
   if (isArray(value)) {
-    checked = looseIndexOf(value, vnode.props!.value) > -1
+    checked = looseIndexOf(value, rawValue) > -1
   } else if (isSet(value)) {
-    checked = value.has(vnode.props!.value)
+    checked = value.has(rawValue)
   } else {
     if (value === oldValue) return
     checked = looseEqual(value, getCheckboxValue(el, true))
@@ -203,43 +259,57 @@ export const vModelSelect: ModelDirective<HTMLSelectElement, 'number'> = {
   // <select multiple> value need to be deep traversed
   deep: true,
   created(el, { value, modifiers: { number } }, vnode) {
-    const isSetModel = isSet(value)
-    addEventListener(el, 'change', () => {
-      const selectedVal = Array.prototype.filter
-        .call(el.options, (o: HTMLOptionElement) => o.selected)
-        .map((o: HTMLOptionElement) =>
-          number ? looseToNumber(getValue(o)) : getValue(o),
-        )
-      el[assignKey](
-        el.multiple
-          ? isSetModel
-            ? new Set(selectedVal)
-            : selectedVal
-          : selectedVal[0],
-      )
-      el._assigning = true
-      nextTick(() => {
-        el._assigning = false
-      })
-    })
+    vModelSelectInit(el, value, number)
     el[assignKey] = getModelAssigner(vnode)
   },
   // set value in mounted & updated because <select> relies on its children
   // <option>s.
   mounted(el, { value }) {
-    setSelected(el, value)
+    vModelSetSelected(el, value)
   },
   beforeUpdate(el, _binding, vnode) {
     el[assignKey] = getModelAssigner(vnode)
   },
   updated(el, { value }) {
-    if (!el._assigning) {
-      setSelected(el, value)
-    }
+    vModelSetSelected(el, value)
   },
 }
 
-function setSelected(el: HTMLSelectElement, value: any) {
+/**
+ * @internal
+ */
+export const vModelSelectInit = (
+  el: HTMLSelectElement & { [assignKey]?: AssignerFn; _assigning?: boolean },
+  value: any,
+  number: boolean | undefined,
+  set?: (v: any) => void,
+): void => {
+  const isSetModel = isSet(value)
+  addEventListener(el, 'change', () => {
+    const selectedVal = Array.prototype.filter
+      .call(el.options, (o: HTMLOptionElement) => o.selected)
+      .map((o: HTMLOptionElement) =>
+        number ? looseToNumber(getValue(o)) : getValue(o),
+      )
+    ;(set || el[assignKey]!)(
+      el.multiple
+        ? isSetModel
+          ? new Set(selectedVal)
+          : selectedVal
+        : selectedVal[0],
+    )
+    el._assigning = true
+    nextTick(() => {
+      el._assigning = false
+    })
+  })
+}
+
+/**
+ * @internal
+ */
+export const vModelSetSelected = (el: HTMLSelectElement, value: any): void => {
+  if ((el as any)._assigning) return
   const isMultiple = el.multiple
   const isArrayValue = isArray(value)
   if (isMultiple && !isArrayValue && !isSet(value)) {
@@ -276,8 +346,10 @@ function setSelected(el: HTMLSelectElement, value: any) {
   }
 }
 
-// retrieve raw value set via :value bindings
-function getValue(el: HTMLOptionElement | HTMLInputElement) {
+/**
+ * @internal retrieve raw value set via :value bindings
+ */
+export function getValue(el: HTMLOptionElement | HTMLInputElement): any {
   return '_value' in el ? (el as any)._value : el.value
 }
 
@@ -287,7 +359,14 @@ function getCheckboxValue(
   checked: boolean,
 ) {
   const key = checked ? '_trueValue' : '_falseValue'
-  return key in el ? el[key] : checked
+  if (key in el) {
+    return el[key]
+  }
+  const attr = checked ? 'true-value' : 'false-value'
+  if (el.hasAttribute(attr)) {
+    return el.getAttribute(attr)
+  }
+  return checked
 }
 
 export const vModelDynamic: ObjectDirective<
diff --git a/packages/runtime-dom/src/directives/vShow.ts b/packages/runtime-dom/src/directives/vShow.ts
index f8f41bb04d4..fab85368025 100644
--- a/packages/runtime-dom/src/directives/vShow.ts
+++ b/packages/runtime-dom/src/directives/vShow.ts
@@ -1,12 +1,13 @@
 import type { ObjectDirective } from '@vue/runtime-core'
 
 export const vShowOriginalDisplay: unique symbol = Symbol('_vod')
+
 export const vShowHidden: unique symbol = Symbol('_vsh')
 
 export interface VShowElement extends HTMLElement {
   // _vod = vue original display
-  [vShowOriginalDisplay]: string
-  [vShowHidden]: boolean
+  [vShowOriginalDisplay]?: string
+  [vShowHidden]?: boolean
 }
 
 export const vShow: ObjectDirective<VShowElement> & { name?: 'show' } = {
@@ -50,7 +51,7 @@ if (__DEV__) {
 }
 
 function setDisplay(el: VShowElement, value: unknown): void {
-  el.style.display = value ? el[vShowOriginalDisplay] : 'none'
+  el.style.display = value ? el[vShowOriginalDisplay]! : 'none'
   el[vShowHidden] = !value
 }
 
diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts
index c69375983d8..b241458dba7 100644
--- a/packages/runtime-dom/src/index.ts
+++ b/packages/runtime-dom/src/index.ts
@@ -1,5 +1,7 @@
 import {
   type App,
+  type Component,
+  type ConcreteComponent,
   type CreateAppFunction,
   type DefineComponent,
   DeprecationTypes,
@@ -71,7 +73,7 @@ let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer
 
 let enabledHydration = false
 
-function ensureRenderer() {
+function ensureRenderer(): Renderer<Element | ShadowRoot> {
   return (
     renderer ||
     (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
@@ -108,7 +110,7 @@ export const createApp = ((...args) => {
     const container = normalizeContainer(containerOrSelector)
     if (!container) return
 
-    const component = app._component
+    const component = app._component as ConcreteComponent
     if (!isFunction(component) && !component.render && !component.template) {
       // __UNSAFE__
       // Reason: potential execution of JS expressions in in-DOM template.
@@ -143,7 +145,7 @@ export const createApp = ((...args) => {
   }
 
   return app
-}) as CreateAppFunction<Element>
+}) as CreateAppFunction<Element, Component>
 
 export const createSSRApp = ((...args) => {
   const app = ensureHydrationRenderer().createApp(...args)
@@ -225,9 +227,12 @@ function injectCompilerOptionsCheck(app: App) {
   }
 }
 
-function normalizeContainer(
-  container: Element | ShadowRoot | string,
-): Element | ShadowRoot | null {
+/**
+ * @internal
+ */
+function normalizeContainer<T extends ParentNode>(
+  container: T | string,
+): T | null {
   if (isString(container)) {
     const res = document.querySelector(container)
     if (__DEV__ && !res) {
@@ -235,7 +240,7 @@ function normalizeContainer(
         `Failed to mount app: mount target selector "${container}" returned null.`,
       )
     }
-    return res
+    return res as any
   }
   if (
     __DEV__ &&
@@ -306,3 +311,40 @@ export const initDirectivesForSSR: () => void = __SSR__
 export * from '@vue/runtime-core'
 
 export * from './jsx'
+
+// VAPOR -----------------------------------------------------------------------
+// Everything below are exposed for vapor only and can change any time.
+// They are also trimmed from non-bundler builds.
+
+/**
+ * @internal
+ */
+export { ensureRenderer, normalizeContainer }
+/**
+ * @internal
+ */
+export { patchStyle } from './modules/style'
+/**
+ * @internal
+ */
+export { shouldSetAsProp } from './patchProp'
+/**
+ * @internal
+ */
+export {
+  vShowOriginalDisplay,
+  vShowHidden,
+  type VShowElement,
+} from './directives/vShow'
+/**
+ * @internal
+ */
+export {
+  vModelTextInit,
+  vModelTextUpdate,
+  vModelCheckboxInit,
+  vModelCheckboxUpdate,
+  getValue as vModelGetValue,
+  vModelSelectInit,
+  vModelSetSelected,
+} from './directives/vModel'
diff --git a/packages/runtime-dom/src/modules/props.ts b/packages/runtime-dom/src/modules/props.ts
index 98608831a9a..a28290acf4a 100644
--- a/packages/runtime-dom/src/modules/props.ts
+++ b/packages/runtime-dom/src/modules/props.ts
@@ -1,5 +1,5 @@
 import { DeprecationTypes, compatUtils, warn } from '@vue/runtime-core'
-import { includeBooleanAttr } from '@vue/shared'
+import { canSetValueDirectly, includeBooleanAttr } from '@vue/shared'
 import { unsafeToTrustedHTML } from '../nodeOps'
 
 // functions. The user is responsible for using them with only trusted content.
@@ -24,12 +24,7 @@ export function patchDOMProp(
 
   const tag = el.tagName
 
-  if (
-    key === 'value' &&
-    tag !== 'PROGRESS' &&
-    // custom elements may use _value internally
-    !tag.includes('-')
-  ) {
+  if (key === 'value' && canSetValueDirectly(tag)) {
     // #4956: <option> value will fallback to its text content so we need to
     // compare against its attribute value instead.
     const oldValue =
diff --git a/packages/runtime-dom/src/modules/style.ts b/packages/runtime-dom/src/modules/style.ts
index 383628a6ad0..2d1db4c0abd 100644
--- a/packages/runtime-dom/src/modules/style.ts
+++ b/packages/runtime-dom/src/modules/style.ts
@@ -7,7 +7,7 @@ import {
 } from '../directives/vShow'
 import { CSS_VAR_TEXT } from '../helpers/useCssVars'
 
-type Style = string | Record<string, string | string[]> | null
+type Style = string | null | undefined | Record<string, unknown>
 
 const displayRE = /(^|;)\s*display\s*:/
 
@@ -67,15 +67,11 @@ export function patchStyle(el: Element, prev: Style, next: Style): void {
 const semicolonRE = /[^\\];\s*$/
 const importantRE = /\s*!important$/
 
-function setStyle(
-  style: CSSStyleDeclaration,
-  name: string,
-  val: string | string[],
-) {
-  if (isArray(val)) {
-    val.forEach(v => setStyle(style, name, v))
+function setStyle(style: CSSStyleDeclaration, name: string, rawVal: unknown) {
+  if (isArray(rawVal)) {
+    rawVal.forEach(v => setStyle(style, name, v))
   } else {
-    if (val == null) val = ''
+    const val = rawVal == null ? '' : String(rawVal)
     if (__DEV__) {
       if (semicolonRE.test(val)) {
         warn(
diff --git a/packages/runtime-dom/src/patchProp.ts b/packages/runtime-dom/src/patchProp.ts
index 27174ddf624..dfa5974ebe9 100644
--- a/packages/runtime-dom/src/patchProp.ts
+++ b/packages/runtime-dom/src/patchProp.ts
@@ -7,19 +7,14 @@ import {
   camelize,
   isFunction,
   isModelListener,
+  isNativeOn,
   isOn,
   isString,
+  shouldSetAsAttr,
 } from '@vue/shared'
 import type { RendererOptions } from '@vue/runtime-core'
 import type { VueElement } from './apiCustomElement'
 
-const isNativeOn = (key: string) =>
-  key.charCodeAt(0) === 111 /* o */ &&
-  key.charCodeAt(1) === 110 /* n */ &&
-  // lowercase letter
-  key.charCodeAt(2) > 96 &&
-  key.charCodeAt(2) < 123
-
 type DOMRendererOptions = RendererOptions<Node, Element>
 
 export const patchProp: DOMRendererOptions['patchProp'] = (
@@ -77,12 +72,12 @@ export const patchProp: DOMRendererOptions['patchProp'] = (
   }
 }
 
-function shouldSetAsProp(
+export function shouldSetAsProp(
   el: Element,
   key: string,
   value: unknown,
   isSVG: boolean,
-) {
+): boolean {
   if (isSVG) {
     // most keys must be set as attribute on svg elements to work
     // ...except innerHTML & textContent
@@ -96,50 +91,10 @@ function shouldSetAsProp(
     return false
   }
 
-  // these are enumerated attrs, however their corresponding DOM properties
-  // are actually booleans - this leads to setting it with a string "false"
-  // value leading it to be coerced to `true`, so we need to always treat
-  // them as attributes.
-  // Note that `contentEditable` doesn't have this problem: its DOM
-  // property is also enumerated string values.
-  if (
-    key === 'spellcheck' ||
-    key === 'draggable' ||
-    key === 'translate' ||
-    key === 'autocorrect'
-  ) {
-    return false
-  }
-
-  // #1787, #2840 form property on form elements is readonly and must be set as
-  // attribute.
-  if (key === 'form') {
-    return false
-  }
-
-  // #1526 <input list> must be set as attribute
-  if (key === 'list' && el.tagName === 'INPUT') {
+  if (shouldSetAsAttr(el.tagName, key)) {
     return false
   }
 
-  // #2766 <textarea type> must be set as attribute
-  if (key === 'type' && el.tagName === 'TEXTAREA') {
-    return false
-  }
-
-  // #8780 the width or height of embedded tags must be set as attribute
-  if (key === 'width' || key === 'height') {
-    const tag = el.tagName
-    if (
-      tag === 'IMG' ||
-      tag === 'VIDEO' ||
-      tag === 'CANVAS' ||
-      tag === 'SOURCE'
-    ) {
-      return false
-    }
-  }
-
   // native onclick with string value, must be set as attribute
   if (isNativeOn(key) && isString(value)) {
     return false
diff --git a/packages/runtime-vapor/LICENSE b/packages/runtime-vapor/LICENSE
new file mode 100644
index 00000000000..15f1f7e7a49
--- /dev/null
+++ b/packages/runtime-vapor/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2018-present, Yuxi (Evan) You
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/packages/runtime-vapor/README.md b/packages/runtime-vapor/README.md
new file mode 100644
index 00000000000..ab8204db555
--- /dev/null
+++ b/packages/runtime-vapor/README.md
@@ -0,0 +1,8 @@
+# @vue/runtime-vapor
+
+This package only ships `esm-bundler` build because:
+
+1. Vapor mode requires SFC build.
+2. Vapor mode runtime only runs in the browser.
+
+The main `vue` package ships `dist/vue.runtime-with-vapor.esm-browser.js` which inlines this package. It is used for the SFC Playground only.
diff --git a/packages/runtime-vapor/__tests__/_utils.ts b/packages/runtime-vapor/__tests__/_utils.ts
new file mode 100644
index 00000000000..12efebf7c7a
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/_utils.ts
@@ -0,0 +1,155 @@
+import { createVaporApp, vaporInteropPlugin } from '../src'
+import { type App, type Component, createApp } from '@vue/runtime-dom'
+import type { VaporComponent, VaporComponentInstance } from '../src/component'
+import type { RawProps } from '../src/componentProps'
+
+export interface RenderContext {
+  component: VaporComponent
+  host: HTMLElement
+  instance: VaporComponentInstance | undefined
+  app: App
+  create: (props?: RawProps) => RenderContext
+  mount: (container?: string | ParentNode) => RenderContext
+  render: (props?: RawProps, container?: string | ParentNode) => RenderContext
+  resetHost: () => HTMLDivElement
+  html: () => string
+}
+
+export function makeRender<C = VaporComponent>(
+  initHost = (): HTMLDivElement => {
+    const host = document.createElement('div')
+    host.setAttribute('id', 'host')
+    document.body.appendChild(host)
+    return host
+  },
+): (comp: C) => RenderContext {
+  let host: HTMLElement
+  function resetHost() {
+    return (host = initHost())
+  }
+
+  beforeEach(() => {
+    resetHost()
+  })
+  afterEach(() => {
+    host.remove()
+  })
+
+  function define(comp: C) {
+    const component = comp as any
+    component.__vapor = true
+    let instance: VaporComponentInstance | undefined
+    let app: App
+
+    function render(
+      props: RawProps | undefined = undefined,
+      container: string | ParentNode = host,
+    ) {
+      create(props)
+      return mount(container)
+    }
+
+    function create(props: RawProps | undefined = undefined) {
+      app?.unmount()
+      app = createVaporApp(component, props)
+      return res()
+    }
+
+    function mount(container: string | ParentNode = host) {
+      app.mount(container)
+      instance = app._instance as VaporComponentInstance
+      return res()
+    }
+
+    function html() {
+      return host.innerHTML
+    }
+
+    const res = () => ({
+      component,
+      host,
+      instance,
+      app,
+      create,
+      mount,
+      render,
+      resetHost,
+      html,
+    })
+
+    return res()
+  }
+
+  return define
+}
+
+export interface InteropRenderContext {
+  mount: (container?: string | ParentNode) => InteropRenderContext
+  render: (
+    props?: RawProps,
+    container?: string | ParentNode,
+  ) => InteropRenderContext
+  host: HTMLElement
+  html: () => string
+}
+
+export function makeInteropRender(): (comp: Component) => InteropRenderContext {
+  let host: HTMLElement
+  beforeEach(() => {
+    host = document.createElement('div')
+  })
+  afterEach(() => {
+    host.remove()
+  })
+
+  function define(comp: Component) {
+    let app: App
+    function render(
+      props: RawProps | undefined = undefined,
+      container: string | ParentNode = host,
+    ) {
+      app?.unmount()
+      app = createApp(comp, props)
+      app.use(vaporInteropPlugin)
+      return mount(container)
+    }
+
+    function mount(container: string | ParentNode = host) {
+      app.mount(container)
+      return res()
+    }
+
+    function html() {
+      return host.innerHTML
+    }
+
+    const res = () => ({
+      host,
+      mount,
+      render,
+      html,
+    })
+
+    return res()
+  }
+
+  return define
+}
+
+export function shuffle(array: Array<any>): any[] {
+  let currentIndex = array.length
+  let temporaryValue
+  let randomIndex
+
+  // while there remain elements to shuffle...
+  while (currentIndex !== 0) {
+    // pick a remaining element...
+    randomIndex = Math.floor(Math.random() * currentIndex)
+    currentIndex -= 1
+    // and swap it with the current element.
+    temporaryValue = array[currentIndex]
+    array[currentIndex] = array[randomIndex]
+    array[randomIndex] = temporaryValue
+  }
+  return array
+}
diff --git a/packages/runtime-vapor/__tests__/apiCreateDynamicComponent.spec.ts b/packages/runtime-vapor/__tests__/apiCreateDynamicComponent.spec.ts
new file mode 100644
index 00000000000..e912af2851a
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/apiCreateDynamicComponent.spec.ts
@@ -0,0 +1,113 @@
+import { ref, shallowRef } from '@vue/reactivity'
+import { nextTick, resolveDynamicComponent } from '@vue/runtime-dom'
+import {
+  createComponentWithFallback,
+  createDynamicComponent,
+  defineVaporComponent,
+  renderEffect,
+  setHtml,
+  setInsertionState,
+  template,
+} from '../src'
+import { makeRender } from './_utils'
+
+const define = makeRender()
+
+describe('api: createDynamicComponent', () => {
+  const A = () => document.createTextNode('AAA')
+  const B = () => document.createTextNode('BBB')
+
+  test('direct value', async () => {
+    const val = shallowRef<any>(A)
+
+    const { html } = define({
+      setup() {
+        return createDynamicComponent(() => val.value)
+      },
+    }).render()
+
+    expect(html()).toBe('AAA<!--dynamic-component-->')
+
+    val.value = B
+    await nextTick()
+    expect(html()).toBe('BBB<!--dynamic-component-->')
+
+    // fallback
+    val.value = 'foo'
+    await nextTick()
+    expect(html()).toBe('<foo></foo><!--dynamic-component-->')
+  })
+
+  test('global registration', async () => {
+    const val = shallowRef('foo')
+
+    const { app, html, mount } = define({
+      setup() {
+        return createDynamicComponent(() => val.value)
+      },
+    }).create()
+
+    app.component('foo', A)
+    app.component('bar', B)
+
+    mount()
+    expect(html()).toBe('AAA<!--dynamic-component-->')
+
+    val.value = 'bar'
+    await nextTick()
+    expect(html()).toBe('BBB<!--dynamic-component-->')
+
+    // fallback
+    val.value = 'baz'
+    await nextTick()
+    expect(html()).toBe('<baz></baz><!--dynamic-component-->')
+  })
+
+  test('render fallback with insertionState', async () => {
+    const { html, mount } = define({
+      setup() {
+        const html = ref('hi')
+        const n1 = template('<div></div>', true)() as any
+        setInsertionState(n1)
+        const n0 = createComponentWithFallback(
+          resolveDynamicComponent('button') as any,
+        ) as any
+        renderEffect(() => setHtml(n0, html.value))
+        return n1
+      },
+    }).create()
+
+    mount()
+    expect(html()).toBe('<div><button>hi</button></div>')
+  })
+
+  test('switch dynamic component children', async () => {
+    const CompA = defineVaporComponent({
+      setup() {
+        return template('<div>A</div>')()
+      },
+    })
+    const CompB = defineVaporComponent({
+      setup() {
+        return template('<div>B</div>')()
+      },
+    })
+
+    const current = shallowRef(CompA)
+    const { html } = define({
+      setup() {
+        const t1 = template('<div></div>')
+        const n2 = t1() as any
+        setInsertionState(n2)
+        createDynamicComponent(() => current.value)
+        return n2
+      },
+    }).render()
+
+    expect(html()).toBe('<div><div>A</div><!--dynamic-component--></div>')
+
+    current.value = CompB
+    await nextTick()
+    expect(html()).toBe('<div><div>B</div><!--dynamic-component--></div>')
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/apiCreateSelector.spec.ts b/packages/runtime-vapor/__tests__/apiCreateSelector.spec.ts
new file mode 100644
index 00000000000..9a3745fb545
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/apiCreateSelector.spec.ts
@@ -0,0 +1,63 @@
+import { ref } from '@vue/reactivity'
+import { makeRender } from './_utils'
+import { createFor } from '../src'
+import { nextTick } from '@vue/runtime-dom'
+
+const define = makeRender()
+
+describe('api: createSelector', () => {
+  test('basic', async () => {
+    let calledTimes = 0
+    let expectedCalledTimes = 0
+
+    const list = ref([{ id: 0 }, { id: 1 }, { id: 2 }])
+    const index = ref(0)
+
+    const { host } = define(() => {
+      let selector: (cb: () => void) => void
+      return createFor(
+        () => list.value,
+        item => {
+          const span = document.createElement('li')
+          selector(() => {
+            calledTimes += 1
+            const { id } = item.value
+            span.textContent = `${id}.${id === index.value ? 't' : 'f'}`
+          })
+          return span
+        },
+        item => item.id,
+        undefined,
+        ({ createSelector }) => {
+          selector = createSelector(() => index.value)
+        },
+      )
+    }).render()
+
+    expect(host.innerHTML).toBe(
+      '<li>0.t</li><li>1.f</li><li>2.f</li><!--for-->',
+    )
+    expect(calledTimes).toBe((expectedCalledTimes += 3))
+
+    index.value = 1
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0.f</li><li>1.t</li><li>2.f</li><!--for-->',
+    )
+    expect(calledTimes).toBe((expectedCalledTimes += 2))
+
+    index.value = 2
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0.f</li><li>1.f</li><li>2.t</li><!--for-->',
+    )
+    expect(calledTimes).toBe((expectedCalledTimes += 2))
+
+    // list.value[2].id = 3
+    // await nextTick()
+    // expect(host.innerHTML).toBe(
+    //   '<li>0.f</li><li>1.f</li><li>3.f</li><!--for-->',
+    // )
+    // expect(calledTimes).toBe((expectedCalledTimes += 1))
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/apiCreateVaporApp.spec.ts b/packages/runtime-vapor/__tests__/apiCreateVaporApp.spec.ts
new file mode 100644
index 00000000000..6b21e5e5569
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/apiCreateVaporApp.spec.ts
@@ -0,0 +1,357 @@
+import {
+  createComponent,
+  createTextNode,
+  createVaporApp,
+  defineVaporComponent,
+  // @ts-expect-error
+  withDirectives,
+} from '../src'
+import {
+  type GenericComponentInstance,
+  type Plugin,
+  currentInstance,
+  inject,
+  provide,
+  resolveComponent,
+  resolveDirective,
+  warn,
+} from '@vue/runtime-dom'
+import { makeRender } from './_utils'
+import type { VaporComponent } from '../src/component'
+
+const define = makeRender()
+
+describe('api: createVaporApp', () => {
+  test('mount', () => {
+    const Comp = defineVaporComponent({
+      props: {
+        count: { default: 0 },
+      },
+      setup(props) {
+        return createTextNode(String(props.count))
+      },
+    })
+
+    const root1 = document.createElement('div')
+    createVaporApp(Comp).mount(root1)
+    expect(root1.innerHTML).toBe(`0`)
+
+    //#5571 mount multiple apps to the same host element
+    createVaporApp(Comp).mount(root1)
+    expect(`mount target container is not empty`).toHaveBeenWarned()
+    expect(
+      `There is already an app instance mounted on the host container`,
+    ).toHaveBeenWarned()
+
+    // mount with props
+    const root2 = document.createElement('div')
+    const app2 = createVaporApp(Comp, { count: () => 1 })
+    app2.mount(root2)
+    expect(root2.innerHTML).toBe(`1`)
+
+    // remount warning
+    const root3 = document.createElement('div')
+    app2.mount(root3)
+    expect(root3.innerHTML).toBe(``)
+    expect(`already been mounted`).toHaveBeenWarned()
+  })
+
+  test('unmount', () => {
+    const Comp = defineVaporComponent({
+      props: {
+        count: { default: 0 },
+      },
+      setup(props) {
+        return createTextNode(String(props.count))
+      },
+    })
+
+    const root = document.createElement('div')
+    const app = createVaporApp(Comp)
+
+    // warning
+    app.unmount()
+    expect(`that is not mounted`).toHaveBeenWarned()
+
+    app.mount(root)
+
+    app.unmount()
+    expect(root.innerHTML).toBe(``)
+  })
+
+  test('provide', () => {
+    const Root = define({
+      setup() {
+        // test override
+        provide('foo', 3)
+        return createComponent(Child)
+      },
+    })
+
+    const Child = defineVaporComponent({
+      setup() {
+        const foo = inject('foo')
+        const bar = inject('bar')
+        try {
+          inject('__proto__')
+        } catch (e: any) {}
+        return createTextNode(`${foo},${bar}`)
+      },
+    })
+
+    const { app, mount, create, html } = Root.create()
+    app.provide('foo', 1)
+    app.provide('bar', 2)
+    mount()
+    expect(html()).toBe(`3,2`)
+    expect('[Vue warn]: injection "__proto__" not found.').toHaveBeenWarned()
+
+    const { app: app2 } = create()
+    app2.provide('bar', 1)
+    app2.provide('bar', 2)
+    expect(`App already provides property with key "bar".`).toHaveBeenWarned()
+  })
+
+  test('runWithContext', () => {
+    const { app } = define({
+      setup() {
+        provide('foo', 'should not be seen')
+        return document.createElement('div')
+      },
+    }).create()
+    app.provide('foo', 1)
+
+    expect(app.runWithContext(() => inject('foo'))).toBe(1)
+
+    expect(
+      app.runWithContext(() => {
+        app.runWithContext(() => {})
+        return inject('foo')
+      }),
+    ).toBe(1)
+
+    // ensure the context is restored
+    inject('foo')
+    expect('inject() can only be used inside setup').toHaveBeenWarned()
+  })
+
+  test('component', () => {
+    const { app, mount, host } = define({
+      setup() {
+        const FooBar = resolveComponent('foo-bar') as VaporComponent
+        const BarBaz = resolveComponent('bar-baz') as VaporComponent
+        return [createComponent(FooBar), createComponent(BarBaz)]
+      },
+    }).create()
+
+    const FooBar = () => createTextNode('foobar!')
+    app.component('FooBar', FooBar)
+    expect(app.component('FooBar')).toBe(FooBar)
+
+    app.component('BarBaz', () => createTextNode('barbaz!'))
+    app.component('BarBaz', () => createTextNode('barbaz!'))
+    expect(
+      'Component "BarBaz" has already been registered in target app.',
+    ).toHaveBeenWarnedTimes(1)
+
+    mount()
+    expect(host.innerHTML).toBe(`foobar!barbaz!`)
+  })
+
+  test.todo('directive', () => {
+    const spy1 = vi.fn()
+    const spy2 = vi.fn()
+
+    const { app, mount } = define({
+      setup() {
+        const FooBar = resolveDirective('foo-bar')
+        const BarBaz = resolveDirective('bar-baz')
+        return withDirectives(document.createElement('div'), [
+          [FooBar],
+          [BarBaz],
+        ])
+      },
+    }).create()
+
+    const FooBar = spy1
+    app.directive('FooBar', FooBar)
+    expect(app.directive('FooBar')).toBe(FooBar)
+
+    app.directive('BarBaz', spy2)
+    app.directive('BarBaz', spy2)
+    expect(
+      'Directive "BarBaz" has already been registered in target app.',
+    ).toHaveBeenWarnedTimes(1)
+
+    mount()
+    expect(spy1).toHaveBeenCalled()
+    expect(spy2).toHaveBeenCalled()
+
+    app.directive('bind', FooBar)
+    expect(
+      `Do not use built-in directive ids as custom directive id: bind`,
+    ).toHaveBeenWarned()
+  })
+
+  test('use', () => {
+    const PluginA: Plugin = app => app.provide('foo', 1)
+    const PluginB: Plugin = {
+      install: (app, arg1, arg2) => app.provide('bar', arg1 + arg2),
+    }
+    class PluginC {
+      someProperty = {}
+      static install() {
+        app.provide('baz', 2)
+      }
+    }
+    const PluginD: any = undefined
+
+    const { app, host, mount } = define({
+      setup() {
+        const foo = inject('foo')
+        const bar = inject('bar')
+        return document.createTextNode(`${foo},${bar}`)
+      },
+    }).create()
+
+    app.use(PluginA)
+    app.use(PluginB, 1, 1)
+    app.use(PluginC)
+
+    mount()
+    expect(host.innerHTML).toBe(`1,2`)
+
+    app.use(PluginA)
+    expect(
+      `Plugin has already been applied to target app`,
+    ).toHaveBeenWarnedTimes(1)
+
+    app.use(PluginD)
+    expect(
+      `A plugin must either be a function or an object with an "install" ` +
+        `function.`,
+    ).toHaveBeenWarnedTimes(1)
+  })
+
+  test('config.errorHandler', () => {
+    const error = new Error()
+    let instance: GenericComponentInstance
+
+    const handler = vi.fn((err, _instance, info) => {
+      expect(err).toBe(error)
+      expect(_instance).toBe(instance)
+      expect(info).toBe(`render function`)
+    })
+
+    const { app, mount } = define({
+      setup() {
+        instance = currentInstance!
+        return {}
+      },
+      render() {
+        throw error
+      },
+    }).create()
+    app.config.errorHandler = handler
+    mount()
+    expect(handler).toHaveBeenCalled()
+  })
+
+  test('config.warnHandler', () => {
+    let instance: GenericComponentInstance
+
+    const handler = vi.fn((msg, _instance, trace) => {
+      expect(msg).toMatch(`warn message`)
+      expect(_instance).toBe(instance)
+      expect(trace).toMatch(`Hello`)
+    })
+
+    const { app, mount } = define({
+      name: 'Hello',
+      setup() {
+        instance = currentInstance!
+        warn('warn message')
+        return []
+      },
+    }).create()
+
+    app.config.warnHandler = handler
+    mount()
+    expect(handler).toHaveBeenCalledTimes(1)
+  })
+
+  describe('config.isNativeTag', () => {
+    const isNativeTag = vi.fn(tag => tag === 'div')
+
+    // Not relevant for vapor
+    // test('Component.name', () => {
+    //   const { app, mount } = define({
+    //     name: 'div',
+    //     render(): any {},
+    //   }).create()
+
+    //   Object.defineProperty(app.config, 'isNativeTag', {
+    //     value: isNativeTag,
+    //     writable: false,
+    //   })
+
+    //   mount()
+    //   expect(
+    //     `Do not use built-in or reserved HTML elements as component id: div`,
+    //   ).toHaveBeenWarned()
+    // })
+
+    test('register using app.component', () => {
+      const { app, mount } = define({
+        render(): any {},
+      }).create()
+
+      Object.defineProperty(app.config, 'isNativeTag', {
+        value: isNativeTag,
+        writable: false,
+      })
+
+      app.component('div', () => createTextNode('div'))
+      mount()
+      expect(
+        `Do not use built-in or reserved HTML elements as component id: div`,
+      ).toHaveBeenWarned()
+    })
+  })
+
+  describe('config.performance', () => {
+    afterEach(() => {
+      window.performance.clearMeasures()
+    })
+
+    test('with performance enabled', () => {
+      const { app, mount } = define({ setup: () => [] }).create()
+
+      app.config.performance = true
+      mount()
+      expect(window.performance.getEntries()).lengthOf(2)
+    })
+
+    test('with performance disabled', () => {
+      const { app, mount } = define({ setup: () => [] }).create()
+
+      app.config.performance = false
+      mount()
+      expect(window.performance.getEntries()).lengthOf(0)
+    })
+  })
+
+  test.todo('config.globalProperty', () => {
+    const { app } = define({
+      setup() {
+        return []
+      },
+    }).create()
+    try {
+      app.config.globalProperties.msg = 'hello world'
+    } catch (e) {}
+    expect(
+      `app.config.globalProperties is not supported in vapor mode components`,
+    ).toHaveBeenWarned()
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/apiExpose.spec.ts b/packages/runtime-vapor/__tests__/apiExpose.spec.ts
new file mode 100644
index 00000000000..03a7e757712
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/apiExpose.spec.ts
@@ -0,0 +1,110 @@
+import { ref, shallowRef } from '@vue/reactivity'
+import { type VaporComponentInstance, createComponent } from '../src/component'
+import { setRef } from '../src/apiTemplateRef'
+import { makeRender } from './_utils'
+import { currentInstance } from '@vue/runtime-dom'
+import { defineVaporComponent } from '../src/apiDefineComponent'
+
+const define = makeRender()
+
+describe('api: expose', () => {
+  test('via setup context + template ref', () => {
+    let i: any
+    const Child = defineVaporComponent({
+      setup(_, { expose }) {
+        expose({
+          foo: 1,
+          bar: ref(2),
+        })
+        return []
+      },
+    })
+    const childRef = ref()
+    define({
+      setup: () => {
+        const n0 = (i = createComponent(Child))
+        setRef(currentInstance as VaporComponentInstance, n0, childRef)
+        return n0
+      },
+    }).render()
+
+    expect(childRef.value).toBe(i.exposeProxy)
+    expect(childRef.value.foo).toBe(1)
+    expect(childRef.value.bar).toBe(2)
+    expect(childRef.value.baz).toBeUndefined()
+  })
+
+  test('via setup context + template ref (expose empty)', () => {
+    let childInstance: VaporComponentInstance | null = null
+    const Child = defineVaporComponent({
+      setup(_) {
+        childInstance = currentInstance as VaporComponentInstance
+        return []
+      },
+    })
+    const childRef = shallowRef()
+    define({
+      setup: () => {
+        const n0 = createComponent(Child)
+        setRef(currentInstance as VaporComponentInstance, n0, childRef)
+        return n0
+      },
+    }).render()
+
+    expect(childInstance!.exposed).toBeNull()
+    expect(childRef.value).toBe(childInstance!)
+  })
+
+  test('with mount', () => {
+    const { app, host } = define({
+      setup(_, { expose }) {
+        expose({
+          foo: 1,
+        })
+        return []
+      },
+    }).create()
+    const exposed = app.mount(host) as any
+    expect(exposed.foo).toBe(1)
+    expect(exposed.bar).toBe(undefined)
+  })
+
+  test('warning for ref', () => {
+    define({
+      setup(_, { expose }) {
+        expose(ref(1))
+        return []
+      },
+    }).render()
+
+    expect(
+      'expose() should be passed a plain object, received ref',
+    ).toHaveBeenWarned()
+  })
+
+  test('warning for array', () => {
+    define({
+      setup(_, { expose }) {
+        expose(['focus'])
+        return []
+      },
+    }).render()
+
+    expect(
+      'expose() should be passed a plain object, received array',
+    ).toHaveBeenWarned()
+  })
+
+  test('warning for function', () => {
+    define({
+      setup(_, { expose }) {
+        expose(() => null)
+        return []
+      },
+    }).render()
+
+    expect(
+      'expose() should be passed a plain object, received function',
+    ).toHaveBeenWarned()
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/apiInject.spec.ts b/packages/runtime-vapor/__tests__/apiInject.spec.ts
new file mode 100644
index 00000000000..ea185e14acc
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/apiInject.spec.ts
@@ -0,0 +1,395 @@
+import {
+  type InjectionKey,
+  type Ref,
+  hasInjectionContext,
+  inject,
+  nextTick,
+  provide,
+  reactive,
+  readonly,
+  ref,
+  toDisplayString,
+} from '@vue/runtime-dom'
+import {
+  createComponent,
+  createTextNode,
+  createVaporApp,
+  renderEffect,
+} from '../src'
+import { makeRender } from './_utils'
+import { setElementText } from '../src/dom/prop'
+
+const define = makeRender<any>()
+
+// reference: https://vue-composition-api-rfc.netlify.com/api.html#provide-inject
+describe('api: provide/inject', () => {
+  it('string keys', () => {
+    const Provider = define({
+      setup() {
+        provide('foo', 1)
+        return createComponent(Middle)
+      },
+    })
+
+    const Middle = {
+      render() {
+        return createComponent(Consumer)
+      },
+    }
+
+    const Consumer = {
+      setup() {
+        const foo = inject('foo')
+        return (() => {
+          const n0 = createTextNode()
+          setElementText(n0, foo)
+          return n0
+        })()
+      },
+    }
+
+    Provider.render()
+    expect(Provider.host.innerHTML).toBe('1')
+  })
+
+  it('symbol keys', () => {
+    // also verifies InjectionKey type sync
+    const key: InjectionKey<number> = Symbol()
+
+    const Provider = define({
+      setup() {
+        provide(key, 1)
+        return createComponent(Middle)
+      },
+    })
+
+    const Middle = {
+      render: () => createComponent(Consumer),
+    }
+
+    const Consumer = {
+      setup() {
+        const foo = inject(key)
+        return (() => {
+          const n0 = createTextNode()
+          setElementText(n0, foo)
+          return n0
+        })()
+      },
+    }
+
+    Provider.render()
+    expect(Provider.host.innerHTML).toBe('1')
+  })
+
+  it('default values', () => {
+    const Provider = define({
+      setup() {
+        provide('foo', 'foo')
+        return createComponent(Middle)
+      },
+    })
+
+    const Middle = {
+      render: () => createComponent(Consumer),
+    }
+
+    const Consumer = {
+      setup() {
+        // default value should be ignored if value is provided
+        const foo = inject('foo', 'fooDefault')
+        // default value should be used if value is not provided
+        const bar = inject('bar', 'bar')
+        return (() => {
+          const n0 = createTextNode()
+          setElementText(n0, foo + bar)
+          return n0
+        })()
+      },
+    }
+
+    Provider.render()
+    expect(Provider.host.innerHTML).toBe('foobar')
+  })
+
+  // NOTE: Options API is not supported
+  // it('bound to instance', () => {})
+
+  it('nested providers', () => {
+    const ProviderOne = define({
+      setup() {
+        provide('foo', 'foo')
+        provide('bar', 'bar')
+        return createComponent(ProviderTwo)
+      },
+    })
+
+    const ProviderTwo = {
+      setup() {
+        // override parent value
+        provide('foo', 'fooOverride')
+        provide('baz', 'baz')
+        return createComponent(Consumer)
+      },
+    }
+
+    const Consumer = {
+      setup() {
+        const foo = inject('foo')
+        const bar = inject('bar')
+        const baz = inject('baz')
+        return (() => {
+          const n0 = createTextNode()
+          setElementText(n0, [foo, bar, baz].join(','))
+          return n0
+        })()
+      },
+    }
+
+    ProviderOne.render()
+    expect(ProviderOne.host.innerHTML).toBe('fooOverride,bar,baz')
+  })
+
+  it('reactivity with refs', async () => {
+    const count = ref(1)
+
+    const Provider = define({
+      setup() {
+        provide('count', count)
+        return createComponent(Middle)
+      },
+    })
+
+    const Middle = {
+      render: () => createComponent(Consumer),
+    }
+
+    const Consumer = {
+      setup() {
+        const count = inject<Ref<number>>('count')!
+        return (() => {
+          const n0 = createTextNode()
+          renderEffect(() => {
+            setElementText(n0, count.value)
+          })
+          return n0
+        })()
+      },
+    }
+
+    Provider.render()
+    expect(Provider.host.innerHTML).toBe('1')
+
+    count.value++
+    await nextTick()
+    expect(Provider.host.innerHTML).toBe('2')
+  })
+
+  it('reactivity with readonly refs', async () => {
+    const count = ref(1)
+
+    const Provider = define({
+      setup() {
+        provide('count', readonly(count))
+        return createComponent(Middle)
+      },
+    })
+
+    const Middle = {
+      render: () => createComponent(Consumer),
+    }
+
+    const Consumer = {
+      setup() {
+        const count = inject<Ref<number>>('count')!
+        // should not work
+        count.value++
+        return (() => {
+          const n0 = createTextNode()
+          renderEffect(() => {
+            setElementText(n0, count.value)
+          })
+          return n0
+        })()
+      },
+    }
+
+    Provider.render()
+    expect(Provider.host.innerHTML).toBe('1')
+
+    expect(
+      `Set operation on key "value" failed: target is readonly`,
+    ).toHaveBeenWarned()
+
+    count.value++
+    await nextTick()
+    expect(Provider.host.innerHTML).toBe('2')
+  })
+
+  it('reactivity with objects', async () => {
+    const rootState = reactive({ count: 1 })
+
+    const Provider = define({
+      setup() {
+        provide('state', rootState)
+        return createComponent(Middle)
+      },
+    })
+
+    const Middle = {
+      render: () => createComponent(Consumer),
+    }
+
+    const Consumer = {
+      setup() {
+        const state = inject<typeof rootState>('state')!
+        return (() => {
+          const n0 = createTextNode()
+          renderEffect(() => {
+            setElementText(n0, state.count)
+          })
+          return n0
+        })()
+      },
+    }
+
+    Provider.render()
+    expect(Provider.host.innerHTML).toBe('1')
+
+    rootState.count++
+    await nextTick()
+    expect(Provider.host.innerHTML).toBe('2')
+  })
+
+  it('reactivity with readonly objects', async () => {
+    const rootState = reactive({ count: 1 })
+
+    const Provider = define({
+      setup() {
+        provide('state', readonly(rootState))
+        return createComponent(Middle)
+      },
+    })
+
+    const Middle = {
+      render: () => createComponent(Consumer),
+    }
+
+    const Consumer = {
+      setup() {
+        const state = inject<typeof rootState>('state')!
+        // should not work
+        state.count++
+        return (() => {
+          const n0 = createTextNode()
+          renderEffect(() => {
+            setElementText(n0, state.count)
+          })
+          return n0
+        })()
+      },
+    }
+
+    Provider.render()
+    expect(Provider.host.innerHTML).toBe('1')
+
+    expect(
+      `Set operation on key "count" failed: target is readonly`,
+    ).toHaveBeenWarned()
+
+    rootState.count++
+    await nextTick()
+    expect(Provider.host.innerHTML).toBe('2')
+  })
+
+  it('should warn unfound', () => {
+    const Provider = define({
+      setup() {
+        return createComponent(Middle)
+      },
+    })
+
+    const Middle = {
+      render: () => createComponent(Consumer),
+    }
+
+    const Consumer = {
+      setup() {
+        const foo = inject('foo')
+        expect(foo).toBeUndefined()
+        return (() => {
+          const n0 = createTextNode()
+          setElementText(n0, foo)
+          return n0
+        })()
+      },
+    }
+
+    Provider.render()
+    expect(Provider.host.innerHTML).toBe('')
+    expect(`injection "foo" not found.`).toHaveBeenWarned()
+  })
+
+  it('should not warn when default value is undefined', () => {
+    const Provider = define({
+      setup() {
+        return createComponent(Middle)
+      },
+    })
+
+    const Middle = {
+      render: () => createComponent(Consumer),
+    }
+
+    const Consumer = {
+      setup() {
+        const foo = inject('foo', undefined)
+        return (() => {
+          const n0 = createTextNode()
+          setElementText(n0, foo)
+          return n0
+        })()
+      },
+    }
+
+    Provider.render()
+    expect(`injection "foo" not found.`).not.toHaveBeenWarned()
+  })
+
+  // #2400
+  it('should not self-inject', () => {
+    const { host } = define({
+      setup() {
+        provide('foo', 'foo')
+        const injection = inject('foo', null)
+        return createTextNode(toDisplayString(injection))
+      },
+    }).render()
+    expect(host.innerHTML).toBe('')
+  })
+
+  describe('hasInjectionContext', () => {
+    it('should be false outside of setup', () => {
+      expect(hasInjectionContext()).toBe(false)
+    })
+
+    it('should be true within setup', () => {
+      expect.assertions(1)
+      const Comp = define({
+        setup() {
+          expect(hasInjectionContext()).toBe(true)
+          return []
+        },
+      })
+
+      Comp.render()
+    })
+
+    it('should be true within app.runWithContext()', () => {
+      expect.assertions(1)
+      createVaporApp({}).runWithContext(() => {
+        expect(hasInjectionContext()).toBe(true)
+      })
+    })
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/apiLifecycle.spec.ts b/packages/runtime-vapor/__tests__/apiLifecycle.spec.ts
new file mode 100644
index 00000000000..2ad7d2e8ff3
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/apiLifecycle.spec.ts
@@ -0,0 +1,617 @@
+import {
+  type DebuggerEvent,
+  type InjectionKey,
+  type Ref,
+  TrackOpTypes,
+  TriggerOpTypes,
+  currentInstance,
+  inject,
+  nextTick,
+  onBeforeMount,
+  onBeforeUnmount,
+  onBeforeUpdate,
+  onMounted,
+  onRenderTracked,
+  onRenderTriggered,
+  onUnmounted,
+  onUpdated,
+  provide,
+  reactive,
+  ref,
+} from '@vue/runtime-dom'
+import {
+  createComponent,
+  createFor,
+  createIf,
+  createTextNode,
+  insert,
+  renderEffect,
+  template,
+} from '../src'
+import { makeRender } from './_utils'
+import { ITERATE_KEY } from '@vue/reactivity'
+import { setElementText } from '../src/dom/prop'
+
+const define = makeRender<any>()
+
+describe('api: lifecycle hooks', () => {
+  it('onBeforeMount', () => {
+    const fn = vi.fn(() => {
+      expect(host.innerHTML).toBe(``)
+    })
+    const { render, host } = define({
+      setup() {
+        onBeforeMount(fn)
+        return []
+      },
+    })
+    render()
+    expect(fn).toHaveBeenCalledTimes(1)
+  })
+
+  it('onMounted', () => {
+    const fn = vi.fn(() => {
+      expect(host.innerHTML).toBe(``)
+    })
+    const { render, host } = define({
+      setup() {
+        onMounted(fn)
+        return []
+      },
+    })
+    render()
+    expect(fn).toHaveBeenCalledTimes(1)
+  })
+
+  it('onBeforeUpdate', async () => {
+    const count = ref(0)
+    const fn = vi.fn(() => {
+      expect(host.innerHTML).toBe('0')
+    })
+    const { render, host } = define({
+      setup() {
+        onBeforeUpdate(fn)
+        const n0 = createTextNode()
+        renderEffect(() => {
+          setElementText(n0, count.value)
+        })
+        return n0
+      },
+    })
+    render()
+    count.value++
+    await nextTick()
+    expect(fn).toHaveBeenCalledTimes(1)
+    expect(host.innerHTML).toBe('1')
+  })
+
+  it('state mutation in onBeforeUpdate', async () => {
+    const count = ref(0)
+    const fn = vi.fn(() => {
+      expect(host.innerHTML).toBe('0')
+      count.value++
+    })
+    const renderSpy = vi.fn()
+
+    const { render, host } = define({
+      setup() {
+        onBeforeUpdate(fn)
+        const n0 = createTextNode()
+        renderEffect(() => {
+          renderSpy()
+          setElementText(n0, count.value)
+        })
+        return n0
+      },
+    })
+    render()
+    expect(renderSpy).toHaveBeenCalledTimes(1)
+  })
+
+  it('onUpdated', async () => {
+    const count = ref(0)
+    const fn = vi.fn(() => {
+      expect(host.innerHTML).toBe('1')
+    })
+
+    const { render, host } = define({
+      setup() {
+        onUpdated(fn)
+
+        const n0 = createTextNode()
+        renderEffect(() => {
+          setElementText(n0, count.value)
+        })
+        return n0
+      },
+    })
+    render()
+
+    count.value++
+    await nextTick()
+    expect(fn).toHaveBeenCalledTimes(1)
+  })
+
+  it('onBeforeUnmount', async () => {
+    const toggle = ref(true)
+    const fn = vi.fn(() => {
+      expect(host.innerHTML).toBe('<div></div><!--if-->')
+    })
+    const { render, host } = define({
+      setup() {
+        const n0 = createIf(
+          () => toggle.value,
+          () => createComponent(Child),
+        )
+        return n0
+      },
+    })
+
+    const Child = {
+      setup() {
+        onBeforeUnmount(fn)
+
+        const t0 = template('<div></div>')
+        const n0 = t0()
+        return n0
+      },
+    }
+
+    render()
+
+    toggle.value = false
+    await nextTick()
+    expect(fn).toHaveBeenCalledTimes(1)
+    expect(host.innerHTML).toBe('<!--if-->')
+  })
+
+  it('onUnmounted', async () => {
+    const toggle = ref(true)
+    const fn = vi.fn(() => {
+      expect(host.innerHTML).toBe('<!--if-->')
+    })
+    const { render, host } = define({
+      setup() {
+        const n0 = createIf(
+          () => toggle.value,
+          () => createComponent(Child),
+        )
+        return n0
+      },
+    })
+
+    const Child = {
+      setup() {
+        onUnmounted(fn)
+
+        const t0 = template('<div></div>')
+        const n0 = t0()
+        return n0
+      },
+    }
+
+    render()
+
+    toggle.value = false
+    await nextTick()
+    expect(fn).toHaveBeenCalledTimes(1)
+    expect(host.innerHTML).toBe('<!--if-->')
+  })
+
+  it('onBeforeUnmount in onMounted', async () => {
+    const toggle = ref(true)
+    const fn = vi.fn(() => {
+      expect(host.innerHTML).toBe('<div></div><!--if-->')
+    })
+    const { render, host } = define({
+      setup() {
+        const n0 = createIf(
+          () => toggle.value,
+          () => createComponent(Child),
+        )
+        return n0
+      },
+    })
+
+    const Child = {
+      setup() {
+        onMounted(() => {
+          onBeforeUnmount(fn)
+        })
+
+        const t0 = template('<div></div>')
+        const n0 = t0()
+        return n0
+      },
+    }
+
+    render()
+
+    toggle.value = false
+    await nextTick()
+    expect(fn).toHaveBeenCalledTimes(1)
+    expect(host.innerHTML).toBe('<!--if-->')
+  })
+
+  it('lifecycle call order', async () => {
+    const count = ref(0)
+    const toggle = ref(true)
+    const calls: string[] = []
+
+    const { render } = define({
+      setup() {
+        onBeforeMount(() => calls.push('root onBeforeMount'))
+        onMounted(() => calls.push('root onMounted'))
+        onBeforeUpdate(() => calls.push('root onBeforeUpdate'))
+        onUpdated(() => calls.push('root onUpdated'))
+        onBeforeUnmount(() => calls.push('root onBeforeUnmount'))
+        onUnmounted(() => calls.push('root onUnmounted'))
+
+        const n0 = createIf(
+          () => toggle.value,
+          () => createComponent(Mid, { count: () => count.value }),
+        )
+        return n0
+      },
+    })
+
+    const Mid = {
+      props: ['count'],
+      setup(props: any) {
+        onBeforeMount(() => calls.push('mid onBeforeMount'))
+        onMounted(() => calls.push('mid onMounted'))
+        onBeforeUpdate(() => calls.push('mid onBeforeUpdate'))
+        onUpdated(() => calls.push('mid onUpdated'))
+        onBeforeUnmount(() => calls.push('mid onBeforeUnmount'))
+        onUnmounted(() => calls.push('mid onUnmounted'))
+
+        const n0 = createComponent(Child, { count: () => props.count })
+        return n0
+      },
+    }
+
+    const Child = {
+      props: ['count'],
+      setup(props: any) {
+        onBeforeMount(() => calls.push('child onBeforeMount'))
+        onMounted(() => calls.push('child onMounted'))
+        onBeforeUpdate(() => calls.push('child onBeforeUpdate'))
+        onUpdated(() => calls.push('child onUpdated'))
+        onBeforeUnmount(() => calls.push('child onBeforeUnmount'))
+        onUnmounted(() => calls.push('child onUnmounted'))
+
+        const t0 = template('<div></div>')
+        const n0 = t0()
+        renderEffect(() => setElementText(n0, props.count))
+        return n0
+      },
+    }
+
+    // mount
+    const ctx = render()
+    expect(calls).toEqual([
+      'root onBeforeMount',
+      'mid onBeforeMount',
+      'child onBeforeMount',
+      'child onMounted',
+      'mid onMounted',
+      'root onMounted',
+    ])
+
+    calls.length = 0
+
+    // update
+    count.value++
+    await nextTick()
+    // only child updated
+    expect(calls).toEqual(['child onBeforeUpdate', 'child onUpdated'])
+
+    calls.length = 0
+
+    // unmount
+    ctx.app.unmount()
+    await nextTick()
+    expect(calls).toEqual([
+      'root onBeforeUnmount',
+      'mid onBeforeUnmount',
+      'child onBeforeUnmount',
+      'child onUnmounted',
+      'mid onUnmounted',
+      'root onUnmounted',
+    ])
+  })
+
+  it('onRenderTracked', async () => {
+    const events: DebuggerEvent[] = []
+    const onTrack = vi.fn((e: DebuggerEvent) => {
+      events.push(e)
+    })
+    const obj = reactive({ foo: 1, bar: 2 })
+
+    const { render } = define({
+      setup() {
+        onRenderTracked(onTrack)
+
+        const n0 = createTextNode()
+        renderEffect(() => {
+          setElementText(n0, [obj.foo, 'bar' in obj, Object.keys(obj).join('')])
+        })
+        return n0
+      },
+    })
+
+    render()
+    expect(onTrack).toHaveBeenCalledTimes(3)
+    expect(events).toMatchObject([
+      {
+        target: obj,
+        type: TrackOpTypes.GET,
+        key: 'foo',
+      },
+      {
+        target: obj,
+        type: TrackOpTypes.HAS,
+        key: 'bar',
+      },
+      {
+        target: obj,
+        type: TrackOpTypes.ITERATE,
+        key: ITERATE_KEY,
+      },
+    ])
+  })
+
+  it('onRenderTrigger', async () => {
+    const events: DebuggerEvent[] = []
+    const onTrigger = vi.fn((e: DebuggerEvent) => {
+      events.push(e)
+    })
+    const obj = reactive<{
+      foo: number
+      bar?: number
+    }>({ foo: 1, bar: 2 })
+
+    const { render } = define({
+      setup() {
+        onRenderTriggered(onTrigger)
+
+        const n0 = createTextNode()
+        renderEffect(() => {
+          setElementText(n0, [obj.foo, 'bar' in obj, Object.keys(obj).join('')])
+        })
+        return n0
+      },
+    })
+
+    render()
+
+    obj.foo++
+    await nextTick()
+    expect(onTrigger).toHaveBeenCalledTimes(1)
+    expect(events[0]).toMatchObject({
+      type: TriggerOpTypes.SET,
+      key: 'foo',
+      oldValue: 1,
+      newValue: 2,
+    })
+
+    delete obj.bar
+    await nextTick()
+    expect(onTrigger).toHaveBeenCalledTimes(2)
+    expect(events[1]).toMatchObject({
+      type: TriggerOpTypes.DELETE,
+      key: 'bar',
+      oldValue: 2,
+    })
+    ;(obj as any).baz = 3
+    await nextTick()
+    expect(onTrigger).toHaveBeenCalledTimes(3)
+    expect(events[2]).toMatchObject({
+      type: TriggerOpTypes.ADD,
+      key: 'baz',
+      newValue: 3,
+    })
+  })
+
+  it('runs shared hook fn for each instance', async () => {
+    const fn = vi.fn()
+    const toggle = ref(true)
+    const { render } = define({
+      setup() {
+        return createIf(
+          () => toggle.value,
+          () => [createComponent(Child), createComponent(Child)],
+        )
+      },
+    })
+    const Child = {
+      setup() {
+        onBeforeMount(fn)
+        onBeforeUnmount(fn)
+        return template('<div></div>')()
+      },
+    }
+
+    render()
+    expect(fn).toHaveBeenCalledTimes(2)
+    toggle.value = false
+    await nextTick()
+    expect(fn).toHaveBeenCalledTimes(4)
+  })
+
+  // #136
+  it('should trigger updated hooks across components. (parent -> child)', async () => {
+    const handleUpdated = vi.fn()
+    const handleUpdatedChild = vi.fn()
+
+    const count = ref(0)
+
+    const { render, host } = define({
+      setup() {
+        onUpdated(() => handleUpdated())
+
+        const n0 = createTextNode()
+        renderEffect(() => setElementText(n0, count.value))
+        const n1 = createComponent(Child, { count: () => count.value })
+        return [n0, n1]
+      },
+    })
+
+    const Child = {
+      props: { count: Number },
+      setup() {
+        onUpdated(() => handleUpdatedChild())
+
+        const props = currentInstance!.props
+        const n2 = createTextNode()
+        renderEffect(() => setElementText(n2, props.count))
+        return n2
+      },
+    }
+
+    render()
+
+    expect(host.innerHTML).toBe('00')
+    expect(handleUpdated).toHaveBeenCalledTimes(0)
+    expect(handleUpdatedChild).toHaveBeenCalledTimes(0)
+
+    count.value++
+    await nextTick()
+    expect(host.innerHTML).toBe('11')
+    expect(handleUpdated).toHaveBeenCalledTimes(1)
+    expect(handleUpdatedChild).toHaveBeenCalledTimes(1)
+  })
+
+  // #136
+  it('should trigger updated hooks across components. (child -> parent)', async () => {
+    const handleUpdated = vi.fn()
+    const handleUpdatedChild = vi.fn()
+
+    const key: InjectionKey<Ref<number>> = Symbol()
+
+    const { render, host } = define({
+      setup() {
+        const count = ref(0)
+        provide(key, count)
+        onUpdated(() => handleUpdated())
+
+        const n0 = createTextNode()
+        renderEffect(() => setElementText(n0, count.value))
+        const n1 = createComponent(Child, { count: () => count.value })
+        return [n0, n1]
+      },
+    })
+
+    let update: any
+    const Child = {
+      props: { count: Number },
+      setup() {
+        onUpdated(() => handleUpdatedChild())
+        const count = inject(key)!
+        update = () => count.value++
+
+        const n2 = createTextNode()
+        renderEffect(() => setElementText(n2, count.value))
+        return n2
+      },
+    }
+
+    render()
+
+    expect(host.innerHTML).toBe('00')
+    expect(handleUpdated).toHaveBeenCalledTimes(0)
+    expect(handleUpdatedChild).toHaveBeenCalledTimes(0)
+
+    update()
+    await nextTick()
+    expect(host.innerHTML).toBe('11')
+    expect(handleUpdated).toHaveBeenCalledTimes(1)
+    expect(handleUpdatedChild).toHaveBeenCalledTimes(1)
+  })
+
+  test('unmount hooks when nested in if block', async () => {
+    const toggle = ref(true)
+    const fn = vi.fn(() => {
+      expect(host.innerHTML).toBe('<div><span></span></div><!--if-->')
+    })
+    const fn2 = vi.fn(() => {
+      expect(host.innerHTML).toBe('<!--if-->')
+    })
+    const { render, host } = define({
+      setup() {
+        const n0 = createIf(
+          () => toggle.value,
+          () => {
+            const n1 = document.createElement('div')
+            const n2 = createComponent(Child)
+            insert(n2, n1)
+            return n1
+          },
+        )
+        return n0
+      },
+    })
+
+    const Child = {
+      setup() {
+        onBeforeUnmount(fn)
+        onUnmounted(fn2)
+
+        const t0 = template('<span></span>')
+        const n0 = t0()
+        return n0
+      },
+    }
+
+    render()
+
+    toggle.value = false
+    await nextTick()
+    expect(fn).toHaveBeenCalledTimes(1)
+    expect(fn2).toHaveBeenCalledTimes(1)
+    expect(host.innerHTML).toBe('<!--if-->')
+  })
+
+  test('unmount hooks when nested in for blocks', async () => {
+    const list = ref([1])
+    const fn = vi.fn(() => {
+      expect(host.innerHTML).toBe('<div><span></span></div><!--for-->')
+    })
+    const fn2 = vi.fn(() => {
+      expect(host.innerHTML).toBe('<!--for-->')
+    })
+    const { render, host } = define({
+      setup() {
+        const n0 = createFor(
+          () => list.value,
+          () => {
+            const n1 = document.createElement('div')
+            const n2 = createComponent(Child)
+            insert(n2, n1)
+            return n1
+          },
+        )
+        return n0
+      },
+    })
+
+    const Child = {
+      setup() {
+        onBeforeUnmount(fn)
+        onUnmounted(fn2)
+
+        const t0 = template('<span></span>')
+        const n0 = t0()
+        return n0
+      },
+    }
+
+    render()
+
+    list.value.pop()
+    await nextTick()
+    expect(fn).toHaveBeenCalledTimes(1)
+    expect(fn2).toHaveBeenCalledTimes(1)
+    expect(host.innerHTML).toBe('<!--for-->')
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/apiSetupContext.spec.ts b/packages/runtime-vapor/__tests__/apiSetupContext.spec.ts
new file mode 100644
index 00000000000..5e0d5d98aa0
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/apiSetupContext.spec.ts
@@ -0,0 +1,218 @@
+import {
+  createComponent,
+  createSlot,
+  createTextNode,
+  defineVaporComponent,
+  delegate,
+  delegateEvents,
+  insert,
+  renderEffect,
+  setDynamicProps,
+  setText,
+  template,
+} from '../src'
+import { nextTick, reactive, ref, watchEffect } from '@vue/runtime-dom'
+import { makeRender } from './_utils'
+
+const define = makeRender()
+
+describe('api: setup context', () => {
+  it('should expose return values to template render context', () => {
+    const { html } = define({
+      setup() {
+        return {
+          ref: ref('foo'),
+          object: reactive({ msg: 'bar' }),
+          value: 'baz',
+        }
+      },
+      render(ctx) {
+        return createTextNode(`${ctx.ref} ${ctx.object.msg} ${ctx.value}`)
+      },
+    }).render()
+    expect(html()).toMatch(`foo bar baz`)
+  })
+
+  it('should support returning render function', () => {
+    const { html } = define({
+      setup() {
+        return createTextNode(`hello`)
+      },
+    }).render()
+    expect(html()).toMatch(`hello`)
+  })
+
+  it('props', async () => {
+    const count = ref(0)
+    let dummy
+
+    const Child = defineVaporComponent({
+      props: { count: Number },
+      setup(props) {
+        watchEffect(() => {
+          dummy = props.count
+        })
+        const n = createTextNode()
+        renderEffect(() => {
+          setText(n, props.count)
+        })
+        return n
+      },
+    })
+
+    const { html } = define({
+      render: () => createComponent(Child, { count: () => count.value }),
+    }).render()
+
+    expect(html()).toMatch(`0`)
+
+    count.value++
+    await nextTick()
+    expect(dummy).toBe(1)
+    expect(html()).toMatch(`1`)
+  })
+
+  it('context.attrs', async () => {
+    const toggle = ref(true)
+
+    const Child = defineVaporComponent({
+      inheritAttrs: false,
+      setup(_props, { attrs }) {
+        const el = document.createElement('div')
+        renderEffect(() => setDynamicProps(el, [attrs]))
+        return el
+      },
+    })
+
+    const { html } = define({
+      render: () =>
+        createComponent(Child, {
+          $: [() => (toggle.value ? { id: 'foo' } : { class: 'baz' })],
+        }),
+    }).render()
+
+    expect(html()).toMatch(`<div id="foo"></div>`)
+
+    toggle.value = false
+    await nextTick()
+    expect(html()).toMatch(`<div class="baz"></div>`)
+  })
+
+  // #4161
+  it('context.attrs in child component slots', async () => {
+    const toggle = ref(true)
+
+    const Wrapper = defineVaporComponent({
+      setup(_) {
+        const n0 = createSlot('default')
+        return n0
+      },
+    })
+
+    const Child = defineVaporComponent({
+      inheritAttrs: false,
+      setup(_: any, { attrs }: any) {
+        const n0 = createComponent(Wrapper, null, {
+          default: () => {
+            const n0 = template('<div>')() as HTMLDivElement
+            renderEffect(() => setDynamicProps(n0, [attrs]))
+            return n0
+          },
+        })
+        return n0
+      },
+    })
+
+    const { html } = define({
+      render: () =>
+        createComponent(Child, {
+          $: [() => (toggle.value ? { id: 'foo' } : { class: 'baz' })],
+        }),
+    }).render()
+
+    expect(html()).toMatch(`<div id="foo"></div>`)
+
+    // should update even though it's not reactive
+    toggle.value = false
+    await nextTick()
+    expect(html()).toMatch(`<div class="baz"></div>`)
+  })
+
+  it('context.slots', async () => {
+    const id = ref('foo')
+
+    const Child = defineVaporComponent({
+      render() {
+        return [createSlot('foo'), createSlot('bar')]
+      },
+    })
+
+    const { html } = define({
+      render() {
+        return createComponent(Child, null, {
+          $: [
+            () => ({
+              name: 'foo',
+              fn: () => {
+                const n = createTextNode()
+                renderEffect(() => setText(n, id.value))
+                return n
+              },
+            }),
+            () => ({
+              name: 'bar',
+              fn: () => createTextNode('bar'),
+            }),
+          ],
+        })
+      },
+    }).render()
+
+    expect(html()).toMatch(`foo<!--slot-->bar<!--slot-->`)
+
+    id.value = 'baz'
+    await nextTick()
+    expect(html()).toMatch(`baz<!--slot-->bar<!--slot-->`)
+  })
+
+  it('context.emit', async () => {
+    const count = ref(0)
+    const spy = vi.fn()
+
+    delegateEvents('click')
+
+    const Child = defineVaporComponent({
+      props: {
+        count: { type: Number, default: 1 },
+      },
+      setup(props, { emit }) {
+        const n0 = template('<div>')() as HTMLDivElement
+        delegate(n0, 'click', () => {
+          emit('inc', props.count + 1)
+        })
+        const n = createTextNode()
+        renderEffect(() => setText(n, props.count))
+        insert(n, n0)
+        return n0
+      },
+    })
+
+    const { host, html } = define({
+      render: () =>
+        createComponent(Child, {
+          count: () => count.value,
+          onInc: () => (newVal: number) => {
+            spy()
+            count.value = newVal
+          },
+        }),
+    }).render()
+
+    expect(html()).toMatch(`<div>0</div>`)
+    ;(host.children[0] as HTMLDivElement).click()
+
+    expect(spy).toHaveBeenCalled()
+    await nextTick()
+    expect(html()).toMatch(`<div>1</div>`)
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/apiSetupHelpers.spec.ts b/packages/runtime-vapor/__tests__/apiSetupHelpers.spec.ts
new file mode 100644
index 00000000000..21edc87f015
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/apiSetupHelpers.spec.ts
@@ -0,0 +1,118 @@
+import { createComponent, defineVaporComponent, template } from '../src'
+import {
+  currentInstance,
+  onMounted,
+  ref,
+  useAttrs,
+  useSlots,
+  withAsyncContext,
+} from '@vue/runtime-dom'
+import { makeRender } from './_utils'
+import type { VaporComponentInstance } from '../src/component'
+
+const define = makeRender<any>()
+
+describe('SFC <script setup> helpers', () => {
+  test('useSlots / useAttrs (no args)', () => {
+    let slots: VaporComponentInstance['slots'] | undefined
+    let attrs: VaporComponentInstance['attrs'] | undefined
+
+    const Comp = defineVaporComponent({
+      setup() {
+        // @ts-expect-error
+        slots = useSlots()
+        attrs = useAttrs()
+        return []
+      },
+    })
+    const count = ref(0)
+    const passedAttrs = { id: () => count.value }
+    const passedSlots = {
+      default: () => template('')(),
+      x: () => template('')(),
+    }
+
+    const { render } = define({
+      render: () => createComponent(Comp, passedAttrs, passedSlots),
+    })
+    render()
+
+    expect(typeof slots!.default).toBe('function')
+    expect(typeof slots!.x).toBe('function')
+    expect(attrs).toMatchObject({ id: 0 })
+
+    count.value++
+    expect(attrs).toMatchObject({ id: 1 })
+  })
+
+  test('useSlots / useAttrs (with args)', () => {
+    let slots: VaporComponentInstance['slots'] | undefined
+    let attrs: VaporComponentInstance['attrs'] | undefined
+    let ctx: VaporComponentInstance | undefined
+    const Comp = defineVaporComponent({
+      setup(_, _ctx) {
+        // @ts-expect-error
+        slots = useSlots()
+        attrs = useAttrs()
+        ctx = _ctx as VaporComponentInstance
+        return []
+      },
+    })
+    const { render } = define({ render: () => createComponent(Comp) })
+    render()
+    expect(slots).toBe(ctx!.slots)
+    expect(attrs).toBe(ctx!.attrs)
+  })
+
+  describe.todo('withAsyncContext', () => {
+    test('basic', async () => {
+      const spy = vi.fn()
+
+      let beforeInstance: VaporComponentInstance | null = null
+      let afterInstance: VaporComponentInstance | null = null
+      let resolve: (msg: string) => void
+
+      const Comp = defineVaporComponent({
+        async setup() {
+          let __temp: any, __restore: any
+
+          beforeInstance = currentInstance as VaporComponentInstance
+
+          const msg =
+            (([__temp, __restore] = withAsyncContext(
+              () =>
+                new Promise(r => {
+                  resolve = r
+                }),
+            )),
+            (__temp = await __temp),
+            __restore(),
+            __temp)
+
+          // register the lifecycle after an await statement
+          onMounted(spy)
+          afterInstance = currentInstance as VaporComponentInstance
+          return document.createTextNode(msg)
+        },
+      })
+
+      const { html } = define(Comp).render()
+
+      expect(spy).not.toHaveBeenCalled()
+      resolve!('hello')
+      // wait a macro task tick for all micro ticks to resolve
+      await new Promise(r => setTimeout(r))
+      // mount hook should have been called
+      expect(spy).toHaveBeenCalled()
+      // should retain same instance before/after the await call
+      expect(beforeInstance).toBe(afterInstance)
+      expect(html()).toBe('hello')
+    })
+
+    test.todo('error handling', async () => {})
+    test.todo('should not leak instance on multiple awaits', async () => {})
+    test.todo('should not leak on multiple awaits + error', async () => {})
+    test.todo('race conditions', async () => {})
+    test.todo('should teardown in-scope effects', async () => {})
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/apiWatch.spec.ts b/packages/runtime-vapor/__tests__/apiWatch.spec.ts
new file mode 100644
index 00000000000..290c509552c
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/apiWatch.spec.ts
@@ -0,0 +1,350 @@
+import {
+  type EffectScope,
+  ReactiveEffect,
+  currentInstance,
+  effectScope,
+  nextTick,
+  onMounted,
+  onUpdated,
+  ref,
+  watch,
+  watchEffect,
+} from '@vue/runtime-dom'
+import {
+  createComponent,
+  createIf,
+  createTemplateRefSetter,
+  defineVaporComponent,
+  renderEffect,
+  template,
+} from '../src'
+import { makeRender } from './_utils'
+import type { VaporComponentInstance } from '../src/component'
+import type { RefEl } from '../src/apiTemplateRef'
+
+const define = makeRender()
+
+// only need to port test cases related to in-component usage
+describe('apiWatch', () => {
+  // #7030
+  it(// need if support
+  'should not fire on child component unmount w/ flush: pre', async () => {
+    const visible = ref(true)
+    const cb = vi.fn()
+    const Parent = defineVaporComponent({
+      props: ['visible'],
+      setup() {
+        return createIf(
+          () => visible.value,
+          () => createComponent(Comp),
+        )
+      },
+    })
+    const Comp = {
+      setup() {
+        watch(visible, cb, { flush: 'pre' })
+        return []
+      },
+    }
+    define(Parent).render({
+      visible: () => visible.value,
+    })
+    expect(cb).not.toHaveBeenCalled()
+    visible.value = false
+    await nextTick()
+    expect(cb).not.toHaveBeenCalled()
+  })
+
+  // #7030
+  it('flush: pre watcher in child component should not fire before parent update', async () => {
+    const b = ref(0)
+    const calls: string[] = []
+
+    const Comp = {
+      setup() {
+        watch(
+          () => b.value,
+          val => {
+            calls.push('watcher child')
+          },
+          { flush: 'pre' },
+        )
+        renderEffect(() => {
+          b.value
+          calls.push('render child')
+        })
+        return []
+      },
+    }
+
+    const Parent = {
+      props: ['a'],
+      setup() {
+        watch(
+          () => b.value,
+          val => {
+            calls.push('watcher parent')
+          },
+          { flush: 'pre' },
+        )
+        renderEffect(() => {
+          b.value
+          calls.push('render parent')
+        })
+
+        return createComponent(Comp)
+      },
+    }
+
+    define(Parent).render({
+      a: () => b.value,
+    })
+
+    expect(calls).toEqual(['render parent', 'render child'])
+
+    b.value++
+    await nextTick()
+    expect(calls).toEqual([
+      'render parent',
+      'render child',
+      'watcher parent',
+      'render parent',
+      'watcher child',
+      'render child',
+    ])
+  })
+
+  // #1763
+  it('flush: pre watcher watching props should fire before child update', async () => {
+    const a = ref(0)
+    const b = ref(0)
+    const c = ref(0)
+    const calls: string[] = []
+
+    const Comp = {
+      props: ['a', 'b'],
+      setup(props: any) {
+        watch(
+          () => props.a + props.b,
+          () => {
+            calls.push('watcher 1')
+            c.value++
+          },
+          { flush: 'pre' },
+        )
+
+        // #1777 chained pre-watcher
+        watch(
+          c,
+          () => {
+            calls.push('watcher 2')
+          },
+          { flush: 'pre' },
+        )
+        renderEffect(() => {
+          c.value
+          calls.push('render')
+        })
+        return []
+      },
+    }
+
+    define(Comp).render({
+      a: () => a.value,
+      b: () => b.value,
+    })
+
+    expect(calls).toEqual(['render'])
+
+    // both props are updated
+    // should trigger pre-flush watcher first and only once
+    // then trigger child render
+    a.value++
+    b.value++
+    await nextTick()
+    expect(calls).toEqual(['render', 'watcher 1', 'watcher 2', 'render'])
+  })
+
+  // #5721
+  it('flush: pre triggered in component setup should be buffered and called before mounted', () => {
+    const count = ref(0)
+    const calls: string[] = []
+    const App = {
+      setup() {
+        watch(
+          count,
+          () => {
+            calls.push('watch ' + count.value)
+          },
+          { flush: 'pre' },
+        )
+        onMounted(() => {
+          calls.push('mounted')
+        })
+        // mutate multiple times
+        count.value++
+        count.value++
+        count.value++
+        return []
+      },
+    }
+    define(App).render()
+    expect(calls).toMatchObject(['watch 3', 'mounted'])
+  })
+
+  // #1852
+  it('flush: post watcher should fire after template refs updated', async () => {
+    const toggle = ref(false)
+    let dom: Element | null = null
+
+    const App = {
+      setup() {
+        const domRef = ref<Element | null>(null)
+
+        watch(
+          toggle,
+          () => {
+            dom = domRef.value
+          },
+          { flush: 'post' },
+        )
+
+        const setRef = createTemplateRefSetter()
+        return createIf(
+          () => toggle.value,
+          () => {
+            const n = template('<p>')()
+            setRef(n as RefEl, domRef)
+            return n
+          },
+        )
+      },
+    }
+
+    define(App).render()
+    expect(dom).toBe(null)
+
+    toggle.value = true
+    await nextTick()
+    expect(dom!.tagName).toBe('P')
+  })
+
+  test('should not leak `this.proxy` to setup()', () => {
+    const source = vi.fn()
+
+    const Comp = defineVaporComponent({
+      setup() {
+        watch(source, () => {})
+        return []
+      },
+    })
+
+    define(Comp).render()
+
+    // should not have any arguments
+    expect(source.mock.calls[0]).toMatchObject([])
+  })
+
+  // #2728
+  test('pre watcher callbacks should not track dependencies', async () => {
+    const a = ref(0)
+    const b = ref(0)
+    const updated = vi.fn()
+
+    const Comp = defineVaporComponent({
+      props: ['a'],
+      setup(props) {
+        onUpdated(updated)
+        watch(
+          () => props.a,
+          () => {
+            b.value
+          },
+        )
+        renderEffect(() => {
+          props.a
+        })
+        return []
+      },
+    })
+
+    define(Comp).render({
+      a: () => a.value,
+    })
+
+    a.value++
+    await nextTick()
+    expect(updated).toHaveBeenCalledTimes(1)
+
+    b.value++
+    await nextTick()
+    // should not track b as dependency of Child
+    expect(updated).toHaveBeenCalledTimes(1)
+  })
+
+  // #4158
+  test('watch should not register in owner component if created inside detached scope', () => {
+    let instance: VaporComponentInstance
+    const Comp = {
+      setup() {
+        instance = currentInstance as VaporComponentInstance
+        effectScope(true).run(() => {
+          watch(
+            () => 1,
+            () => {},
+          )
+        })
+        return []
+      },
+    }
+    define(Comp).render()
+    // should not record watcher in detached scope
+    // the 1 is the props validation effect
+    expect(getEffectsCount(instance!.scope)).toBe(1)
+  })
+
+  test('watchEffect should keep running if created in a detached scope', async () => {
+    const trigger = ref(0)
+    let countWE = 0
+    let countW = 0
+    const Comp = {
+      setup() {
+        effectScope(true).run(() => {
+          watchEffect(() => {
+            trigger.value
+            countWE++
+          })
+          watch(trigger, () => countW++)
+        })
+        return []
+      },
+    }
+    const { app } = define(Comp).render()
+    // only watchEffect as ran so far
+    expect(countWE).toBe(1)
+    expect(countW).toBe(0)
+    trigger.value++
+    await nextTick()
+    // both watchers run while component is mounted
+    expect(countWE).toBe(2)
+    expect(countW).toBe(1)
+
+    app.unmount()
+    await nextTick()
+    trigger.value++
+    await nextTick()
+    // both watchers run again event though component has been unmounted
+    expect(countWE).toBe(3)
+    expect(countW).toBe(2)
+  })
+})
+
+function getEffectsCount(scope: EffectScope): number {
+  let n = 0
+  for (let dep = scope.deps; dep !== undefined; dep = dep.nextDep) {
+    if (dep.dep instanceof ReactiveEffect) {
+      n++
+    }
+  }
+  return n
+}
diff --git a/packages/runtime-vapor/__tests__/block.spec.ts b/packages/runtime-vapor/__tests__/block.spec.ts
new file mode 100644
index 00000000000..9f76c7f0333
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/block.spec.ts
@@ -0,0 +1,64 @@
+import {
+  VaporFragment,
+  insert,
+  normalizeBlock,
+  prepend,
+  remove,
+} from '../src/block'
+
+const node1 = document.createTextNode('node1')
+const node2 = document.createTextNode('node2')
+const node3 = document.createTextNode('node3')
+const anchor = document.createTextNode('anchor')
+
+describe('block + node ops', () => {
+  test('normalizeBlock', () => {
+    expect(normalizeBlock([node1, node2, node3])).toEqual([node1, node2, node3])
+    expect(normalizeBlock([node1, [node2, [node3]]])).toEqual([
+      node1,
+      node2,
+      node3,
+    ])
+    const frag = new VaporFragment(node2)
+    frag.anchor = anchor
+    expect(normalizeBlock([node1, frag, [node3]])).toEqual([
+      node1,
+      node2,
+      anchor,
+      node3,
+    ])
+  })
+
+  test('insert', () => {
+    const container = document.createElement('div')
+    insert([anchor], container)
+    insert([node1], container)
+    insert([node2], container, anchor)
+    insert([], container, node3)
+    expect(Array.from(container.childNodes)).toEqual([node2, anchor, node1])
+
+    expect(() => insert(node3, container, node3)).toThrowError(
+      'The child can not be found in the parent.',
+    )
+  })
+
+  test('prepend', () => {
+    const container = document.createElement('div')
+    prepend(container, [node1], node2)
+    prepend(container, new VaporFragment(node3))
+    expect(Array.from(container.childNodes)).toEqual([node3, node1, node2])
+  })
+
+  test('remove', () => {
+    const container = document.createElement('div')
+    container.append(node1, node2, node3)
+    const frag = new VaporFragment(node3)
+    remove([node1], container)
+    remove(frag, container)
+    expect(Array.from(container.childNodes)).toEqual([node2])
+
+    expect(() => remove(anchor, container)).toThrowError(
+      'The node to be removed is not a child of this node.',
+    )
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/component.spec.ts b/packages/runtime-vapor/__tests__/component.spec.ts
new file mode 100644
index 00000000000..b96a932a2f3
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/component.spec.ts
@@ -0,0 +1,367 @@
+import {
+  type EffectScope,
+  ReactiveEffect,
+  type Ref,
+  inject,
+  nextTick,
+  onMounted,
+  onUpdated,
+  provide,
+  ref,
+  watch,
+  watchEffect,
+} from '@vue/runtime-dom'
+import {
+  createComponent,
+  createIf,
+  createTextNode,
+  renderEffect,
+  setInsertionState,
+  template,
+} from '../src'
+import { makeRender } from './_utils'
+import type { VaporComponentInstance } from '../src/component'
+import { setElementText, setText } from '../src/dom/prop'
+
+const define = makeRender()
+
+describe('component', () => {
+  it('should update parent(hoc) component host el when child component self update', async () => {
+    const value = ref(true)
+    let childNode1: Node | null = null
+    let childNode2: Node | null = null
+
+    const { component: Child } = define({
+      setup() {
+        return createIf(
+          () => value.value,
+          () => (childNode1 = template('<div></div>')()),
+          () => (childNode2 = template('<span></span>')()),
+        )
+      },
+    })
+
+    const { host } = define({
+      setup() {
+        return createComponent(Child)
+      },
+    }).render()
+
+    expect(host.innerHTML).toBe('<div></div><!--if-->')
+    expect(host.children[0]).toBe(childNode1)
+
+    value.value = false
+    await nextTick()
+    expect(host.innerHTML).toBe('<span></span><!--if-->')
+    expect(host.children[0]).toBe(childNode2)
+  })
+
+  it('should create a component with props', () => {
+    const { component: Comp } = define({
+      setup() {
+        return template('<div>', true)()
+      },
+    })
+
+    const { host } = define({
+      setup() {
+        return createComponent(Comp, { id: () => 'foo', class: () => 'bar' })
+      },
+    }).render()
+
+    expect(host.innerHTML).toBe('<div id="foo" class="bar"></div>')
+  })
+
+  it('should not update Component if only changed props are declared emit listeners', async () => {
+    const updatedSyp = vi.fn()
+    const { component: Comp } = define({
+      emits: ['foo'],
+      setup() {
+        onUpdated(updatedSyp)
+        return template('<div>', true)()
+      },
+    })
+
+    const toggle = ref(true)
+    const fn1 = () => {}
+    const fn2 = () => {}
+    define({
+      setup() {
+        const _on_foo = () => (toggle.value ? fn1() : fn2())
+        return createComponent(Comp, { onFoo: () => _on_foo })
+      },
+    }).render()
+    expect(updatedSyp).toHaveBeenCalledTimes(0)
+
+    toggle.value = false
+    await nextTick()
+    expect(updatedSyp).toHaveBeenCalledTimes(0)
+  })
+
+  it('component child synchronously updating parent state should trigger parent re-render', async () => {
+    const { component: Child } = define({
+      setup() {
+        const n = inject<Ref<number>>('foo')!
+        n.value++
+        const n0 = template('<div></div>')()
+        renderEffect(() => setElementText(n0, n.value))
+        return n0
+      },
+    })
+
+    const { host } = define({
+      setup() {
+        const n = ref(0)
+        provide('foo', n)
+        const n0 = template('<div></div>')()
+        renderEffect(() => setElementText(n0, n.value))
+        return [n0, createComponent(Child)]
+      },
+    }).render()
+
+    expect(host.innerHTML).toBe('<div>0</div><div>1</div>')
+    await nextTick()
+    expect(host.innerHTML).toBe('<div>1</div><div>1</div>')
+  })
+
+  it('component child updating parent state in pre-flush should trigger parent re-render', async () => {
+    const { component: Child } = define({
+      props: ['value'],
+      setup(props: any, { emit }) {
+        watch(
+          () => props.value,
+          val => emit('update', val),
+        )
+        const n0 = template('<div></div>')()
+        renderEffect(() => setElementText(n0, props.value))
+        return n0
+      },
+    })
+
+    const outer = ref(0)
+    const { host } = define({
+      setup() {
+        const inner = ref(0)
+        const n0 = template('<div></div>')()
+        renderEffect(() => setElementText(n0, inner.value))
+        const n1 = createComponent(Child, {
+          value: () => outer.value,
+          onUpdate: () => (val: number) => (inner.value = val),
+        })
+        return [n0, n1]
+      },
+    }).render()
+
+    expect(host.innerHTML).toBe('<div>0</div><div>0</div>')
+    outer.value++
+    await nextTick()
+    expect(host.innerHTML).toBe('<div>1</div><div>1</div>')
+  })
+
+  it('child only updates once when triggered in multiple ways', async () => {
+    const a = ref(0)
+    const calls: string[] = []
+
+    const { component: Child } = define({
+      props: ['count'],
+      setup(props: any) {
+        onUpdated(() => calls.push('update child'))
+        const n = createTextNode()
+        renderEffect(() => {
+          setText(n, `${props.count} - ${a.value}`)
+        })
+        return n
+      },
+    })
+
+    const { host } = define({
+      setup() {
+        return createComponent(Child, { count: () => a.value })
+      },
+    }).render()
+
+    expect(host.innerHTML).toBe('0 - 0')
+    expect(calls).toEqual([])
+
+    // This will trigger child rendering directly, as well as via a prop change
+    a.value++
+    await nextTick()
+    expect(host.innerHTML).toBe('1 - 1')
+    expect(calls).toEqual(['update child'])
+  })
+
+  it(`an earlier update doesn't lead to excessive subsequent updates`, async () => {
+    const globalCount = ref(0)
+    const parentCount = ref(0)
+    const calls: string[] = []
+
+    const { component: Child } = define({
+      props: ['count'],
+      setup(props: any) {
+        watch(
+          () => props.count,
+          () => {
+            calls.push('child watcher')
+            globalCount.value = props.count
+          },
+        )
+        onUpdated(() => calls.push('update child'))
+        return []
+      },
+    })
+
+    const { component: Parent } = define({
+      props: ['count'],
+      setup(props: any) {
+        onUpdated(() => calls.push('update parent'))
+        const n1 = createTextNode()
+        const n2 = createComponent(Child, { count: () => parentCount.value })
+        renderEffect(() => {
+          setText(n1, `${globalCount.value} - ${props.count}`)
+        })
+        return [n1, n2]
+      },
+    })
+
+    const { host } = define({
+      setup() {
+        onUpdated(() => calls.push('update root'))
+        return createComponent(Parent, { count: () => globalCount.value })
+      },
+    }).render()
+
+    expect(host.innerHTML).toBe(`0 - 0`)
+    expect(calls).toEqual([])
+
+    parentCount.value++
+    await nextTick()
+    expect(host.innerHTML).toBe(`1 - 1`)
+    expect(calls).toEqual(['child watcher', 'update parent'])
+  })
+
+  it('child component props update should not lead to double update', async () => {
+    const text = ref(0)
+    const spy = vi.fn()
+
+    const { component: Comp } = define({
+      props: ['text'],
+      setup(props: any) {
+        const n1 = template('<h1></h1>')()
+        renderEffect(() => {
+          spy()
+          setElementText(n1, props.text)
+        })
+        return n1
+      },
+    })
+
+    const { host } = define({
+      setup() {
+        return createComponent(Comp, { text: () => text.value })
+      },
+    }).render()
+
+    expect(host.innerHTML).toBe('<h1>0</h1>')
+    expect(spy).toHaveBeenCalledTimes(1)
+
+    text.value++
+    await nextTick()
+    expect(host.innerHTML).toBe('<h1>1</h1>')
+    expect(spy).toHaveBeenCalledTimes(2)
+  })
+
+  it('properly mount child component when using setInsertionState', async () => {
+    const spy = vi.fn()
+
+    const { component: Comp } = define({
+      setup() {
+        onMounted(spy)
+        return template('<h1>hi</h1>')()
+      },
+    })
+
+    const { host } = define({
+      setup() {
+        const n2 = template('<div></div>', true)()
+        setInsertionState(n2 as any)
+        createComponent(Comp)
+        return n2
+      },
+    }).render()
+
+    expect(host.innerHTML).toBe('<div><h1>hi</h1></div>')
+    expect(spy).toHaveBeenCalledTimes(1)
+  })
+
+  it('unmount component', async () => {
+    const { host, app, instance } = define(() => {
+      const count = ref(0)
+      const t0 = template('<div></div>')
+      const n0 = t0()
+      watchEffect(() => {
+        setElementText(n0, count.value)
+      })
+      renderEffect(() => {})
+      return n0
+    }).render()
+
+    const i = instance as VaporComponentInstance
+    // watchEffect + renderEffect + props validation effect
+    expect(getEffectsCount(i.scope)).toBe(3)
+    expect(host.innerHTML).toBe('<div>0</div>')
+
+    app.unmount()
+    expect(host.innerHTML).toBe('')
+    expect(getEffectsCount(i.scope)).toBe(0)
+  })
+
+  test('should mount component only with template in production mode', () => {
+    __DEV__ = false
+    const { component: Child } = define({
+      render() {
+        return template('<div> HI </div>', true)()
+      },
+    })
+
+    const { host } = define({
+      setup() {
+        return createComponent(Child, null, null, true)
+      },
+    }).render()
+
+    expect(host.innerHTML).toBe('<div> HI </div>')
+    __DEV__ = true
+  })
+
+  it('warn if functional vapor component not return a block', () => {
+    define(() => {
+      return () => {}
+    }).render()
+
+    expect(
+      'Functional vapor component must return a block directly',
+    ).toHaveBeenWarned()
+  })
+
+  it('warn if setup return a function and no render function', () => {
+    define({
+      setup() {
+        return () => []
+      },
+    }).render()
+
+    expect(
+      'Vapor component setup() returned non-block value, and has no render function',
+    ).toHaveBeenWarned()
+  })
+})
+
+function getEffectsCount(scope: EffectScope): number {
+  let n = 0
+  for (let dep = scope.deps; dep !== undefined; dep = dep.nextDep) {
+    if (dep.dep instanceof ReactiveEffect) {
+      n++
+    }
+  }
+  return n
+}
diff --git a/packages/runtime-vapor/__tests__/componentAttrs.spec.ts b/packages/runtime-vapor/__tests__/componentAttrs.spec.ts
new file mode 100644
index 00000000000..1f43ebba8c0
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/componentAttrs.spec.ts
@@ -0,0 +1,402 @@
+import { type Ref, nextTick, ref } from '@vue/runtime-dom'
+import {
+  createComponent,
+  createDynamicComponent,
+  createSlot,
+  defineVaporComponent,
+  renderEffect,
+  setClass,
+  setDynamicProps,
+  setProp,
+  setStyle,
+  template,
+} from '../src'
+import { makeRender } from './_utils'
+import { stringifyStyle } from '@vue/shared'
+import { setElementText } from '../src/dom/prop'
+
+const define = makeRender<any>()
+
+// TODO: port more tests from rendererAttrsFallthrough.spec.ts
+
+describe('attribute fallthrough', () => {
+  it('should allow attrs to fallthrough', async () => {
+    const t0 = template('<div>', true)
+    const { component: Child } = define({
+      props: ['foo'],
+      setup(props: any) {
+        const n0 = t0() as Element
+        renderEffect(() => setElementText(n0, props.foo))
+        return n0
+      },
+    })
+
+    const foo = ref(1)
+    const id = ref('a')
+    const { host } = define({
+      setup() {
+        return createComponent(
+          Child,
+          {
+            foo: () => foo.value,
+            id: () => id.value,
+          },
+          null,
+          true,
+        )
+      },
+    }).render()
+    expect(host.innerHTML).toBe('<div id="a">1</div>')
+
+    foo.value++
+    await nextTick()
+    expect(host.innerHTML).toBe('<div id="a">2</div>')
+
+    id.value = 'b'
+    await nextTick()
+    expect(host.innerHTML).toBe('<div id="b">2</div>')
+  })
+
+  it('should not fallthrough if explicitly pass inheritAttrs: false', async () => {
+    const t0 = template('<div>', true)
+    const { component: Child } = define({
+      props: ['foo'],
+      inheritAttrs: false,
+      setup(props: any) {
+        const n0 = t0() as Element
+        renderEffect(() => setElementText(n0, props.foo))
+        return n0
+      },
+    })
+
+    const foo = ref(1)
+    const id = ref('a')
+    const { host } = define({
+      setup() {
+        return createComponent(
+          Child,
+          {
+            foo: () => foo.value,
+            id: () => id.value,
+          },
+          null,
+          true,
+        )
+      },
+    }).render()
+    expect(host.innerHTML).toBe('<div>1</div>')
+
+    foo.value++
+    await nextTick()
+    expect(host.innerHTML).toBe('<div>2</div>')
+
+    id.value = 'b'
+    await nextTick()
+    expect(host.innerHTML).toBe('<div>2</div>')
+  })
+
+  it('should pass through attrs in nested single root components', async () => {
+    const t0 = template('<div>', true)
+    const { component: Grandson } = define({
+      props: ['custom-attr'],
+      setup(_: any, { attrs }: any) {
+        const n0 = t0() as Element
+        renderEffect(() => setElementText(n0, attrs.foo))
+        return n0
+      },
+    })
+
+    const { component: Child } = define({
+      setup() {
+        const n0 = createComponent(
+          Grandson,
+          {
+            'custom-attr': () => 'custom-attr',
+          },
+          null,
+          true,
+        )
+        return n0
+      },
+    })
+
+    const foo = ref(1)
+    const id = ref('a')
+    const { host } = define({
+      setup() {
+        return createComponent(
+          Child,
+          {
+            foo: () => foo.value,
+            id: () => id.value,
+          },
+          null,
+          true,
+        )
+      },
+    }).render()
+    expect(host.innerHTML).toBe('<div foo="1" id="a">1</div>')
+
+    foo.value++
+    await nextTick()
+    expect(host.innerHTML).toBe('<div foo="2" id="a">2</div>')
+
+    id.value = 'b'
+    await nextTick()
+    expect(host.innerHTML).toBe('<div foo="2" id="b">2</div>')
+  })
+
+  it('should merge classes', async () => {
+    const rootClass = ref('root')
+    const parentClass = ref('parent')
+    const childClass = ref('child')
+
+    const t0 = template('<div>', true /* root */)
+    const Child = defineVaporComponent({
+      setup() {
+        const n = t0() as Element
+        renderEffect(() => {
+          // binding on template root generates incremental class setter
+          setClass(n, childClass.value)
+        })
+        return n
+      },
+    })
+
+    const Parent = defineVaporComponent({
+      setup() {
+        return createComponent(
+          Child,
+          {
+            class: () => parentClass.value,
+          },
+          null,
+          true, // pass single root flag
+        )
+      },
+    })
+
+    const { host } = define({
+      setup() {
+        return createComponent(Parent, {
+          class: () => rootClass.value,
+        })
+      },
+    }).render()
+
+    const list = host.children[0].classList
+    // assert classes without being order-sensitive
+    function assertClasses(cls: string[]) {
+      expect(list.length).toBe(cls.length)
+      for (const c of cls) {
+        expect(list.contains(c)).toBe(true)
+      }
+    }
+
+    assertClasses(['root', 'parent', 'child'])
+
+    rootClass.value = 'root1'
+    await nextTick()
+    assertClasses(['root1', 'parent', 'child'])
+
+    parentClass.value = 'parent1'
+    await nextTick()
+    assertClasses(['root1', 'parent1', 'child'])
+
+    childClass.value = 'child1'
+    await nextTick()
+    assertClasses(['root1', 'parent1', 'child1'])
+  })
+
+  it('should merge styles', async () => {
+    const rootStyle: Ref<string | Record<string, string>> = ref('color:red')
+    const parentStyle: Ref<string | null> = ref('font-size:12px')
+    const childStyle = ref('font-weight:bold')
+
+    const t0 = template('<div>', true /* root */)
+    const Child = defineVaporComponent({
+      setup() {
+        const n = t0() as Element
+        renderEffect(() => {
+          // binding on template root generates incremental class setter
+          setStyle(n, childStyle.value)
+        })
+        return n
+      },
+    })
+
+    const Parent = defineVaporComponent({
+      setup() {
+        return createComponent(
+          Child,
+          {
+            style: () => parentStyle.value,
+          },
+          null,
+          true, // pass single root flag
+        )
+      },
+    })
+
+    const { host } = define({
+      setup() {
+        return createComponent(Parent, {
+          style: () => rootStyle.value,
+        })
+      },
+    }).render()
+
+    const el = host.children[0] as HTMLElement
+
+    function getCSS() {
+      return el.style.cssText.replace(/\s+/g, '')
+    }
+
+    function assertStyles() {
+      const css = getCSS()
+      expect(css).toContain(stringifyStyle(rootStyle.value))
+      if (parentStyle.value) {
+        expect(css).toContain(stringifyStyle(parentStyle.value))
+      }
+      expect(css).toContain(stringifyStyle(childStyle.value))
+    }
+
+    assertStyles()
+
+    rootStyle.value = { color: 'green' }
+    await nextTick()
+    assertStyles()
+    expect(getCSS()).not.toContain('color:red')
+
+    parentStyle.value = null
+    await nextTick()
+    assertStyles()
+    expect(getCSS()).not.toContain('font-size:12px')
+
+    childStyle.value = 'font-weight:500'
+    await nextTick()
+    assertStyles()
+    expect(getCSS()).not.toContain('font-size:bold')
+  })
+
+  it('should fallthrough attrs to dynamic component', async () => {
+    const Comp = defineVaporComponent({
+      setup() {
+        const n1 = createDynamicComponent(
+          () => 'button',
+          null,
+          {
+            default: () => {
+              const n0 = createSlot('default', null)
+              return n0
+            },
+          },
+          true,
+        )
+        return n1
+      },
+    })
+
+    const { html } = define({
+      setup() {
+        return createComponent(
+          Comp,
+          {
+            class: () => 'foo',
+          },
+          null,
+          true,
+        )
+      },
+    }).render()
+
+    expect(html()).toBe(
+      '<button class="foo"><!--slot--></button><!--dynamic-component-->',
+    )
+  })
+
+  it('parent value should take priority', async () => {
+    const parentVal = ref('parent')
+    const childVal = ref('child')
+
+    const t0 = template('<div>', true /* root */)
+    const Child = defineVaporComponent({
+      setup() {
+        const n = t0()
+        renderEffect(() => {
+          // prop bindings on template root generates extra `root: true` flag
+          setProp(n, 'id', childVal.value)
+          setProp(n, 'aria-x', childVal.value)
+          setDynamicProps(n, [{ 'aria-y': childVal.value }])
+        })
+        return n
+      },
+    })
+
+    const { host } = define({
+      setup() {
+        return createComponent(Child, {
+          id: () => parentVal.value,
+          'aria-x': () => parentVal.value,
+          'aria-y': () => parentVal.value,
+        })
+      },
+    }).render()
+
+    const el = host.children[0]
+    expect(el.id).toBe(parentVal.value)
+    expect(el.getAttribute('aria-x')).toBe(parentVal.value)
+    expect(el.getAttribute('aria-y')).toBe(parentVal.value)
+
+    childVal.value = 'child1'
+    await nextTick()
+    expect(el.id).toBe(parentVal.value)
+    expect(el.getAttribute('aria-x')).toBe(parentVal.value)
+    expect(el.getAttribute('aria-y')).toBe(parentVal.value)
+
+    parentVal.value = 'parent1'
+    await nextTick()
+    expect(el.id).toBe(parentVal.value)
+    expect(el.getAttribute('aria-x')).toBe(parentVal.value)
+    expect(el.getAttribute('aria-y')).toBe(parentVal.value)
+  })
+
+  it('empty string should not be passed to classList.add', async () => {
+    const t0 = template('<div>', true /* root */)
+    const Child = defineVaporComponent({
+      setup() {
+        const n = t0() as Element
+        renderEffect(() => {
+          setClass(n, {
+            foo: false,
+          })
+        })
+        return n
+      },
+    })
+
+    const Parent = defineVaporComponent({
+      setup() {
+        return createComponent(
+          Child,
+          {
+            class: () => ({
+              bar: false,
+            }),
+          },
+          null,
+          true,
+        )
+      },
+    })
+
+    const { host } = define({
+      setup() {
+        return createComponent(Parent)
+      },
+    }).render()
+
+    const el = host.children[0]
+    expect(el.classList.length).toBe(0)
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/componentEmits.spec.ts b/packages/runtime-vapor/__tests__/componentEmits.spec.ts
new file mode 100644
index 00000000000..8c8a56085ba
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/componentEmits.spec.ts
@@ -0,0 +1,427 @@
+// NOTE: this test cases are based on paclages/runtime-core/__tests__/componentEmits.spec.ts
+
+// Note: emits and listener fallthrough is tested in
+// ./rendererAttrsFallthrough.spec.ts.
+
+import {
+  isEmitListener,
+  nextTick,
+  onBeforeUnmount,
+  toHandlers,
+} from '@vue/runtime-dom'
+import { createComponent, defineVaporComponent } from '../src'
+import { makeRender } from './_utils'
+
+const define = makeRender()
+
+describe('component: emit', () => {
+  test('trigger handlers', () => {
+    const { render } = define({
+      setup(_, { emit }) {
+        emit('foo')
+        emit('bar')
+        emit('!baz')
+        return []
+      },
+    })
+    const onFoo = vi.fn()
+    const onBar = vi.fn()
+    const onBaz = vi.fn()
+    render({
+      onfoo: () => onFoo,
+      onBar: () => onBar,
+      ['on!baz']: () => onBaz,
+    })
+
+    expect(onFoo).not.toHaveBeenCalled()
+    expect(onBar).toHaveBeenCalled()
+    expect(onBaz).toHaveBeenCalled()
+  })
+
+  test('trigger dynamic emits', () => {
+    const { render } = define({
+      setup(_, { emit }) {
+        emit('foo')
+        emit('bar')
+        emit('!baz')
+        return []
+      },
+    })
+    const onFoo = vi.fn()
+    const onBar = vi.fn()
+    const onBaz = vi.fn()
+    render({
+      onfoo: () => onFoo,
+      onBar: () => onBar,
+      ['on!baz']: () => onBaz,
+    })
+
+    expect(onFoo).not.toHaveBeenCalled()
+    expect(onBar).toHaveBeenCalled()
+    expect(onBaz).toHaveBeenCalled()
+  })
+
+  test('trigger camelCase handler', () => {
+    const { render } = define({
+      setup(_, { emit }) {
+        emit('test-event')
+        return []
+      },
+    })
+
+    const fooSpy = vi.fn()
+    render({ onTestEvent: () => fooSpy })
+    expect(fooSpy).toHaveBeenCalled()
+  })
+
+  test('trigger kebab-case handler', () => {
+    const { render } = define({
+      setup(_, { emit }) {
+        emit('test-event')
+        return []
+      },
+    })
+
+    const fooSpy = vi.fn()
+    render({ ['onTest-event']: () => fooSpy })
+    expect(fooSpy).toHaveBeenCalledTimes(1)
+  })
+
+  // #3527
+  test('trigger mixed case handlers', () => {
+    const { render } = define({
+      setup(_, { emit }) {
+        emit('test-event')
+        emit('testEvent')
+        return []
+      },
+    })
+
+    const fooSpy = vi.fn()
+    const barSpy = vi.fn()
+    render(
+      toHandlers({
+        'test-event': () => fooSpy,
+        testEvent: () => barSpy,
+      }),
+    )
+    expect(fooSpy).toHaveBeenCalledTimes(1)
+    expect(barSpy).toHaveBeenCalledTimes(1)
+  })
+
+  // for v-model:foo-bar usage in DOM templates
+  test('trigger hyphenated events for update:xxx events', () => {
+    const { render } = define({
+      setup(_, { emit }) {
+        emit('update:fooProp')
+        emit('update:barProp')
+        return []
+      },
+    })
+
+    const fooSpy = vi.fn()
+    const barSpy = vi.fn()
+    render({
+      ['onUpdate:fooProp']: () => fooSpy,
+      ['onUpdate:bar-prop']: () => barSpy,
+    })
+
+    expect(fooSpy).toHaveBeenCalled()
+    expect(barSpy).toHaveBeenCalled()
+  })
+
+  test('should trigger array of listeners', async () => {
+    const { render } = define({
+      setup(_, { emit }) {
+        emit('foo', 1)
+        return []
+      },
+    })
+
+    const fn1 = vi.fn()
+    const fn2 = vi.fn()
+
+    render({ onFoo: () => [fn1, fn2] })
+    expect(fn1).toHaveBeenCalledTimes(1)
+    expect(fn1).toHaveBeenCalledWith(1)
+    expect(fn2).toHaveBeenCalledTimes(1)
+    expect(fn2).toHaveBeenCalledWith(1)
+  })
+
+  test('warning for undeclared event (array)', () => {
+    const { render } = define({
+      emits: ['foo'],
+
+      setup(_, { emit }) {
+        emit('bar')
+        return []
+      },
+    })
+    render()
+    expect(
+      `Component emitted event "bar" but it is neither declared`,
+    ).toHaveBeenWarned()
+  })
+
+  test('warning for undeclared event (object)', () => {
+    const { render } = define({
+      emits: {
+        foo: null,
+      },
+
+      setup(_, { emit }) {
+        emit('bar')
+        return []
+      },
+    })
+    render()
+    expect(
+      `Component emitted event "bar" but it is neither declared`,
+    ).toHaveBeenWarned()
+  })
+
+  test('should not warn if has equivalent onXXX prop', () => {
+    define({
+      props: ['onFoo'],
+      emits: [],
+
+      setup(_, { emit }) {
+        emit('foo')
+        return []
+      },
+    }).render()
+    expect(
+      `Component emitted event "foo" but it is neither declared`,
+    ).not.toHaveBeenWarned()
+  })
+
+  test('validator warning', () => {
+    define({
+      emits: {
+        foo: (arg: number) => arg > 0,
+      },
+      setup(_, { emit }) {
+        emit('foo', -1)
+        return []
+      },
+    }).render()
+    expect(`event validation failed for event "foo"`).toHaveBeenWarned()
+  })
+
+  test('.once', () => {
+    const { render } = define({
+      emits: {
+        foo: null,
+        bar: null,
+      },
+      setup(_, { emit }) {
+        emit('foo')
+        emit('foo')
+        emit('bar')
+        emit('bar')
+        return []
+      },
+    })
+    const fn = vi.fn()
+    const barFn = vi.fn()
+    render({
+      onFooOnce: () => fn,
+      onBarOnce: () => barFn,
+    })
+    expect(fn).toHaveBeenCalledTimes(1)
+    expect(barFn).toHaveBeenCalledTimes(1)
+  })
+
+  test('.once with normal listener of the same name', () => {
+    const { render } = define({
+      emits: {
+        foo: null,
+      },
+      setup(_, { emit }) {
+        emit('foo')
+        emit('foo')
+        return []
+      },
+    })
+    const onFoo = vi.fn()
+    const onFooOnce = vi.fn()
+    render({
+      onFoo: () => onFoo,
+      onFooOnce: () => onFooOnce,
+    })
+    expect(onFoo).toHaveBeenCalledTimes(2)
+    expect(onFooOnce).toHaveBeenCalledTimes(1)
+  })
+
+  test('.number modifier should work with v-model on component', () => {
+    const { render } = define({
+      setup(_, { emit }) {
+        emit('update:modelValue', '1')
+        emit('update:foo', '2')
+        return []
+      },
+    })
+    const fn1 = vi.fn()
+    const fn2 = vi.fn()
+    render({
+      modelValue: () => null,
+      modelModifiers: () => ({ number: true }),
+      ['onUpdate:modelValue']: () => fn1,
+      foo: () => null,
+      fooModifiers: () => ({ number: true }),
+      ['onUpdate:foo']: () => fn2,
+    })
+    expect(fn1).toHaveBeenCalledTimes(1)
+    expect(fn1).toHaveBeenCalledWith(1)
+    expect(fn2).toHaveBeenCalledTimes(1)
+    expect(fn2).toHaveBeenCalledWith(2)
+  })
+
+  test('.trim modifier should work with v-model on component', () => {
+    const { render } = define({
+      setup(_, { emit }) {
+        emit('update:modelValue', ' one ')
+        emit('update:foo', '  two  ')
+        return []
+      },
+    })
+    const fn1 = vi.fn()
+    const fn2 = vi.fn()
+    render({
+      modelValue() {
+        return null
+      },
+      modelModifiers() {
+        return { trim: true }
+      },
+      ['onUpdate:modelValue']() {
+        return fn1
+      },
+      foo() {
+        return null
+      },
+      fooModifiers() {
+        return { trim: true }
+      },
+      'onUpdate:foo'() {
+        return fn2
+      },
+    })
+    expect(fn1).toHaveBeenCalledTimes(1)
+    expect(fn1).toHaveBeenCalledWith('one')
+    expect(fn2).toHaveBeenCalledTimes(1)
+    expect(fn2).toHaveBeenCalledWith('two')
+  })
+
+  test('.trim and .number modifiers should work with v-model on component', () => {
+    const { render } = define({
+      setup(_, { emit }) {
+        emit('update:modelValue', '    +01.2    ')
+        emit('update:foo', '    1    ')
+        return []
+      },
+    })
+    const fn1 = vi.fn()
+    const fn2 = vi.fn()
+    render({
+      modelValue() {
+        return null
+      },
+      modelModifiers() {
+        return { trim: true, number: true }
+      },
+      ['onUpdate:modelValue']() {
+        return fn1
+      },
+      foo() {
+        return null
+      },
+      fooModifiers() {
+        return { trim: true, number: true }
+      },
+      ['onUpdate:foo']() {
+        return fn2
+      },
+    })
+    expect(fn1).toHaveBeenCalledTimes(1)
+    expect(fn1).toHaveBeenCalledWith(1.2)
+    expect(fn2).toHaveBeenCalledTimes(1)
+    expect(fn2).toHaveBeenCalledWith(1)
+  })
+
+  test('only trim string parameter when work with v-model on component', () => {
+    const { render } = define({
+      setup(_, { emit }) {
+        emit('update:modelValue', ' foo ', { bar: ' bar ' })
+        return []
+      },
+    })
+    const fn = vi.fn()
+    render({
+      modelValue() {
+        return null
+      },
+      modelModifiers() {
+        return { trim: true }
+      },
+      ['onUpdate:modelValue']() {
+        return fn
+      },
+    })
+    expect(fn).toHaveBeenCalledTimes(1)
+    expect(fn).toHaveBeenCalledWith('foo', { bar: ' bar ' })
+  })
+
+  test('isEmitListener', () => {
+    const options = {
+      get click() {
+        return null
+      },
+      get 'test-event'() {
+        return null
+      },
+      get fooBar() {
+        return null
+      },
+      get FooBaz() {
+        return null
+      },
+    }
+    expect(isEmitListener(options, 'onClick')).toBe(true)
+    expect(isEmitListener(options, 'onclick')).toBe(false)
+    expect(isEmitListener(options, 'onBlick')).toBe(false)
+    // .once listeners
+    expect(isEmitListener(options, 'onClickOnce')).toBe(true)
+    expect(isEmitListener(options, 'onclickOnce')).toBe(false)
+    // kebab-case option
+    expect(isEmitListener(options, 'onTestEvent')).toBe(true)
+    // camelCase option
+    expect(isEmitListener(options, 'onFooBar')).toBe(true)
+    // PascalCase option
+    expect(isEmitListener(options, 'onFooBaz')).toBe(true)
+  })
+
+  test('does not emit after unmount', async () => {
+    const fn = vi.fn()
+
+    const Foo = defineVaporComponent({
+      emits: ['closing'],
+      setup(_, { emit }) {
+        onBeforeUnmount(async () => {
+          await nextTick()
+          emit('closing', true)
+        })
+        return []
+      },
+    })
+
+    const { app } = define(() =>
+      createComponent(Foo, { onClosing: () => fn }),
+    ).render()
+
+    await nextTick()
+    app.unmount()
+    await nextTick()
+    expect(fn).not.toHaveBeenCalled()
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/componentProps.spec.ts b/packages/runtime-vapor/__tests__/componentProps.spec.ts
new file mode 100644
index 00000000000..55b492408c0
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/componentProps.spec.ts
@@ -0,0 +1,594 @@
+// NOTE: This test is implemented based on the case of `runtime-core/__test__/componentProps.spec.ts`.
+
+import {
+  // currentInstance,
+  inject,
+  nextTick,
+  provide,
+  ref,
+  toRefs,
+  watch,
+} from '@vue/runtime-dom'
+import {
+  createComponent,
+  defineVaporComponent,
+  renderEffect,
+  template,
+} from '../src'
+import { makeRender } from './_utils'
+import { setElementText } from '../src/dom/prop'
+
+const define = makeRender<any>()
+
+describe('component: props', () => {
+  test('stateful', () => {
+    let props: any
+    let attrs: any
+
+    const { render } = define({
+      props: ['fooBar', 'barBaz'],
+      setup(_props: any, { attrs: _attrs }: any) {
+        props = _props
+        attrs = _attrs
+        return []
+      },
+    })
+
+    render({ fooBar: () => 1, bar: () => 2 })
+    expect(props).toEqual({ fooBar: 1 })
+    expect(attrs).toEqual({ bar: 2 })
+
+    // test passing kebab-case and resolving to camelCase
+    render({ 'foo-bar': () => 2, bar: () => 3, baz: () => 4 })
+    expect(props).toEqual({ fooBar: 2 })
+    expect(attrs).toEqual({ bar: 3, baz: 4 })
+
+    // test updating kebab-case should not delete it (#955)
+    render({ 'foo-bar': () => 3, bar: () => 3, baz: () => 4, barBaz: () => 5 })
+    expect(props).toEqual({ fooBar: 3, barBaz: 5 })
+    expect(attrs).toEqual({ bar: 3, baz: 4 })
+
+    // remove the props with camelCase key (#1412)
+    render({ qux: () => 5 })
+    expect(props).toEqual({})
+    expect(attrs).toEqual({ qux: 5 })
+  })
+
+  test('stateful with setup', () => {
+    let props: any
+    let attrs: any
+
+    const { render } = define({
+      props: ['foo'],
+      setup(_props: any, { attrs: _attrs }: any) {
+        props = _props
+        attrs = _attrs
+        return []
+      },
+    })
+
+    render({ foo: () => 1, bar: () => 2 })
+    expect(props).toEqual({ foo: 1 })
+    expect(attrs).toEqual({ bar: 2 })
+
+    render({ foo: () => 2, bar: () => 3, baz: () => 4 })
+    expect(props).toEqual({ foo: 2 })
+    expect(attrs).toEqual({ bar: 3, baz: 4 })
+
+    render({ qux: () => 5 })
+    expect(props).toEqual({})
+    expect(attrs).toEqual({ qux: 5 })
+  })
+
+  test('functional with declaration', () => {
+    let props: any
+    let attrs: any
+
+    const { component: Comp, render } = define(
+      (_props: any, { attrs: _attrs }: any) => {
+        props = _props
+        attrs = _attrs
+        return []
+      },
+    )
+    Comp.props = ['foo']
+
+    render({ foo: () => 1, bar: () => 2 })
+    expect(props).toEqual({ foo: 1 })
+    expect(attrs).toEqual({ bar: 2 })
+
+    render({ foo: () => 2, bar: () => 3, baz: () => 4 })
+    expect(props).toEqual({ foo: 2 })
+    expect(attrs).toEqual({ bar: 3, baz: 4 })
+
+    render({ qux: () => 5 })
+    expect(props).toEqual({})
+    expect(attrs).toEqual({ qux: 5 })
+  })
+
+  test('functional without declaration', () => {
+    let props: any
+    let attrs: any
+
+    const { render } = define((_props: any, { attrs: _attrs }: any) => {
+      props = _props
+      attrs = _attrs
+      return []
+    })
+
+    render({ foo: () => 1 })
+    expect(props).toEqual({ foo: 1 })
+    expect(attrs).toEqual({ foo: 1 })
+    expect(props).toBe(attrs)
+
+    render({ bar: () => 2 })
+    expect(props).toEqual({ bar: 2 })
+    expect(attrs).toEqual({ bar: 2 })
+    expect(props).toBe(attrs)
+  })
+
+  test('functional defineVaporComponent without declaration', () => {
+    let props: any
+    let attrs: any
+
+    const { render } = define(
+      defineVaporComponent((_props: any, { attrs: _attrs }: any) => {
+        props = _props
+        attrs = _attrs
+        return []
+      }),
+    )
+
+    render({ foo: () => 1 })
+    expect(props).toEqual({})
+    expect(attrs).toEqual({ foo: 1 })
+
+    render({ bar: () => 2 })
+    expect(props).toEqual({})
+    expect(attrs).toEqual({ bar: 2 })
+  })
+
+  test('boolean casting', () => {
+    let props: any
+    const { render } = define({
+      props: {
+        foo: Boolean,
+        bar: Boolean,
+        baz: Boolean,
+        qux: Boolean,
+      },
+      setup(_props: any) {
+        props = _props
+        return []
+      },
+    })
+
+    render({
+      // absent should cast to false
+      bar: () => '', // empty string should cast to true
+      baz: () => 'baz', // same string should cast to true
+      qux: () => 'ok', // other values should be left in-tact (but raise warning)
+    })
+
+    expect(props.foo).toBe(false)
+    expect(props.bar).toBe(true)
+    expect(props.baz).toBe(true)
+    expect(props.qux).toBe('ok')
+    expect('type check failed for prop "qux"').toHaveBeenWarned()
+  })
+
+  test('default value', () => {
+    let props: any
+    const defaultFn = vi.fn(() => ({ a: 1 }))
+    const defaultBaz = vi.fn(() => ({ b: 1 }))
+
+    const { render } = define({
+      props: {
+        foo: {
+          default: 1,
+        },
+        bar: {
+          default: defaultFn,
+        },
+        baz: {
+          type: Function,
+          default: defaultBaz,
+        },
+      },
+      setup(_props: any) {
+        props = _props
+        return []
+      },
+    })
+
+    render({ foo: () => 2 })
+    expect(props.foo).toBe(2)
+    expect(props.bar).toEqual({ a: 1 })
+    expect(props.baz).toEqual(defaultBaz)
+    expect(defaultFn).toHaveBeenCalledTimes(1)
+    expect(defaultBaz).toHaveBeenCalledTimes(0)
+
+    // #999: updates should not cause default factory of unchanged prop to be
+    // called again
+    render({ foo: () => 3 })
+
+    expect(props.foo).toBe(3)
+    expect(props.bar).toEqual({ a: 1 })
+
+    render({ bar: () => ({ b: 2 }) })
+    expect(props.foo).toBe(1)
+    expect(props.bar).toEqual({ b: 2 })
+
+    render({
+      foo: () => 3,
+      bar: () => ({ b: 3 }),
+    })
+    expect(props.foo).toBe(3)
+    expect(props.bar).toEqual({ b: 3 })
+
+    render({ bar: () => ({ b: 4 }) })
+    expect(props.foo).toBe(1)
+    expect(props.bar).toEqual({ b: 4 })
+  })
+
+  test('using inject in default value factory', () => {
+    let props: any
+
+    const Child = defineVaporComponent({
+      props: {
+        test: {
+          default: () => inject('test', 'default'),
+        },
+      },
+      setup(_props) {
+        props = _props
+        return []
+      },
+    })
+
+    const { render } = define({
+      setup() {
+        provide('test', 'injected')
+        return createComponent(Child)
+      },
+    })
+
+    render()
+
+    expect(props.test).toBe('injected')
+  })
+
+  test('optimized props updates', async () => {
+    const t0 = template('<div>')
+    const { component: Child } = define({
+      props: ['foo'],
+      setup(props: any) {
+        const n0 = t0()
+        renderEffect(() => setElementText(n0, props.foo))
+        return n0
+      },
+    })
+
+    const foo = ref(1)
+    const id = ref('a')
+    const { host } = define({
+      setup() {
+        return { foo, id }
+      },
+      render(_ctx: Record<string, any>) {
+        return createComponent(
+          Child,
+          {
+            foo: () => _ctx.foo,
+            id: () => _ctx.id,
+          },
+          null,
+          true,
+        )
+      },
+    }).render()
+    expect(host.innerHTML).toBe('<div id="a">1</div>')
+
+    foo.value++
+    await nextTick()
+    expect(host.innerHTML).toBe('<div id="a">2</div>')
+
+    id.value = 'b'
+    await nextTick()
+    expect(host.innerHTML).toBe('<div id="b">2</div>')
+  })
+
+  describe('validator', () => {
+    test('validator should be called with two arguments', () => {
+      const mockFn = vi.fn((...args: any[]) => true)
+      const props = {
+        foo: () => 1,
+        bar: () => 2,
+      }
+
+      const t0 = template('<div/>')
+      define({
+        props: {
+          foo: {
+            type: Number,
+            validator: (value: any, props: any) => mockFn(value, props),
+          },
+          bar: {
+            type: Number,
+          },
+        },
+        setup() {
+          return t0()
+        },
+      }).render(props)
+
+      expect(mockFn).toHaveBeenCalledWith(1, { foo: 1, bar: 2 })
+    })
+
+    test('validator should not be able to mutate other props', async () => {
+      const mockFn = vi.fn((...args: any[]) => true)
+      define({
+        props: {
+          foo: {
+            type: Number,
+            validator: (value: any, props: any) => !!(props.bar = 1),
+          },
+          bar: {
+            type: Number,
+            validator: (value: any) => mockFn(value),
+          },
+        },
+        setup() {
+          const t0 = template('<div/>')
+          const n0 = t0()
+          return n0
+        },
+      }).render!({
+        foo() {
+          return 1
+        },
+        bar() {
+          return 2
+        },
+      })
+
+      expect(
+        `Set operation on key "bar" failed: target is readonly.`,
+      ).toHaveBeenWarnedLast()
+      expect(mockFn).toHaveBeenCalledWith(2)
+    })
+  })
+
+  test('warn props mutation', () => {
+    let props: any
+    const { render } = define({
+      props: ['foo'],
+      setup(_props: any) {
+        props = _props
+        return []
+      },
+    })
+    render({ foo: () => 1 })
+    expect(props.foo).toBe(1)
+
+    props.foo = 2
+    expect(`Attempt to mutate prop "foo" failed`).toHaveBeenWarned()
+  })
+
+  test('warn absent required props', () => {
+    define({
+      props: {
+        bool: { type: Boolean, required: true },
+        str: { type: String, required: true },
+        num: { type: Number, required: true },
+      },
+      setup() {
+        return []
+      },
+    }).render()
+    expect(`Missing required prop: "bool"`).toHaveBeenWarned()
+    expect(`Missing required prop: "str"`).toHaveBeenWarned()
+    expect(`Missing required prop: "num"`).toHaveBeenWarned()
+  })
+
+  // NOTE: type check is not supported in vapor
+  // test('warn on type mismatch', () => {})
+
+  // #3495
+  test('should not warn required props using kebab-case', async () => {
+    define({
+      props: {
+        fooBar: { type: String, required: true },
+      },
+      setup() {
+        return []
+      },
+    }).render({
+      ['foo-bar']: () => 'hello',
+    })
+    expect(`Missing required prop: "fooBar"`).not.toHaveBeenWarned()
+  })
+
+  test('props type support BigInt', () => {
+    const t0 = template('<div>')
+    const { host } = define({
+      props: {
+        foo: BigInt,
+      },
+      setup(props: any) {
+        const n0 = t0()
+        renderEffect(() => setElementText(n0, props.foo))
+        return n0
+      },
+    }).render({
+      foo: () =>
+        BigInt(BigInt(100000111)) + BigInt(2000000000) * BigInt(30000000),
+    })
+    expect(host.innerHTML).toBe('<div>60000000100000111</div>')
+  })
+
+  // #3474
+  test('should cache the value returned from the default factory to avoid unnecessary watcher trigger', async () => {
+    let count = 0
+
+    const { render, html } = define({
+      props: {
+        foo: {
+          type: Object,
+          default: () => ({ val: 1 }),
+        },
+        bar: Number,
+      },
+      setup(props: any) {
+        watch(
+          () => props.foo,
+          () => {
+            count++
+          },
+        )
+        const t0 = template('<h1></h1>')
+        const n0 = t0()
+        renderEffect(() => {
+          setElementText(n0, String(props.foo.val) + String(props.bar))
+        })
+        return n0
+      },
+    })
+
+    const foo = ref()
+    const bar = ref(0)
+    render({ foo: () => foo.value, bar: () => bar.value })
+    expect(html()).toBe(`<h1>10</h1>`)
+    expect(count).toBe(0)
+
+    bar.value++
+    await nextTick()
+    expect(html()).toBe(`<h1>11</h1>`)
+    expect(count).toBe(0)
+  })
+
+  // #3288
+  test('declared prop key should be present even if not passed', async () => {
+    let initialKeys: string[] = []
+    const changeSpy = vi.fn()
+    const passFoo = ref(false)
+
+    const Comp: any = {
+      props: {
+        foo: String,
+      },
+      setup(props: any) {
+        initialKeys = Object.keys(props)
+        const { foo } = toRefs(props)
+        watch(foo, changeSpy)
+        return []
+      },
+    }
+
+    define(() =>
+      createComponent(Comp, {
+        $: [() => (passFoo.value ? { foo: 'ok' } : {})],
+      }),
+    ).render()
+
+    expect(initialKeys).toMatchObject(['foo'])
+    passFoo.value = true
+    await nextTick()
+    expect(changeSpy).toHaveBeenCalledTimes(1)
+  })
+
+  test('should not warn invalid watch source when directly watching props', async () => {
+    const changeSpy = vi.fn()
+    const { render, html } = define({
+      props: {
+        foo: {
+          type: String,
+        },
+      },
+      setup(props: any) {
+        watch(props, changeSpy)
+        const t0 = template('<h1></h1>')
+        const n0 = t0()
+        renderEffect(() => {
+          setElementText(n0, String(props.foo))
+        })
+        return n0
+      },
+    })
+
+    const foo = ref('foo')
+    render({ foo: () => foo.value })
+    expect(html()).toBe(`<h1>foo</h1>`)
+    expect('Invalid watch source').not.toHaveBeenWarned()
+
+    foo.value = 'bar'
+    await nextTick()
+    expect(html()).toBe(`<h1>bar</h1>`)
+    expect(changeSpy).toHaveBeenCalledTimes(1)
+  })
+
+  test('support null in required + multiple-type declarations', () => {
+    const { render } = define({
+      props: {
+        foo: { type: [Function, null], required: true },
+      },
+      setup() {
+        return []
+      },
+    })
+
+    expect(() => {
+      render({ foo: () => () => {} })
+    }).not.toThrow()
+
+    expect(() => {
+      render({ foo: () => null })
+    }).not.toThrow()
+  })
+
+  // #5016
+  test('handling attr with undefined value', () => {
+    const { render, host } = define({
+      inheritAttrs: false,
+      setup(_: any, { attrs }: any) {
+        const t0 = template('<div></div>')
+        const n0 = t0()
+        renderEffect(() =>
+          setElementText(n0, JSON.stringify(attrs) + Object.keys(attrs)),
+        )
+        return n0
+      },
+    })
+
+    const attrs: any = { foo: () => undefined }
+    render(attrs)
+
+    expect(host.innerHTML).toBe(
+      `<div>${JSON.stringify(attrs) + Object.keys(attrs)}</div>`,
+    )
+  })
+
+  // #6915
+  test('should not mutate original props long-form definition object', () => {
+    const props = {
+      msg: {
+        type: String,
+      },
+    }
+    define({ props, setup: () => [] }).render({ msg: () => 'test' })
+
+    expect(Object.keys(props.msg).length).toBe(1)
+  })
+
+  test('should warn against reserved prop names', () => {
+    const { render } = define({
+      props: {
+        $foo: String,
+      },
+      setup: () => [],
+    })
+
+    render({ msg: () => 'test' })
+    expect(`Invalid prop name: "$foo"`).toHaveBeenWarned()
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts
new file mode 100644
index 00000000000..58076fff9ee
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts
@@ -0,0 +1,506 @@
+// NOTE: This test is implemented based on the case of `runtime-core/__test__/componentSlots.spec.ts`.
+
+import {
+  createComponent,
+  createForSlots,
+  createIf,
+  createSlot,
+  createVaporApp,
+  defineVaporComponent,
+  insert,
+  prepend,
+  renderEffect,
+  template,
+} from '../src'
+import { currentInstance, nextTick, ref } from '@vue/runtime-dom'
+import { makeRender } from './_utils'
+import type { DynamicSlot } from '../src/componentSlots'
+import { setElementText } from '../src/dom/prop'
+
+const define = makeRender<any>()
+
+function renderWithSlots(slots: any): any {
+  let instance: any
+  const Comp = defineVaporComponent({
+    setup() {
+      const t0 = template('<div></div>')
+      const n0 = t0()
+      instance = currentInstance
+      return n0
+    },
+  })
+
+  const { render } = define({
+    render() {
+      return createComponent(Comp, {}, slots)
+    },
+  })
+
+  render()
+  return instance
+}
+
+describe('component: slots', () => {
+  test('initSlots: instance.slots should be set correctly', () => {
+    const { slots } = renderWithSlots({
+      default: () => template('<span></span>')(),
+    })
+
+    expect(slots.default()).toMatchObject(document.createElement('span'))
+  })
+
+  test('updateSlots: instance.slots should be updated correctly', async () => {
+    const flag1 = ref(true)
+
+    let instance: any
+    const Child = () => {
+      instance = currentInstance
+      return template('child')()
+    }
+
+    const { render } = define({
+      render() {
+        return createComponent(
+          Child,
+          {},
+          {
+            $: [
+              () =>
+                flag1.value
+                  ? { name: 'one', fn: () => template('<span></span>')() }
+                  : { name: 'two', fn: () => template('<div></div>')() },
+            ],
+          },
+        )
+      },
+    })
+
+    render()
+
+    expect(instance.slots).toHaveProperty('one')
+    expect(instance.slots).not.toHaveProperty('two')
+
+    flag1.value = false
+    await nextTick()
+
+    expect(instance.slots).not.toHaveProperty('one')
+    expect(instance.slots).toHaveProperty('two')
+  })
+
+  test('should work with createFlorSlots', async () => {
+    const loop = ref([1, 2, 3])
+
+    let instance: any
+    const Child = () => {
+      instance = currentInstance
+      return template('child')()
+    }
+
+    const { render } = define({
+      setup() {
+        return createComponent(Child, null, {
+          $: [
+            () =>
+              createForSlots(loop.value, (item, i) => ({
+                name: item,
+                fn: () => template(item + i)(),
+              })),
+          ],
+        })
+      },
+    })
+    render()
+
+    expect(instance.slots).toHaveProperty('1')
+    expect(instance.slots).toHaveProperty('2')
+    expect(instance.slots).toHaveProperty('3')
+    loop.value.push(4)
+    await nextTick()
+    expect(instance.slots).toHaveProperty('4')
+    loop.value.shift()
+    await nextTick()
+    expect(instance.slots).not.toHaveProperty('1')
+  })
+
+  // passes but no warning for slot invocation in vapor currently
+  test.todo('should not warn when mounting another app in setup', () => {
+    const Comp = defineVaporComponent({
+      setup(_, { slots }) {
+        return slots.default!()
+      },
+    })
+    const mountComp = () => {
+      createVaporApp({
+        render() {
+          return createComponent(
+            Comp,
+            {},
+            { default: () => template('msg')() },
+          )!
+        },
+      })
+    }
+    const App = {
+      setup() {
+        mountComp()
+        return []
+      },
+    }
+    createVaporApp(App).mount(document.createElement('div'))
+    expect(
+      'Slot "default" invoked outside of the render function',
+    ).not.toHaveBeenWarned()
+  })
+
+  describe('createSlot', () => {
+    test('slot should be rendered correctly', () => {
+      const Comp = defineVaporComponent(() => {
+        const n0 = template('<div>')()
+        insert(createSlot('header'), n0 as any as ParentNode)
+        return n0
+      })
+
+      const { host } = define(() => {
+        return createComponent(Comp, null, {
+          header: () => template('header')(),
+        })
+      }).render()
+
+      expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
+    })
+
+    test('slot should be rendered correctly with slot props', async () => {
+      const src = ref('header')
+
+      const Comp = defineVaporComponent(() => {
+        const n0 = template('<div></div>')()
+        insert(
+          createSlot('header', { title: () => src.value }),
+          n0 as any as ParentNode,
+        )
+        return n0
+      })
+
+      const { host } = define(() => {
+        return createComponent(Comp, null, {
+          header: props => {
+            const el = template('<h1></h1>')()
+            renderEffect(() => {
+              setElementText(el, props.title)
+            })
+            return el
+          },
+        })
+      }).render()
+
+      expect(host.innerHTML).toBe('<div><h1>header</h1><!--slot--></div>')
+
+      src.value = 'footer'
+      await nextTick()
+      expect(host.innerHTML).toBe('<div><h1>footer</h1><!--slot--></div>')
+    })
+
+    test('dynamic slot props', async () => {
+      let props: any
+
+      const bindObj = ref<Record<string, any>>({ foo: 1, baz: 'qux' })
+      const Comp = defineVaporComponent(() =>
+        createSlot('default', { $: [() => bindObj.value] }),
+      )
+      define(() =>
+        createComponent(Comp, null, {
+          default: _props => ((props = _props), []),
+        }),
+      ).render()
+
+      expect(props).toEqual({ foo: 1, baz: 'qux' })
+
+      bindObj.value.foo = 2
+      await nextTick()
+      expect(props).toEqual({ foo: 2, baz: 'qux' })
+
+      delete bindObj.value.baz
+      await nextTick()
+      expect(props).toEqual({ foo: 2 })
+    })
+
+    test('dynamic slot props with static slot props', async () => {
+      let props: any
+
+      const foo = ref(0)
+      const bindObj = ref<Record<string, any>>({ foo: 100, baz: 'qux' })
+      const Comp = defineVaporComponent(() =>
+        createSlot('default', {
+          foo: () => foo.value,
+          $: [() => bindObj.value],
+        }),
+      )
+      define(() =>
+        createComponent(Comp, null, {
+          default: _props => ((props = _props), []),
+        }),
+      ).render()
+
+      expect(props).toEqual({ foo: 100, baz: 'qux' })
+
+      foo.value = 2
+      await nextTick()
+      expect(props).toEqual({ foo: 100, baz: 'qux' })
+
+      delete bindObj.value.foo
+      await nextTick()
+      expect(props).toEqual({ foo: 2, baz: 'qux' })
+    })
+
+    test('dynamic slot should be rendered correctly with slot props', async () => {
+      const val = ref('header')
+
+      const Comp = defineVaporComponent(() => {
+        const n0 = template('<div></div>')()
+        prepend(
+          n0 as any as ParentNode,
+          createSlot('header', { title: () => val.value }),
+        )
+        return n0
+      })
+
+      const { host } = define(() => {
+        // dynamic slot
+        return createComponent(Comp, null, {
+          $: [
+            () => ({
+              name: 'header',
+              fn: (props: any) => {
+                const el = template('<h1></h1>')()
+                renderEffect(() => {
+                  setElementText(el, props.title)
+                })
+                return el
+              },
+            }),
+          ],
+        })
+      }).render()
+
+      expect(host.innerHTML).toBe('<div><h1>header</h1><!--slot--></div>')
+
+      val.value = 'footer'
+      await nextTick()
+      expect(host.innerHTML).toBe('<div><h1>footer</h1><!--slot--></div>')
+    })
+
+    test('dynamic slot outlet should be render correctly with slot props', async () => {
+      const val = ref('header')
+
+      const Comp = defineVaporComponent(() => {
+        const n0 = template('<div></div>')()
+        prepend(
+          n0 as any as ParentNode,
+          createSlot(
+            () => val.value, // dynamic slot outlet name
+          ),
+        )
+        return n0
+      })
+
+      const { host } = define(() => {
+        return createComponent(Comp, null, {
+          header: () => template('header')(),
+          footer: () => template('footer')(),
+        })
+      }).render()
+
+      expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
+
+      val.value = 'footer'
+      await nextTick()
+      expect(host.innerHTML).toBe('<div>footer<!--slot--></div>')
+    })
+
+    test('fallback should be render correctly', () => {
+      const Comp = defineVaporComponent(() => {
+        const n0 = template('<div></div>')()
+        insert(
+          createSlot('header', undefined, () => template('fallback')()),
+          n0 as any as ParentNode,
+        )
+        return n0
+      })
+
+      const { host } = define(() => {
+        return createComponent(Comp, {}, {})
+      }).render()
+
+      expect(host.innerHTML).toBe('<div>fallback<!--slot--></div>')
+    })
+
+    test('dynamic slot should be updated correctly', async () => {
+      const flag1 = ref(true)
+
+      const Child = defineVaporComponent(() => {
+        const temp0 = template('<p></p>')
+        const el0 = temp0()
+        const el1 = temp0()
+        const slot1 = createSlot('one', null, () => template('one fallback')())
+        const slot2 = createSlot('two', null, () => template('two fallback')())
+        insert(slot1, el0 as any as ParentNode)
+        insert(slot2, el1 as any as ParentNode)
+        return [el0, el1]
+      })
+
+      const { host } = define(() => {
+        return createComponent(Child, null, {
+          $: [
+            () =>
+              flag1.value
+                ? { name: 'one', fn: () => template('one content')() }
+                : { name: 'two', fn: () => template('two content')() },
+          ],
+        })
+      }).render()
+
+      expect(host.innerHTML).toBe(
+        '<p>one content<!--slot--></p><p>two fallback<!--slot--></p>',
+      )
+
+      flag1.value = false
+      await nextTick()
+
+      expect(host.innerHTML).toBe(
+        '<p>one fallback<!--slot--></p><p>two content<!--slot--></p>',
+      )
+
+      flag1.value = true
+      await nextTick()
+
+      expect(host.innerHTML).toBe(
+        '<p>one content<!--slot--></p><p>two fallback<!--slot--></p>',
+      )
+    })
+
+    test('dynamic slot outlet should be updated correctly', async () => {
+      const slotOutletName = ref('one')
+
+      const Child = defineVaporComponent(() => {
+        const temp0 = template('<p>')
+        const el0 = temp0()
+        const slot1 = createSlot(
+          () => slotOutletName.value,
+          undefined,
+          () => template('fallback')(),
+        )
+        insert(slot1, el0 as any as ParentNode)
+        return el0
+      })
+
+      const { host } = define(() => {
+        return createComponent(
+          Child,
+          {},
+          {
+            one: () => template('one content')(),
+            two: () => template('two content')(),
+          },
+        )
+      }).render()
+
+      expect(host.innerHTML).toBe('<p>one content<!--slot--></p>')
+
+      slotOutletName.value = 'two'
+      await nextTick()
+
+      expect(host.innerHTML).toBe('<p>two content<!--slot--></p>')
+
+      slotOutletName.value = 'none'
+      await nextTick()
+
+      expect(host.innerHTML).toBe('<p>fallback<!--slot--></p>')
+    })
+
+    test('non-exist slot', async () => {
+      const Child = defineVaporComponent(() => {
+        const el0 = template('<p>')()
+        const slot = createSlot('not-exist', undefined)
+        insert(slot, el0 as any as ParentNode)
+        return el0
+      })
+
+      const { host } = define(() => {
+        return createComponent(Child)
+      }).render()
+
+      expect(host.innerHTML).toBe('<p><!--slot--></p>')
+    })
+
+    test('use fallback when inner content changes', async () => {
+      const Child = {
+        setup() {
+          return createSlot('default', null, () =>
+            document.createTextNode('fallback'),
+          )
+        },
+      }
+
+      const toggle = ref(true)
+
+      const { html } = define({
+        setup() {
+          return createComponent(Child, null, {
+            default: () => {
+              return createIf(
+                () => toggle.value,
+                () => {
+                  return document.createTextNode('content')
+                },
+              )
+            },
+          })
+        },
+      }).render()
+
+      expect(html()).toBe('content<!--if--><!--slot-->')
+
+      toggle.value = false
+      await nextTick()
+      expect(html()).toBe('fallback<!--if--><!--slot-->')
+
+      toggle.value = true
+      await nextTick()
+      expect(html()).toBe('content<!--if--><!--slot-->')
+    })
+
+    test('dynamic slot work with v-if', async () => {
+      const val = ref('header')
+      const toggle = ref(false)
+
+      const Comp = defineVaporComponent(() => {
+        const n0 = template('<div></div>')()
+        prepend(n0 as any as ParentNode, createSlot('header', null))
+        return n0
+      })
+
+      const { host } = define(() => {
+        // dynamic slot
+        return createComponent(Comp, null, {
+          $: [
+            () =>
+              (toggle.value
+                ? {
+                    name: val.value,
+                    fn: () => {
+                      return template('<h1></h1>')()
+                    },
+                  }
+                : void 0) as DynamicSlot,
+          ],
+        })
+      }).render()
+
+      expect(host.innerHTML).toBe('<div><!--slot--></div>')
+
+      toggle.value = true
+      await nextTick()
+      expect(host.innerHTML).toBe('<div><h1></h1><!--slot--></div>')
+    })
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/directives/customDirective.spec.ts b/packages/runtime-vapor/__tests__/directives/customDirective.spec.ts
new file mode 100644
index 00000000000..1fd5f95eee0
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/directives/customDirective.spec.ts
@@ -0,0 +1,39 @@
+import { effectScope, ref } from '@vue/reactivity'
+import { type VaporDirective, withVaporDirectives } from '../../src'
+import { nextTick, watchEffect } from '@vue/runtime-dom'
+
+describe('custom directive', () => {
+  it('should work', async () => {
+    const teardown = vi.fn()
+    const dir: VaporDirective = vi.fn((el, source) => {
+      watchEffect(() => {
+        el.textContent = source()
+      })
+      return teardown
+    })
+    const scope = effectScope()
+    const el = document.createElement('div')
+    const n = ref(1)
+    const source = () => n.value
+    const modifiers = { mod: true }
+    scope.run(() => {
+      withVaporDirectives(el, [[dir, source, undefined, modifiers]])
+    })
+    expect(dir).toHaveBeenCalledWith(el, source, undefined, modifiers)
+    expect(teardown).not.toHaveBeenCalled()
+
+    expect(el.textContent).toBe('1')
+
+    n.value = 2
+    await nextTick()
+    expect(el.textContent).toBe('2')
+
+    scope.stop()
+    expect(teardown).toHaveBeenCalled()
+
+    n.value = 3
+    await nextTick()
+    // should be stopped and not update
+    expect(el.textContent).toBe('2')
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/directives/vModel.spec.ts b/packages/runtime-vapor/__tests__/directives/vModel.spec.ts
new file mode 100644
index 00000000000..521542144a9
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/directives/vModel.spec.ts
@@ -0,0 +1,1005 @@
+import { reactive, ref } from '@vue/reactivity'
+import {
+  applyCheckboxModel,
+  applyRadioModel,
+  applySelectModel,
+  applyTextModel,
+  delegate,
+  delegateEvents,
+  on,
+  setClass,
+  setProp,
+  setValue,
+  template,
+} from '../../src'
+import { makeRender } from '../_utils'
+import { nextTick } from '@vue/runtime-dom'
+
+const define = makeRender()
+
+const triggerEvent = (type: string, el: Element) => {
+  const event = new Event(type, { bubbles: true })
+  el.dispatchEvent(event)
+}
+
+const setDOMProps = (el: any, props: Array<[key: string, value: any]>) => {
+  props.forEach(prop => {
+    const [key, value] = prop
+    key === 'class' ? setClass(el, value) : setProp(el, key, value)
+  })
+}
+
+describe('directive: v-model', () => {
+  test('should work with text input', async () => {
+    const spy = vi.fn()
+
+    const data = ref<string | null | undefined>('')
+    const { host } = define(() => {
+      const t0 = template('<input />')
+      delegateEvents('input')
+      const n0 = t0() as HTMLInputElement
+      applyTextModel(
+        n0,
+        () => data.value,
+        val => (data.value = val),
+      )
+      delegate(n0, 'input', () => spy(data.value))
+      return n0
+    }).render()
+
+    const input = host.querySelector('input')!
+    expect(input.value).toEqual('')
+
+    input.value = 'foo'
+    triggerEvent('input', input)
+    await nextTick()
+    expect(data.value).toEqual('foo')
+    expect(spy).toHaveBeenCalledWith('foo')
+
+    data.value = 'bar'
+    await nextTick()
+    expect(input.value).toEqual('bar')
+
+    data.value = undefined
+    await nextTick()
+    expect(input.value).toEqual('')
+  })
+
+  test('should work with select', async () => {
+    const spy = vi.fn()
+    const data = ref<string | null>('')
+    const { host } = define(() => {
+      const t0 = template(
+        '<select><option>red</option><option>green</option><option>blue</option></select>',
+      )
+      const n0 = t0() as HTMLSelectElement
+      applySelectModel(
+        n0,
+        () => data.value,
+        val => (data.value = val),
+      )
+      on(n0, 'change', () => spy(data.value))
+      return n0
+    }).render()
+
+    const select = host.querySelector('select')!
+    expect(select.value).toEqual('')
+
+    select.value = 'red'
+    triggerEvent('change', select)
+    await nextTick()
+    expect(data.value).toEqual('red')
+    expect(spy).toHaveBeenCalledWith('red')
+
+    data.value = 'blue'
+    await nextTick()
+    expect(select.value).toEqual('blue')
+  })
+
+  test('should work with number input', async () => {
+    const data = ref<number | null>(null)
+    const { host } = define(() => {
+      const t0 = template('<input />')
+      const n0 = t0() as HTMLInputElement
+      applyTextModel(
+        n0,
+        () => data.value,
+        val => (data.value = val),
+      )
+      n0.type = 'number'
+      return n0
+    }).render()
+
+    const input = host.querySelector('input')!
+    expect(input.value).toEqual('')
+    expect(input.type).toEqual('number')
+
+    // @ts-expect-error
+    input.value = 1
+    triggerEvent('input', input)
+    await nextTick()
+    expect(typeof data.value).toEqual('number')
+    expect(data.value).toEqual(1)
+  })
+
+  test('should work with textarea', async () => {
+    const data = ref<string>('')
+    const { host } = define(() => {
+      const t0 = template('<textarea />')
+      const n0 = t0() as HTMLInputElement
+      applyTextModel(
+        n0,
+        () => data.value,
+        val => (data.value = val),
+      )
+      return n0
+    }).render()
+
+    const input = host.querySelector('textarea')!
+
+    input.value = 'foo'
+    triggerEvent('input', input)
+    await nextTick()
+    expect(data.value).toEqual('foo')
+
+    data.value = 'bar'
+    await nextTick()
+    expect(input.value).toEqual('bar')
+  })
+
+  test('should support modifiers', async () => {
+    const data = reactive<{
+      number: number | null
+      trim: string | null
+      lazy: string | null
+      trimNumber: number | null
+    }>({ number: null, trim: null, lazy: null, trimNumber: null })
+
+    const { host } = define(() => {
+      const t0 = template(`<div>${'<input/>'.repeat(4)}</div>`)
+      const n0 = t0() as HTMLInputElement
+      const [input1, input2, input3, input4] = Array.from(
+        n0.children,
+      ) as Array<HTMLInputElement>
+
+      // number
+      setClass(input1, 'number')
+      applyTextModel(
+        input1,
+        () => data.number,
+        val => (data.number = val),
+        { number: true },
+      )
+
+      // trim
+      setClass(input2, 'trim')
+      applyTextModel(
+        input2,
+        () => data.trim,
+        val => (data.trim = val),
+        { trim: true },
+      )
+
+      // trim & number
+      setClass(input3, 'trim-number')
+      applyTextModel(
+        input3,
+        () => data.trimNumber,
+        val => (data.trimNumber = val),
+        { trim: true, number: true },
+      )
+
+      // lazy
+      setClass(input4, 'lazy')
+      applyTextModel(
+        input4,
+        () => data.lazy,
+        val => (data.lazy = val),
+        { lazy: true },
+      )
+
+      return n0
+    }).render()
+
+    const number = host.querySelector('.number') as HTMLInputElement
+    const trim = host.querySelector('.trim') as HTMLInputElement
+    const trimNumber = host.querySelector('.trim-number') as HTMLInputElement
+    const lazy = host.querySelector('.lazy') as HTMLInputElement
+
+    number.value = '+01.2'
+    triggerEvent('input', number)
+    await nextTick()
+    expect(data.number).toEqual(1.2)
+
+    trim.value = '    hello, world    '
+    triggerEvent('input', trim)
+    await nextTick()
+    expect(data.trim).toEqual('hello, world')
+
+    trimNumber.value = '    1    '
+    triggerEvent('input', trimNumber)
+    await nextTick()
+    expect(data.trimNumber).toEqual(1)
+
+    trimNumber.value = '    +01.2    '
+    triggerEvent('input', trimNumber)
+    await nextTick()
+    expect(data.trimNumber).toEqual(1.2)
+
+    lazy.value = 'foo'
+    triggerEvent('change', lazy)
+    await nextTick()
+    expect(data.lazy).toEqual('foo')
+  })
+
+  test('should work with range', async () => {
+    const data = ref<number>(25)
+    let n1: HTMLInputElement, n2: HTMLInputElement
+    define(() => {
+      const t0 = template(
+        `<div>` +
+          `<input type="range" min="1" max="100">` +
+          `<input type="range" min="1" max="100">` +
+          `</div>`,
+      )
+      const n0 = t0() as HTMLInputElement
+      ;[n1, n2] = Array.from(n0.children) as Array<HTMLInputElement>
+
+      applyTextModel(
+        n1,
+        () => data.value,
+        val => (data.value = val),
+        { number: true },
+      )
+
+      applyTextModel(
+        n2,
+        () => data.value,
+        val => (data.value = val),
+        {
+          lazy: true,
+        },
+      )
+
+      return n0
+    }).render()
+
+    // @ts-expect-error
+    n1.value = 20
+    triggerEvent('input', n1!)
+    await nextTick()
+    expect(data.value).toEqual(20)
+
+    // @ts-expect-error
+    n1.value = 200
+    triggerEvent('input', n1!)
+    await nextTick()
+    expect(data.value).toEqual(100)
+
+    // @ts-expect-error
+    n1.value = -1
+    triggerEvent('input', n1!)
+    await nextTick()
+    expect(data.value).toEqual(1)
+
+    // @ts-expect-error
+    n2.value = 30
+    triggerEvent('change', n2!)
+    await nextTick()
+    expect(data.value).toEqual('30')
+
+    // @ts-expect-error
+    n2.value = 200
+    triggerEvent('change', n2!)
+    await nextTick()
+    expect(data.value).toEqual('100')
+
+    // @ts-expect-error
+    n2.value = -1
+    triggerEvent('change', n2!)
+    await nextTick()
+    expect(data.value).toEqual('1')
+
+    data.value = 60
+    await nextTick()
+    expect(n1!.value).toEqual('60')
+    expect(n2!.value).toEqual('60')
+
+    data.value = -1
+    await nextTick()
+    expect(n1!.value).toEqual('1')
+    expect(n2!.value).toEqual('1')
+
+    data.value = 200
+    await nextTick()
+    expect(n1!.value).toEqual('100')
+    expect(n2!.value).toEqual('100')
+  })
+
+  test('should work with checkbox', async () => {
+    const data = ref<boolean | null>(null)
+    const { host } = define(() => {
+      const t0 = template('<input type="checkbox" />')
+      const n0 = t0() as HTMLInputElement
+      applyCheckboxModel(
+        n0,
+        () => data.value,
+        val => (data.value = val),
+      )
+      return n0
+    }).render()
+
+    const input = host.querySelector('input') as HTMLInputElement
+
+    input.checked = true
+    triggerEvent('change', input)
+    await nextTick()
+    expect(data.value).toEqual(true)
+
+    data.value = false
+    await nextTick()
+    expect(input.checked).toEqual(false)
+
+    data.value = true
+    await nextTick()
+    expect(input.checked).toEqual(true)
+
+    input.checked = false
+    triggerEvent('change', input)
+    await nextTick()
+    expect(data.value).toEqual(false)
+  })
+
+  test('should work with checkbox and true-value/false-value', async () => {
+    const data = ref<string | null>('yes')
+    const { host } = define(() => {
+      const t0 = template(
+        '<input type="checkbox" true-value="yes" false-value="no" />',
+      )
+      const n0 = t0() as HTMLInputElement
+      applyCheckboxModel(
+        n0,
+        () => data.value,
+        val => (data.value = val),
+      )
+      return n0
+    }).render()
+
+    const input = host.querySelector('input') as HTMLInputElement
+
+    // DOM checked state should respect initial true-value/false-value
+    expect(input.checked).toEqual(true)
+    input.checked = false
+    triggerEvent('change', input)
+    await nextTick()
+    expect(data.value).toEqual('no')
+
+    data.value = 'yes'
+    await nextTick()
+    expect(input.checked).toEqual(true)
+
+    data.value = 'no'
+    await nextTick()
+    expect(input.checked).toEqual(false)
+
+    input.checked = true
+    triggerEvent('change', input)
+    await nextTick()
+    expect(data.value).toEqual('yes')
+  })
+
+  test('should work with checkbox and true-value/false-value with object values', async () => {
+    const data = ref<{ yes?: 'yes'; no?: 'no' } | null>(null)
+    const { host } = define(() => {
+      const t0 = template('<input type="checkbox" />')
+      const n0 = t0() as HTMLInputElement
+      setDOMProps(n0, [
+        ['true-value', { yes: 'yes' }],
+        ['false-value', { no: 'no' }],
+      ])
+      applyCheckboxModel(
+        n0,
+        () => data.value,
+        val => (data.value = val),
+      )
+      return n0
+    }).render()
+
+    const input = host.querySelector('input') as HTMLInputElement
+    input.checked = true
+    triggerEvent('change', input)
+    await nextTick()
+    expect(data.value).toEqual({ yes: 'yes' })
+
+    data.value = { no: 'no' }
+    await nextTick()
+    expect(input.checked).toEqual(false)
+
+    data.value = { yes: 'yes' }
+    await nextTick()
+    expect(input.checked).toEqual(true)
+
+    input.checked = false
+    triggerEvent('change', input)
+    await nextTick()
+    expect(data.value).toEqual({ no: 'no' })
+  })
+
+  test(`should support array as a checkbox model`, async () => {
+    const data = ref<Array<string>>([])
+    let n1: HTMLInputElement, n2: HTMLInputElement
+    define(() => {
+      const t0 = template(
+        `<div>` +
+          `<input type="checkbox" value="foo">` +
+          `<input type="checkbox" value="bar">` +
+          `</div>`,
+      )
+      const n0 = t0() as HTMLInputElement
+      ;[n1, n2] = Array.from(n0.children) as Array<HTMLInputElement>
+
+      applyCheckboxModel(
+        n1,
+        () => data.value,
+        val => (data.value = val),
+      )
+      applyCheckboxModel(
+        n2,
+        () => data.value,
+        val => (data.value = val),
+      )
+      return n0
+    }).render()
+
+    n1!.checked = true
+    triggerEvent('change', n1!)
+    await nextTick()
+    expect(data.value).toMatchObject(['foo'])
+
+    n2!.checked = true
+    triggerEvent('change', n2!)
+    await nextTick()
+    expect(data.value).toMatchObject(['foo', 'bar'])
+
+    n2!.checked = false
+    triggerEvent('change', n2!)
+    await nextTick()
+    expect(data.value).toMatchObject(['foo'])
+
+    n1!.checked = false
+    triggerEvent('change', n1!)
+    await nextTick()
+    expect(data.value).toMatchObject([])
+
+    data.value = ['foo']
+    await nextTick()
+    expect(n2!.checked).toEqual(false)
+    expect(n1!.checked).toEqual(true)
+
+    data.value = ['bar']
+    await nextTick()
+    expect(n1!.checked).toEqual(false)
+    expect(n2!.checked).toEqual(true)
+
+    data.value = []
+    await nextTick()
+    expect(n1!.checked).toEqual(false)
+    expect(n2!.checked).toEqual(false)
+  })
+
+  test(`should support Set as a checkbox model`, async () => {
+    const data = ref<Set<string>>(new Set())
+    let n1: HTMLInputElement, n2: HTMLInputElement
+    define(() => {
+      const t0 = template(
+        `<div>` +
+          `<input type="checkbox" value="foo">` +
+          `<input type="checkbox" value="bar">` +
+          `</div>`,
+      )
+      const n0 = t0() as HTMLInputElement
+      ;[n1, n2] = Array.from(n0.children) as Array<HTMLInputElement>
+
+      applyCheckboxModel(
+        n1,
+        () => data.value,
+        val => (data.value = val),
+      )
+      applyCheckboxModel(
+        n2,
+        () => data.value,
+        val => (data.value = val),
+      )
+
+      return n0
+    }).render()
+
+    n1!.checked = true
+    triggerEvent('change', n1!)
+    await nextTick()
+    expect(data.value).toMatchObject(new Set(['foo']))
+
+    n2!.checked = true
+    triggerEvent('change', n2!)
+    await nextTick()
+    expect(data.value).toMatchObject(new Set(['foo', 'bar']))
+
+    n2!.checked = false
+    triggerEvent('change', n2!)
+    await nextTick()
+    expect(data.value).toMatchObject(new Set(['foo']))
+
+    n1!.checked = false
+    triggerEvent('change', n1!)
+    await nextTick()
+    expect(data.value).toMatchObject(new Set())
+
+    data.value = new Set(['foo'])
+    await nextTick()
+    expect(n2!.checked).toEqual(false)
+    expect(n1!.checked).toEqual(true)
+
+    data.value = new Set(['bar'])
+    await nextTick()
+    expect(n1!.checked).toEqual(false)
+    expect(n2!.checked).toEqual(true)
+
+    data.value = new Set()
+    await nextTick()
+    expect(n1!.checked).toEqual(false)
+    expect(n2!.checked).toEqual(false)
+  })
+
+  test('should work with radio', async () => {
+    const data = ref<string | null>(null)
+    let n1: HTMLInputElement, n2: HTMLInputElement
+    define(() => {
+      const t0 = template(
+        `<div>` +
+          `<input type="radio" value="foo">` +
+          `<input type="radio" value="bar">` +
+          `</div>`,
+      )
+      const n0 = t0() as HTMLInputElement
+      ;[n1, n2] = Array.from(n0.children) as Array<HTMLInputElement>
+
+      applyRadioModel(
+        n1,
+        () => data.value,
+        val => (data.value = val),
+      )
+      applyRadioModel(
+        n2,
+        () => data.value,
+        val => (data.value = val),
+      )
+      return n0
+    }).render()
+
+    n1!.checked = true
+    triggerEvent('change', n1!)
+    await nextTick()
+    expect(data.value).toEqual('foo')
+
+    n2!.checked = true
+    triggerEvent('change', n2!)
+    await nextTick()
+    expect(data.value).toEqual('bar')
+
+    data.value = null
+    await nextTick()
+    expect(n1!.checked).toEqual(false)
+    expect(n2!.checked).toEqual(false)
+
+    data.value = 'foo'
+    await nextTick()
+    expect(n1!.checked).toEqual(true)
+    expect(n2!.checked).toEqual(false)
+
+    data.value = 'bar'
+    await nextTick()
+    expect(n1!.checked).toEqual(false)
+    expect(n2!.checked).toEqual(true)
+  })
+
+  test('should work with single select', async () => {
+    const data = ref<string | null>(null)
+    let select: HTMLSelectElement, n1: HTMLOptionElement, n2: HTMLOptionElement
+    define(() => {
+      const t0 = template(
+        '<select><option value="foo"></option><option value="bar"></option></select>',
+      )
+      select = t0() as HTMLSelectElement
+      ;[n1, n2] = Array.from(select.childNodes) as Array<HTMLOptionElement>
+
+      applySelectModel(
+        select,
+        () => data.value,
+        val => (data.value = val),
+      )
+      return select
+    }).render()
+
+    n1!.selected = true
+    triggerEvent('change', select!)
+    await nextTick()
+    expect(data.value).toEqual('foo')
+
+    n1!.selected = false
+    n2!.selected = true
+    triggerEvent('change', select!)
+    await nextTick()
+    expect(data.value).toEqual('bar')
+
+    n1!.selected = false
+    n2!.selected = false
+    data.value = 'foo'
+    await nextTick()
+    expect(select!.value).toEqual('foo')
+    expect(n1!.selected).toEqual(true)
+    expect(n2!.selected).toEqual(false)
+
+    n1!.selected = true
+    n2!.selected = false
+    data.value = 'bar'
+    await nextTick()
+    expect(select!.value).toEqual('bar')
+    expect(n1!.selected).toEqual(false)
+    expect(n2!.selected).toEqual(true)
+  })
+
+  test('should work with multiple select (model is Array)', async () => {
+    const data = ref<Array<string>>([])
+    let select: HTMLSelectElement, n1: HTMLOptionElement, n2: HTMLOptionElement
+    define(() => {
+      const t0 = template(
+        '<select multiple>' +
+          '<option value="foo"></option><option value="bar"></option>' +
+          '</select>',
+      )
+      select = t0() as HTMLSelectElement
+      ;[n1, n2] = Array.from(select.childNodes) as Array<HTMLOptionElement>
+
+      applySelectModel(
+        select,
+        () => data.value,
+        val => (data.value = val),
+      )
+      return select
+    }).render()
+
+    n1!.selected = true
+    triggerEvent('change', select!)
+    await nextTick()
+    expect(data.value).toMatchObject(['foo'])
+
+    n1!.selected = false
+    n2!.selected = true
+    triggerEvent('change', select!)
+    await nextTick()
+    expect(data.value).toMatchObject(['bar'])
+
+    n1!.selected = true
+    n2!.selected = true
+    triggerEvent('change', select!)
+    await nextTick()
+    expect(data.value).toMatchObject(['foo', 'bar'])
+
+    n1!.selected = false
+    n2!.selected = false
+    data.value = ['foo']
+    await nextTick()
+    expect(select!.value).toEqual('foo')
+    expect(n1!.selected).toEqual(true)
+    expect(n2!.selected).toEqual(false)
+
+    n1!.selected = false
+    n2!.selected = false
+    data.value = ['foo', 'bar']
+    await nextTick()
+    expect(n1!.selected).toEqual(true)
+    expect(n2!.selected).toEqual(true)
+  })
+
+  test('v-model.number should work with single select', async () => {
+    const data = ref<string | null>(null)
+    let select: HTMLSelectElement, n1: HTMLOptionElement
+    define(() => {
+      const t0 = template(
+        '<select><option value="1"></option><option value="2"></option></select>',
+      )
+      select = t0() as HTMLSelectElement
+      n1 = select.childNodes[0] as HTMLOptionElement
+      applySelectModel(
+        select,
+        () => data.value,
+        val => (data.value = val),
+        { number: true },
+      )
+      return select
+    }).render()
+
+    n1!.selected = true
+    triggerEvent('change', select!)
+    await nextTick()
+    expect(typeof data.value).toEqual('number')
+    expect(data.value).toEqual(1)
+  })
+
+  test('v-model.number should work with multiple select', async () => {
+    const data = ref<Array<number>>([])
+    let select: HTMLSelectElement
+    const { host } = define(() => {
+      const t0 = template(
+        '<select multiple>' +
+          '<option value="1"></option><option value="2"></option>' +
+          '</select>',
+      )
+      select = t0() as HTMLSelectElement
+      applySelectModel(
+        select,
+        () => data.value,
+        val => (data.value = val),
+        { number: true },
+      )
+      return select
+    }).render()
+
+    const one = host.querySelector('option[value="1"]') as HTMLOptionElement
+    const two = host.querySelector('option[value="2"]') as HTMLOptionElement
+
+    one.selected = true
+    two.selected = false
+    triggerEvent('change', select!)
+    await nextTick()
+    expect(data.value).toMatchObject([1])
+
+    one.selected = false
+    two.selected = true
+    triggerEvent('change', select!)
+    await nextTick()
+    expect(data.value).toMatchObject([2])
+
+    one.selected = true
+    two.selected = true
+    triggerEvent('change', select!)
+    await nextTick()
+    expect(data.value).toMatchObject([1, 2])
+
+    one.selected = false
+    two.selected = false
+    data.value = [1]
+    await nextTick()
+    expect(one.selected).toEqual(true)
+    expect(two.selected).toEqual(false)
+
+    one.selected = false
+    two.selected = false
+    data.value = [1, 2]
+    await nextTick()
+    expect(one.selected).toEqual(true)
+    expect(two.selected).toEqual(true)
+  })
+
+  test('multiple select (model is Array, option value is object)', async () => {
+    const fooValue = { foo: 1 }
+    const barValue = { bar: 1 }
+
+    const data = ref<Array<number>>([])
+
+    let select: HTMLSelectElement
+    const { host } = define(() => {
+      const t0 = template(
+        '<select multiple><option></option><option></option></select>',
+      )
+      select = t0() as HTMLSelectElement
+      const [n1, n2] = Array.from(select.childNodes) as Array<HTMLOptionElement>
+      setValue(n1, fooValue)
+      setValue(n2, barValue)
+      applySelectModel(
+        select,
+        () => data.value,
+        val => (data.value = val),
+      )
+      return select
+    }).render()
+
+    const [foo, bar] = Array.from(
+      host.querySelectorAll('option'),
+    ) as Array<HTMLOptionElement>
+
+    foo.selected = true
+    triggerEvent('change', select!)
+    await nextTick()
+    expect(data.value).toMatchObject([fooValue])
+
+    foo.selected = false
+    bar.selected = true
+    triggerEvent('change', select!)
+    await nextTick()
+    expect(data.value).toMatchObject([barValue])
+
+    foo.selected = true
+    bar.selected = true
+    triggerEvent('change', select!)
+    await nextTick()
+    expect(data.value).toMatchObject([fooValue, barValue])
+
+    // reset
+    foo.selected = false
+    bar.selected = false
+    triggerEvent('change', select!)
+    await nextTick()
+    expect(data.value).toMatchObject([])
+
+    // @ts-expect-error
+    data.value = [fooValue, barValue]
+    await nextTick()
+    expect(foo.selected).toEqual(true)
+    expect(bar.selected).toEqual(true)
+
+    // reset
+    foo.selected = false
+    bar.selected = false
+    triggerEvent('change', select!)
+    await nextTick()
+    expect(data.value).toMatchObject([])
+
+    // @ts-expect-error
+    data.value = [{ foo: 1 }, { bar: 1 }]
+    await nextTick()
+    // looseEqual
+    expect(foo.selected).toEqual(true)
+    expect(bar.selected).toEqual(true)
+  })
+
+  test('multiple select (model is Set)', async () => {
+    const data = ref<Set<string>>(new Set())
+    const { host } = define(() => {
+      const t0 = template(
+        '<select multiple><option value="foo"></option><option value="bar"></option></select>',
+      )
+      const n0 = t0() as HTMLSelectElement
+      applySelectModel(
+        n0,
+        () => data.value,
+        val => (data.value = val),
+      )
+      return n0
+    }).render()
+
+    const select = host.querySelector('select') as HTMLSelectElement
+    const foo = host.querySelector('option[value=foo]') as HTMLOptionElement
+    const bar = host.querySelector('option[value=bar]') as HTMLOptionElement
+
+    foo.selected = true
+    triggerEvent('change', select)
+    await nextTick()
+    expect(data.value).toBeInstanceOf(Set)
+    expect(data.value).toMatchObject(new Set(['foo']))
+
+    foo.selected = false
+    bar.selected = true
+    triggerEvent('change', select)
+    await nextTick()
+    expect(data.value).toBeInstanceOf(Set)
+    expect(data.value).toMatchObject(new Set(['bar']))
+
+    foo.selected = true
+    bar.selected = true
+    triggerEvent('change', select)
+    await nextTick()
+    expect(data.value).toBeInstanceOf(Set)
+    expect(data.value).toMatchObject(new Set(['foo', 'bar']))
+
+    foo.selected = false
+    bar.selected = false
+    data.value = new Set(['foo'])
+    await nextTick()
+    expect(select.value).toEqual('foo')
+    expect(foo.selected).toEqual(true)
+    expect(bar.selected).toEqual(false)
+
+    foo.selected = false
+    bar.selected = false
+    data.value = new Set(['foo', 'bar'])
+    await nextTick()
+    expect(foo.selected).toEqual(true)
+    expect(bar.selected).toEqual(true)
+  })
+
+  test('multiple select (model is set, option value is object)', async () => {
+    const fooValue = { foo: 1 }
+    const barValue = { bar: 1 }
+
+    const data = ref<Set<string>>(new Set())
+    const { host } = define(() => {
+      const t0 = template(
+        '<select multiple><option></option><option></option></select>',
+      )
+      const n0 = t0() as HTMLSelectElement
+      const [n1, n2] = Array.from(n0.childNodes) as Array<HTMLOptionElement>
+      setValue(n1, fooValue)
+      setValue(n2, barValue)
+      applySelectModel(
+        n0,
+        () => data.value,
+        val => (data.value = val),
+      )
+      return n0
+    }).render()
+
+    const select = host.querySelector('select') as HTMLSelectElement
+    const [foo, bar] = Array.from(
+      host.querySelectorAll('option'),
+    ) as Array<HTMLOptionElement>
+
+    foo.selected = true
+    triggerEvent('change', select)
+    await nextTick()
+    expect(data.value).toMatchObject(new Set([fooValue]))
+
+    foo.selected = false
+    bar.selected = true
+    triggerEvent('change', select)
+    await nextTick()
+    expect(data.value).toMatchObject(new Set([barValue]))
+
+    foo.selected = true
+    bar.selected = true
+    triggerEvent('change', select)
+    await nextTick()
+    expect(data.value).toMatchObject(new Set([fooValue, barValue]))
+
+    foo.selected = false
+    bar.selected = false
+    // @ts-expect-error
+    data.value = new Set([fooValue, barValue])
+    await nextTick()
+    expect(foo.selected).toEqual(true)
+    expect(bar.selected).toEqual(true)
+
+    foo.selected = false
+    bar.selected = false
+    // @ts-expect-error
+    data.value = new Set([{ foo: 1 }, { bar: 1 }])
+    await nextTick()
+    // without looseEqual, here is different from Array
+    expect(foo.selected).toEqual(false)
+    expect(bar.selected).toEqual(false)
+  })
+
+  test('should work with composition session', async () => {
+    const data = ref<string>('')
+    const { host } = define(() => {
+      const t0 = template('<input />')
+      const n0 = t0() as HTMLInputElement
+      applyTextModel(
+        n0,
+        () => data.value,
+        val => (data.value = val),
+      )
+      return n0
+    }).render()
+
+    const input = host.querySelector('input') as HTMLInputElement
+
+    //developer.mozilla.org/en-US/docs/Web/API/Element/compositionstart_event
+    //compositionstart event could be fired after a user starts entering a Chinese character using a Pinyin IME
+    input.value = '使用拼音'
+    triggerEvent('compositionstart', input)
+    await nextTick()
+    expect(data.value).toEqual('')
+
+    // input event has no effect during composition session
+    input.value = '使用拼音输入'
+    triggerEvent('input', input)
+    await nextTick()
+    expect(data.value).toEqual('')
+
+    // After compositionend event being fired, an input event will be automatically trigger
+    triggerEvent('compositionend', input)
+    await nextTick()
+    expect(data.value).toEqual('使用拼音输入')
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/directives/vShow.spec.ts b/packages/runtime-vapor/__tests__/directives/vShow.spec.ts
new file mode 100644
index 00000000000..da553785e18
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/directives/vShow.spec.ts
@@ -0,0 +1,140 @@
+import {
+  applyVShow,
+  createComponent,
+  createIf,
+  defineVaporComponent,
+  on,
+  template,
+} from '../../src'
+import { type VShowElement, nextTick, ref } from 'vue'
+import { describe, expect, test } from 'vitest'
+import { makeRender } from '../_utils'
+
+const define = makeRender()
+
+const createDemo = (defaultValue: boolean) =>
+  define(() => {
+    const visible = ref(defaultValue)
+    function handleClick() {
+      visible.value = !visible.value
+    }
+    const t0 = template(
+      '<div><button>toggle</button><h1>hello world</h1></div>',
+    )
+    const n0 = t0()
+    const n1 = n0.firstChild!
+    const n2 = n1.nextSibling
+    applyVShow(n2 as VShowElement, () => visible.value)
+    on(n1 as HTMLElement, 'click', handleClick)
+    return n0
+  })
+
+describe('directive: v-show', () => {
+  test('basic', async () => {
+    const { host } = createDemo(true).render()
+    const btn = host.querySelector('button')
+    expect(host.innerHTML).toBe(
+      '<div><button>toggle</button><h1>hello world</h1></div>',
+    )
+    btn?.click()
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<div><button>toggle</button><h1 style="display: none;">hello world</h1></div>',
+    )
+  })
+  test('should hide content when default value is false', async () => {
+    const { host } = createDemo(false).render()
+    const btn = host.querySelector('button')
+    const h1 = host.querySelector('h1')
+    expect(h1?.style.display).toBe('none')
+    btn?.click()
+    await nextTick()
+    expect(h1?.style.display).toBe('')
+  })
+
+  test('should work on component', async () => {
+    const t0 = template('<div>child</div>')
+    const visible = ref(true)
+
+    const { component: Child } = define({
+      setup() {
+        return t0()
+      },
+    })
+
+    const { host } = define({
+      setup() {
+        const n1 = createComponent(Child, null, null, true)
+        applyVShow(n1, () => visible.value)
+        return n1
+      },
+    }).render()
+
+    expect(host.innerHTML).toBe('<div>child</div>')
+
+    visible.value = !visible.value
+    await nextTick()
+    expect(host.innerHTML).toBe('<div style="display: none;">child</div>')
+  })
+
+  test('warn on non-single-element-root component', () => {
+    const Child = defineVaporComponent({
+      setup() {
+        return document.createTextNode('b')
+      },
+    })
+    define({
+      setup() {
+        const n1 = createComponent(Child)
+        applyVShow(n1, () => true)
+        return n1
+      },
+    }).render()
+    expect(
+      'v-show used on component with non-single-element root node',
+    ).toHaveBeenWarned()
+  })
+
+  test('should work on component with dynamic fragment root', async () => {
+    const t0 = template('<div>child</div>')
+    const t1 = template('<span>child</span>')
+    const childIf = ref(true)
+    const visible = ref(true)
+
+    const { component: Child } = define({
+      setup() {
+        return createIf(
+          () => childIf.value,
+          () => t0(),
+          () => t1(),
+        )
+      },
+    })
+
+    const { host } = define({
+      setup() {
+        const n1 = createComponent(Child, null, null, true)
+        applyVShow(n1, () => visible.value)
+        return n1
+      },
+    }).render()
+
+    expect(host.innerHTML).toBe('<div>child</div><!--if-->')
+
+    visible.value = !visible.value
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<div style="display: none;">child</div><!--if-->',
+    )
+
+    childIf.value = !childIf.value
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<span style="display: none;">child</span><!--if-->',
+    )
+
+    visible.value = !visible.value
+    await nextTick()
+    expect(host.innerHTML).toBe('<span style="">child</span><!--if-->')
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/dom/event.spec.ts b/packages/runtime-vapor/__tests__/dom/event.spec.ts
new file mode 100644
index 00000000000..4b598820129
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/dom/event.spec.ts
@@ -0,0 +1,106 @@
+import { effectScope } from '@vue/reactivity'
+import {
+  delegate,
+  delegateEvents,
+  on,
+  renderEffect,
+  setDynamicEvents,
+} from '../../src'
+
+describe('dom event', () => {
+  delegateEvents('click')
+
+  test('on', () => {
+    const el = document.createElement('div')
+    const handler = vi.fn()
+    on(el, 'click', handler)
+    el.click()
+    expect(handler).toHaveBeenCalled()
+  })
+
+  test('delegate with direct attachment', () => {
+    const el = document.createElement('div')
+    document.body.appendChild(el)
+    const handler = vi.fn()
+    ;(el as any).$evtclick = handler
+    el.click()
+    expect(handler).toHaveBeenCalled()
+  })
+
+  test('delegate', () => {
+    const el = document.createElement('div')
+    document.body.appendChild(el)
+    const handler = vi.fn()
+    delegate(el, 'click', handler)
+    el.click()
+    expect(handler).toHaveBeenCalled()
+  })
+
+  test('delegate with stopPropagation', () => {
+    const parent = document.createElement('div')
+    const child = document.createElement('div')
+    parent.appendChild(child)
+    document.body.appendChild(parent)
+    const parentHandler = vi.fn()
+    delegate(parent, 'click', parentHandler)
+    const childHandler = vi.fn(e => e.stopPropagation())
+    delegate(child, 'click', childHandler)
+    child.click()
+    expect(parentHandler).not.toHaveBeenCalled()
+    expect(childHandler).toHaveBeenCalled()
+  })
+
+  test('delegate with stopImmediatePropagation', () => {
+    const parent = document.createElement('div')
+    const child = document.createElement('div')
+    parent.appendChild(child)
+    document.body.appendChild(parent)
+    const parentHandler = vi.fn()
+    delegate(parent, 'click', parentHandler)
+    const childHandler = vi.fn(e => e.stopImmediatePropagation())
+    delegate(child, 'click', childHandler)
+    child.click()
+    expect(parentHandler).not.toHaveBeenCalled()
+    expect(childHandler).toHaveBeenCalled()
+  })
+
+  test('delegate with multiple handlers', () => {
+    const el = document.createElement('div')
+    document.body.appendChild(el)
+    const handler1 = vi.fn()
+    const handler2 = vi.fn()
+    delegate(el, 'click', handler1)
+    delegate(el, 'click', handler2)
+    el.click()
+    expect(handler1).toHaveBeenCalled()
+    expect(handler2).toHaveBeenCalled()
+  })
+
+  test('delegate with multiple handlers + stopImmediatePropagation', () => {
+    const el = document.createElement('div')
+    document.body.appendChild(el)
+    const handler1 = vi.fn(e => e.stopImmediatePropagation())
+    const handler2 = vi.fn()
+    delegate(el, 'click', handler1)
+    delegate(el, 'click', handler2)
+    el.click()
+    expect(handler1).toHaveBeenCalled()
+    expect(handler2).not.toHaveBeenCalled()
+  })
+
+  test('setDynamicEvents', () => {
+    const el = document.createElement('div')
+    const handler = vi.fn()
+    const scope = effectScope()
+    scope.run(() => {
+      renderEffect(() => {
+        setDynamicEvents(el, {
+          click: handler,
+        })
+      })
+    })
+    el.click()
+    expect(handler).toHaveBeenCalled()
+    scope.stop()
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/dom/prop.spec.ts b/packages/runtime-vapor/__tests__/dom/prop.spec.ts
new file mode 100644
index 00000000000..9d07b413541
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/dom/prop.spec.ts
@@ -0,0 +1,447 @@
+import { NOOP } from '@vue/shared'
+import {
+  setDynamicProp as _setDynamicProp,
+  setAttr,
+  setClass,
+  setDynamicProps,
+  setElementText,
+  setHtml,
+  setProp,
+  setText,
+  setValue,
+} from '../../src/dom/prop'
+import { setStyle } from '../../src/dom/prop'
+import { VaporComponentInstance } from '../../src/component'
+import { ref, setCurrentInstance } from '@vue/runtime-dom'
+
+let removeComponentInstance = NOOP
+beforeEach(() => {
+  const instance = new VaporComponentInstance({}, {}, null)
+  const prev = setCurrentInstance(instance)
+  removeComponentInstance = () => setCurrentInstance(...prev)
+})
+afterEach(() => {
+  removeComponentInstance()
+})
+
+describe('patchProp', () => {
+  describe('setClass', () => {
+    test('should set class', () => {
+      const el = document.createElement('div')
+      setClass(el, 'foo')
+      expect(el.className).toBe('foo')
+      setClass(el, ['bar', 'baz'])
+      expect(el.className).toBe('bar baz')
+      setClass(el, { a: true, b: false })
+      expect(el.className).toBe('a')
+    })
+  })
+
+  describe('setStyle', () => {
+    test('should set style', () => {
+      const el = document.createElement('div')
+      setStyle(el, 'color: red')
+      expect(el.style.cssText).toBe('color: red;')
+    })
+
+    test('should work with camelCase', () => {
+      const el = document.createElement('div')
+      setStyle(el, { fontSize: '12px' })
+      expect(el.style.cssText).toBe('font-size: 12px;')
+    })
+
+    test('shoud set style with object and array property', () => {
+      const el = document.createElement('div')
+      setStyle(el, { color: 'red' })
+      expect(el.style.cssText).toBe('color: red;')
+      setStyle(el, [{ color: 'blue' }, { fontSize: '12px' }])
+      expect(el.style.cssText).toBe('color: blue; font-size: 12px;')
+    })
+
+    test('should remove if falsy value', () => {
+      const el = document.createElement('div')
+      setStyle(el, { color: undefined, borderRadius: null })
+      expect(el.style.cssText).toBe('')
+      setStyle(el, { color: 'red' })
+      expect(el.style.cssText).toBe('color: red;')
+      setStyle(el, { color: undefined, borderRadius: null })
+      expect(el.style.cssText).toBe('')
+    })
+
+    test('should work with !important', () => {
+      const el = document.createElement('div')
+      setStyle(el, { color: 'red !important' })
+      expect(el.style.cssText).toBe('color: red !important;')
+    })
+
+    test('should work with camelCase and !important', () => {
+      const el = document.createElement('div')
+      setStyle(el, { fontSize: '12px !important' })
+      expect(el.style.cssText).toBe('font-size: 12px !important;')
+    })
+
+    test('should work with multiple entries', () => {
+      const el = document.createElement('div')
+      setStyle(el, { color: 'red', marginRight: '10px' })
+      expect(el.style.getPropertyValue('color')).toBe('red')
+      expect(el.style.getPropertyValue('margin-right')).toBe('10px')
+    })
+
+    test('should patch with falsy style value', () => {
+      const el = document.createElement('div')
+      setStyle(el, { width: '100px' })
+      expect(el.style.cssText).toBe('width: 100px;')
+      setStyle(el, { width: 0 })
+      expect(el.style.cssText).toBe('width: 0px;')
+    })
+
+    test('should remove style attribute on falsy value', () => {
+      const el = document.createElement('div')
+      setStyle(el, { width: '100px' })
+      expect(el.style.cssText).toBe('width: 100px;')
+      setStyle(el, { width: undefined })
+      expect(el.style.cssText).toBe('')
+
+      setStyle(el, { width: '100px' })
+      expect(el.style.cssText).toBe('width: 100px;')
+      setStyle(el, null)
+      expect(el.hasAttribute('style')).toBe(false)
+      expect(el.style.cssText).toBe('')
+    })
+
+    test('should warn for trailing semicolons', () => {
+      const el = document.createElement('div')
+      setStyle(el, { color: 'red;' })
+      expect(
+        `Unexpected semicolon at the end of 'color' style value: 'red;'`,
+      ).toHaveBeenWarned()
+
+      setStyle(el, { '--custom': '100; ' })
+      expect(
+        `Unexpected semicolon at the end of '--custom' style value: '100; '`,
+      ).toHaveBeenWarned()
+    })
+
+    test('should not warn for trailing semicolons', () => {
+      const el = document.createElement('div')
+      setStyle(el, { '--custom': '100\\;' })
+      expect(el.style.getPropertyValue('--custom')).toBe('100\\;')
+    })
+
+    test('should work with shorthand properties', () => {
+      const el = document.createElement('div')
+      setStyle(el, {
+        borderBottom: '1px solid red',
+        border: '1px solid green',
+      })
+      expect(el.style.border).toBe('1px solid green')
+      expect(el.style.borderBottom).toBe('1px solid green')
+    })
+
+    // JSDOM doesn't support custom properties on style object so we have to
+    // mock it here.
+    function mockElementWithStyle() {
+      const store: any = {}
+      return {
+        style: {
+          display: '',
+          WebkitTransition: '',
+          setProperty(key: string, val: string) {
+            store[key] = val
+          },
+          getPropertyValue(key: string) {
+            return store[key]
+          },
+        },
+      }
+    }
+
+    test('should work with css custom properties', () => {
+      const el = mockElementWithStyle()
+      setStyle(el as any, { '--theme': 'red' })
+      expect(el.style.getPropertyValue('--theme')).toBe('red')
+    })
+
+    test('should auto vendor prefixing', () => {
+      const el = mockElementWithStyle()
+      setStyle(el as any, { transition: 'all 1s' })
+      expect(el.style.WebkitTransition).toBe('all 1s')
+    })
+
+    test('should work with multiple values', () => {
+      const el = mockElementWithStyle()
+      setStyle(el as any, {
+        display: ['-webkit-box', '-ms-flexbox', 'flex'],
+      })
+      expect(el.style.display).toBe('flex')
+    })
+  })
+
+  describe.todo('setClassIncremental', () => {})
+
+  describe.todo('setStyleIncremental', () => {})
+
+  describe('setAttr', () => {
+    test('should set attribute', () => {
+      const el = document.createElement('div')
+      setAttr(el, 'id', 'foo')
+      expect(el.getAttribute('id')).toBe('foo')
+      setAttr(el, 'name', 'bar')
+      expect(el.getAttribute('name')).toBe('bar')
+    })
+
+    test('should remove attribute', () => {
+      const el = document.createElement('div')
+      setAttr(el, 'id', 'foo')
+      setAttr(el, 'data', 'bar')
+      expect(el.getAttribute('id')).toBe('foo')
+      expect(el.getAttribute('data')).toBe('bar')
+      setAttr(el, 'id', null)
+      expect(el.getAttribute('id')).toBeNull()
+      setAttr(el, 'data', undefined)
+      expect(el.getAttribute('data')).toBeNull()
+    })
+
+    test('should set boolean attribute to string', () => {
+      const el = document.createElement('div')
+      setAttr(el, 'disabled', true)
+      expect(el.getAttribute('disabled')).toBe('true')
+      setAttr(el, 'disabled', false)
+      expect(el.getAttribute('disabled')).toBe('false')
+    })
+  })
+
+  describe('setValue', () => {
+    test('should set value prop', () => {
+      const el = document.createElement('input')
+      setValue(el, 'foo')
+      expect(el.value).toBe('foo')
+      setValue(el, null)
+      expect(el.value).toBe('')
+      expect(el.getAttribute('value')).toBe(null)
+      const obj = {}
+      setValue(el, obj)
+      expect(el.value).toBe(obj.toString())
+      expect((el as any)._value).toBe(obj)
+
+      const option = document.createElement('option')
+      setElementText(option, 'foo')
+      expect(option.value).toBe('foo')
+      expect(option.getAttribute('value')).toBe(null)
+
+      setValue(option, 'bar')
+      expect(option.textContent).toBe('foo')
+      expect(option.value).toBe('bar')
+      expect(option.getAttribute('value')).toBe('bar')
+    })
+  })
+
+  describe('setDOMProp', () => {
+    test('should be boolean prop', () => {
+      const el = document.createElement('select')
+      // In vapor static attrs are part of the template and this never happens
+      // setDOMProp(el, 'multiple', '')
+      // expect(el.multiple).toBe(true)
+      setProp(el, 'multiple', null)
+      expect(el.multiple).toBe(false)
+      setProp(el, 'multiple', true)
+      expect(el.multiple).toBe(true)
+      setProp(el, 'multiple', 0)
+      expect(el.multiple).toBe(false)
+      setProp(el, 'multiple', '0')
+      expect(el.multiple).toBe(true)
+      setProp(el, 'multiple', false)
+      expect(el.multiple).toBe(false)
+      setProp(el, 'multiple', 1)
+      expect(el.multiple).toBe(true)
+      setProp(el, 'multiple', undefined)
+      expect(el.multiple).toBe(false)
+    })
+
+    test('should remove attribute when value is falsy', () => {
+      const el = document.createElement('div')
+      el.setAttribute('id', '')
+      setProp(el, 'id', null)
+      expect(el.hasAttribute('id')).toBe(false)
+
+      el.setAttribute('id', '')
+      setProp(el, 'id', undefined)
+      expect(el.hasAttribute('id')).toBe(false)
+
+      setProp(el, 'id', '')
+      expect(el.hasAttribute('id')).toBe(false)
+
+      const img = document.createElement('img')
+      setProp(img, 'width', 0)
+      expect(img.hasAttribute('width')).toBe(false) // skipped
+
+      setProp(img, 'width', null)
+      expect(img.hasAttribute('width')).toBe(false)
+      setProp(img, 'width', 1)
+      expect(img.hasAttribute('width')).toBe(true)
+
+      setProp(img, 'width', undefined)
+      expect(img.hasAttribute('width')).toBe(false)
+      setProp(img, 'width', 1)
+      expect(img.hasAttribute('width')).toBe(true)
+    })
+
+    test('should warn when set prop error', () => {
+      const el = document.createElement('div')
+      Object.defineProperty(el, 'someProp', {
+        set() {
+          throw new TypeError('Invalid type')
+        },
+      })
+      setProp(el, 'someProp', 'foo')
+
+      expect(
+        `Failed setting prop "someProp" on <div>: value foo is invalid.`,
+      ).toHaveBeenWarnedLast()
+    })
+  })
+
+  describe('setDynamicProp', () => {
+    const element = document.createElement('div')
+    function setDynamicProp(
+      key: string,
+      value: any,
+      el = element.cloneNode(true) as HTMLElement,
+    ) {
+      _setDynamicProp(el, key, value)
+      return el
+    }
+
+    test('should be able to set id', () => {
+      let res = setDynamicProp('id', 'bar')
+      expect(res.id).toBe('bar')
+    })
+
+    test('should be able to set class', () => {
+      let res = setDynamicProp('class', 'foo')
+      expect(res.className).toBe('foo')
+    })
+
+    test('should be able to set style', () => {
+      let res = setDynamicProp('style', 'color: red')
+      expect(res.style.cssText).toBe('color: red;')
+    })
+
+    test('should be able to set .prop', () => {
+      let res = setDynamicProp('.foo', 'bar')
+      expect((res as any)['foo']).toBe('bar')
+      expect(res.getAttribute('foo')).toBeNull()
+    })
+
+    test('should be able to set ^attr', () => {
+      let res = setDynamicProp('^foo', 'bar')
+      expect(res.getAttribute('foo')).toBe('bar')
+      expect((res as any)['foo']).toBeUndefined()
+    })
+
+    test('should be able to set boolean prop', () => {
+      let res = setDynamicProp(
+        'disabled',
+        true,
+        document.createElement('button'),
+      )
+      expect(res.getAttribute('disabled')).toBe('')
+      setDynamicProp('disabled', false, res)
+      expect(res.getAttribute('disabled')).toBeNull()
+    })
+
+    // The function shouldSetAsProp has complete tests elsewhere,
+    // so here we only do a simple test.
+    test('should be able to set innerHTML and textContent', () => {
+      let res = setDynamicProp('innerHTML', '<p>bar</p>')
+      expect(res.innerHTML).toBe('<p>bar</p>')
+      res = setDynamicProp('textContent', 'foo')
+      expect(res.textContent).toBe('foo')
+    })
+
+    test.todo('should be able to set something on SVG')
+  })
+
+  describe('setDynamicProps', () => {
+    test('basic set dynamic props', () => {
+      const el = document.createElement('div')
+      setDynamicProps(el, [{ foo: 'val' }, { bar: 'val' }])
+      expect(el.getAttribute('foo')).toBe('val')
+      expect(el.getAttribute('bar')).toBe('val')
+    })
+
+    test('should merge props', () => {
+      const el = document.createElement('div')
+      setDynamicProps(el, [{ foo: 'val' }, { foo: 'newVal' }])
+      expect(el.getAttribute('foo')).toBe('newVal')
+    })
+
+    test('should reset old props', () => {
+      const el = document.createElement('div')
+      setDynamicProps(el, [{ foo: 'val' }])
+      expect(el.attributes.length).toBe(1)
+      expect(el.getAttribute('foo')).toBe('val')
+
+      setDynamicProps(el, [{ bar: 'val' }])
+      expect(el.attributes.length).toBe(1)
+      expect(el.getAttribute('bar')).toBe('val')
+      expect(el.getAttribute('foo')).toBeNull()
+    })
+
+    test('should reset old modifier props', () => {
+      const el = document.createElement('div')
+
+      setDynamicProps(el, [{ ['.foo']: 'val' }])
+      expect((el as any).foo).toBe('val')
+
+      setDynamicProps(el, [{ ['.bar']: 'val' }])
+      expect((el as any).bar).toBe('val')
+      expect((el as any).foo).toBe('')
+
+      setDynamicProps(el, [{ ['^foo']: 'val' }])
+      expect(el.attributes.length).toBe(1)
+      expect(el.getAttribute('foo')).toBe('val')
+
+      setDynamicProps(el, [{ ['^bar']: 'val' }])
+      expect(el.attributes.length).toBe(1)
+      expect(el.getAttribute('bar')).toBe('val')
+      expect(el.getAttribute('foo')).toBeNull()
+    })
+  })
+
+  describe('setText', () => {
+    test('should set nodeValue', () => {
+      const el = document.createTextNode('foo')
+      setText(el, '')
+      expect(el.textContent).toBe('')
+      setText(el, 'foo')
+      expect(el.textContent).toBe('foo')
+      setText(el, 'bar')
+      expect(el.textContent).toBe('bar')
+    })
+  })
+
+  describe('setElementText', () => {
+    test('should set textContent w/ toDisplayString', () => {
+      const el = document.createElement('div')
+      setElementText(el, null)
+      expect(el.textContent).toBe('')
+      setElementText(el, { a: 1 })
+      expect(el.textContent).toBe(JSON.stringify({ a: 1 }, null, 2))
+      setElementText(el, ref('bar'))
+      expect(el.textContent).toBe('bar')
+    })
+  })
+
+  describe('setHtml', () => {
+    test('should set innerHTML', () => {
+      const el = document.createElement('div')
+      setHtml(el, null)
+      expect(el.innerHTML).toBe('')
+      setHtml(el, '<p>foo</p>')
+      expect(el.innerHTML).toBe('<p>foo</p>')
+      setHtml(el, '<p>bar</p>')
+      expect(el.innerHTML).toBe('<p>bar</p>')
+    })
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/dom/template.spec.ts b/packages/runtime-vapor/__tests__/dom/template.spec.ts
new file mode 100644
index 00000000000..85de30987a5
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/dom/template.spec.ts
@@ -0,0 +1,43 @@
+import { template } from '../../src/dom/template'
+import { child, next, nthChild } from '../../src/dom/node'
+
+describe('api: template', () => {
+  test('create element', () => {
+    const t = template('<div>')
+    const root = t()
+    expect(root).toBeInstanceOf(HTMLDivElement)
+
+    const root2 = t()
+    expect(root2).toBeInstanceOf(HTMLDivElement)
+    expect(root2).not.toBe(root)
+  })
+
+  test('create root element', () => {
+    const t = template('<div>', true)
+    const root = t()
+    expect(root.$root).toBe(true)
+  })
+
+  test('nthChild', () => {
+    const t = template('<div><span><b>nested</b></span><p></p></div>')
+    const root = t()
+    const span = nthChild(root, 0)
+    const b = nthChild(span, 0)
+    const p = nthChild(root, 1)
+    expect(span).toBe(root.firstChild)
+    expect(b).toBe(root.firstChild!.firstChild)
+    expect(p).toBe(root.firstChild!.nextSibling)
+  })
+
+  test('next', () => {
+    const t = template('<div><span></span><b></b><p></p></div>')
+    const root = t()
+    const span = child(root as ParentNode)
+    const b = next(span)
+
+    expect(span).toBe(root.childNodes[0])
+    expect(b).toBe(root.childNodes[1])
+    expect(nthChild(root, 2)).toBe(root.childNodes[2])
+    expect(next(b)).toBe(root.childNodes[2])
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts b/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts
new file mode 100644
index 00000000000..f1ce23ac156
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts
@@ -0,0 +1,755 @@
+import type { NodeRef } from '../../src/apiTemplateRef'
+import {
+  child,
+  createComponent,
+  createDynamicComponent,
+  createFor,
+  createIf,
+  createSlot,
+  createTemplateRefSetter,
+  defineVaporComponent,
+  insert,
+  renderEffect,
+  template,
+} from '../../src'
+import { makeRender } from '../_utils'
+import {
+  type ShallowRef,
+  currentInstance,
+  nextTick,
+  reactive,
+  ref,
+  useTemplateRef,
+  watchEffect,
+} from '@vue/runtime-dom'
+import { setElementText, setText } from '../../src/dom/prop'
+import type { VaporComponent } from '../../src/component'
+
+const define = makeRender()
+
+describe('api: template ref', () => {
+  test('string ref mount', () => {
+    const t0 = template('<div ref="refKey"></div>')
+    const el = ref(null)
+    const { render } = define({
+      setup() {
+        return {
+          refKey: el,
+        }
+      },
+      render() {
+        const n0 = t0()
+        createTemplateRefSetter()(n0 as Element, 'refKey')
+        return n0
+      },
+    })
+
+    const { host } = render()
+    expect(el.value).toBe(host.children[0])
+  })
+
+  it('string ref update', async () => {
+    const t0 = template('<div></div>')
+    const fooEl = ref(null)
+    const barEl = ref(null)
+    const refKey = ref('foo')
+
+    const { render } = define({
+      setup() {
+        return {
+          foo: fooEl,
+          bar: barEl,
+        }
+      },
+      render() {
+        const n0 = t0()
+        let r0: NodeRef | undefined
+        renderEffect(() => {
+          r0 = createTemplateRefSetter()(n0 as Element, refKey.value, r0)
+        })
+        return n0
+      },
+    })
+    const { host } = render()
+    expect(fooEl.value).toBe(host.children[0])
+    expect(barEl.value).toBe(null)
+
+    refKey.value = 'bar'
+    await nextTick()
+    expect(barEl.value).toBe(host.children[0])
+    expect(fooEl.value).toBe(null)
+  })
+
+  it('string ref unmount', async () => {
+    const t0 = template('<div></div>')
+    const el = ref(null)
+    const toggle = ref(true)
+
+    const { render } = define({
+      setup() {
+        return {
+          refKey: el,
+        }
+      },
+      render() {
+        const setRef = createTemplateRefSetter()
+        const n0 = createIf(
+          () => toggle.value,
+          () => {
+            const n1 = t0()
+            setRef(n1 as Element, 'refKey')
+            return n1
+          },
+        )
+        return n0
+      },
+    })
+    const { host } = render()
+    expect(el.value).toBe(host.children[0])
+
+    toggle.value = false
+    await nextTick()
+    expect(el.value).toBe(null)
+  })
+
+  it('function ref mount', () => {
+    const fn = vi.fn()
+    const t0 = template('<div></div>')
+    const { render } = define({
+      render() {
+        const n0 = t0()
+        createTemplateRefSetter()(n0 as Element, fn)
+        return n0
+      },
+    })
+
+    const { host } = render()
+    expect(fn.mock.calls[0][0]).toBe(host.children[0])
+  })
+
+  it('function ref update', async () => {
+    const fn1 = vi.fn()
+    const fn2 = vi.fn()
+    const fn = ref(fn1)
+
+    const t0 = template('<div></div>')
+    const { render } = define({
+      render() {
+        const n0 = t0()
+        let r0: NodeRef | undefined
+        renderEffect(() => {
+          r0 = createTemplateRefSetter()(n0 as Element, fn.value, r0)
+        })
+        return n0
+      },
+    })
+
+    const { host } = render()
+
+    expect(fn1.mock.calls).toHaveLength(1)
+    expect(fn1.mock.calls[0][0]).toBe(host.children[0])
+    expect(fn2.mock.calls).toHaveLength(0)
+
+    fn.value = fn2
+    await nextTick()
+    expect(fn1.mock.calls).toHaveLength(1)
+    expect(fn2.mock.calls).toHaveLength(1)
+    expect(fn2.mock.calls[0][0]).toBe(host.children[0])
+  })
+
+  it('function ref unmount', async () => {
+    const fn = vi.fn()
+    const toggle = ref(true)
+
+    const t0 = template('<div></div>')
+    const { render } = define({
+      render() {
+        const n0 = createIf(
+          () => toggle.value,
+          () => {
+            const n1 = t0()
+            createTemplateRefSetter()(n1 as Element, fn)
+            return n1
+          },
+        )
+        return n0
+      },
+    })
+    const { host } = render()
+    expect(fn.mock.calls[0][0]).toBe(host.children[0])
+    toggle.value = false
+    await nextTick()
+    expect(fn.mock.calls[1][0]).toBe(undefined)
+  })
+
+  test('useTemplateRef mount', () => {
+    const t0 = template('<div ref="refKey"></div>')
+    let r
+    let n
+    const { render } = define({
+      setup() {
+        r = useTemplateRef('foo')
+        n = t0()
+        createTemplateRefSetter()(n as Element, 'foo')
+        return n
+      },
+    })
+
+    const { host } = render()
+    expect(r!.value).toBe(host.children[0])
+  })
+
+  test('useTemplateRef update', async () => {
+    const t0 = template('<div></div>')
+    let fooEl: ShallowRef
+    let barEl: ShallowRef
+    const refKey = ref('foo')
+
+    const { render } = define({
+      setup() {
+        return {
+          foo: fooEl,
+          bar: barEl,
+        }
+      },
+      render() {
+        fooEl = useTemplateRef('foo')
+        barEl = useTemplateRef('bar')
+        const n0 = t0()
+        let r0: NodeRef | undefined
+        renderEffect(() => {
+          r0 = createTemplateRefSetter()(n0 as Element, refKey.value, r0)
+        })
+        return n0
+      },
+    })
+    const { host } = render()
+    expect(fooEl!.value).toBe(host.children[0])
+    expect(barEl!.value).toBe(null)
+
+    refKey.value = 'bar'
+    await nextTick()
+    expect(barEl!.value).toBe(host.children[0])
+    expect(fooEl!.value).toBe(null)
+  })
+
+  it('should work with direct reactive property', () => {
+    const state = reactive({
+      refKey: null,
+    })
+
+    const t0 = template('<div></div>')
+    const { render } = define({
+      setup() {
+        return state
+      },
+      render() {
+        const n0 = t0()
+        createTemplateRefSetter()(n0 as Element, 'refKey')
+        return n0
+      },
+    })
+    const { host } = render()
+    expect(state.refKey).toBe(host.children[0])
+  })
+
+  test('multiple root refs', () => {
+    const refKey1 = ref(null)
+    const refKey2 = ref(null)
+    const refKey3 = ref(null)
+
+    const t0 = template('<div></div>')
+    const t1 = template('<div></div>')
+    const t2 = template('<div></div>')
+    const { render } = define({
+      setup() {
+        return {
+          refKey1,
+          refKey2,
+          refKey3,
+        }
+      },
+      render() {
+        const n0 = t0()
+        const n1 = t1()
+        const n2 = t2()
+        createTemplateRefSetter()(n0 as Element, 'refKey1')
+        createTemplateRefSetter()(n1 as Element, 'refKey2')
+        createTemplateRefSetter()(n2 as Element, 'refKey3')
+        return [n0, n1, n2]
+      },
+    })
+    const { host } = render()
+    // Note: toBe Condition is different from core test case
+    // Core test case is expecting refKey1.value to be host.children[1]
+    expect(refKey1.value).toBe(host.children[0])
+    expect(refKey2.value).toBe(host.children[1])
+    expect(refKey3.value).toBe(host.children[2])
+  })
+
+  // #1505
+  test('reactive template ref in the same template', async () => {
+    const t0 = template('<div id="foo"></div>')
+    const el = ref<HTMLElement>()
+    const { render } = define({
+      render() {
+        const n0 = t0()
+        createTemplateRefSetter()(n0 as Element, el)
+        renderEffect(() => {
+          setElementText(n0, el.value && el.value.getAttribute('id'))
+        })
+        return n0
+      },
+    })
+
+    const { host } = render()
+    // ref not ready on first render, but should queue an update immediately
+    expect(host.innerHTML).toBe(`<div id="foo"></div>`)
+    await nextTick()
+    // ref should be updated
+    expect(host.innerHTML).toBe(`<div id="foo">foo</div>`)
+  })
+
+  // #1834
+  test('exchange refs', async () => {
+    const refToggle = ref(false)
+    const spy = vi.fn()
+
+    const t0 = template('<p></p>')
+    const t1 = template('<i></i>')
+    const { render } = define({
+      render() {
+        const instance = currentInstance!
+        const n0 = t0()
+        const n1 = t1()
+        let r0: NodeRef | undefined
+        let r1: NodeRef | undefined
+        renderEffect(() => {
+          r0 = createTemplateRefSetter()(
+            n0 as Element,
+            refToggle.value ? 'foo' : 'bar',
+            r0,
+          )
+        })
+        renderEffect(() => {
+          r1 = createTemplateRefSetter()(
+            n1 as Element,
+            refToggle.value ? 'bar' : 'foo',
+            r1,
+          )
+        })
+        watchEffect(
+          () => {
+            refToggle.value
+            spy(
+              (instance.refs.foo as HTMLElement).tagName,
+              (instance.refs.bar as HTMLElement).tagName,
+            )
+          },
+          {
+            flush: 'post',
+          },
+        )
+        return [n0, n1]
+      },
+    })
+
+    render()
+
+    expect(spy.mock.calls[0][0]).toBe('I')
+    expect(spy.mock.calls[0][1]).toBe('P')
+    refToggle.value = true
+    await nextTick()
+    expect(spy.mock.calls[1][0]).toBe('P')
+    expect(spy.mock.calls[1][1]).toBe('I')
+  })
+
+  // #1789
+  test('toggle the same ref to different elements', async () => {
+    const refToggle = ref(false)
+    const spy = vi.fn()
+
+    const t0 = template('<p></p>')
+    const t1 = template('<i></i>')
+    const { render } = define({
+      render() {
+        const instance = currentInstance!
+        const setRef = createTemplateRefSetter()
+        const n0 = createIf(
+          () => refToggle.value,
+          () => {
+            const n1 = t0()
+            setRef(n1 as Element, 'foo')
+            return n1
+          },
+          () => {
+            const n1 = t1()
+            setRef(n1 as Element, 'foo')
+            return n1
+          },
+        )
+        watchEffect(
+          () => {
+            refToggle.value
+            spy((instance.refs.foo as HTMLElement).tagName)
+          },
+          {
+            flush: 'post',
+          },
+        )
+        return [n0]
+      },
+    })
+
+    render()
+
+    expect(spy.mock.calls[0][0]).toBe('I')
+    refToggle.value = true
+    await nextTick()
+    expect(spy.mock.calls[1][0]).toBe('P')
+  })
+
+  // compiled output of v-for + template ref
+  test('ref in v-for', async () => {
+    const show = ref(true)
+    const list = reactive([1, 2, 3])
+    const listRefs = ref([])
+    const mapRefs = () => listRefs.value.map((n: HTMLElement) => n.innerHTML)
+
+    const t0 = template('<ul></ul>')
+    const t1 = template('<li></li>')
+    const { render } = define({
+      render() {
+        const n0 = createIf(
+          () => show.value,
+          () => {
+            const n1 = t0()
+            const n2 = createFor(
+              () => list,
+              item => {
+                const n1 = t1()
+                createTemplateRefSetter()(
+                  n1 as Element,
+                  listRefs,
+                  undefined,
+                  true,
+                )
+                renderEffect(() => {
+                  setElementText(n1, item)
+                })
+                return n1
+              },
+            )
+            insert(n2, n1 as ParentNode)
+            return n1
+          },
+        )
+        return n0
+      },
+    })
+    render()
+
+    expect(mapRefs()).toMatchObject(['1', '2', '3'])
+
+    list.push(4)
+    await nextTick()
+    expect(mapRefs()).toMatchObject(['1', '2', '3', '4'])
+
+    list.shift()
+    await nextTick()
+    expect(mapRefs()).toMatchObject(['2', '3', '4'])
+
+    show.value = !show.value
+    await nextTick()
+
+    expect(mapRefs()).toMatchObject([])
+
+    show.value = !show.value
+    await nextTick()
+    expect(mapRefs()).toMatchObject(['2', '3', '4'])
+  })
+
+  test('named ref in v-for', async () => {
+    const show = ref(true)
+    const list = reactive([1, 2, 3])
+    const listRefs = ref([])
+    const mapRefs = () => listRefs.value.map((n: HTMLElement) => n.innerHTML)
+
+    const t0 = template('<ul></ul>')
+    const t1 = template('<li></li>')
+    const { render } = define({
+      setup() {
+        return { listRefs }
+      },
+      render() {
+        const n0 = createIf(
+          () => show.value,
+          () => {
+            const n1 = t0()
+            const n2 = createFor(
+              () => list,
+              item => {
+                const n1 = t1()
+                createTemplateRefSetter()(
+                  n1 as Element,
+                  'listRefs',
+                  undefined,
+                  true,
+                )
+                renderEffect(() => {
+                  setElementText(n1, item)
+                })
+                return n1
+              },
+            )
+            insert(n2, n1 as ParentNode)
+            return n1
+          },
+        )
+        return n0
+      },
+    })
+    render()
+
+    expect(mapRefs()).toMatchObject(['1', '2', '3'])
+
+    list.push(4)
+    await nextTick()
+    expect(mapRefs()).toMatchObject(['1', '2', '3', '4'])
+
+    list.shift()
+    await nextTick()
+    expect(mapRefs()).toMatchObject(['2', '3', '4'])
+
+    show.value = !show.value
+    await nextTick()
+
+    expect(mapRefs()).toMatchObject([])
+
+    show.value = !show.value
+    await nextTick()
+    expect(mapRefs()).toMatchObject(['2', '3', '4'])
+  })
+
+  // #6697 v-for ref behaves differently under production and development
+  test('named ref in v-for , should be responsive when rendering', async () => {
+    const list = ref([1, 2, 3])
+    const listRefs = ref([])
+
+    const t0 = template('<div><div></div><ul></ul></div>')
+    const t1 = template('<li></li>')
+    const { render } = define({
+      setup() {
+        return { listRefs }
+      },
+      render() {
+        const n0 = t0()
+        const n1 = n0.firstChild
+        const n2 = n1!.nextSibling!
+        const n3 = createFor(
+          () => list.value,
+          item => {
+            const n4 = t1()
+            createTemplateRefSetter()(
+              n4 as Element,
+              'listRefs',
+              undefined,
+              true,
+            )
+            renderEffect(() => {
+              setElementText(n4, item)
+            })
+            return n4
+          },
+        )
+        insert(n3, n2 as unknown as ParentNode)
+        renderEffect(() => {
+          setElementText(n1!, String(listRefs.value))
+        })
+        return n0
+      },
+    })
+
+    const { host } = render()
+
+    await nextTick()
+    expect(String(listRefs.value)).toBe(
+      '[object HTMLLIElement],[object HTMLLIElement],[object HTMLLIElement]',
+    )
+    expect(host.innerHTML).toBe(
+      '<div><div>[object HTMLLIElement],[object HTMLLIElement],[object HTMLLIElement]</div><ul><li>1</li><li>2</li><li>3</li><!--for--></ul></div>',
+    )
+
+    list.value.splice(0, 1)
+    await nextTick()
+    expect(String(listRefs.value)).toBe(
+      '[object HTMLLIElement],[object HTMLLIElement]',
+    )
+    expect(host.innerHTML).toBe(
+      '<div><div>[object HTMLLIElement],[object HTMLLIElement]</div><ul><li>2</li><li>3</li><!--for--></ul></div>',
+    )
+  })
+
+  test('string ref inside slots', () => {
+    const { component: Child } = define({
+      setup() {
+        return createSlot('default')
+      },
+    })
+
+    const r = ref()
+    let n
+
+    const { render } = define({
+      setup() {
+        return {
+          foo: r,
+        }
+      },
+      render() {
+        const setRef = createTemplateRefSetter()
+        const n0 = createComponent(Child, null, {
+          default: () => {
+            n = document.createElement('div')
+            setRef(n, 'foo')
+            return n
+          },
+        })
+        return n0
+      },
+    })
+
+    render()
+    expect(r.value).toBe(n)
+  })
+
+  test('inline ref inside slots', () => {
+    const { component: Child } = define({
+      setup() {
+        return createSlot('default')
+      },
+    })
+
+    const r = ref()
+    let n
+
+    const { render } = define({
+      setup() {
+        const setRef = createTemplateRefSetter()
+        const n0 = createComponent(Child, null, {
+          default: () => {
+            n = document.createElement('div')
+            setRef(n, r)
+            return n
+          },
+        })
+        return n0
+      },
+    })
+
+    render()
+    expect(r.value).toBe(n)
+  })
+
+  test('useTemplateRef ref inside slots', () => {
+    const { component: Child } = define({
+      setup() {
+        return createSlot('default')
+      },
+    })
+
+    let r: ShallowRef
+    let n
+
+    const { render } = define({
+      setup() {
+        r = useTemplateRef('foo')
+        const setRef = createTemplateRefSetter()
+        const n0 = createComponent(Child, null, {
+          default: () => {
+            n = document.createElement('div')
+            setRef(n, 'foo')
+            return n
+          },
+        })
+        return n0
+      },
+    })
+
+    render()
+    expect(r!.value).toBe(n)
+  })
+
+  test('work with dynamic component', async () => {
+    const Child = defineVaporComponent({
+      setup(_, { expose }) {
+        const msg = ref('one')
+        expose({ setMsg: (m: string) => (msg.value = m) })
+        const n0 = template(`<div> </div>`)() as any
+        const x0 = child(n0) as any
+        renderEffect(() => setText(x0, msg.value))
+        return n0
+      },
+    })
+
+    const views: Record<string, VaporComponent> = { child: Child }
+    const view = ref('child')
+    const refKey = ref<any>(null)
+
+    const { html } = define({
+      setup() {
+        const setRef = createTemplateRefSetter()
+        const n0 = createDynamicComponent(() => views[view.value]) as any
+        setRef(n0, refKey)
+        return n0
+      },
+    }).render()
+
+    expect(html()).toBe('<div>one</div><!--dynamic-component-->')
+    expect(refKey.value).toBeDefined()
+
+    refKey.value.setMsg('changed')
+    await nextTick()
+    expect(html()).toBe('<div>changed</div><!--dynamic-component-->')
+  })
+
+  // TODO: can not reproduce in Vapor
+  // // #2078
+  // test('handling multiple merged refs', async () => {
+  //   const Foo = {
+  //     render: () => h('div', 'foo'),
+  //   }
+  //   const Bar = {
+  //     render: () => h('div', 'bar'),
+  //   }
+
+  //   const viewRef = shallowRef<any>(Foo)
+  //   const elRef1 = ref()
+  //   const elRef2 = ref()
+
+  //   const App = {
+  //     render() {
+  //       if (!viewRef.value) {
+  //         return null
+  //       }
+  //       const view = h(viewRef.value, { ref: elRef1 })
+  //       return h(view, { ref: elRef2 })
+  //     },
+  //   }
+  //   const root = nodeOps.createElement('div')
+  //   render(h(App), root)
+
+  //   expect(serializeInner(elRef1.value.$el)).toBe('foo')
+  //   expect(elRef1.value).toBe(elRef2.value)
+
+  //   viewRef.value = Bar
+  //   await nextTick()
+  //   expect(serializeInner(elRef1.value.$el)).toBe('bar')
+  //   expect(elRef1.value).toBe(elRef2.value)
+
+  //   viewRef.value = null
+  //   await nextTick()
+  //   expect(elRef1.value).toBeNull()
+  //   expect(elRef1.value).toBe(elRef2.value)
+  // })
+})
diff --git a/packages/runtime-vapor/__tests__/errorHandling.spec.ts b/packages/runtime-vapor/__tests__/errorHandling.spec.ts
new file mode 100644
index 00000000000..87a79614d41
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/errorHandling.spec.ts
@@ -0,0 +1,578 @@
+import {
+  nextTick,
+  onErrorCaptured,
+  onMounted,
+  ref,
+  watch,
+  watchEffect,
+} from '@vue/runtime-dom'
+import { createComponent, createTemplateRefSetter, template } from '../src'
+import { makeRender } from './_utils'
+import type { VaporComponent } from '../src/component'
+import type { RefEl } from '../src/apiTemplateRef'
+
+const define = makeRender()
+
+describe('error handling', () => {
+  test('propagation', () => {
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const Comp: VaporComponent = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info, 'root')
+          return false
+        })
+
+        return createComponent(Child)
+      },
+    }
+
+    const Child: VaporComponent = {
+      name: 'Child',
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info, 'child')
+        })
+        return createComponent(GrandChild)
+      },
+    }
+
+    const GrandChild: VaporComponent = {
+      setup() {
+        onMounted(() => {
+          throw err
+        })
+        return []
+      },
+    }
+
+    define(Comp).render()
+    expect(fn).toHaveBeenCalledTimes(2)
+    expect(fn).toHaveBeenCalledWith(err, 'mounted hook', 'root')
+    expect(fn).toHaveBeenCalledWith(err, 'mounted hook', 'child')
+  })
+
+  test('propagation stoppage', () => {
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const Comp: VaporComponent = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info, 'root')
+          return false
+        })
+        return createComponent(Child)
+      },
+    }
+
+    const Child: VaporComponent = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info, 'child')
+          return false
+        })
+        return createComponent(GrandChild)
+      },
+    }
+
+    const GrandChild: VaporComponent = {
+      setup() {
+        onMounted(() => {
+          throw err
+        })
+        return []
+      },
+    }
+
+    define(Comp).render()
+    expect(fn).toHaveBeenCalledTimes(1)
+    expect(fn).toHaveBeenCalledWith(err, 'mounted hook', 'child')
+  })
+
+  test('async error handling', async () => {
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const Comp: VaporComponent = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child)
+      },
+    }
+
+    const Child: VaporComponent = {
+      setup() {
+        onMounted(async () => {
+          throw err
+        })
+        return []
+      },
+    }
+
+    define(Comp).render()
+    expect(fn).not.toHaveBeenCalled()
+    await new Promise(r => setTimeout(r))
+    expect(fn).toHaveBeenCalledWith(err, 'mounted hook')
+  })
+
+  test('error thrown in onErrorCaptured', () => {
+    const err = new Error('foo')
+    const err2 = new Error('bar')
+    const fn = vi.fn()
+
+    const Comp: VaporComponent = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child)
+      },
+    }
+
+    const Child: VaporComponent = {
+      setup() {
+        onErrorCaptured(() => {
+          throw err2
+        })
+        return createComponent(GrandChild)
+      },
+    }
+
+    const GrandChild: VaporComponent = {
+      setup() {
+        onMounted(() => {
+          throw err
+        })
+        return []
+      },
+    }
+
+    define(Comp).render()
+    expect(fn).toHaveBeenCalledTimes(2)
+    expect(fn).toHaveBeenCalledWith(err, 'mounted hook')
+    expect(fn).toHaveBeenCalledWith(err2, 'errorCaptured hook')
+  })
+
+  test('setup function', () => {
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child)
+      },
+    }
+
+    const Child = {
+      setup() {
+        throw err
+      },
+    }
+
+    define(Comp).render()
+    expect(fn).toHaveBeenCalledWith(err, 'setup function')
+    expect(`returned non-block value`).toHaveBeenWarned()
+  })
+
+  test('in render function', () => {
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child)
+      },
+    }
+
+    const Child = {
+      render() {
+        throw err
+      },
+    }
+
+    define(Comp).render()
+    expect(fn).toHaveBeenCalledWith(err, 'render function')
+  })
+
+  test('in function ref', () => {
+    const err = new Error('foo')
+    const ref = () => {
+      throw err
+    }
+    const fn = vi.fn()
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child)
+      },
+    }
+
+    const Child = {
+      render() {
+        const el = template('<div>')()
+        const setRef = createTemplateRefSetter()
+        setRef(el as RefEl, ref)
+        return el
+      },
+    }
+
+    define(Comp).render()
+    expect(fn).toHaveBeenCalledWith(err, 'ref function')
+  })
+
+  test('in effect', () => {
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const Comp: VaporComponent = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child)
+      },
+    }
+
+    const Child: VaporComponent = {
+      setup() {
+        watchEffect(() => {
+          throw err
+        })
+        return []
+      },
+    }
+
+    define(Comp).render()
+    expect(fn).toHaveBeenCalledWith(err, 'watcher callback')
+  })
+
+  test('in watch getter', () => {
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child)
+      },
+    }
+
+    const Child: VaporComponent = {
+      setup() {
+        watch(
+          () => {
+            throw err
+          },
+          () => {},
+        )
+        return []
+      },
+    }
+
+    define(Comp).render()
+    expect(fn).toHaveBeenCalledWith(err, 'watcher getter')
+  })
+
+  test('in watch callback', async () => {
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child)
+      },
+    }
+
+    const count = ref(0)
+    const Child: VaporComponent = {
+      setup() {
+        watch(
+          () => count.value,
+          () => {
+            throw err
+          },
+        )
+        return []
+      },
+    }
+
+    define(Comp).render()
+
+    count.value++
+    await nextTick()
+    expect(fn).toHaveBeenCalledWith(err, 'watcher callback')
+  })
+
+  test('in effect cleanup', async () => {
+    const err = new Error('foo')
+    const count = ref(0)
+    const fn = vi.fn()
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child)
+      },
+    }
+
+    const Child: VaporComponent = {
+      setup() {
+        watchEffect(onCleanup => {
+          count.value
+          onCleanup(() => {
+            throw err
+          })
+        })
+        return []
+      },
+    }
+
+    define(Comp).render()
+
+    count.value++
+    await nextTick()
+    expect(fn).toHaveBeenCalledWith(err, 'watcher cleanup function')
+  })
+
+  test('in component event handler via emit', () => {
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child, {
+          onFoo: () => () => {
+            throw err
+          },
+        })
+      },
+    }
+
+    const Child: VaporComponent = {
+      setup(props: any, { emit }: any) {
+        emit('foo')
+        return []
+      },
+    }
+
+    define(Comp).render()
+    expect(fn).toHaveBeenCalledWith(err, 'component event handler')
+  })
+
+  test('in component event handler via emit (async)', async () => {
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child, {
+          onFoo: () => async () => {
+            throw err
+          },
+        })
+      },
+    }
+
+    const Child: VaporComponent = {
+      props: ['onFoo'],
+      setup(props: any, { emit }: any) {
+        emit('foo')
+        return []
+      },
+    }
+
+    define(Comp).render()
+    await nextTick()
+    expect(fn).toHaveBeenCalledWith(err, 'component event handler')
+  })
+
+  test('in component event handler via emit (async + array)', async () => {
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const res: Promise<any>[] = []
+    const createAsyncHandler = (p: Promise<any>) => () => {
+      res.push(p)
+      return p
+    }
+
+    const handlers = [
+      createAsyncHandler(Promise.reject(err)),
+      createAsyncHandler(Promise.resolve(1)),
+    ]
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child, {
+          onFoo: () => handlers,
+        })
+      },
+    }
+
+    const Child: VaporComponent = {
+      setup(props: any, { emit }: any) {
+        emit('foo')
+        return []
+      },
+    }
+
+    define(Comp).render()
+
+    await expect(() => Promise.all(res)).rejects.toThrowError()
+    expect(fn).toHaveBeenCalledWith(err, 'component event handler')
+  })
+
+  it('should warn unhandled', () => {
+    const groupCollapsed = vi.spyOn(console, 'groupCollapsed')
+    groupCollapsed.mockImplementation(() => {})
+    const log = vi.spyOn(console, 'log')
+    log.mockImplementation(() => {})
+
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+        })
+        return createComponent(Child)
+      },
+    }
+
+    const Child = {
+      setup() {
+        throw err
+      },
+    }
+
+    let caughtError
+    try {
+      define(Comp).render()
+    } catch (caught) {
+      caughtError = caught
+    }
+    expect(fn).toHaveBeenCalledWith(err, 'setup function')
+    expect(
+      `Unhandled error during execution of setup function`,
+    ).toHaveBeenWarned()
+    expect(caughtError).toBe(err)
+
+    groupCollapsed.mockRestore()
+    log.mockRestore()
+  })
+
+  //# 3127
+  test.fails('handle error in watch & watchEffect', async () => {
+    const error1 = new Error('error1')
+    const error2 = new Error('error2')
+    const error3 = new Error('error3')
+    const error4 = new Error('error4')
+    const handler = vi.fn()
+
+    const app = define({
+      setup() {
+        const count = ref(1)
+        watch(
+          count,
+          () => {
+            throw error1
+          },
+          { immediate: true },
+        )
+        watch(
+          count,
+          async () => {
+            throw error2
+          },
+          { immediate: true },
+        )
+        watchEffect(() => {
+          throw error3
+        })
+        watchEffect(async () => {
+          throw error4
+        })
+        return []
+      },
+    }).create()
+
+    app.app.config.errorHandler = handler
+    app.mount()
+
+    await nextTick()
+    expect(handler).toHaveBeenCalledWith(error1, {}, 'watcher callback')
+    expect(handler).toHaveBeenCalledWith(error2, {}, 'watcher callback')
+    expect(handler).toHaveBeenCalledWith(error3, {}, 'watcher callback')
+    expect(handler).toHaveBeenCalledWith(error4, {}, 'watcher callback')
+    expect(handler).toHaveBeenCalledTimes(4)
+  })
+
+  // #9574
+  test.fails('should pause tracking in error handler', async () => {
+    const error = new Error('error')
+    const x = ref(Math.random())
+
+    const handler = vi.fn(() => {
+      x.value
+      x.value = Math.random()
+    })
+
+    const app = define({
+      setup() {
+        throw error
+      },
+    }).create()
+
+    app.app.config.errorHandler = handler
+    app.mount()
+
+    await nextTick()
+    expect(handler).toHaveBeenCalledWith(error, {}, 'render function')
+    expect(handler).toHaveBeenCalledTimes(1)
+  })
+
+  // native event handler handling should be tested in respective renderers
+})
diff --git a/packages/runtime-vapor/__tests__/for.spec.ts b/packages/runtime-vapor/__tests__/for.spec.ts
new file mode 100644
index 00000000000..572c8af85ae
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/for.spec.ts
@@ -0,0 +1,1471 @@
+import {
+  child,
+  createFor,
+  createIf,
+  getDefaultValue,
+  getRestElement,
+  renderEffect,
+  setClass,
+  setInsertionState,
+  setStyle,
+  setText,
+  template,
+} from '../src'
+import {
+  type Ref,
+  nextTick,
+  reactive,
+  readonly,
+  ref,
+  shallowRef,
+  toDisplayString,
+  triggerRef,
+} from '@vue/runtime-dom'
+import { makeRender, shuffle } from './_utils'
+
+const define = makeRender()
+
+describe('createFor', () => {
+  test('array source', async () => {
+    const list = ref([{ name: '1' }, { name: '2' }, { name: '3' }])
+    function reverse() {
+      list.value = list.value.reverse()
+    }
+
+    const { host } = define(() => {
+      const n1 = createFor(
+        () => list.value,
+        (item, key, index) => {
+          const span = document.createElement('li')
+          renderEffect(() => {
+            span.innerHTML = `${key.value}. ${item.value.name}`
+
+            // index should be undefined if source is not an object
+            expect(index.value).toBe(undefined)
+          })
+          return span
+        },
+        item => item.name,
+      )
+      return n1
+    }).render()
+
+    expect(host.innerHTML).toBe(
+      '<li>0. 1</li><li>1. 2</li><li>2. 3</li><!--for-->',
+    )
+
+    // add
+    list.value.push({ name: '4' })
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. 1</li><li>1. 2</li><li>2. 3</li><li>3. 4</li><!--for-->',
+    )
+
+    // move
+    reverse()
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. 4</li><li>1. 3</li><li>2. 2</li><li>3. 1</li><!--for-->',
+    )
+
+    reverse()
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. 1</li><li>1. 2</li><li>2. 3</li><li>3. 4</li><!--for-->',
+    )
+
+    // change
+    list.value[0].name = 'a'
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. a</li><li>1. 2</li><li>2. 3</li><li>3. 4</li><!--for-->',
+    )
+
+    // remove
+    list.value.splice(1, 1)
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. a</li><li>1. 3</li><li>2. 4</li><!--for-->',
+    )
+
+    // clear
+    list.value = []
+    await nextTick()
+    expect(host.innerHTML).toBe('<!--for-->')
+  })
+
+  test('number source', async () => {
+    const count = ref(3)
+
+    const { host } = define(() => {
+      const n1 = createFor(
+        () => count.value,
+        (item, key, index) => {
+          const span = document.createElement('li')
+          renderEffect(() => {
+            span.innerHTML = `${key.value}. ${item.value}`
+
+            // index should be undefined if source is not an object
+            expect(index.value).toBe(undefined)
+          })
+          return span
+        },
+        item => item,
+      )
+      return n1
+    }).render()
+
+    expect(host.innerHTML).toBe(
+      '<li>0. 1</li><li>1. 2</li><li>2. 3</li><!--for-->',
+    )
+
+    // add
+    count.value = 4
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. 1</li><li>1. 2</li><li>2. 3</li><li>3. 4</li><!--for-->',
+    )
+
+    // remove
+    count.value = 2
+    await nextTick()
+    expect(host.innerHTML).toBe('<li>0. 1</li><li>1. 2</li><!--for-->')
+
+    // clear
+    count.value = 0
+    await nextTick()
+    expect(host.innerHTML).toBe('<!--for-->')
+  })
+
+  test('object source', async () => {
+    const initial = () => ({ a: 1, b: 2, c: 3 })
+    const data = ref<Record<string, number>>(initial())
+
+    const { host } = define(() => {
+      const n1 = createFor(
+        () => data.value,
+        (item, key, index) => {
+          const span = document.createElement('li')
+          renderEffect(() => {
+            span.innerHTML = `${key.value}${index.value}. ${item.value}`
+            expect(index.value).not.toBe(undefined)
+          })
+          return span
+        },
+        item => {
+          return item
+        },
+      )
+      return n1
+    }).render()
+
+    expect(host.innerHTML).toBe(
+      '<li>a0. 1</li><li>b1. 2</li><li>c2. 3</li><!--for-->',
+    )
+
+    // move
+    data.value = {
+      c: 3,
+      b: 2,
+      a: 1,
+    }
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>c0. 3</li><li>b1. 2</li><li>a2. 1</li><!--for-->',
+    )
+
+    // add
+    data.value.d = 4
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>c0. 3</li><li>b1. 2</li><li>a2. 1</li><li>d3. 4</li><!--for-->',
+    )
+
+    // change
+    data.value.b = 100
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>c0. 3</li><li>b1. 100</li><li>a2. 1</li><li>d3. 4</li><!--for-->',
+    )
+
+    // remove
+    delete data.value.c
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>b0. 100</li><li>a1. 1</li><li>d2. 4</li><!--for-->',
+    )
+
+    // clear
+    data.value = {}
+    await nextTick()
+    expect(host.innerHTML).toBe('<!--for-->')
+  })
+
+  test('de-structured value', async () => {
+    const list = ref([{ name: '1' }, { name: '2' }, { name: '3' }])
+    function reverse() {
+      list.value = list.value.reverse()
+    }
+
+    const { host } = define(() => {
+      const n1 = createFor(
+        () => list.value,
+        (item, key, index) => {
+          const span = document.createElement('li')
+          renderEffect(() => {
+            // compiler rewrites { name } destructure to inline access
+            span.innerHTML = `${key.value}. ${item.value.name}`
+            // index should be undefined if source is not an object
+            expect(index.value).toBe(undefined)
+          })
+          return span
+        },
+        item => item.name,
+      )
+      return n1
+    }).render()
+
+    expect(host.innerHTML).toBe(
+      '<li>0. 1</li><li>1. 2</li><li>2. 3</li><!--for-->',
+    )
+
+    // add
+    list.value.push({ name: '4' })
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. 1</li><li>1. 2</li><li>2. 3</li><li>3. 4</li><!--for-->',
+    )
+
+    // move
+    reverse()
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. 4</li><li>1. 3</li><li>2. 2</li><li>3. 1</li><!--for-->',
+    )
+
+    reverse()
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. 1</li><li>1. 2</li><li>2. 3</li><li>3. 4</li><!--for-->',
+    )
+
+    // change
+    list.value[0].name = 'a'
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. a</li><li>1. 2</li><li>2. 3</li><li>3. 4</li><!--for-->',
+    )
+
+    // remove
+    list.value.splice(1, 1)
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. a</li><li>1. 3</li><li>2. 4</li><!--for-->',
+    )
+
+    // clear
+    list.value = []
+    await nextTick()
+    expect(host.innerHTML).toBe('<!--for-->')
+  })
+
+  test('de-structured value (rest element)', async () => {
+    const list = ref([
+      { name: '1', a: 1 },
+      { name: '2', a: 2 },
+      { name: '3', a: 3 },
+    ])
+    function reverse() {
+      list.value = list.value.reverse()
+    }
+
+    const { host } = define(() => {
+      const n1 = createFor(
+        () => list.value,
+        (item, _key, index) => {
+          const span = document.createElement('li')
+          renderEffect(() => {
+            span.innerHTML = JSON.stringify(
+              getRestElement(item.value, ['name']),
+            )
+            // index should be undefined if source is not an object
+            expect(index.value).toBe(undefined)
+          })
+          return span
+        },
+        item => item.name,
+      )
+      return n1
+    }).render()
+
+    expect(host.innerHTML).toBe(
+      '<li>{"a":1}</li><li>{"a":2}</li><li>{"a":3}</li><!--for-->',
+    )
+
+    // add
+    list.value.push({ name: '4', a: 4 })
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>{"a":1}</li><li>{"a":2}</li><li>{"a":3}</li><li>{"a":4}</li><!--for-->',
+    )
+
+    // move
+    reverse()
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>{"a":4}</li><li>{"a":3}</li><li>{"a":2}</li><li>{"a":1}</li><!--for-->',
+    )
+
+    reverse()
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>{"a":1}</li><li>{"a":2}</li><li>{"a":3}</li><li>{"a":4}</li><!--for-->',
+    )
+
+    // change
+    list.value[0].a = 5
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>{"a":5}</li><li>{"a":2}</li><li>{"a":3}</li><li>{"a":4}</li><!--for-->',
+    )
+
+    // remove
+    list.value.splice(1, 1)
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>{"a":5}</li><li>{"a":3}</li><li>{"a":4}</li><!--for-->',
+    )
+
+    // clear
+    list.value = []
+    await nextTick()
+    expect(host.innerHTML).toBe('<!--for-->')
+  })
+
+  test('de-structured value (default value)', async () => {
+    const list = ref<any[]>([{ name: '1' }, { name: '2' }, { name: '3' }])
+
+    const { host } = define(() => {
+      const n1 = createFor(
+        () => list.value,
+        (item, _key, index) => {
+          const span = document.createElement('li')
+          renderEffect(() => {
+            span.innerHTML = getDefaultValue(item.value.x, '0')
+            // index should be undefined if source is not an object
+            expect(index.value).toBe(undefined)
+          })
+          return span
+        },
+        item => item.name,
+      )
+      return n1
+    }).render()
+
+    expect(host.innerHTML).toBe('<li>0</li><li>0</li><li>0</li><!--for-->')
+
+    // change
+    list.value[0].x = 5
+    await nextTick()
+    expect(host.innerHTML).toBe('<li>5</li><li>0</li><li>0</li><!--for-->')
+
+    // clear
+    list.value = []
+    await nextTick()
+    expect(host.innerHTML).toBe('<!--for-->')
+  })
+
+  test('shallowRef source', async () => {
+    const list = shallowRef([{ name: '1' }, { name: '2' }, { name: '3' }])
+    const setList = (update = list.value.slice()) => (list.value = update)
+    function reverse() {
+      list.value = list.value.reverse()
+    }
+
+    const { host } = define(() => {
+      const n1 = createFor(
+        () => list.value,
+        (item, key, index) => {
+          const span = document.createElement('li')
+          renderEffect(() => {
+            span.innerHTML = `${key.value}. ${item.value.name}`
+
+            // index should be undefined if source is not an object
+            expect(index.value).toBe(undefined)
+          })
+          return span
+        },
+      )
+      return n1
+    }).render()
+
+    expect(host.innerHTML).toBe(
+      '<li>0. 1</li><li>1. 2</li><li>2. 3</li><!--for-->',
+    )
+
+    // add
+    list.value.push({ name: '4' })
+    setList()
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. 1</li><li>1. 2</li><li>2. 3</li><li>3. 4</li><!--for-->',
+    )
+
+    // move
+    reverse()
+    setList()
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. 4</li><li>1. 3</li><li>2. 2</li><li>3. 1</li><!--for-->',
+    )
+
+    reverse()
+    setList()
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. 1</li><li>1. 2</li><li>2. 3</li><li>3. 4</li><!--for-->',
+    )
+
+    // change deep value should not update
+    list.value[0].name = 'a'
+    setList()
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. 1</li><li>1. 2</li><li>2. 3</li><li>3. 4</li><!--for-->',
+    )
+
+    // remove
+    list.value.splice(1, 1)
+    setList()
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. 1</li><li>1. 3</li><li>2. 4</li><!--for-->',
+    )
+
+    // clear
+    setList([])
+    await nextTick()
+    expect(host.innerHTML).toBe('<!--for-->')
+  })
+
+  test('should optimize call frequency during list operations', async () => {
+    let sourceCalledTimes = 0
+    let renderCalledTimes = 0
+    let effectLabelCalledTimes = 0
+    let effectIndexCalledTimes = 0
+
+    const resetCounter = () => {
+      sourceCalledTimes = 0
+      renderCalledTimes = 0
+      effectLabelCalledTimes = 0
+      effectIndexCalledTimes = 0
+    }
+    const expectCalledTimesToBe = (
+      message: string,
+      source: number,
+      render: number,
+      label: number,
+      index: number,
+    ) => {
+      expect(
+        {
+          source: sourceCalledTimes,
+          render: renderCalledTimes,
+          label: effectLabelCalledTimes,
+          index: effectIndexCalledTimes,
+        },
+        message,
+      ).toEqual({ source, render, label, index })
+      resetCounter()
+    }
+
+    const createItem = (
+      (id = 0) =>
+      (label = id) => ({ id: id++, label })
+    )()
+    const createItems = (length: number) =>
+      Array.from({ length }, (_, i) => createItem(i))
+    const list = ref(createItems(100))
+    const length = () => list.value.length
+
+    define(() => {
+      const n1 = createFor(
+        () => (++sourceCalledTimes, list.value),
+        (item, index) => {
+          ++renderCalledTimes
+          const span = document.createElement('li')
+          renderEffect(() => {
+            ++effectLabelCalledTimes
+            item.value.label
+          })
+          renderEffect(() => {
+            ++effectIndexCalledTimes
+            index.value
+          })
+          return span
+        },
+        item => item.id,
+      )
+      return n1
+    }).render()
+
+    // Create rows
+    expectCalledTimesToBe('Create rows', 1, length(), length(), length())
+
+    // Update every 10th row
+    for (let i = 0; i < length(); i += 10) {
+      list.value[i].label += 10000
+    }
+    await nextTick()
+    expectCalledTimesToBe('Update every 10th row', 0, 0, length() / 10, 0)
+
+    // Append rows
+    list.value.push(...createItems(100))
+    await nextTick()
+    expectCalledTimesToBe('Append rows', 1, 100, 100, 100)
+
+    // Inserts rows at the beginning
+    const tempLen = length()
+    list.value.unshift(...createItems(100))
+    await nextTick()
+    expectCalledTimesToBe(
+      'Inserts rows at the beginning',
+      1,
+      100,
+      100,
+      100 + tempLen,
+    )
+
+    // Inserts rows in the middle
+    const middleIdx = length() / 2
+    list.value.splice(middleIdx, 0, ...createItems(100))
+    await nextTick()
+    expectCalledTimesToBe(
+      'Inserts rows in the middle',
+      1,
+      100,
+      100,
+      100 + middleIdx,
+    )
+
+    // Swap rows
+    const temp = list.value[1]
+    list.value[1] = list.value[length() - 2]
+    list.value[length() - 2] = temp
+    await nextTick()
+    expectCalledTimesToBe('Swap rows', 1, 0, 0, 2)
+
+    // Remove rows
+    list.value.splice(1, 1)
+    list.value.splice(length() - 2, 1)
+    await nextTick()
+    expectCalledTimesToBe('Remove rows', 1, 0, 0, length() - 1)
+
+    // Clear rows
+    list.value = []
+    await nextTick()
+    expectCalledTimesToBe('Clear rows', 1, 0, 0, 0)
+  })
+
+  test('should optimize call frequency during list operations with shallowRef', async () => {
+    let sourceCalledTimes = 0
+    let renderCalledTimes = 0
+    let effectLabelCalledTimes = 0
+    let effectIndexCalledTimes = 0
+
+    const resetCounter = () => {
+      sourceCalledTimes = 0
+      renderCalledTimes = 0
+      effectLabelCalledTimes = 0
+      effectIndexCalledTimes = 0
+    }
+    const expectCalledTimesToBe = (
+      message: string,
+      source: number,
+      render: number,
+      label: number,
+      index: number,
+    ) => {
+      expect(
+        {
+          source: sourceCalledTimes,
+          render: renderCalledTimes,
+          label: effectLabelCalledTimes,
+          index: effectIndexCalledTimes,
+        },
+        message,
+      ).toEqual({ source, render, label, index })
+      resetCounter()
+    }
+
+    const createItem = (
+      (id = 0) =>
+      (label = id) => ({ id: id++, label: shallowRef(label) })
+    )()
+    const createItems = (length: number) =>
+      Array.from({ length }, (_, i) => createItem(i))
+    const list = shallowRef(createItems(100))
+    const length = () => list.value.length
+
+    define(() => {
+      const n1 = createFor(
+        () => (++sourceCalledTimes, list.value),
+        (item, index) => {
+          ++renderCalledTimes
+          const span = document.createElement('li')
+          renderEffect(() => {
+            ++effectLabelCalledTimes
+            item.value.label.value
+          })
+          renderEffect(() => {
+            ++effectIndexCalledTimes
+            index.value
+          })
+          return span
+        },
+        item => item.id,
+      )
+      return n1
+    }).render()
+
+    // Create rows
+    expectCalledTimesToBe('Create rows', 1, length(), length(), length())
+
+    // Update every 10th row
+    for (let i = 0; i < length(); i += 10) {
+      list.value[i].label.value += 10000
+    }
+    await nextTick()
+    expectCalledTimesToBe('Update every 10th row', 0, 0, length() / 10, 0)
+
+    // Append rows
+    list.value.push(...createItems(100))
+    triggerRef(list)
+    await nextTick()
+    expectCalledTimesToBe('Append rows', 1, 100, 100, 100)
+
+    // Inserts rows at the beginning
+    const tempLen = length()
+    list.value.unshift(...createItems(100))
+    triggerRef(list)
+    await nextTick()
+    expectCalledTimesToBe(
+      'Inserts rows at the beginning',
+      1,
+      100,
+      100,
+      100 + tempLen,
+    )
+
+    // Inserts rows in the middle
+    const middleIdx = length() / 2
+    list.value.splice(middleIdx, 0, ...createItems(100))
+    triggerRef(list)
+    await nextTick()
+    expectCalledTimesToBe(
+      'Inserts rows in the middle',
+      1,
+      100,
+      100,
+      100 + middleIdx,
+    )
+
+    // Swap rows
+    const temp = list.value[1]
+    list.value[1] = list.value[length() - 2]
+    list.value[length() - 2] = temp
+    triggerRef(list)
+    await nextTick()
+    expectCalledTimesToBe('Swap rows', 1, 0, 0, 2)
+
+    // Remove rows
+    list.value.splice(1, 1)
+    list.value.splice(length() - 2, 1)
+    triggerRef(list)
+    await nextTick()
+    expectCalledTimesToBe('Remove rows', 1, 0, 0, length() - 1)
+
+    // Clear rows
+    list.value = []
+    await nextTick()
+    expectCalledTimesToBe('Clear rows', 1, 0, 0, 0)
+  })
+
+  describe('readonly source', () => {
+    test('should not allow mutation', () => {
+      const arr = readonly(reactive([{ foo: 1 }]))
+
+      const { host } = define(() => {
+        const n1 = createFor(
+          () => arr,
+          (item, key, index) => {
+            const span = document.createElement('li')
+            renderEffect(() => {
+              item.value.foo = 0
+              span.innerHTML = `${item.value.foo}`
+            })
+            return span
+          },
+          idx => idx,
+        )
+        return n1
+      }).render()
+
+      expect(host.innerHTML).toBe('<li>1</li><!--for-->')
+      expect(
+        `Set operation on key "foo" failed: target is readonly.`,
+      ).toHaveBeenWarned()
+    })
+
+    test('should trigger effect for deep mutations', async () => {
+      const arr = reactive([{ foo: 1 }])
+      const readonlyArr = readonly(arr)
+
+      const { host } = define(() => {
+        const n1 = createFor(
+          () => readonlyArr,
+          (item, key, index) => {
+            const span = document.createElement('li')
+            renderEffect(() => {
+              span.innerHTML = `${item.value.foo}`
+            })
+            return span
+          },
+          idx => idx,
+        )
+        return n1
+      }).render()
+
+      expect(host.innerHTML).toBe('<li>1</li><!--for-->')
+
+      arr[0].foo = 2
+      await nextTick()
+      expect(host.innerHTML).toBe('<li>2</li><!--for-->')
+    })
+  })
+
+  // ported from packages/runtime-core/__tests__/rendererChildren.spec.ts
+  describe('renderer: keyed children', () => {
+    const render = (arr: Ref<number[]>) => {
+      return define({
+        setup() {
+          const n0 = createFor(
+            () => arr.value,
+            _for_item0 => {
+              const n2 = template('<span> </span>', true)() as any
+              const x2 = child(n2) as any
+              renderEffect(() => setText(x2, toDisplayString(_for_item0.value)))
+              return n2
+            },
+            item => item,
+          )
+          return n0
+        },
+      }).render()
+    }
+
+    test('append', async () => {
+      const arr = ref<number[]>([1])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(1)
+      expect(html()).toBe('<span>1</span><!--for-->')
+
+      arr.value = [1, 2, 3]
+      await nextTick()
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        '<span>1</span><span>2</span><span>3</span><!--for-->',
+      )
+    })
+
+    test('prepend', async () => {
+      const arr = ref<number[]>([4, 5])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(2)
+      expect(html()).toBe('<span>4</span><span>5</span><!--for-->')
+
+      arr.value = [1, 2, 3, 4, 5]
+      await nextTick()
+      expect(host.children.length).toBe(5)
+      expect(html()).toBe(
+        `<span>1</span>` +
+          `<span>2</span>` +
+          `<span>3</span>` +
+          `<span>4</span>` +
+          `<span>5</span>` +
+          `<!--for-->`,
+      )
+    })
+
+    test('insert in middle', async () => {
+      const arr = ref<number[]>([1, 2, 4, 5])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(4)
+      expect(html()).toBe(
+        '<span>1</span><span>2</span><span>4</span><span>5</span><!--for-->',
+      )
+
+      arr.value = [1, 2, 3, 4, 5]
+      await nextTick()
+      expect(host.children.length).toBe(5)
+      expect(html()).toBe(
+        `<span>1</span>` +
+          `<span>2</span>` +
+          `<span>3</span>` +
+          `<span>4</span>` +
+          `<span>5</span>` +
+          `<!--for-->`,
+      )
+    })
+
+    test('insert at beginning and end', async () => {
+      const arr = ref<number[]>([2, 3, 4])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        `<span>2</span><span>3</span><span>4</span><!--for-->`,
+      )
+
+      arr.value = [1, 2, 3, 4, 5]
+      await nextTick()
+      expect(host.children.length).toBe(5)
+      expect(html()).toBe(
+        `<span>1</span>` +
+          `<span>2</span>` +
+          `<span>3</span>` +
+          `<span>4</span>` +
+          `<span>5</span>` +
+          `<!--for-->`,
+      )
+    })
+
+    test('insert to empty parent', async () => {
+      const arr = ref<number[]>([])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(0)
+      expect(html()).toBe('<!--for-->')
+
+      arr.value = [1, 2, 3, 4, 5]
+      await nextTick()
+      expect(host.children.length).toBe(5)
+      expect(html()).toBe(
+        `<span>1</span>` +
+          `<span>2</span>` +
+          `<span>3</span>` +
+          `<span>4</span>` +
+          `<span>5</span>` +
+          `<!--for-->`,
+      )
+    })
+
+    test('remove all children from parent', async () => {
+      const arr = ref<number[]>([1, 2, 3, 4, 5])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(5)
+      expect(html()).toBe(
+        `<span>1</span>` +
+          `<span>2</span>` +
+          `<span>3</span>` +
+          `<span>4</span>` +
+          `<span>5</span>` +
+          `<!--for-->`,
+      )
+
+      arr.value = []
+      await nextTick()
+      expect(host.children.length).toBe(0)
+      expect(html()).toBe('<!--for-->')
+    })
+
+    test('remove from beginning', async () => {
+      const arr = ref<number[]>([1, 2, 3, 4, 5])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(5)
+      expect(html()).toBe(
+        `<span>1</span>` +
+          `<span>2</span>` +
+          `<span>3</span>` +
+          `<span>4</span>` +
+          `<span>5</span>` +
+          `<!--for-->`,
+      )
+
+      arr.value = [3, 4, 5]
+      await nextTick()
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        `<span>3</span><span>4</span><span>5</span><!--for-->`,
+      )
+    })
+
+    test('remove from end', async () => {
+      const arr = ref<number[]>([1, 2, 3, 4, 5])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(5)
+      expect(html()).toBe(
+        `<span>1</span>` +
+          `<span>2</span>` +
+          `<span>3</span>` +
+          `<span>4</span>` +
+          `<span>5</span>` +
+          `<!--for-->`,
+      )
+
+      arr.value = [1, 2, 3]
+      await nextTick()
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        `<span>1</span><span>2</span><span>3</span><!--for-->`,
+      )
+    })
+
+    test('remove from middle', async () => {
+      const arr = ref<number[]>([1, 2, 3, 4, 5])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(5)
+      expect(html()).toBe(
+        `<span>1</span>` +
+          `<span>2</span>` +
+          `<span>3</span>` +
+          `<span>4</span>` +
+          `<span>5</span>` +
+          `<!--for-->`,
+      )
+
+      arr.value = [1, 2, 4, 5]
+      await nextTick()
+      expect(host.children.length).toBe(4)
+      expect(html()).toBe(
+        `<span>1</span><span>2</span><span>4</span><span>5</span><!--for-->`,
+      )
+    })
+
+    test('remove from beginning and insert at end', async () => {
+      const arr = ref<number[]>([1, 2, 3])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        `<span>1</span><span>2</span><span>3</span><!--for-->`,
+      )
+
+      arr.value = [2, 3, 4]
+      await nextTick()
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        `<span>2</span><span>3</span><span>4</span><!--for-->`,
+      )
+    })
+
+    test('moving single child forward', async () => {
+      const arr = ref<number[]>([1, 2, 3, 4])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(4)
+      expect(html()).toBe(
+        `<span>1</span>` +
+          `<span>2</span>` +
+          `<span>3</span>` +
+          `<span>4</span>` +
+          `<!--for-->`,
+      )
+
+      arr.value = [2, 3, 1, 4]
+      await nextTick()
+      expect(host.children.length).toBe(4)
+      expect(html()).toBe(
+        `<span>2</span><span>3</span><span>1</span><span>4</span><!--for-->`,
+      )
+    })
+
+    test('moving single child backwards', async () => {
+      const arr = ref<number[]>([1, 2, 3, 4])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(4)
+      expect(html()).toBe(
+        `<span>1</span>` +
+          `<span>2</span>` +
+          `<span>3</span>` +
+          `<span>4</span>` +
+          `<!--for-->`,
+      )
+
+      arr.value = [1, 4, 2, 3]
+      await nextTick()
+      expect(host.children.length).toBe(4)
+      expect(html()).toBe(
+        `<span>1</span><span>4</span><span>2</span><span>3</span><!--for-->`,
+      )
+    })
+
+    test('moving single child to end', async () => {
+      const arr = ref<number[]>([1, 2, 3])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        `<span>1</span><span>2</span><span>3</span><!--for-->`,
+      )
+
+      arr.value = [2, 3, 1]
+      await nextTick()
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        `<span>2</span><span>3</span><span>1</span><!--for-->`,
+      )
+    })
+
+    test('swap first and last', async () => {
+      const arr = ref<number[]>([1, 2, 3, 4])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(4)
+      expect(html()).toBe(
+        `<span>1</span><span>2</span><span>3</span><span>4</span><!--for-->`,
+      )
+
+      arr.value = [4, 2, 3, 1]
+      await nextTick()
+      expect(host.children.length).toBe(4)
+      expect(html()).toBe(
+        `<span>4</span><span>2</span><span>3</span><span>1</span><!--for-->`,
+      )
+    })
+
+    test('move to left & replace', async () => {
+      const arr = ref<number[]>([1, 2, 3, 4, 5])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(5)
+      expect(html()).toBe(
+        `<span>1</span><span>2</span><span>3</span><span>4</span><span>5</span><!--for-->`,
+      )
+
+      arr.value = [4, 1, 2, 3, 6]
+      await nextTick()
+      expect(host.children.length).toBe(5)
+      expect(html()).toBe(
+        `<span>4</span><span>1</span><span>2</span><span>3</span><span>6</span><!--for-->`,
+      )
+    })
+
+    test('move to left and leaves hold', async () => {
+      const arr = ref<number[]>([1, 4, 5])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        `<span>1</span><span>4</span><span>5</span><!--for-->`,
+      )
+
+      arr.value = [4, 6]
+      await nextTick()
+      expect(host.children.length).toBe(2)
+      expect(html()).toBe(`<span>4</span><span>6</span><!--for-->`)
+    })
+
+    test('moved and set to undefined element ending at the end', async () => {
+      const arr = ref<number[]>([2, 4, 5])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        `<span>2</span><span>4</span><span>5</span><!--for-->`,
+      )
+
+      arr.value = [4, 5, 3]
+      await nextTick()
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        `<span>4</span><span>5</span><span>3</span><!--for-->`,
+      )
+    })
+
+    test('reverse element', async () => {
+      const arr = ref<number[]>([1, 2, 3, 4, 5, 6, 7, 8])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(8)
+      expect(html()).toBe(
+        `<span>1</span><span>2</span><span>3</span><span>4</span>` +
+          `<span>5</span><span>6</span><span>7</span><span>8</span><!--for-->`,
+      )
+
+      arr.value = [8, 7, 6, 5, 4, 3, 2, 1]
+      await nextTick()
+      expect(host.children.length).toBe(8)
+      expect(html()).toBe(
+        `<span>8</span><span>7</span><span>6</span><span>5</span>` +
+          `<span>4</span><span>3</span><span>2</span><span>1</span><!--for-->`,
+      )
+    })
+
+    test('something', async () => {
+      const arr = ref<number[]>([0, 1, 2, 3, 4, 5])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(6)
+      expect(html()).toBe(
+        `<span>0</span><span>1</span><span>2</span><span>3</span><span>4</span>` +
+          `<span>5</span><!--for-->`,
+      )
+
+      arr.value = [4, 3, 2, 1, 5, 0]
+      await nextTick()
+      expect(host.children.length).toBe(6)
+      expect(html()).toBe(
+        `<span>4</span><span>3</span><span>2</span><span>1</span>` +
+          `<span>5</span><span>0</span><!--for-->`,
+      )
+    })
+
+    test('random shuffle', async () => {
+      const elms = 14
+      const samples = 5
+
+      for (let n = 0; n < samples; ++n) {
+        const arr = ref([...Array(elms).keys()])
+        const opacities = ref<string[]>([])
+
+        const host = document.createElement('div')
+        define({
+          setup() {
+            const n3 = template('<span></span>')() as any
+            setInsertionState(n3)
+            createFor(
+              () => arr.value,
+              _for_item0 => {
+                const n2 = template('<span> </span>')() as any
+                const x2 = child(n2) as any
+                renderEffect(() => {
+                  const _opacities = opacities.value
+                  const _item = _for_item0.value
+                  const _opacities_item = _opacities[_item] || '1'
+                  setStyle(n2, { opacity: _opacities_item })
+                  setText(x2, toDisplayString(_item))
+                })
+                return n2
+              },
+              item => item,
+              1,
+            )
+            return n3
+          },
+        }).render(undefined, host)
+
+        let rootChild = host.children[0]
+        for (let i = 0; i < elms; ++i) {
+          const child = rootChild.children[i] as HTMLSpanElement
+          expect(child.innerHTML).toBe(i.toString())
+          expect(child.style.opacity).toBe('1')
+          opacities.value[i] = Math.random().toFixed(5).toString()
+        }
+
+        await nextTick()
+        for (let i = 0; i < elms; ++i) {
+          const child = rootChild.children[i] as any
+          expect(child.$sty.opacity).toBe(opacities.value[i])
+        }
+
+        arr.value = shuffle(arr.value.slice(0))
+        await nextTick()
+
+        for (let i = 0; i < elms; ++i) {
+          const child = rootChild.children[i] as any
+          expect(child.innerHTML).toBe(arr.value[i].toString())
+          expect(child.$sty.opacity).toBe(opacities.value[arr.value[i]])
+        }
+      }
+    })
+
+    test('children with the same key but with different tag', async () => {
+      const items = ref([
+        { key: 1, tag: 'div', text: 'one' },
+        { key: 2, tag: 'div', text: 'two' },
+        { key: 3, tag: 'div', text: 'three' },
+        { key: 4, tag: 'div', text: 'four' },
+      ])
+
+      const { host } = define(() => {
+        return createFor(
+          () => items.value,
+          _for_item0 => {
+            const n2 = createIf(
+              () => _for_item0.value.tag === 'div',
+              () => {
+                const n4 = template('<div> </div>')() as any
+                const x4 = child(n4) as any
+                renderEffect(() =>
+                  setText(x4, toDisplayString(_for_item0.value.text)),
+                )
+                return n4
+              },
+              () => {
+                const n6 = template('<span> </span>', true)() as any
+                const x6 = child(n6) as any
+                renderEffect(() =>
+                  setText(x6, toDisplayString(_for_item0.value.text)),
+                )
+                return n6
+              },
+            )
+            return n2
+          },
+          item => item.key,
+        )
+      }).render()
+
+      expect(host.children.length).toBe(4)
+      expect(
+        Array.from(host.children).map(c =>
+          (c as HTMLElement).tagName.toLowerCase(),
+        ),
+      ).toEqual(['div', 'div', 'div', 'div'])
+      expect(Array.from(host.children).map(c => c.textContent)).toEqual([
+        'one',
+        'two',
+        'three',
+        'four',
+      ])
+
+      items.value = [
+        { key: 4, tag: 'div', text: 'four' },
+        { key: 3, tag: 'span', text: 'three' },
+        { key: 2, tag: 'span', text: 'two' },
+        { key: 1, tag: 'div', text: 'one' },
+      ]
+      await nextTick()
+
+      expect(host.children.length).toBe(4)
+      expect(
+        Array.from(host.children).map(c =>
+          (c as HTMLElement).tagName.toLowerCase(),
+        ),
+      ).toEqual(['div', 'span', 'span', 'div'])
+      expect(Array.from(host.children).map(c => c.textContent)).toEqual([
+        'four',
+        'three',
+        'two',
+        'one',
+      ])
+    })
+
+    test('children with the same tag, same key, but one with data and one without data', async () => {
+      const items = ref([{ key: 1, text: 'one', className: 'hi' }])
+      const { host } = define(() => {
+        const n1 = createFor(
+          () => items.value,
+          _for_item0 => {
+            const n2 = template('<div> </div>')() as any
+            const x2 = child(n2) as any
+
+            renderEffect(() => {
+              const _item = _for_item0.value
+              setText(x2, _item.text)
+
+              if (_item.className) {
+                setClass(n2, _item.className)
+              } else {
+                setClass(n2, null)
+              }
+            })
+
+            return n2
+          },
+          item => item.key,
+        )
+        return n1
+      }).render()
+
+      expect(host.children.length).toBe(1)
+      const firstChild = host.children[0] as HTMLDivElement
+      expect(firstChild.textContent?.trim()).toBe('one')
+      expect(firstChild.className).toBe('hi')
+
+      items.value = [{ key: 1, text: 'four', className: null as any }]
+      await nextTick()
+
+      expect(host.children.length).toBe(1)
+      const updatedChild = host.children[0] as HTMLDivElement
+      expect(updatedChild.textContent?.trim()).toBe('four')
+      expect(updatedChild.className).toBe('')
+
+      expect(updatedChild).toBe(firstChild)
+    })
+
+    test('should warn with duplicate keys', async () => {
+      const arr = ref<number[]>([1, 2, 3])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        `<span>1</span><span>2</span><span>3</span><!--for-->`,
+      )
+
+      arr.value = [1, 2, 2]
+      await nextTick()
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        `<span>1</span><span>2</span><span>2</span><!--for-->`,
+      )
+      expect(`Duplicate keys`).toHaveBeenWarned()
+    })
+  })
+
+  describe('renderer: unkeyed children', () => {
+    const render = (arr: Ref<number[]>) => {
+      return define({
+        setup() {
+          const n0 = createFor(
+            () => arr.value,
+            _for_item0 => {
+              const n2 = template('<span> </span>', true)() as any
+              const x2 = child(n2) as any
+              renderEffect(() => setText(x2, toDisplayString(_for_item0.value)))
+              return n2
+            },
+          )
+          return n0
+        },
+      }).render()
+    }
+
+    test('move a key in non-keyed nodes with a size up', async () => {
+      const arr = ref<any[]>([1, 'a', 'b', 'c'])
+      const { host, html } = define({
+        setup() {
+          const n0 = createFor(
+            () => arr.value,
+            _for_item0 => {
+              const n2 = template('<span> </span>', true)() as any
+              const x2 = child(n2) as any
+              renderEffect(() => setText(x2, toDisplayString(_for_item0.value)))
+              return n2
+            },
+            item => (typeof item === 'number' ? item : undefined),
+          )
+          return n0
+        },
+      }).render()
+
+      expect(host.children.length).toBe(4)
+      expect(html()).toBe(
+        `<span>1</span><span>a</span><span>b</span><span>c</span><!--for-->`,
+      )
+
+      arr.value = ['d', 'a', 'b', 'c', 1, 'e']
+      await nextTick()
+      expect(host.children.length).toBe(6)
+      expect(html()).toBe(
+        `<span>d</span><span>a</span><span>b</span><span>c</span>` +
+          `<span>1</span><span>e</span><!--for-->`,
+      )
+    })
+
+    test('append elements with updating children without keys', async () => {
+      const arr = ref<string[]>(['hello'])
+      const { html } = render(arr as any)
+      expect(html()).toBe('<span>hello</span><!--for-->')
+
+      arr.value = ['hello', 'world']
+      await nextTick()
+      expect(html()).toBe('<span>hello</span><span>world</span><!--for-->')
+    })
+
+    test('updating children without keys', async () => {
+      const arr = ref<string[]>(['hello', 'foo'])
+      const { html } = render(arr as any)
+      expect(html()).toBe('<span>hello</span><span>foo</span><!--for-->')
+
+      arr.value = ['hello', 'bar']
+      await nextTick()
+      expect(html()).toBe('<span>hello</span><span>bar</span><!--for-->')
+
+      arr.value = ['world', 'bar']
+      await nextTick()
+      expect(html()).toBe('<span>world</span><span>bar</span><!--for-->')
+    })
+
+    test('prepend element with updating children without keys', async () => {
+      const arr = ref<string[]>(['foo', 'bar'])
+      const { html } = render(arr as any)
+      expect(html()).toBe('<span>foo</span><span>bar</span><!--for-->')
+
+      arr.value = ['hello', 'foo', 'bar']
+      await nextTick()
+      expect(html()).toBe(
+        '<span>hello</span><span>foo</span><span>bar</span><!--for-->',
+      )
+    })
+
+    test('prepend element of different tag type with updating children without keys', async () => {
+      const items = ref([{ tag: 'span', text: 'world' }])
+      const { host, html } = define(() => {
+        return createFor(
+          () => items.value,
+          _for_item0 => {
+            const n2 = createIf(
+              () => _for_item0.value.tag === 'div',
+              () => {
+                const n4 = template('<div> </div>')() as any
+                const x4 = child(n4) as any
+                renderEffect(() =>
+                  setText(x4, toDisplayString(_for_item0.value.text)),
+                )
+                return n4
+              },
+              () => {
+                const n6 = template('<span> </span>', true)() as any
+                const x6 = child(n6) as any
+                renderEffect(() =>
+                  setText(x6, toDisplayString(_for_item0.value.text)),
+                )
+                return n6
+              },
+            )
+            return n2
+          },
+        )
+      }).render()
+
+      expect(host.children.length).toBe(1)
+      expect(html()).toBe('<span>world</span><!--if--><!--for-->')
+
+      items.value = [
+        { tag: 'div', text: 'hello' },
+        { tag: 'span', text: 'world' },
+      ]
+      await nextTick()
+      expect(host.children.length).toBe(2)
+      expect(html()).toBe(
+        '<div>hello</div><!--if--><span>world</span><!--if--><!--for-->',
+      )
+    })
+
+    test('remove elements with updating children without keys', async () => {
+      const arr = ref<string[]>(['one', 'two', 'three'])
+      const { html } = render(arr as any)
+      expect(html()).toBe(
+        '<span>one</span><span>two</span><span>three</span><!--for-->',
+      )
+
+      arr.value = ['two']
+      await nextTick()
+      expect(html()).toBe('<span>two</span><!--for-->')
+    })
+
+    test('remove a single node when children are updated', async () => {
+      const arr = ref<string[]>(['one'])
+      const { html } = render(arr as any)
+      expect(html()).toBe('<span>one</span><!--for-->')
+
+      arr.value = ['two', 'three']
+      await nextTick()
+      expect(html()).toBe('<span>two</span><span>three</span><!--for-->')
+    })
+
+    test('reorder elements', async () => {
+      const arr = ref<string[]>(['one', 'two', 'three'])
+      const { html } = render(arr as any)
+      expect(html()).toBe(
+        '<span>one</span><span>two</span><span>three</span><!--for-->',
+      )
+
+      arr.value = ['three', 'two', 'one']
+      await nextTick()
+      expect(html()).toBe(
+        '<span>three</span><span>two</span><span>one</span><!--for-->',
+      )
+    })
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/hydration.spec.ts b/packages/runtime-vapor/__tests__/hydration.spec.ts
new file mode 100644
index 00000000000..6ba2bf895fb
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/hydration.spec.ts
@@ -0,0 +1,1793 @@
+import { createVaporSSRApp, delegateEvents } from '../src'
+import { nextTick, ref } from '@vue/runtime-dom'
+import { compileScript, parse } from '@vue/compiler-sfc'
+import * as runtimeVapor from '../src'
+import * as runtimeDom from '@vue/runtime-dom'
+import * as VueServerRenderer from '@vue/server-renderer'
+
+const Vue = { ...runtimeDom, ...runtimeVapor }
+
+function compile(
+  sfc: string,
+  data: runtimeDom.Ref<any>,
+  components: Record<string, any> = {},
+  ssr = false,
+) {
+  if (!sfc.includes(`<script`)) {
+    sfc =
+      `<script vapor>const data = _data; const components = _components;</script>` +
+      sfc
+  }
+  const descriptor = parse(sfc).descriptor
+
+  const script = compileScript(descriptor, {
+    id: 'x',
+    isProd: true,
+    inlineTemplate: true,
+    genDefaultAs: '__sfc__',
+    vapor: true,
+    templateOptions: {
+      ssr,
+    },
+  })
+
+  const code =
+    script.content
+      .replace(/\bimport {/g, 'const {')
+      .replace(/ as _/g, ': _')
+      .replace(/} from ['"]vue['"]/g, `} = Vue`)
+      .replace(/} from "vue\/server-renderer"/g, '} = VueServerRenderer') +
+    '\nreturn __sfc__'
+
+  return new Function('Vue', 'VueServerRenderer', '_data', '_components', code)(
+    Vue,
+    VueServerRenderer,
+    data,
+    components,
+  )
+}
+
+async function testHydration(
+  code: string,
+  components: Record<string, string> = {},
+) {
+  const data = ref('foo')
+  const ssrComponents: any = {}
+  const clientComponents: any = {}
+  for (const key in components) {
+    clientComponents[key] = compile(components[key], data, clientComponents)
+    ssrComponents[key] = compile(components[key], data, ssrComponents, true)
+  }
+
+  const serverComp = compile(code, data, ssrComponents, true)
+  const html = await VueServerRenderer.renderToString(
+    runtimeDom.createSSRApp(serverComp),
+  )
+  const container = document.createElement('div')
+  document.body.appendChild(container)
+  container.innerHTML = html
+
+  const clientComp = compile(code, data, clientComponents)
+  const app = createVaporSSRApp(clientComp)
+  app.mount(container)
+  return { data, container }
+}
+
+const triggerEvent = (type: string, el: Element) => {
+  const event = new Event(type, { bubbles: true })
+  el.dispatchEvent(event)
+}
+
+describe('Vapor Mode hydration', () => {
+  delegateEvents('click')
+
+  beforeEach(() => {
+    document.body.innerHTML = ''
+  })
+
+  test('root text', async () => {
+    const { data, container } = await testHydration(`
+      <template>{{ data }}</template>
+    `)
+    expect(container.innerHTML).toMatchInlineSnapshot(`"foo"`)
+
+    data.value = 'bar'
+    await nextTick()
+    expect(container.innerHTML).toMatchInlineSnapshot(`"bar"`)
+  })
+
+  test('root comment', async () => {
+    const { container } = await testHydration(`
+      <template><!----></template>
+    `)
+    expect(container.innerHTML).toBe('<!---->')
+    expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned()
+  })
+
+  test('root with mixed element and text', async () => {
+    const { container, data } = await testHydration(`
+      <template> A<span>{{ data }}</span>{{ data }}</template>
+    `)
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<!--[--> A<span>foo</span>foo<!--]-->"`,
+    )
+
+    data.value = 'bar'
+    await nextTick()
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<!--[--> A<span>bar</span>bar<!--]-->"`,
+    )
+  })
+
+  test('empty element', async () => {
+    const { container } = await testHydration(`
+      <template><div/></template>
+    `)
+    expect(container.innerHTML).toBe('<div></div>')
+    expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned()
+  })
+
+  test('element with binding and text children', async () => {
+    const { container, data } = await testHydration(`
+      <template><div :class="data">{{ data }}</div></template>
+    `)
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<div class="foo">foo</div>"`,
+    )
+
+    data.value = 'bar'
+    await nextTick()
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<div class="bar">bar</div>"`,
+    )
+  })
+
+  test('element with elements children', async () => {
+    const { container } = await testHydration(`
+      <template>
+        <div>
+          <span>{{ data }}</span>
+          <span :class="data" @click="data = 'bar'"/>
+        </div>
+      </template>
+    `)
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<div><span>foo</span><span class="foo"></span></div>"`,
+    )
+
+    // event handler
+    triggerEvent('click', container.querySelector('.foo')!)
+
+    await nextTick()
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<div><span>bar</span><span class="bar"></span></div>"`,
+    )
+  })
+
+  test('basic component', async () => {
+    const { container, data } = await testHydration(
+      `
+      <template><div><span></span><components.Child/></div></template>
+      `,
+      { Child: `<template>{{ data }}</template>` },
+    )
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<div><span></span>foo</div>"`,
+    )
+
+    data.value = 'bar'
+    await nextTick()
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<div><span></span>bar</div>"`,
+    )
+  })
+
+  test('fragment component', async () => {
+    const { container, data } = await testHydration(
+      `
+      <template><div><span></span><components.Child/></div></template>
+      `,
+      { Child: `<template><div>{{ data }}</div>-{{ data }}-</template>` },
+    )
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<div><span></span><!--[--><div>foo</div>-foo-<!--]--></div>"`,
+    )
+
+    data.value = 'bar'
+    await nextTick()
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<div><span></span><!--[--><div>bar</div>-bar-<!--]--></div>"`,
+    )
+  })
+
+  test('fragment component with prepend', async () => {
+    const { container, data } = await testHydration(
+      `
+      <template><div><components.Child/><span></span></div></template>
+      `,
+      { Child: `<template><div>{{ data }}</div>-{{ data }}-</template>` },
+    )
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<div><!--[--><div>foo</div>-foo-<!--]--><span></span></div>"`,
+    )
+
+    data.value = 'bar'
+    await nextTick()
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<div><!--[--><div>bar</div>-bar-<!--]--><span></span></div>"`,
+    )
+  })
+
+  test('nested fragment components', async () => {
+    const { container, data } = await testHydration(
+      `
+      <template><div><components.Parent/><span></span></div></template>
+      `,
+      {
+        Parent: `<template><div/><components.Child/><div/></template>`,
+        Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
+      },
+    )
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<div><!--[--><div></div><!--[--><div>foo</div>-foo-<!--]--><div></div><!--]--><span></span></div>"`,
+    )
+
+    data.value = 'bar'
+    await nextTick()
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<div><!--[--><div></div><!--[--><div>bar</div>-bar-<!--]--><div></div><!--]--><span></span></div>"`,
+    )
+  })
+
+  // problem is the <!> placeholder does not exist in SSR output
+  test.todo('component with anchor insertion', async () => {
+    const { container, data } = await testHydration(
+      `
+      <template>
+        <div>
+          <span/>
+          <components.Child/>
+          <span/>
+        </div>
+      </template>
+      `,
+      {
+        Child: `<template>{{ data }}</template>`,
+      },
+    )
+    expect(container.innerHTML).toMatchInlineSnapshot()
+
+    data.value = 'bar'
+    await nextTick()
+    expect(container.innerHTML).toMatchInlineSnapshot()
+  })
+
+  test.todo('consecutive component with anchor insertion', async () => {
+    const { container, data } = await testHydration(
+      `<template>
+        <div>
+          <span/>
+          <components.Child/>
+          <components.Child/>
+          <span/>
+        </div>
+      </template>
+      `,
+      {
+        Child: `<template>{{ data }}</template>`,
+      },
+    )
+    expect(container.innerHTML).toMatchInlineSnapshot()
+
+    data.value = 'bar'
+    await nextTick()
+    expect(container.innerHTML).toMatchInlineSnapshot()
+  })
+
+  test.todo('if')
+
+  test.todo('for')
+
+  test.todo('slots')
+
+  // test('element with ref', () => {
+  //   const el = ref()
+  //   const { vnode, container } = mountWithHydration('<div></div>', () =>
+  //     h('div', { ref: el }),
+  //   )
+  //   expect(vnode.el).toBe(container.firstChild)
+  //   expect(el.value).toBe(vnode.el)
+  // })
+
+  // test('with data-allow-mismatch component when using onServerPrefetch', async () => {
+  //   const Comp = {
+  //     template: `
+  //       <div>Comp2</div>
+  //     `,
+  //   }
+  //   let foo: any
+  //   const App = {
+  //     setup() {
+  //       const flag = ref(true)
+  //       foo = () => {
+  //         flag.value = false
+  //       }
+  //       onServerPrefetch(() => (flag.value = false))
+  //       return { flag }
+  //     },
+  //     components: {
+  //       Comp,
+  //     },
+  //     template: `
+  //       <span data-allow-mismatch>
+  //         <Comp v-if="flag"></Comp>
+  //       </span>
+  //     `,
+  //   }
+  //   // hydrate
+  //   const container = document.createElement('div')
+  //   container.innerHTML = await renderToString(h(App))
+  //   createSSRApp(App).mount(container)
+  //   expect(container.innerHTML).toBe(
+  //     '<span data-allow-mismatch=""><div>Comp2</div></span>',
+  //   )
+  //   foo()
+  //   await nextTick()
+  //   expect(container.innerHTML).toBe(
+  //     '<span data-allow-mismatch=""><!--v-if--></span>',
+  //   )
+  // })
+
+  // // compile SSR + client render fn from the same template & hydrate
+  // test('full compiler integration', async () => {
+  //   const mounted: string[] = []
+  //   const log = vi.fn()
+  //   const toggle = ref(true)
+
+  //   const Child = {
+  //     data() {
+  //       return {
+  //         count: 0,
+  //         text: 'hello',
+  //         style: {
+  //           color: 'red',
+  //         },
+  //       }
+  //     },
+  //     mounted() {
+  //       mounted.push('child')
+  //     },
+  //     template: `
+  //     <div>
+  //       <span class="count" :style="style">{{ count }}</span>
+  //       <button class="inc" @click="count++">inc</button>
+  //       <button class="change" @click="style.color = 'green'" >change color</button>
+  //       <button class="emit" @click="$emit('foo')">emit</button>
+  //       <span class="text">{{ text }}</span>
+  //       <input v-model="text">
+  //     </div>
+  //     `,
+  //   }
+
+  //   const App = {
+  //     setup() {
+  //       return { toggle }
+  //     },
+  //     mounted() {
+  //       mounted.push('parent')
+  //     },
+  //     template: `
+  //       <div>
+  //         <span>hello</span>
+  //         <template v-if="toggle">
+  //           <Child @foo="log('child')"/>
+  //           <template v-if="true">
+  //             <button class="parent-click" @click="log('click')">click me</button>
+  //           </template>
+  //         </template>
+  //         <span>hello</span>
+  //       </div>`,
+  //     components: {
+  //       Child,
+  //     },
+  //     methods: {
+  //       log,
+  //     },
+  //   }
+
+  //   const container = document.createElement('div')
+  //   // server render
+  //   container.innerHTML = await renderToString(h(App))
+  //   // hydrate
+  //   createSSRApp(App).mount(container)
+
+  //   // assert interactions
+  //   // 1. parent button click
+  //   triggerEvent('click', container.querySelector('.parent-click')!)
+  //   expect(log).toHaveBeenCalledWith('click')
+
+  //   // 2. child inc click + text interpolation
+  //   const count = container.querySelector('.count') as HTMLElement
+  //   expect(count.textContent).toBe(`0`)
+  //   triggerEvent('click', container.querySelector('.inc')!)
+  //   await nextTick()
+  //   expect(count.textContent).toBe(`1`)
+
+  //   // 3. child color click + style binding
+  //   expect(count.style.color).toBe('red')
+  //   triggerEvent('click', container.querySelector('.change')!)
+  //   await nextTick()
+  //   expect(count.style.color).toBe('green')
+
+  //   // 4. child event emit
+  //   triggerEvent('click', container.querySelector('.emit')!)
+  //   expect(log).toHaveBeenCalledWith('child')
+
+  //   // 5. child v-model
+  //   const text = container.querySelector('.text')!
+  //   const input = container.querySelector('input')!
+  //   expect(text.textContent).toBe('hello')
+  //   input.value = 'bye'
+  //   triggerEvent('input', input)
+  //   await nextTick()
+  //   expect(text.textContent).toBe('bye')
+  // })
+
+  // test('handle click error in ssr mode', async () => {
+  //   const App = {
+  //     setup() {
+  //       const throwError = () => {
+  //         throw new Error('Sentry Error')
+  //       }
+  //       return { throwError }
+  //     },
+  //     template: `
+  //       <div>
+  //         <button class="parent-click" @click="throwError">click me</button>
+  //       </div>`,
+  //   }
+
+  //   const container = document.createElement('div')
+  //   // server render
+  //   container.innerHTML = await renderToString(h(App))
+  //   // hydrate
+  //   const app = createSSRApp(App)
+  //   const handler = (app.config.errorHandler = vi.fn())
+  //   app.mount(container)
+  //   // assert interactions
+  //   // parent button click
+  //   triggerEvent('click', container.querySelector('.parent-click')!)
+  //   expect(handler).toHaveBeenCalled()
+  // })
+
+  // test('handle blur error in ssr mode', async () => {
+  //   const App = {
+  //     setup() {
+  //       const throwError = () => {
+  //         throw new Error('Sentry Error')
+  //       }
+  //       return { throwError }
+  //     },
+  //     template: `
+  //       <div>
+  //         <input class="parent-click" @blur="throwError"/>
+  //       </div>`,
+  //   }
+
+  //   const container = document.createElement('div')
+  //   // server render
+  //   container.innerHTML = await renderToString(h(App))
+  //   // hydrate
+  //   const app = createSSRApp(App)
+  //   const handler = (app.config.errorHandler = vi.fn())
+  //   app.mount(container)
+  //   // assert interactions
+  //   // parent blur event
+  //   triggerEvent('blur', container.querySelector('.parent-click')!)
+  //   expect(handler).toHaveBeenCalled()
+  // })
+
+  // test('async component', async () => {
+  //   const spy = vi.fn()
+  //   const Comp = () =>
+  //     h(
+  //       'button',
+  //       {
+  //         onClick: spy,
+  //       },
+  //       'hello!',
+  //     )
+
+  //   let serverResolve: any
+  //   let AsyncComp = defineAsyncComponent(
+  //     () =>
+  //       new Promise(r => {
+  //         serverResolve = r
+  //       }),
+  //   )
+
+  //   const App = {
+  //     render() {
+  //       return ['hello', h(AsyncComp), 'world']
+  //     },
+  //   }
+
+  //   // server render
+  //   const htmlPromise = renderToString(h(App))
+  //   serverResolve(Comp)
+  //   const html = await htmlPromise
+  //   expect(html).toMatchInlineSnapshot(
+  //     `"<!--[-->hello<button>hello!</button>world<!--]-->"`,
+  //   )
+
+  //   // hydration
+  //   let clientResolve: any
+  //   AsyncComp = defineAsyncComponent(
+  //     () =>
+  //       new Promise(r => {
+  //         clientResolve = r
+  //       }),
+  //   )
+
+  //   const container = document.createElement('div')
+  //   container.innerHTML = html
+  //   createSSRApp(App).mount(container)
+
+  //   // hydration not complete yet
+  //   triggerEvent('click', container.querySelector('button')!)
+  //   expect(spy).not.toHaveBeenCalled()
+
+  //   // resolve
+  //   clientResolve(Comp)
+  //   await new Promise(r => setTimeout(r))
+
+  //   // should be hydrated now
+  //   triggerEvent('click', container.querySelector('button')!)
+  //   expect(spy).toHaveBeenCalled()
+  // })
+
+  // test('update async wrapper before resolve', async () => {
+  //   const Comp = {
+  //     render() {
+  //       return h('h1', 'Async component')
+  //     },
+  //   }
+  //   let serverResolve: any
+  //   let AsyncComp = defineAsyncComponent(
+  //     () =>
+  //       new Promise(r => {
+  //         serverResolve = r
+  //       }),
+  //   )
+
+  //   const toggle = ref(true)
+  //   const App = {
+  //     setup() {
+  //       onMounted(() => {
+  //         // change state, this makes updateComponent(AsyncComp) execute before
+  //         // the async component is resolved
+  //         toggle.value = false
+  //       })
+
+  //       return () => {
+  //         return [toggle.value ? 'hello' : 'world', h(AsyncComp)]
+  //       }
+  //     },
+  //   }
+
+  //   // server render
+  //   const htmlPromise = renderToString(h(App))
+  //   serverResolve(Comp)
+  //   const html = await htmlPromise
+  //   expect(html).toMatchInlineSnapshot(
+  //     `"<!--[-->hello<h1>Async component</h1><!--]-->"`,
+  //   )
+
+  //   // hydration
+  //   let clientResolve: any
+  //   AsyncComp = defineAsyncComponent(
+  //     () =>
+  //       new Promise(r => {
+  //         clientResolve = r
+  //       }),
+  //   )
+
+  //   const container = document.createElement('div')
+  //   container.innerHTML = html
+  //   createSSRApp(App).mount(container)
+
+  //   // resolve
+  //   clientResolve(Comp)
+  //   await new Promise(r => setTimeout(r))
+
+  //   // should be hydrated now
+  //   expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+  //   expect(container.innerHTML).toMatchInlineSnapshot(
+  //     `"<!--[-->world<h1>Async component</h1><!--]-->"`,
+  //   )
+  // })
+
+  // test('hydrate safely when property used by async setup changed before render', async () => {
+  //   const toggle = ref(true)
+
+  //   const AsyncComp = {
+  //     async setup() {
+  //       await new Promise<void>(r => setTimeout(r, 10))
+  //       return () => h('h1', 'Async component')
+  //     },
+  //   }
+
+  //   const AsyncWrapper = {
+  //     render() {
+  //       return h(AsyncComp)
+  //     },
+  //   }
+
+  //   const SiblingComp = {
+  //     setup() {
+  //       toggle.value = false
+  //       return () => h('span')
+  //     },
+  //   }
+
+  //   const App = {
+  //     setup() {
+  //       return () =>
+  //         h(
+  //           Suspense,
+  //           {},
+  //           {
+  //             default: () => [
+  //               h('main', {}, [
+  //                 h(AsyncWrapper, {
+  //                   prop: toggle.value ? 'hello' : 'world',
+  //                 }),
+  //                 h(SiblingComp),
+  //               ]),
+  //             ],
+  //           },
+  //         )
+  //     },
+  //   }
+
+  //   // server render
+  //   const html = await renderToString(h(App))
+
+  //   expect(html).toMatchInlineSnapshot(
+  //     `"<main><h1 prop="hello">Async component</h1><span></span></main>"`,
+  //   )
+
+  //   expect(toggle.value).toBe(false)
+
+  //   // hydration
+
+  //   // reset the value
+  //   toggle.value = true
+  //   expect(toggle.value).toBe(true)
+
+  //   const container = document.createElement('div')
+  //   container.innerHTML = html
+  //   createSSRApp(App).mount(container)
+
+  //   await new Promise(r => setTimeout(r, 10))
+
+  //   expect(toggle.value).toBe(false)
+
+  //   // should be hydrated now
+  //   expect(container.innerHTML).toMatchInlineSnapshot(
+  //     `"<main><h1 prop="world">Async component</h1><span></span></main>"`,
+  //   )
+  // })
+
+  // test('hydrate safely when property used by deep nested async setup changed before render', async () => {
+  //   const toggle = ref(true)
+
+  //   const AsyncComp = {
+  //     async setup() {
+  //       await new Promise<void>(r => setTimeout(r, 10))
+  //       return () => h('h1', 'Async component')
+  //     },
+  //   }
+
+  //   const AsyncWrapper = { render: () => h(AsyncComp) }
+  //   const AsyncWrapperWrapper = { render: () => h(AsyncWrapper) }
+
+  //   const SiblingComp = {
+  //     setup() {
+  //       toggle.value = false
+  //       return () => h('span')
+  //     },
+  //   }
+
+  //   const App = {
+  //     setup() {
+  //       return () =>
+  //         h(
+  //           Suspense,
+  //           {},
+  //           {
+  //             default: () => [
+  //               h('main', {}, [
+  //                 h(AsyncWrapperWrapper, {
+  //                   prop: toggle.value ? 'hello' : 'world',
+  //                 }),
+  //                 h(SiblingComp),
+  //               ]),
+  //             ],
+  //           },
+  //         )
+  //     },
+  //   }
+
+  //   // server render
+  //   const html = await renderToString(h(App))
+
+  //   expect(html).toMatchInlineSnapshot(
+  //     `"<main><h1 prop="hello">Async component</h1><span></span></main>"`,
+  //   )
+
+  //   expect(toggle.value).toBe(false)
+
+  //   // hydration
+
+  //   // reset the value
+  //   toggle.value = true
+  //   expect(toggle.value).toBe(true)
+
+  //   const container = document.createElement('div')
+  //   container.innerHTML = html
+  //   createSSRApp(App).mount(container)
+
+  //   await new Promise(r => setTimeout(r, 10))
+
+  //   expect(toggle.value).toBe(false)
+
+  //   // should be hydrated now
+  //   expect(container.innerHTML).toMatchInlineSnapshot(
+  //     `"<main><h1 prop="world">Async component</h1><span></span></main>"`,
+  //   )
+  // })
+
+  // // #3787
+  // test('unmount async wrapper before load', async () => {
+  //   let resolve: any
+  //   const AsyncComp = defineAsyncComponent(
+  //     () =>
+  //       new Promise(r => {
+  //         resolve = r
+  //       }),
+  //   )
+
+  //   const show = ref(true)
+  //   const root = document.createElement('div')
+  //   root.innerHTML = '<div><div>async</div></div>'
+
+  //   createSSRApp({
+  //     render() {
+  //       return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
+  //     },
+  //   }).mount(root)
+
+  //   show.value = false
+  //   await nextTick()
+  //   expect(root.innerHTML).toBe('<div><div>hi</div></div>')
+  //   resolve({})
+  // })
+
+  // //#12362
+  // test('nested async wrapper', async () => {
+  //   const Toggle = defineAsyncComponent(
+  //     () =>
+  //       new Promise(r => {
+  //         r(
+  //           defineComponent({
+  //             setup(_, { slots }) {
+  //               const show = ref(false)
+  //               onMounted(() => {
+  //                 nextTick(() => {
+  //                   show.value = true
+  //                 })
+  //               })
+  //               return () =>
+  //                 withDirectives(
+  //                   h('div', null, [renderSlot(slots, 'default')]),
+  //                   [[vShow, show.value]],
+  //                 )
+  //             },
+  //           }) as any,
+  //         )
+  //       }),
+  //   )
+
+  //   const Wrapper = defineAsyncComponent(() => {
+  //     return new Promise(r => {
+  //       r(
+  //         defineComponent({
+  //           render(this: any) {
+  //             return renderSlot(this.$slots, 'default')
+  //           },
+  //         }) as any,
+  //       )
+  //     })
+  //   })
+
+  //   const count = ref(0)
+  //   const fn = vi.fn()
+  //   const Child = {
+  //     setup() {
+  //       onMounted(() => {
+  //         fn()
+  //         count.value++
+  //       })
+  //       return () => h('div', count.value)
+  //     },
+  //   }
+
+  //   const App = {
+  //     render() {
+  //       return h(Toggle, null, {
+  //         default: () =>
+  //           h(Wrapper, null, {
+  //             default: () =>
+  //               h(Wrapper, null, {
+  //                 default: () => h(Child),
+  //               }),
+  //           }),
+  //       })
+  //     },
+  //   }
+
+  //   const root = document.createElement('div')
+  //   root.innerHTML = await renderToString(h(App))
+  //   expect(root.innerHTML).toMatchInlineSnapshot(
+  //     `"<div style="display:none;"><!--[--><!--[--><!--[--><div>0</div><!--]--><!--]--><!--]--></div>"`,
+  //   )
+
+  //   createSSRApp(App).mount(root)
+  //   await nextTick()
+  //   await nextTick()
+  //   expect(root.innerHTML).toMatchInlineSnapshot(
+  //     `"<div style=""><!--[--><!--[--><!--[--><div>1</div><!--]--><!--]--><!--]--></div>"`,
+  //   )
+  //   expect(fn).toBeCalledTimes(1)
+  // })
+
+  // test('unmount async wrapper before load (fragment)', async () => {
+  //   let resolve: any
+  //   const AsyncComp = defineAsyncComponent(
+  //     () =>
+  //       new Promise(r => {
+  //         resolve = r
+  //       }),
+  //   )
+
+  //   const show = ref(true)
+  //   const root = document.createElement('div')
+  //   root.innerHTML = '<div><!--[-->async<!--]--></div>'
+
+  //   createSSRApp({
+  //     render() {
+  //       return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
+  //     },
+  //   }).mount(root)
+
+  //   show.value = false
+  //   await nextTick()
+  //   expect(root.innerHTML).toBe('<div><div>hi</div></div>')
+  //   resolve({})
+  // })
+
+  // test('elements with camel-case in svg ', () => {
+  //   const { vnode, container } = mountWithHydration(
+  //     '<animateTransform></animateTransform>',
+  //     () => h('animateTransform'),
+  //   )
+  //   expect(vnode.el).toBe(container.firstChild)
+  //   expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+  // })
+
+  // test('SVG as a mount container', () => {
+  //   const svgContainer = document.createElement('svg')
+  //   svgContainer.innerHTML = '<g></g>'
+  //   const app = createSSRApp({
+  //     render: () => h('g'),
+  //   })
+
+  //   expect(
+  //     (
+  //       app.mount(svgContainer).$.subTree as VNode<Node, Element> & {
+  //         el: Element
+  //       }
+  //     ).el instanceof SVGElement,
+  //   )
+  // })
+
+  // test('force hydrate prop with `.prop` modifier', () => {
+  //   const { container } = mountWithHydration('<input type="checkbox">', () =>
+  //     h('input', {
+  //       type: 'checkbox',
+  //       '.indeterminate': true,
+  //     }),
+  //   )
+  //   expect((container.firstChild! as any).indeterminate).toBe(true)
+  // })
+
+  // test('force hydrate input v-model with non-string value bindings', () => {
+  //   const { container } = mountWithHydration(
+  //     '<input type="checkbox" value="true">',
+  //     () =>
+  //       withDirectives(
+  //         createVNode(
+  //           'input',
+  //           { type: 'checkbox', 'true-value': true },
+  //           null,
+  //           PatchFlags.PROPS,
+  //           ['true-value'],
+  //         ),
+  //         [[vModelCheckbox, true]],
+  //       ),
+  //   )
+  //   expect((container.firstChild as any)._trueValue).toBe(true)
+  // })
+
+  // test('force hydrate checkbox with indeterminate', () => {
+  //   const { container } = mountWithHydration(
+  //     '<input type="checkbox" indeterminate>',
+  //     () =>
+  //       createVNode(
+  //         'input',
+  //         { type: 'checkbox', indeterminate: '' },
+  //         null,
+  //         PatchFlags.CACHED,
+  //       ),
+  //   )
+  //   expect((container.firstChild as any).indeterminate).toBe(true)
+  // })
+
+  // test('force hydrate select option with non-string value bindings', () => {
+  //   const { container } = mountWithHydration(
+  //     '<select><option value="true">ok</option></select>',
+  //     () =>
+  //       h('select', [
+  //         // hoisted because bound value is a constant...
+  //         createVNode('option', { value: true }, null, -1 /* HOISTED */),
+  //       ]),
+  //   )
+  //   expect((container.firstChild!.firstChild as any)._value).toBe(true)
+  // })
+
+  // // #7203
+  // test('force hydrate custom element with dynamic props', () => {
+  //   class MyElement extends HTMLElement {
+  //     foo = ''
+  //     constructor() {
+  //       super()
+  //     }
+  //   }
+  //   customElements.define('my-element-7203', MyElement)
+
+  //   const msg = ref('bar')
+  //   const container = document.createElement('div')
+  //   container.innerHTML = '<my-element-7203></my-element-7203>'
+  //   const app = createSSRApp({
+  //     render: () => h('my-element-7203', { foo: msg.value }),
+  //   })
+  //   app.mount(container)
+  //   expect((container.firstChild as any).foo).toBe(msg.value)
+  // })
+
+  // // #5728
+  // test('empty text node in slot', () => {
+  //   const Comp = {
+  //     render(this: any) {
+  //       return renderSlot(this.$slots, 'default', {}, () => [
+  //         createTextVNode(''),
+  //       ])
+  //     },
+  //   }
+  //   const { container, vnode } = mountWithHydration('<!--[--><!--]-->', () =>
+  //     h(Comp),
+  //   )
+  //   expect(container.childNodes.length).toBe(3)
+  //   const text = container.childNodes[1]
+  //   expect(text.nodeType).toBe(3)
+  //   expect(vnode.el).toBe(container.childNodes[0])
+  //   // component => slot fragment => text node
+  //   expect((vnode as any).component?.subTree.children[0].el).toBe(text)
+  // })
+
+  // // #7215
+  // test('empty text node', () => {
+  //   const Comp = {
+  //     render(this: any) {
+  //       return h('p', [''])
+  //     },
+  //   }
+  //   const { container } = mountWithHydration('<p></p>', () => h(Comp))
+  //   expect(container.childNodes.length).toBe(1)
+  //   const p = container.childNodes[0]
+  //   expect(p.childNodes.length).toBe(1)
+  //   const text = p.childNodes[0]
+  //   expect(text.nodeType).toBe(3)
+  // })
+
+  // // #11372
+  // test('object style value tracking in prod', async () => {
+  //   __DEV__ = false
+  //   try {
+  //     const style = reactive({ color: 'red' })
+  //     const Comp = {
+  //       render(this: any) {
+  //         return (
+  //           openBlock(),
+  //           createElementBlock(
+  //             'div',
+  //             {
+  //               style: normalizeStyle(style),
+  //             },
+  //             null,
+  //             4 /* STYLE */,
+  //           )
+  //         )
+  //       },
+  //     }
+  //     const { container } = mountWithHydration(
+  //       `<div style="color: red;"></div>`,
+  //       () => h(Comp),
+  //     )
+  //     style.color = 'green'
+  //     await nextTick()
+  //     expect(container.innerHTML).toBe(`<div style="color: green;"></div>`)
+  //   } finally {
+  //     __DEV__ = true
+  //   }
+  // })
+
+  // test('app.unmount()', async () => {
+  //   const container = document.createElement('DIV')
+  //   container.innerHTML = '<button></button>'
+  //   const App = defineComponent({
+  //     setup(_, { expose }) {
+  //       const count = ref(0)
+
+  //       expose({ count })
+
+  //       return () =>
+  //         h('button', {
+  //           onClick: () => count.value++,
+  //         })
+  //     },
+  //   })
+
+  //   const app = createSSRApp(App)
+  //   const vm = app.mount(container)
+  //   await nextTick()
+  //   expect((container as any)._vnode).toBeDefined()
+  //   // @ts-expect-error - expose()'d properties are not available on vm type
+  //   expect(vm.count).toBe(0)
+
+  //   app.unmount()
+  //   expect((container as any)._vnode).toBe(null)
+  // })
+
+  // // #6637
+  // test('stringified root fragment', () => {
+  //   mountWithHydration(`<!--[--><div></div><!--]-->`, () =>
+  //     createStaticVNode(`<div></div>`, 1),
+  //   )
+  //   expect(`mismatch`).not.toHaveBeenWarned()
+  // })
+
+  // test('transition appear', () => {
+  //   const { vnode, container } = mountWithHydration(
+  //     `<template><div>foo</div></template>`,
+  //     () =>
+  //       h(
+  //         Transition,
+  //         { appear: true },
+  //         {
+  //           default: () => h('div', 'foo'),
+  //         },
+  //       ),
+  //   )
+  //   expect(container.firstChild).toMatchInlineSnapshot(`
+  //     <div
+  //       class="v-enter-from v-enter-active"
+  //     >
+  //       foo
+  //     </div>
+  //   `)
+  //   expect(vnode.el).toBe(container.firstChild)
+  //   expect(`mismatch`).not.toHaveBeenWarned()
+  // })
+
+  // test('transition appear with v-if', () => {
+  //   const show = false
+  //   const { vnode, container } = mountWithHydration(
+  //     `<template><!----></template>`,
+  //     () =>
+  //       h(
+  //         Transition,
+  //         { appear: true },
+  //         {
+  //           default: () => (show ? h('div', 'foo') : createCommentVNode('')),
+  //         },
+  //       ),
+  //   )
+  //   expect(container.firstChild).toMatchInlineSnapshot('<!---->')
+  //   expect(vnode.el).toBe(container.firstChild)
+  //   expect(`mismatch`).not.toHaveBeenWarned()
+  // })
+
+  // test('transition appear with v-show', () => {
+  //   const show = false
+  //   const { vnode, container } = mountWithHydration(
+  //     `<template><div style="display: none;">foo</div></template>`,
+  //     () =>
+  //       h(
+  //         Transition,
+  //         { appear: true },
+  //         {
+  //           default: () =>
+  //             withDirectives(createVNode('div', null, 'foo'), [[vShow, show]]),
+  //         },
+  //       ),
+  //   )
+  //   expect(container.firstChild).toMatchInlineSnapshot(`
+  //     <div
+  //       class="v-enter-from v-enter-active"
+  //       style="display: none;"
+  //     >
+  //       foo
+  //     </div>
+  //   `)
+  //   expect((container.firstChild as any)[vShowOriginalDisplay]).toBe('')
+  //   expect(vnode.el).toBe(container.firstChild)
+  //   expect(`mismatch`).not.toHaveBeenWarned()
+  // })
+
+  // test('transition appear w/ event listener', async () => {
+  //   const container = document.createElement('div')
+  //   container.innerHTML = `<template><button>0</button></template>`
+  //   createSSRApp({
+  //     data() {
+  //       return {
+  //         count: 0,
+  //       }
+  //     },
+  //     template: `
+  //       <Transition appear>
+  //         <button @click="count++">{{count}}</button>
+  //       </Transition>
+  //     `,
+  //   }).mount(container)
+
+  //   expect(container.firstChild).toMatchInlineSnapshot(`
+  //     <button
+  //       class="v-enter-from v-enter-active"
+  //     >
+  //       0
+  //     </button>
+  //   `)
+
+  //   triggerEvent('click', container.querySelector('button')!)
+  //   await nextTick()
+  //   expect(container.firstChild).toMatchInlineSnapshot(`
+  //     <button
+  //       class="v-enter-from v-enter-active"
+  //     >
+  //       1
+  //     </button>
+  //   `)
+  // })
+
+  // test('Suspense + transition appear', async () => {
+  //   const { vnode, container } = mountWithHydration(
+  //     `<template><div>foo</div></template>`,
+  //     () =>
+  //       h(Suspense, {}, () =>
+  //         h(
+  //           Transition,
+  //           { appear: true },
+  //           {
+  //             default: () => h('div', 'foo'),
+  //           },
+  //         ),
+  //       ),
+  //   )
+
+  //   expect(vnode.el).toBe(container.firstChild)
+  //   // wait for hydration to finish
+  //   await new Promise(r => setTimeout(r))
+
+  //   expect(container.firstChild).toMatchInlineSnapshot(`
+  //     <div
+  //       class="v-enter-from v-enter-active"
+  //     >
+  //       foo
+  //     </div>
+  //   `)
+  //   await nextTick()
+  //   expect(vnode.el).toBe(container.firstChild)
+  // })
+
+  // // #10607
+  // test('update component stable slot (prod + optimized mode)', async () => {
+  //   __DEV__ = false
+  //   try {
+  //     const container = document.createElement('div')
+  //     container.innerHTML = `<template><div show="false"><!--[--><div><div><!----></div></div><div>0</div><!--]--></div></template>`
+  //     const Comp = {
+  //       render(this: any) {
+  //         return (
+  //           openBlock(),
+  //           createElementBlock('div', null, [
+  //             renderSlot(this.$slots, 'default'),
+  //           ])
+  //         )
+  //       },
+  //     }
+  //     const show = ref(false)
+  //     const clicked = ref(false)
+
+  //     const Wrapper = {
+  //       setup() {
+  //         const items = ref<number[]>([])
+  //         onMounted(() => {
+  //           items.value = [1]
+  //         })
+  //         return () => {
+  //           return (
+  //             openBlock(),
+  //             createBlock(Comp, null, {
+  //               default: withCtx(() => [
+  //                 createElementVNode('div', null, [
+  //                   createElementVNode('div', null, [
+  //                     clicked.value
+  //                       ? (openBlock(),
+  //                         createElementBlock('div', { key: 0 }, 'foo'))
+  //                       : createCommentVNode('v-if', true),
+  //                   ]),
+  //                 ]),
+  //                 createElementVNode(
+  //                   'div',
+  //                   null,
+  //                   items.value.length,
+  //                   1 /* TEXT */,
+  //                 ),
+  //               ]),
+  //               _: 1 /* STABLE */,
+  //             })
+  //           )
+  //         }
+  //       },
+  //     }
+  //     createSSRApp({
+  //       components: { Wrapper },
+  //       data() {
+  //         return { show }
+  //       },
+  //       template: `<Wrapper :show="show"/>`,
+  //     }).mount(container)
+
+  //     await nextTick()
+  //     expect(container.innerHTML).toBe(
+  //       `<div show="false"><!--[--><div><div><!----></div></div><div>1</div><!--]--></div>`,
+  //     )
+
+  //     show.value = true
+  //     await nextTick()
+  //     expect(async () => {
+  //       clicked.value = true
+  //       await nextTick()
+  //     }).not.toThrow("Cannot read properties of null (reading 'insertBefore')")
+
+  //     await nextTick()
+  //     expect(container.innerHTML).toBe(
+  //       `<div show="true"><!--[--><div><div><div>foo</div></div></div><div>1</div><!--]--></div>`,
+  //     )
+  //   } catch (e) {
+  //     throw e
+  //   } finally {
+  //     __DEV__ = true
+  //   }
+  // })
+
+  // describe('mismatch handling', () => {
+  //   test('text node', () => {
+  //     const { container } = mountWithHydration(`foo`, () => 'bar')
+  //     expect(container.textContent).toBe('bar')
+  //     expect(`Hydration text mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   test('element text content', () => {
+  //     const { container } = mountWithHydration(`<div>foo</div>`, () =>
+  //       h('div', 'bar'),
+  //     )
+  //     expect(container.innerHTML).toBe('<div>bar</div>')
+  //     expect(`Hydration text content mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   test('not enough children', () => {
+  //     const { container } = mountWithHydration(`<div></div>`, () =>
+  //       h('div', [h('span', 'foo'), h('span', 'bar')]),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div><span>foo</span><span>bar</span></div>',
+  //     )
+  //     expect(`Hydration children mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   test('too many children', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div><span>foo</span><span>bar</span></div>`,
+  //       () => h('div', [h('span', 'foo')]),
+  //     )
+  //     expect(container.innerHTML).toBe('<div><span>foo</span></div>')
+  //     expect(`Hydration children mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   test('complete mismatch', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div><span>foo</span><span>bar</span></div>`,
+  //       () => h('div', [h('div', 'foo'), h('p', 'bar')]),
+  //     )
+  //     expect(container.innerHTML).toBe('<div><div>foo</div><p>bar</p></div>')
+  //     expect(`Hydration node mismatch`).toHaveBeenWarnedTimes(2)
+  //   })
+
+  //   test('fragment mismatch removal', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
+  //       () => h('div', [h('span', 'replaced')]),
+  //     )
+  //     expect(container.innerHTML).toBe('<div><span>replaced</span></div>')
+  //     expect(`Hydration node mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   test('fragment not enough children', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
+  //       () => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')]),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>',
+  //     )
+  //     expect(`Hydration node mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   test('fragment too many children', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
+  //       () => h('div', [[h('div', 'foo')], h('div', 'baz')]),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div><!--[--><div>foo</div><!--]--><div>baz</div></div>',
+  //     )
+  //     // fragment ends early and attempts to hydrate the extra <div>bar</div>
+  //     // as 2nd fragment child.
+  //     expect(`Hydration text content mismatch`).toHaveBeenWarned()
+  //     // excessive children removal
+  //     expect(`Hydration children mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   test('Teleport target has empty children', () => {
+  //     const teleportContainer = document.createElement('div')
+  //     teleportContainer.id = 'teleport'
+  //     document.body.appendChild(teleportContainer)
+
+  //     mountWithHydration('<!--teleport start--><!--teleport end-->', () =>
+  //       h(Teleport, { to: '#teleport' }, [h('span', 'value')]),
+  //     )
+  //     expect(teleportContainer.innerHTML).toBe(`<span>value</span>`)
+  //     expect(`Hydration children mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   test('comment mismatch (element)', () => {
+  //     const { container } = mountWithHydration(`<div><span></span></div>`, () =>
+  //       h('div', [createCommentVNode('hi')]),
+  //     )
+  //     expect(container.innerHTML).toBe('<div><!--hi--></div>')
+  //     expect(`Hydration node mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   test('comment mismatch (text)', () => {
+  //     const { container } = mountWithHydration(`<div>foobar</div>`, () =>
+  //       h('div', [createCommentVNode('hi')]),
+  //     )
+  //     expect(container.innerHTML).toBe('<div><!--hi--></div>')
+  //     expect(`Hydration node mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   test('class mismatch', () => {
+  //     mountWithHydration(`<div class="foo bar"></div>`, () =>
+  //       h('div', { class: ['foo', 'bar'] }),
+  //     )
+  //     mountWithHydration(`<div class="foo bar"></div>`, () =>
+  //       h('div', { class: { foo: true, bar: true } }),
+  //     )
+  //     mountWithHydration(`<div class="foo bar"></div>`, () =>
+  //       h('div', { class: 'foo bar' }),
+  //     )
+  //     // SVG classes
+  //     mountWithHydration(`<svg class="foo bar"></svg>`, () =>
+  //       h('svg', { class: 'foo bar' }),
+  //     )
+  //     // class with different order
+  //     mountWithHydration(`<div class="foo bar"></div>`, () =>
+  //       h('div', { class: 'bar foo' }),
+  //     )
+  //     expect(`Hydration class mismatch`).not.toHaveBeenWarned()
+  //     mountWithHydration(`<div class="foo bar"></div>`, () =>
+  //       h('div', { class: 'foo' }),
+  //     )
+  //     expect(`Hydration class mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   test('style mismatch', () => {
+  //     mountWithHydration(`<div style="color:red;"></div>`, () =>
+  //       h('div', { style: { color: 'red' } }),
+  //     )
+  //     mountWithHydration(`<div style="color:red;"></div>`, () =>
+  //       h('div', { style: `color:red;` }),
+  //     )
+  //     mountWithHydration(
+  //       `<div style="color:red; font-size: 12px;"></div>`,
+  //       () => h('div', { style: `font-size: 12px; color:red;` }),
+  //     )
+  //     mountWithHydration(`<div style="color:red;display:none;"></div>`, () =>
+  //       withDirectives(createVNode('div', { style: 'color: red' }, ''), [
+  //         [vShow, false],
+  //       ]),
+  //     )
+  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+  //     mountWithHydration(`<div style="color:red;"></div>`, () =>
+  //       h('div', { style: { color: 'green' } }),
+  //     )
+  //     expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
+  //   })
+
+  //   test('style mismatch when no style attribute is present', () => {
+  //     mountWithHydration(`<div></div>`, () =>
+  //       h('div', { style: { color: 'red' } }),
+  //     )
+  //     expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
+  //   })
+
+  //   test('style mismatch w/ v-show', () => {
+  //     mountWithHydration(`<div style="color:red;display:none"></div>`, () =>
+  //       withDirectives(createVNode('div', { style: 'color: red' }, ''), [
+  //         [vShow, false],
+  //       ]),
+  //     )
+  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+  //     mountWithHydration(`<div style="color:red;"></div>`, () =>
+  //       withDirectives(createVNode('div', { style: 'color: red' }, ''), [
+  //         [vShow, false],
+  //       ]),
+  //     )
+  //     expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
+  //   })
+
+  //   test('attr mismatch', () => {
+  //     mountWithHydration(`<div id="foo"></div>`, () => h('div', { id: 'foo' }))
+  //     mountWithHydration(`<div spellcheck></div>`, () =>
+  //       h('div', { spellcheck: '' }),
+  //     )
+  //     mountWithHydration(`<div></div>`, () => h('div', { id: undefined }))
+  //     // boolean
+  //     mountWithHydration(`<select multiple></div>`, () =>
+  //       h('select', { multiple: true }),
+  //     )
+  //     mountWithHydration(`<select multiple></div>`, () =>
+  //       h('select', { multiple: 'multiple' }),
+  //     )
+  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+
+  //     mountWithHydration(`<div></div>`, () => h('div', { id: 'foo' }))
+  //     expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(1)
+
+  //     mountWithHydration(`<div id="bar"></div>`, () => h('div', { id: 'foo' }))
+  //     expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(2)
+  //   })
+
+  //   test('attr special case: textarea value', () => {
+  //     mountWithHydration(`<textarea>foo</textarea>`, () =>
+  //       h('textarea', { value: 'foo' }),
+  //     )
+  //     mountWithHydration(`<textarea></textarea>`, () =>
+  //       h('textarea', { value: '' }),
+  //     )
+  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+
+  //     mountWithHydration(`<textarea>foo</textarea>`, () =>
+  //       h('textarea', { value: 'bar' }),
+  //     )
+  //     expect(`Hydration attribute mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   // #11873
+  //   test('<textarea> with newlines at the beginning', async () => {
+  //     const render = () => h('textarea', null, '\nhello')
+  //     const html = await renderToString(createSSRApp({ render }))
+  //     mountWithHydration(html, render)
+  //     expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('<pre> with newlines at the beginning', async () => {
+  //     const render = () => h('pre', null, '\n')
+  //     const html = await renderToString(createSSRApp({ render }))
+  //     mountWithHydration(html, render)
+  //     expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('boolean attr handling', () => {
+  //     mountWithHydration(`<input />`, () => h('input', { readonly: false }))
+  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+
+  //     mountWithHydration(`<input readonly />`, () =>
+  //       h('input', { readonly: true }),
+  //     )
+  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+
+  //     mountWithHydration(`<input readonly="readonly" />`, () =>
+  //       h('input', { readonly: true }),
+  //     )
+  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('client value is null or undefined', () => {
+  //     mountWithHydration(`<div></div>`, () =>
+  //       h('div', { draggable: undefined }),
+  //     )
+  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+
+  //     mountWithHydration(`<input />`, () => h('input', { type: null }))
+  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('should not warn against object values', () => {
+  //     mountWithHydration(`<input />`, () => h('input', { from: {} }))
+  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('should not warn on falsy bindings of non-property keys', () => {
+  //     mountWithHydration(`<button />`, () => h('button', { href: undefined }))
+  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('should not warn on non-renderable option values', () => {
+  //     mountWithHydration(`<select><option>hello</option></select>`, () =>
+  //       h('select', [h('option', { value: ['foo'] }, 'hello')]),
+  //     )
+  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('should not warn css v-bind', () => {
+  //     const container = document.createElement('div')
+  //     container.innerHTML = `<div style="--foo:red;color:var(--foo);" />`
+  //     const app = createSSRApp({
+  //       setup() {
+  //         useCssVars(() => ({
+  //           foo: 'red',
+  //         }))
+  //         return () => h('div', { style: { color: 'var(--foo)' } })
+  //       },
+  //     })
+  //     app.mount(container)
+  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   // #10317 - test case from #10325
+  //   test('css vars should only be added to expected on component root dom', () => {
+  //     const container = document.createElement('div')
+  //     container.innerHTML = `<div style="--foo:red;"><div style="color:var(--foo);" /></div>`
+  //     const app = createSSRApp({
+  //       setup() {
+  //         useCssVars(() => ({
+  //           foo: 'red',
+  //         }))
+  //         return () =>
+  //           h('div', null, [h('div', { style: { color: 'var(--foo)' } })])
+  //       },
+  //     })
+  //     app.mount(container)
+  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   // #11188
+  //   test('css vars support fallthrough', () => {
+  //     const container = document.createElement('div')
+  //     container.innerHTML = `<div style="padding: 4px;--foo:red;"></div>`
+  //     const app = createSSRApp({
+  //       setup() {
+  //         useCssVars(() => ({
+  //           foo: 'red',
+  //         }))
+  //         return () => h(Child)
+  //       },
+  //     })
+  //     const Child = {
+  //       setup() {
+  //         return () => h('div', { style: 'padding: 4px' })
+  //       },
+  //     }
+  //     app.mount(container)
+  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   // #11189
+  //   test('should not warn for directives that mutate DOM in created', () => {
+  //     const container = document.createElement('div')
+  //     container.innerHTML = `<div class="test red"></div>`
+  //     const vColor: ObjectDirective = {
+  //       created(el, binding) {
+  //         el.classList.add(binding.value)
+  //       },
+  //     }
+  //     const app = createSSRApp({
+  //       setup() {
+  //         return () =>
+  //           withDirectives(h('div', { class: 'test' }), [[vColor, 'red']])
+  //       },
+  //     })
+  //     app.mount(container)
+  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('escape css var name', () => {
+  //     const container = document.createElement('div')
+  //     container.innerHTML = `<div style="padding: 4px;--foo\\.bar:red;"></div>`
+  //     const app = createSSRApp({
+  //       setup() {
+  //         useCssVars(() => ({
+  //           'foo.bar': 'red',
+  //         }))
+  //         return () => h(Child)
+  //       },
+  //     })
+  //     const Child = {
+  //       setup() {
+  //         return () => h('div', { style: 'padding: 4px' })
+  //       },
+  //     }
+  //     app.mount(container)
+  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+  //   })
+  // })
+
+  // describe('data-allow-mismatch', () => {
+  //   test('element text content', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div data-allow-mismatch="text">foo</div>`,
+  //       () => h('div', 'bar'),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div data-allow-mismatch="text">bar</div>',
+  //     )
+  //     expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('not enough children', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div data-allow-mismatch="children"></div>`,
+  //       () => h('div', [h('span', 'foo'), h('span', 'bar')]),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>',
+  //     )
+  //     expect(`Hydration children mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('too many children', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>`,
+  //       () => h('div', [h('span', 'foo')]),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div data-allow-mismatch="children"><span>foo</span></div>',
+  //     )
+  //     expect(`Hydration children mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('complete mismatch', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>`,
+  //       () => h('div', [h('div', 'foo'), h('p', 'bar')]),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div data-allow-mismatch="children"><div>foo</div><p>bar</p></div>',
+  //     )
+  //     expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('fragment mismatch removal', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
+  //       () => h('div', [h('span', 'replaced')]),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div data-allow-mismatch="children"><span>replaced</span></div>',
+  //     )
+  //     expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('fragment not enough children', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div data-allow-mismatch="children"><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
+  //       () => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')]),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>',
+  //     )
+  //     expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('fragment too many children', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
+  //       () => h('div', [[h('div', 'foo')], h('div', 'baz')]),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div data-allow-mismatch="children"><!--[--><div>foo</div><!--]--><div>baz</div></div>',
+  //     )
+  //     // fragment ends early and attempts to hydrate the extra <div>bar</div>
+  //     // as 2nd fragment child.
+  //     expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
+  //     // excessive children removal
+  //     expect(`Hydration children mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('comment mismatch (element)', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div data-allow-mismatch="children"><span></span></div>`,
+  //       () => h('div', [createCommentVNode('hi')]),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div data-allow-mismatch="children"><!--hi--></div>',
+  //     )
+  //     expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('comment mismatch (text)', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div data-allow-mismatch="children">foobar</div>`,
+  //       () => h('div', [createCommentVNode('hi')]),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div data-allow-mismatch="children"><!--hi--></div>',
+  //     )
+  //     expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('class mismatch', () => {
+  //     mountWithHydration(
+  //       `<div class="foo bar" data-allow-mismatch="class"></div>`,
+  //       () => h('div', { class: 'foo' }),
+  //     )
+  //     expect(`Hydration class mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('style mismatch', () => {
+  //     mountWithHydration(
+  //       `<div style="color:red;" data-allow-mismatch="style"></div>`,
+  //       () => h('div', { style: { color: 'green' } }),
+  //     )
+  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('attr mismatch', () => {
+  //     mountWithHydration(`<div data-allow-mismatch="attribute"></div>`, () =>
+  //       h('div', { id: 'foo' }),
+  //     )
+  //     mountWithHydration(
+  //       `<div id="bar" data-allow-mismatch="attribute"></div>`,
+  //       () => h('div', { id: 'foo' }),
+  //     )
+  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+  //   })
+  // })
+
+  test.todo('Teleport')
+  test.todo('Suspense')
+})
diff --git a/packages/runtime-vapor/__tests__/if.spec.ts b/packages/runtime-vapor/__tests__/if.spec.ts
new file mode 100644
index 00000000000..eed0a95ef61
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/if.spec.ts
@@ -0,0 +1,243 @@
+import {
+  child,
+  createIf,
+  insert,
+  renderEffect,
+  template,
+  // @ts-expect-error
+  withDirectives,
+} from '../src'
+import { nextTick, ref } from '@vue/runtime-dom'
+import type { Mock } from 'vitest'
+import { makeRender } from './_utils'
+import { unmountComponent } from '../src/component'
+import { setElementText } from '../src/dom/prop'
+
+const define = makeRender()
+
+describe('createIf', () => {
+  test('basic', async () => {
+    // mock this template:
+    //  <div>
+    //    <p v-if="counter">{{counter}}</p>
+    //    <p v-else>zero</p>
+    //  </div>
+
+    let spyIfFn: Mock<any>
+    let spyElseFn: Mock<any>
+    const count = ref(0)
+
+    const spyConditionFn = vi.fn(() => count.value)
+
+    // templates can be reused through caching.
+    const t0 = template('<div></div>')
+    const t1 = template('<p></p>')
+    const t2 = template('<p>zero</p>')
+
+    const { host } = define(() => {
+      const n0 = t0()
+
+      insert(
+        createIf(
+          spyConditionFn,
+          // v-if
+          (spyIfFn ||= vi.fn(() => {
+            const n2 = t1()
+            renderEffect(() => {
+              setElementText(n2, count.value)
+            })
+            return n2
+          })),
+          // v-else
+          (spyElseFn ||= vi.fn(() => {
+            const n4 = t2()
+            return n4
+          })),
+        ),
+        n0 as any as ParentNode,
+      )
+      return n0
+    }).render()
+
+    expect(host.innerHTML).toBe('<div><p>zero</p><!--if--></div>')
+    expect(spyConditionFn).toHaveBeenCalledTimes(1)
+    expect(spyIfFn!).toHaveBeenCalledTimes(0)
+    expect(spyElseFn!).toHaveBeenCalledTimes(1)
+
+    count.value++
+    await nextTick()
+    expect(host.innerHTML).toBe('<div><p>1</p><!--if--></div>')
+    expect(spyConditionFn).toHaveBeenCalledTimes(2)
+    expect(spyIfFn!).toHaveBeenCalledTimes(1)
+    expect(spyElseFn!).toHaveBeenCalledTimes(1)
+
+    count.value++
+    await nextTick()
+    expect(host.innerHTML).toBe('<div><p>2</p><!--if--></div>')
+    expect(spyConditionFn).toHaveBeenCalledTimes(3)
+    expect(spyIfFn!).toHaveBeenCalledTimes(1)
+    expect(spyElseFn!).toHaveBeenCalledTimes(1)
+
+    count.value = 0
+    await nextTick()
+    expect(host.innerHTML).toBe('<div><p>zero</p><!--if--></div>')
+    expect(spyConditionFn).toHaveBeenCalledTimes(4)
+    expect(spyIfFn!).toHaveBeenCalledTimes(1)
+    expect(spyElseFn!).toHaveBeenCalledTimes(2)
+  })
+
+  test('should handle nested template', async () => {
+    // mock this template:
+    //  <template v-if="ok1">
+    //    Hello <template v-if="ok2">Vapor</template>
+    //  </template>
+
+    const ok1 = ref(true)
+    const ok2 = ref(true)
+
+    const t0 = template('Vapor')
+    const t1 = template('Hello ')
+    const { host } = define(() => {
+      const n1 = createIf(
+        () => ok1.value,
+        () => {
+          const n2 = t1()
+          const n3 = createIf(
+            () => ok2.value,
+            () => {
+              const n4 = t0()
+              return n4
+            },
+          )
+          return [n2, n3]
+        },
+      )
+      return n1
+    }).render()
+
+    expect(host.innerHTML).toBe('Hello Vapor<!--if--><!--if-->')
+
+    ok1.value = false
+    await nextTick()
+    expect(host.innerHTML).toBe('<!--if-->')
+
+    ok1.value = true
+    await nextTick()
+    expect(host.innerHTML).toBe('Hello Vapor<!--if--><!--if-->')
+
+    ok2.value = false
+    await nextTick()
+    expect(host.innerHTML).toBe('Hello <!--if--><!--if-->')
+
+    ok1.value = false
+    await nextTick()
+    expect(host.innerHTML).toBe('<!--if-->')
+  })
+
+  test.todo('should work with directive hooks', async () => {
+    const calls: string[] = []
+    const show1 = ref(true)
+    const show2 = ref(true)
+    const update = ref(0)
+
+    const spyConditionFn1 = vi.fn(() => show1.value)
+    const spyConditionFn2 = vi.fn(() => show2.value)
+
+    const vDirective: any = {
+      created: (el: any, { value }: any) => calls.push(`${value} created`),
+      beforeMount: (el: any, { value }: any) =>
+        calls.push(`${value} beforeMount`),
+      mounted: (el: any, { value }: any) => calls.push(`${value} mounted`),
+      beforeUpdate: (el: any, { value }: any) =>
+        calls.push(`${value} beforeUpdate`),
+      updated: (el: any, { value }: any) => calls.push(`${value} updated`),
+      beforeUnmount: (el: any, { value }: any) =>
+        calls.push(`${value} beforeUnmount`),
+      unmounted: (el: any, { value }: any) => calls.push(`${value} unmounted`),
+    }
+
+    const t0 = template('<p></p>')
+    const { instance } = define(() => {
+      const n1 = createIf(
+        spyConditionFn1,
+        () => {
+          const n2 = t0() as ParentNode
+          withDirectives(child(n2), [[vDirective, () => (update.value, '1')]])
+          return n2
+        },
+        () =>
+          createIf(
+            spyConditionFn2,
+            () => {
+              const n2 = t0() as ParentNode
+              withDirectives(child(n2), [[vDirective, () => '2']])
+              return n2
+            },
+            () => {
+              const n2 = t0() as ParentNode
+              withDirectives(child(n2), [[vDirective, () => '3']])
+              return n2
+            },
+          ),
+      )
+      return [n1]
+    }).render()
+
+    await nextTick()
+    expect(calls).toEqual(['1 created', '1 beforeMount', '1 mounted'])
+    calls.length = 0
+    expect(spyConditionFn1).toHaveBeenCalledTimes(1)
+    expect(spyConditionFn2).toHaveBeenCalledTimes(0)
+
+    show1.value = false
+    await nextTick()
+    expect(calls).toEqual([
+      '1 beforeUnmount',
+      '2 created',
+      '2 beforeMount',
+      '1 unmounted',
+      '2 mounted',
+    ])
+    calls.length = 0
+    expect(spyConditionFn1).toHaveBeenCalledTimes(2)
+    expect(spyConditionFn2).toHaveBeenCalledTimes(1)
+
+    show2.value = false
+    await nextTick()
+    expect(calls).toEqual([
+      '2 beforeUnmount',
+      '3 created',
+      '3 beforeMount',
+      '2 unmounted',
+      '3 mounted',
+    ])
+    calls.length = 0
+    expect(spyConditionFn1).toHaveBeenCalledTimes(2)
+    expect(spyConditionFn2).toHaveBeenCalledTimes(2)
+
+    show1.value = true
+    await nextTick()
+    expect(calls).toEqual([
+      '3 beforeUnmount',
+      '1 created',
+      '1 beforeMount',
+      '3 unmounted',
+      '1 mounted',
+    ])
+    calls.length = 0
+    expect(spyConditionFn1).toHaveBeenCalledTimes(3)
+    expect(spyConditionFn2).toHaveBeenCalledTimes(2)
+
+    update.value++
+    await nextTick()
+    expect(calls).toEqual(['1 beforeUpdate', '1 updated'])
+    calls.length = 0
+    expect(spyConditionFn1).toHaveBeenCalledTimes(3)
+    expect(spyConditionFn2).toHaveBeenCalledTimes(2)
+
+    unmountComponent(instance!)
+    expect(calls).toEqual(['1 beforeUnmount', '1 unmounted'])
+    expect(spyConditionFn1).toHaveBeenCalledTimes(3)
+    expect(spyConditionFn2).toHaveBeenCalledTimes(2)
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/renderEffect.spec.ts b/packages/runtime-vapor/__tests__/renderEffect.spec.ts
new file mode 100644
index 00000000000..425d84990d2
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/renderEffect.spec.ts
@@ -0,0 +1,245 @@
+import {
+  EffectScope,
+  type GenericComponentInstance,
+  currentInstance,
+  getCurrentScope,
+  nextTick,
+  onBeforeUpdate,
+  onUpdated,
+  ref,
+  watchEffect,
+  watchPostEffect,
+  watchSyncEffect,
+} from '@vue/runtime-dom'
+import { renderEffect, template } from '../src'
+import { onEffectCleanup } from '@vue/reactivity'
+import { makeRender } from './_utils'
+
+const define = makeRender<any>()
+const createDemo = (setupFn: () => any, renderFn: (ctx: any) => any) =>
+  define({
+    setup: () => {
+      const returned = setupFn()
+      Object.defineProperty(returned, '__isScriptSetup', {
+        enumerable: false,
+        value: true,
+      })
+      return returned
+    },
+    render: (ctx: any) => {
+      const t0 = template('<div></div>')
+      renderFn(ctx)
+      return t0()
+    },
+  })
+
+describe('renderEffect', () => {
+  test('basic', async () => {
+    let dummy: any
+    const source = ref(0)
+    renderEffect(() => {
+      dummy = source.value
+    })
+    expect(dummy).toBe(0)
+    await nextTick()
+    expect(dummy).toBe(0)
+
+    source.value++
+    expect(dummy).toBe(0)
+    await nextTick()
+    expect(dummy).toBe(1)
+
+    source.value++
+    expect(dummy).toBe(1)
+    await nextTick()
+    expect(dummy).toBe(2)
+
+    source.value++
+    expect(dummy).toBe(2)
+    await nextTick()
+    expect(dummy).toBe(3)
+  })
+
+  test('should run with the scheduling order', async () => {
+    const calls: string[] = []
+
+    const { instance } = createDemo(
+      () => {
+        // setup
+        const source = ref(0)
+        const renderSource = ref(0)
+        const change = () => source.value++
+        const changeRender = () => renderSource.value++
+
+        // Life Cycle Hooks
+        onUpdated(() => {
+          calls.push(`updated ${source.value}`)
+        })
+        onBeforeUpdate(() => {
+          calls.push(`beforeUpdate ${source.value}`)
+        })
+
+        // Watch API
+        watchPostEffect(() => {
+          const current = source.value
+          calls.push(`post ${current}`)
+          onEffectCleanup(() => calls.push(`post cleanup ${current}`))
+        })
+        watchEffect(() => {
+          const current = source.value
+          calls.push(`pre ${current}`)
+          onEffectCleanup(() => calls.push(`pre cleanup ${current}`))
+        })
+        watchSyncEffect(() => {
+          const current = source.value
+          calls.push(`sync ${current}`)
+          onEffectCleanup(() => calls.push(`sync cleanup ${current}`))
+        })
+        return { source, change, renderSource, changeRender }
+      },
+      // render
+      _ctx => {
+        // Render Watch API
+        renderEffect(() => {
+          const current = _ctx.renderSource
+          calls.push(`renderEffect ${current}`)
+          onEffectCleanup(() => calls.push(`renderEffect cleanup ${current}`))
+        })
+      },
+    ).render()
+
+    const { change, changeRender } = instance?.setupState as any
+
+    await nextTick()
+    expect(calls).toEqual(['pre 0', 'sync 0', 'renderEffect 0', 'post 0'])
+    calls.length = 0
+
+    // Update
+    changeRender()
+    change()
+
+    expect(calls).toEqual(['sync cleanup 0', 'sync 1'])
+    calls.length = 0
+
+    await nextTick()
+    expect(calls).toEqual([
+      'pre cleanup 0',
+      'pre 1',
+      'renderEffect cleanup 0',
+      'beforeUpdate 1',
+      'renderEffect 1',
+      'post cleanup 0',
+      'post 1',
+      'updated 1',
+    ])
+    calls.length = 0
+
+    // Update
+    changeRender()
+    change()
+
+    expect(calls).toEqual(['sync cleanup 1', 'sync 2'])
+    calls.length = 0
+
+    await nextTick()
+    expect(calls).toEqual([
+      'pre cleanup 1',
+      'pre 2',
+      'renderEffect cleanup 1',
+      'beforeUpdate 2',
+      'renderEffect 2',
+      'post cleanup 1',
+      'post 2',
+      'updated 2',
+    ])
+  })
+
+  test('errors should include the execution location with beforeUpdate hook', async () => {
+    const { instance } = createDemo(
+      // setup
+      () => {
+        const source = ref()
+        const update = () => source.value++
+        onBeforeUpdate(() => {
+          throw 'error in beforeUpdate'
+        })
+        return { source, update }
+      },
+      // render
+      ctx => {
+        renderEffect(() => {
+          ctx.source
+        })
+      },
+    ).render()
+    const { update } = instance?.setupState as any
+
+    await expect(async () => {
+      update()
+      await nextTick()
+    }).rejects.toThrow('error in beforeUpdate')
+
+    expect(
+      '[Vue warn]: Unhandled error during execution of beforeUpdate hook',
+    ).toHaveBeenWarned()
+    expect(
+      '[Vue warn]: Unhandled error during execution of component update',
+    ).toHaveBeenWarned()
+  })
+
+  test('errors should include the execution location with updated hook', async () => {
+    const { instance } = createDemo(
+      // setup
+      () => {
+        const source = ref(0)
+        const update = () => source.value++
+        onUpdated(() => {
+          throw 'error in updated'
+        })
+        return { source, update }
+      },
+      // render
+      ctx => {
+        renderEffect(() => {
+          ctx.source
+        })
+      },
+    ).render()
+
+    const { update } = instance?.setupState as any
+
+    await expect(async () => {
+      update()
+      await nextTick()
+    }).rejects.toThrow('error in updated')
+
+    expect(
+      '[Vue warn]: Unhandled error during execution of updated',
+    ).toHaveBeenWarned()
+  })
+
+  test('should be called with the current instance and current scope', async () => {
+    const source = ref(0)
+    const scope = new EffectScope()
+    let instanceSnap: GenericComponentInstance | null = null
+    let scopeSnap: EffectScope | undefined = undefined
+    const { instance } = define(() => {
+      scope.run(() => {
+        renderEffect(() => {
+          source.value
+          instanceSnap = currentInstance
+          scopeSnap = getCurrentScope()
+        })
+      })
+      return []
+    }).render()
+
+    expect(instanceSnap).toBe(instance)
+    expect(scopeSnap).toBe(scope)
+
+    source.value++
+    await nextTick()
+    expect(instanceSnap).toBe(instance)
+    expect(scopeSnap).toBe(scope)
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/vdomInterop.spec.ts b/packages/runtime-vapor/__tests__/vdomInterop.spec.ts
new file mode 100644
index 00000000000..98c4cdbc9d0
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/vdomInterop.spec.ts
@@ -0,0 +1,220 @@
+import {
+  createVNode,
+  defineComponent,
+  h,
+  nextTick,
+  ref,
+  renderSlot,
+  toDisplayString,
+  useModel,
+} from '@vue/runtime-dom'
+import { makeInteropRender } from './_utils'
+import {
+  applyTextModel,
+  child,
+  createComponent,
+  defineVaporComponent,
+  renderEffect,
+  setText,
+  template,
+} from '../src'
+
+const define = makeInteropRender()
+
+describe('vdomInterop', () => {
+  describe('props', () => {
+    test('should work if props are not provided', () => {
+      const VaporChild = defineVaporComponent({
+        props: {
+          msg: String,
+        },
+        setup(_, { attrs }) {
+          return [document.createTextNode(attrs.class || 'foo')]
+        },
+      })
+
+      const { html } = define({
+        setup() {
+          return () => h(VaporChild as any)
+        },
+      }).render()
+
+      expect(html()).toBe('foo')
+    })
+  })
+
+  describe('v-model', () => {
+    test('basic work', async () => {
+      const VaporChild = defineVaporComponent({
+        props: {
+          modelValue: {},
+          modelModifiers: {},
+        },
+        emits: ['update:modelValue'],
+        setup(__props) {
+          const modelValue = useModel(__props, 'modelValue')
+
+          const n0 = template('<h1> </h1>')() as any
+          const n1 = template('<input>')() as any
+          const x0 = child(n0) as any
+          applyTextModel(
+            n1,
+            () => modelValue.value,
+            _value => (modelValue.value = _value),
+          )
+          renderEffect(() => setText(x0, toDisplayString(modelValue.value)))
+          return [n0, n1]
+        },
+      })
+
+      const { html, host } = define({
+        setup() {
+          const msg = ref('foo')
+          return () =>
+            h(VaporChild as any, {
+              modelValue: msg.value,
+              'onUpdate:modelValue': (value: string) => {
+                msg.value = value
+              },
+            })
+        },
+      }).render()
+
+      expect(html()).toBe('<h1>foo</h1><input>')
+
+      const inputEl = host.querySelector('input')!
+      inputEl.value = 'bar'
+      inputEl.dispatchEvent(new Event('input'))
+
+      await nextTick()
+      expect(html()).toBe('<h1>bar</h1><input>')
+    })
+  })
+
+  describe('emit', () => {
+    test('emit from vapor child to vdom parent', () => {
+      const VaporChild = defineVaporComponent({
+        emits: ['click'],
+        setup(_, { emit }) {
+          emit('click')
+          return []
+        },
+      })
+
+      const fn = vi.fn()
+      define({
+        setup() {
+          return () => h(VaporChild as any, { onClick: fn })
+        },
+      }).render()
+
+      // fn should be called once
+      expect(fn).toHaveBeenCalledTimes(1)
+    })
+  })
+
+  describe('slots', () => {
+    test('basic', () => {
+      const VDomChild = defineComponent({
+        setup(_, { slots }) {
+          return () => renderSlot(slots, 'default')
+        },
+      })
+
+      const VaporChild = defineVaporComponent({
+        setup() {
+          return createComponent(
+            VDomChild as any,
+            null,
+            {
+              default: () => document.createTextNode('default slot'),
+            },
+            true,
+          )
+        },
+      })
+
+      const { html } = define({
+        setup() {
+          return () => h(VaporChild as any)
+        },
+      }).render()
+
+      expect(html()).toBe('default slot')
+    })
+
+    test('functional slot', () => {
+      const VDomChild = defineComponent({
+        setup(_, { slots }) {
+          return () => createVNode(slots.default!)
+        },
+      })
+
+      const VaporChild = defineVaporComponent({
+        setup() {
+          return createComponent(
+            VDomChild as any,
+            null,
+            {
+              default: () => document.createTextNode('default slot'),
+            },
+            true,
+          )
+        },
+      })
+
+      const { html } = define({
+        setup() {
+          return () => h(VaporChild as any)
+        },
+      }).render()
+
+      expect(html()).toBe('default slot')
+    })
+  })
+
+  describe.todo('provide', () => {})
+
+  describe.todo('inject', () => {})
+
+  describe.todo('template ref', () => {})
+
+  describe.todo('dynamic component', () => {})
+
+  describe('attribute fallthrough', () => {
+    it('should not fallthrough emit handlers to vdom child', () => {
+      const VDomChild = defineComponent({
+        emits: ['click'],
+        setup(_, { emit }) {
+          return () => h('button', { onClick: () => emit('click') }, 'click me')
+        },
+      })
+
+      const fn = vi.fn()
+      const VaporChild = defineVaporComponent({
+        emits: ['click'],
+        setup() {
+          return createComponent(
+            VDomChild as any,
+            { onClick: () => fn },
+            null,
+            true,
+          )
+        },
+      })
+
+      const { host, html } = define({
+        setup() {
+          return () => h(VaporChild as any)
+        },
+      }).render()
+
+      expect(html()).toBe('<button>click me</button>')
+      const button = host.querySelector('button')!
+      button.dispatchEvent(new Event('click'))
+
+      // fn should be called once
+      expect(fn).toHaveBeenCalledTimes(1)
+    })
+  })
+})
diff --git a/packages/runtime-vapor/package.json b/packages/runtime-vapor/package.json
new file mode 100644
index 00000000000..f27ef46b7e7
--- /dev/null
+++ b/packages/runtime-vapor/package.json
@@ -0,0 +1,47 @@
+{
+  "name": "@vue/runtime-vapor",
+  "version": "3.6.0-alpha.2",
+  "description": "@vue/runtime-vapor",
+  "main": "index.js",
+  "module": "dist/runtime-vapor.esm-bundler.js",
+  "types": "dist/runtime-vapor.d.ts",
+  "files": [
+    "index.js",
+    "dist"
+  ],
+  "exports": {
+    ".": {
+      "types": "./dist/runtime-vapor.d.ts",
+      "default": "./dist/runtime-vapor.esm-bundler.js"
+    },
+    "./*": "./*"
+  },
+  "sideEffects": false,
+  "buildOptions": {
+    "name": "VueRuntimeVapor",
+    "formats": [
+      "esm-bundler"
+    ]
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/vuejs/core.git",
+    "directory": "packages/runtime-vapor"
+  },
+  "keywords": [
+    "vue"
+  ],
+  "author": "Evan You",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/vuejs/core/issues"
+  },
+  "homepage": "https://github.com/vuejs/core/tree/dev/packages/runtime-vapor#readme",
+  "dependencies": {
+    "@vue/shared": "workspace:*",
+    "@vue/reactivity": "workspace:*"
+  },
+  "peerDependencies": {
+    "@vue/runtime-dom": "workspace:*"
+  }
+}
diff --git a/packages/runtime-vapor/src/.gitignore b/packages/runtime-vapor/src/.gitignore
new file mode 100644
index 00000000000..a1f960b5e5a
--- /dev/null
+++ b/packages/runtime-vapor/src/.gitignore
@@ -0,0 +1 @@
+_old
diff --git a/packages/runtime-vapor/src/apiCreateApp.ts b/packages/runtime-vapor/src/apiCreateApp.ts
new file mode 100644
index 00000000000..bcc680eae42
--- /dev/null
+++ b/packages/runtime-vapor/src/apiCreateApp.ts
@@ -0,0 +1,120 @@
+import {
+  type VaporComponent,
+  type VaporComponentInstance,
+  createComponent,
+  getExposed,
+  mountComponent,
+  unmountComponent,
+} from './component'
+import {
+  type App,
+  type AppMountFn,
+  type AppUnmountFn,
+  type CreateAppFunction,
+  createAppAPI,
+  flushOnAppMount,
+  initFeatureFlags,
+  normalizeContainer,
+  setDevtoolsHook,
+  warn,
+} from '@vue/runtime-dom'
+import type { RawProps } from './componentProps'
+import { getGlobalThis } from '@vue/shared'
+import { optimizePropertyLookup } from './dom/prop'
+import { withHydration } from './dom/hydration'
+
+let _createApp: CreateAppFunction<ParentNode, VaporComponent>
+
+const mountApp: AppMountFn<ParentNode> = (app, container) => {
+  optimizePropertyLookup()
+
+  // clear content before mounting
+  if (container.nodeType === 1 /* Node.ELEMENT_NODE */) {
+    if (__DEV__ && container.childNodes.length) {
+      warn('mount target container is not empty and will be cleared.')
+    }
+    container.textContent = ''
+  }
+
+  const instance = createComponent(
+    app._component,
+    app._props as RawProps,
+    null,
+    false,
+    app._context,
+  )
+  mountComponent(instance, container)
+  flushOnAppMount()
+
+  return instance!
+}
+
+let _hydrateApp: CreateAppFunction<ParentNode, VaporComponent>
+
+const hydrateApp: AppMountFn<ParentNode> = (app, container) => {
+  optimizePropertyLookup()
+
+  let instance: VaporComponentInstance
+  withHydration(container, () => {
+    instance = createComponent(
+      app._component,
+      app._props as RawProps,
+      null,
+      false,
+      app._context,
+    )
+    mountComponent(instance, container)
+    flushOnAppMount()
+  })
+
+  return instance!
+}
+
+const unmountApp: AppUnmountFn = app => {
+  unmountComponent(app._instance as VaporComponentInstance, app._container)
+}
+
+function prepareApp() {
+  // compile-time feature flags check
+  if (__ESM_BUNDLER__ && !__TEST__) {
+    initFeatureFlags()
+  }
+
+  const target = getGlobalThis()
+  target.__VUE__ = true
+  if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
+    setDevtoolsHook(target.__VUE_DEVTOOLS_GLOBAL_HOOK__, target)
+  }
+}
+
+function postPrepareApp(app: App) {
+  app.vapor = true
+  const mount = app.mount
+  app.mount = (container, ...args: any[]) => {
+    container = normalizeContainer(container) as ParentNode
+    return mount(container, ...args)
+  }
+}
+
+export const createVaporApp: CreateAppFunction<ParentNode, VaporComponent> = (
+  comp,
+  props,
+) => {
+  prepareApp()
+  if (!_createApp) _createApp = createAppAPI(mountApp, unmountApp, getExposed)
+  const app = _createApp(comp, props)
+  postPrepareApp(app)
+  return app
+}
+
+export const createVaporSSRApp: CreateAppFunction<
+  ParentNode,
+  VaporComponent
+> = (comp, props) => {
+  prepareApp()
+  if (!_hydrateApp)
+    _hydrateApp = createAppAPI(hydrateApp, unmountApp, getExposed)
+  const app = _hydrateApp(comp, props)
+  postPrepareApp(app)
+  return app
+}
diff --git a/packages/runtime-vapor/src/apiCreateDynamicComponent.ts b/packages/runtime-vapor/src/apiCreateDynamicComponent.ts
new file mode 100644
index 00000000000..945e0f38d87
--- /dev/null
+++ b/packages/runtime-vapor/src/apiCreateDynamicComponent.ts
@@ -0,0 +1,51 @@
+import { resolveDynamicComponent } from '@vue/runtime-dom'
+import { DynamicFragment, type VaporFragment, insert } from './block'
+import { createComponentWithFallback } from './component'
+import { renderEffect } from './renderEffect'
+import type { RawProps } from './componentProps'
+import type { RawSlots } from './componentSlots'
+import {
+  insertionAnchor,
+  insertionParent,
+  resetInsertionState,
+} from './insertionState'
+import { isHydrating, locateHydrationNode } from './dom/hydration'
+
+export function createDynamicComponent(
+  getter: () => any,
+  rawProps?: RawProps | null,
+  rawSlots?: RawSlots | null,
+  isSingleRoot?: boolean,
+): VaporFragment {
+  const _insertionParent = insertionParent
+  const _insertionAnchor = insertionAnchor
+  if (isHydrating) {
+    locateHydrationNode()
+  } else {
+    resetInsertionState()
+  }
+
+  const frag = __DEV__
+    ? new DynamicFragment('dynamic-component')
+    : new DynamicFragment()
+
+  renderEffect(() => {
+    const value = getter()
+    frag.update(
+      () =>
+        createComponentWithFallback(
+          resolveDynamicComponent(value) as any,
+          rawProps,
+          rawSlots,
+          isSingleRoot,
+        ),
+      value,
+    )
+  })
+
+  if (!isHydrating && _insertionParent) {
+    insert(frag, _insertionParent, _insertionAnchor)
+  }
+
+  return frag
+}
diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts
new file mode 100644
index 00000000000..bdd2af7b348
--- /dev/null
+++ b/packages/runtime-vapor/src/apiCreateFor.ts
@@ -0,0 +1,579 @@
+import {
+  EffectScope,
+  type ShallowRef,
+  isReactive,
+  isReadonly,
+  isShallow,
+  setActiveSub,
+  shallowReadArray,
+  shallowRef,
+  toReactive,
+  toReadonly,
+  watch,
+} from '@vue/reactivity'
+import { isArray, isObject, isString } from '@vue/shared'
+import { createComment, createTextNode } from './dom/node'
+import {
+  type Block,
+  VaporFragment,
+  insert,
+  remove as removeBlock,
+} from './block'
+import { warn } from '@vue/runtime-dom'
+import { currentInstance, isVaporComponent } from './component'
+import type { DynamicSlot } from './componentSlots'
+import { renderEffect } from './renderEffect'
+import { VaporVForFlags } from '../../shared/src/vaporFlags'
+import { isHydrating, locateHydrationNode } from './dom/hydration'
+import {
+  insertionAnchor,
+  insertionParent,
+  resetInsertionState,
+} from './insertionState'
+
+class ForBlock extends VaporFragment {
+  scope: EffectScope | undefined
+  key: any
+
+  itemRef: ShallowRef<any>
+  keyRef: ShallowRef<any> | undefined
+  indexRef: ShallowRef<number | undefined> | undefined
+
+  constructor(
+    nodes: Block,
+    scope: EffectScope | undefined,
+    item: ShallowRef<any>,
+    key: ShallowRef<any> | undefined,
+    index: ShallowRef<number | undefined> | undefined,
+    renderKey: any,
+  ) {
+    super(nodes)
+    this.scope = scope
+    this.itemRef = item
+    this.keyRef = key
+    this.indexRef = index
+    this.key = renderKey
+  }
+}
+
+type Source = any[] | Record<any, any> | number | Set<any> | Map<any, any>
+
+type ResolvedSource = {
+  values: any[]
+  needsWrap: boolean
+  isReadonlySource: boolean
+  keys?: string[]
+}
+
+export const createFor = (
+  src: () => Source,
+  renderItem: (
+    item: ShallowRef<any>,
+    key: ShallowRef<any>,
+    index: ShallowRef<number | undefined>,
+  ) => Block,
+  getKey?: (item: any, key: any, index?: number) => any,
+  flags = 0,
+  setup?: (_: {
+    createSelector: (source: () => any) => (cb: () => void) => void
+  }) => void,
+): VaporFragment => {
+  const _insertionParent = insertionParent
+  const _insertionAnchor = insertionAnchor
+  if (isHydrating) {
+    locateHydrationNode()
+  } else {
+    resetInsertionState()
+  }
+
+  let isMounted = false
+  let oldBlocks: ForBlock[] = []
+  let newBlocks: ForBlock[]
+  let parent: ParentNode | undefined | null
+  // useSelector only
+  let currentKey: any
+  // TODO handle this in hydration
+  const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
+  const frag = new VaporFragment(oldBlocks)
+  const instance = currentInstance!
+  const canUseFastRemove = !!(flags & VaporVForFlags.FAST_REMOVE)
+  const isComponent = !!(flags & VaporVForFlags.IS_COMPONENT)
+  const selectors: {
+    deregister: (key: any) => void
+    cleanup: () => void
+  }[] = []
+
+  if (__DEV__ && !instance) {
+    warn('createFor() can only be used inside setup()')
+  }
+
+  const renderList = () => {
+    const source = normalizeSource(src())
+    const newLength = source.values.length
+    const oldLength = oldBlocks.length
+    newBlocks = new Array(newLength)
+
+    const prevSub = setActiveSub()
+
+    if (!isMounted) {
+      isMounted = true
+      for (let i = 0; i < newLength; i++) {
+        mount(source, i)
+      }
+    } else {
+      parent = parent || parentAnchor!.parentNode
+      if (!oldLength) {
+        // fast path for all new
+        for (let i = 0; i < newLength; i++) {
+          mount(source, i)
+        }
+      } else if (!newLength) {
+        // fast path for clearing all
+        for (const selector of selectors) {
+          selector.cleanup()
+        }
+        const doRemove = !canUseFastRemove
+        for (let i = 0; i < oldLength; i++) {
+          unmount(oldBlocks[i], doRemove, false)
+        }
+        if (canUseFastRemove) {
+          parent!.textContent = ''
+          parent!.appendChild(parentAnchor)
+        }
+      } else if (!getKey) {
+        // unkeyed fast path
+        const commonLength = Math.min(newLength, oldLength)
+        for (let i = 0; i < commonLength; i++) {
+          update((newBlocks[i] = oldBlocks[i]), getItem(source, i)[0])
+        }
+        for (let i = oldLength; i < newLength; i++) {
+          mount(source, i)
+        }
+        for (let i = newLength; i < oldLength; i++) {
+          unmount(oldBlocks[i])
+        }
+      } else {
+        if (__DEV__) {
+          const keyToIndexMap: Map<any, number> = new Map()
+          for (let i = 0; i < newLength; i++) {
+            const item = getItem(source, i)
+            const key = getKey(...item)
+            if (key != null) {
+              if (keyToIndexMap.has(key)) {
+                warn(
+                  `Duplicate keys found during update:`,
+                  JSON.stringify(key),
+                  `Make sure keys are unique.`,
+                )
+              }
+              keyToIndexMap.set(key, i)
+            }
+          }
+        }
+
+        const sharedBlockCount = Math.min(oldLength, newLength)
+        const previousKeyIndexPairs: [any, number][] = new Array(oldLength)
+        const queuedBlocks: [
+          blockIndex: number,
+          blockItem: ReturnType<typeof getItem>,
+          blockKey: any,
+        ][] = new Array(newLength)
+
+        let anchorFallback: Node = parentAnchor
+        let endOffset = 0
+        let startOffset = 0
+        let queuedBlocksInsertIndex = 0
+        let previousKeyIndexInsertIndex = 0
+
+        while (endOffset < sharedBlockCount) {
+          const currentIndex = newLength - endOffset - 1
+          const currentItem = getItem(source, currentIndex)
+          const currentKey = getKey(...currentItem)
+          const existingBlock = oldBlocks[oldLength - endOffset - 1]
+          if (existingBlock.key === currentKey) {
+            update(existingBlock, ...currentItem)
+            newBlocks[currentIndex] = existingBlock
+            endOffset++
+            continue
+          }
+          break
+        }
+
+        if (endOffset !== 0) {
+          anchorFallback = normalizeAnchor(
+            newBlocks[newLength - endOffset].nodes,
+          )
+        }
+
+        while (startOffset < sharedBlockCount - endOffset) {
+          const currentItem = getItem(source, startOffset)
+          const currentKey = getKey(...currentItem)
+          const previousBlock = oldBlocks[startOffset]
+          const previousKey = previousBlock.key
+          if (previousKey === currentKey) {
+            update((newBlocks[startOffset] = previousBlock), currentItem[0])
+          } else {
+            queuedBlocks[queuedBlocksInsertIndex++] = [
+              startOffset,
+              currentItem,
+              currentKey,
+            ]
+            previousKeyIndexPairs[previousKeyIndexInsertIndex++] = [
+              previousKey,
+              startOffset,
+            ]
+          }
+          startOffset++
+        }
+
+        for (let i = startOffset; i < oldLength - endOffset; i++) {
+          previousKeyIndexPairs[previousKeyIndexInsertIndex++] = [
+            oldBlocks[i].key,
+            i,
+          ]
+        }
+
+        const preparationBlockCount = Math.min(
+          newLength - endOffset,
+          sharedBlockCount,
+        )
+        for (let i = startOffset; i < preparationBlockCount; i++) {
+          const blockItem = getItem(source, i)
+          const blockKey = getKey(...blockItem)
+          queuedBlocks[queuedBlocksInsertIndex++] = [i, blockItem, blockKey]
+        }
+
+        if (!queuedBlocksInsertIndex && !previousKeyIndexInsertIndex) {
+          for (let i = preparationBlockCount; i < newLength - endOffset; i++) {
+            const blockItem = getItem(source, i)
+            const blockKey = getKey(...blockItem)
+            mount(source, i, anchorFallback, blockItem, blockKey)
+          }
+        } else {
+          queuedBlocks.length = queuedBlocksInsertIndex
+          previousKeyIndexPairs.length = previousKeyIndexInsertIndex
+
+          const previousKeyIndexMap = new Map(previousKeyIndexPairs)
+          const operations: (() => void)[] = []
+
+          let mountCounter = 0
+          const relocateOrMountBlock = (
+            blockIndex: number,
+            blockItem: ReturnType<typeof getItem>,
+            blockKey: any,
+            anchorOffset: number,
+          ) => {
+            const previousIndex = previousKeyIndexMap.get(blockKey)
+            if (previousIndex !== undefined) {
+              const reusedBlock = (newBlocks[blockIndex] =
+                oldBlocks[previousIndex])
+              update(reusedBlock, ...blockItem)
+              previousKeyIndexMap.delete(blockKey)
+              if (previousIndex !== blockIndex) {
+                operations.push(() =>
+                  insert(
+                    reusedBlock,
+                    parent!,
+                    anchorOffset === -1
+                      ? anchorFallback
+                      : normalizeAnchor(newBlocks[anchorOffset].nodes),
+                  ),
+                )
+              }
+            } else {
+              mountCounter++
+              operations.push(() =>
+                mount(
+                  source,
+                  blockIndex,
+                  anchorOffset === -1
+                    ? anchorFallback
+                    : normalizeAnchor(newBlocks[anchorOffset].nodes),
+                  blockItem,
+                  blockKey,
+                ),
+              )
+            }
+          }
+
+          for (let i = queuedBlocks.length - 1; i >= 0; i--) {
+            const [blockIndex, blockItem, blockKey] = queuedBlocks[i]
+            relocateOrMountBlock(
+              blockIndex,
+              blockItem,
+              blockKey,
+              blockIndex < preparationBlockCount - 1 ? blockIndex + 1 : -1,
+            )
+          }
+
+          for (let i = preparationBlockCount; i < newLength - endOffset; i++) {
+            const blockItem = getItem(source, i)
+            const blockKey = getKey(...blockItem)
+            relocateOrMountBlock(i, blockItem, blockKey, -1)
+          }
+
+          const useFastRemove = mountCounter === newLength
+
+          for (const leftoverIndex of previousKeyIndexMap.values()) {
+            unmount(
+              oldBlocks[leftoverIndex],
+              !(useFastRemove && canUseFastRemove),
+              !useFastRemove,
+            )
+          }
+          if (useFastRemove) {
+            for (const selector of selectors) {
+              selector.cleanup()
+            }
+            if (canUseFastRemove) {
+              parent!.textContent = ''
+              parent!.appendChild(parentAnchor)
+            }
+          }
+
+          // perform mount and move operations
+          for (const action of operations) {
+            action()
+          }
+        }
+      }
+    }
+
+    frag.nodes = [(oldBlocks = newBlocks)]
+    if (parentAnchor) {
+      frag.nodes.push(parentAnchor)
+    }
+
+    setActiveSub(prevSub)
+  }
+
+  const needKey = renderItem.length > 1
+  const needIndex = renderItem.length > 2
+
+  const mount = (
+    source: ResolvedSource,
+    idx: number,
+    anchor: Node | undefined = parentAnchor,
+    [item, key, index] = getItem(source, idx),
+    key2 = getKey && getKey(item, key, index),
+  ): ForBlock => {
+    const itemRef = shallowRef(item)
+    // avoid creating refs if the render fn doesn't need it
+    const keyRef = needKey ? shallowRef(key) : undefined
+    const indexRef = needIndex ? shallowRef(index) : undefined
+
+    currentKey = key2
+    let nodes: Block
+    let scope: EffectScope | undefined
+    if (isComponent) {
+      // component already has its own scope so no outer scope needed
+      nodes = renderItem(itemRef, keyRef as any, indexRef as any)
+    } else {
+      scope = new EffectScope()
+      nodes = scope.run(() =>
+        renderItem(itemRef, keyRef as any, indexRef as any),
+      )!
+    }
+
+    const block = (newBlocks[idx] = new ForBlock(
+      nodes,
+      scope,
+      itemRef,
+      keyRef,
+      indexRef,
+      key2,
+    ))
+
+    if (parent) insert(block.nodes, parent, anchor)
+
+    return block
+  }
+
+  const update = (
+    { itemRef, keyRef, indexRef }: ForBlock,
+    newItem: any,
+    newKey?: any,
+    newIndex?: any,
+  ) => {
+    if (newItem !== itemRef.value) {
+      itemRef.value = newItem
+    }
+    if (keyRef && newKey !== undefined && newKey !== keyRef.value) {
+      keyRef.value = newKey
+    }
+    if (indexRef && newIndex !== undefined && newIndex !== indexRef.value) {
+      indexRef.value = newIndex
+    }
+  }
+
+  const unmount = (block: ForBlock, doRemove = true, doDeregister = true) => {
+    if (!isComponent) {
+      block.scope!.stop()
+    }
+    if (doRemove) {
+      removeBlock(block.nodes, parent!)
+    }
+    if (doDeregister) {
+      for (const selector of selectors) {
+        selector.deregister(block.key)
+      }
+    }
+  }
+
+  if (setup) {
+    setup({ createSelector })
+  }
+
+  if (flags & VaporVForFlags.ONCE) {
+    renderList()
+  } else {
+    renderEffect(renderList)
+  }
+
+  if (!isHydrating && _insertionParent) {
+    insert(frag, _insertionParent, _insertionAnchor)
+  }
+
+  return frag
+
+  function createSelector(source: () => any): (cb: () => void) => void {
+    let operMap = new Map<any, (() => void)[]>()
+    let activeKey = source()
+    let activeOpers: (() => void)[] | undefined
+
+    watch(source, newValue => {
+      if (activeOpers !== undefined) {
+        for (const oper of activeOpers) {
+          oper()
+        }
+      }
+      activeOpers = operMap.get(newValue)
+      if (activeOpers !== undefined) {
+        for (const oper of activeOpers) {
+          oper()
+        }
+      }
+    })
+
+    selectors.push({ deregister, cleanup })
+    return register
+
+    function cleanup() {
+      operMap = new Map()
+      activeOpers = undefined
+    }
+
+    function register(oper: () => void) {
+      oper()
+      let opers = operMap.get(currentKey)
+      if (opers !== undefined) {
+        opers.push(oper)
+      } else {
+        opers = [oper]
+        operMap.set(currentKey, opers)
+        if (currentKey === activeKey) {
+          activeOpers = opers
+        }
+      }
+    }
+
+    function deregister(key: any) {
+      operMap.delete(key)
+      if (key === activeKey) {
+        activeOpers = undefined
+      }
+    }
+  }
+}
+
+export function createForSlots(
+  rawSource: Source,
+  getSlot: (item: any, key: any, index?: number) => DynamicSlot,
+): DynamicSlot[] {
+  const source = normalizeSource(rawSource)
+  const sourceLength = source.values.length
+  const slots = new Array<DynamicSlot>(sourceLength)
+  for (let i = 0; i < sourceLength; i++) {
+    slots[i] = getSlot(...getItem(source, i))
+  }
+  return slots
+}
+
+function normalizeSource(source: any): ResolvedSource {
+  let values = source
+  let needsWrap = false
+  let isReadonlySource = false
+  let keys
+  if (isArray(source)) {
+    if (isReactive(source)) {
+      needsWrap = !isShallow(source)
+      values = shallowReadArray(source)
+      isReadonlySource = isReadonly(source)
+    }
+  } else if (isString(source)) {
+    values = source.split('')
+  } else if (typeof source === 'number') {
+    if (__DEV__ && !Number.isInteger(source)) {
+      warn(`The v-for range expect an integer value but got ${source}.`)
+    }
+    values = new Array(source)
+    for (let i = 0; i < source; i++) values[i] = i + 1
+  } else if (isObject(source)) {
+    if (source[Symbol.iterator as any]) {
+      values = Array.from(source as Iterable<any>)
+    } else {
+      keys = Object.keys(source)
+      values = new Array(keys.length)
+      for (let i = 0, l = keys.length; i < l; i++) {
+        values[i] = source[keys[i]]
+      }
+    }
+  }
+  return {
+    values,
+    needsWrap,
+    isReadonlySource,
+    keys,
+  }
+}
+
+function getItem(
+  { keys, values, needsWrap, isReadonlySource }: ResolvedSource,
+  idx: number,
+): [item: any, key: any, index?: number] {
+  const value = needsWrap
+    ? isReadonlySource
+      ? toReadonly(toReactive(values[idx]))
+      : toReactive(values[idx])
+    : values[idx]
+  if (keys) {
+    return [value, keys[idx], idx]
+  } else {
+    return [value, idx, undefined]
+  }
+}
+
+function normalizeAnchor(node: Block): Node {
+  if (node instanceof Node) {
+    return node
+  } else if (isArray(node)) {
+    return normalizeAnchor(node[0])
+  } else if (isVaporComponent(node)) {
+    return normalizeAnchor(node.block!)
+  } else {
+    return normalizeAnchor(node.nodes!)
+  }
+}
+
+// runtime helper for rest element destructure
+export function getRestElement(val: any, keys: string[]): any {
+  const res: any = {}
+  for (const key in val) {
+    if (!keys.includes(key)) res[key] = val[key]
+  }
+  return res
+}
+
+export function getDefaultValue(val: any, defaultVal: any): any {
+  return val === undefined ? defaultVal : val
+}
diff --git a/packages/runtime-vapor/src/apiCreateIf.ts b/packages/runtime-vapor/src/apiCreateIf.ts
new file mode 100644
index 00000000000..f573a61b16b
--- /dev/null
+++ b/packages/runtime-vapor/src/apiCreateIf.ts
@@ -0,0 +1,37 @@
+import { type Block, type BlockFn, DynamicFragment, insert } from './block'
+import { isHydrating, locateHydrationNode } from './dom/hydration'
+import {
+  insertionAnchor,
+  insertionParent,
+  resetInsertionState,
+} from './insertionState'
+import { renderEffect } from './renderEffect'
+
+export function createIf(
+  condition: () => any,
+  b1: BlockFn,
+  b2?: BlockFn,
+  once?: boolean,
+): Block {
+  const _insertionParent = insertionParent
+  const _insertionAnchor = insertionAnchor
+  if (isHydrating) {
+    locateHydrationNode()
+  } else {
+    resetInsertionState()
+  }
+
+  let frag: Block
+  if (once) {
+    frag = condition() ? b1() : b2 ? b2() : []
+  } else {
+    frag = __DEV__ ? new DynamicFragment('if') : new DynamicFragment()
+    renderEffect(() => (frag as DynamicFragment).update(condition() ? b1 : b2))
+  }
+
+  if (!isHydrating && _insertionParent) {
+    insert(frag, _insertionParent, _insertionAnchor)
+  }
+
+  return frag
+}
diff --git a/packages/runtime-vapor/src/apiDefineComponent.ts b/packages/runtime-vapor/src/apiDefineComponent.ts
new file mode 100644
index 00000000000..430f87cdd50
--- /dev/null
+++ b/packages/runtime-vapor/src/apiDefineComponent.ts
@@ -0,0 +1,21 @@
+import type { ObjectVaporComponent, VaporComponent } from './component'
+import { extend, isFunction } from '@vue/shared'
+
+/*! #__NO_SIDE_EFFECTS__ */
+export function defineVaporComponent(
+  comp: VaporComponent,
+  extraOptions?: Omit<ObjectVaporComponent, 'setup'>,
+): VaporComponent {
+  if (isFunction(comp)) {
+    // #8236: extend call and options.name access are considered side-effects
+    // by Rollup, so we have to wrap it in a pure-annotated IIFE.
+    return /*@__PURE__*/ (() =>
+      extend({ name: comp.name }, extraOptions, {
+        setup: comp,
+        __vapor: true,
+      }))()
+  }
+  // TODO type inference
+  comp.__vapor = true
+  return comp
+}
diff --git a/packages/runtime-vapor/src/apiTemplateRef.ts b/packages/runtime-vapor/src/apiTemplateRef.ts
new file mode 100644
index 00000000000..7a30d219811
--- /dev/null
+++ b/packages/runtime-vapor/src/apiTemplateRef.ts
@@ -0,0 +1,154 @@
+import { type Ref, isRef, onScopeDispose } from '@vue/reactivity'
+import {
+  type VaporComponentInstance,
+  currentInstance,
+  getExposed,
+  isVaporComponent,
+} from './component'
+import {
+  ErrorCodes,
+  type SchedulerJob,
+  callWithErrorHandling,
+  queuePostFlushCb,
+  warn,
+} from '@vue/runtime-dom'
+import {
+  EMPTY_OBJ,
+  hasOwn,
+  isArray,
+  isFunction,
+  isString,
+  remove,
+} from '@vue/shared'
+import { DynamicFragment } from './block'
+
+export type NodeRef = string | Ref | ((ref: Element) => void)
+export type RefEl = Element | VaporComponentInstance
+
+export type setRefFn = (
+  el: RefEl,
+  ref: NodeRef,
+  oldRef?: NodeRef,
+  refFor?: boolean,
+) => NodeRef | undefined
+
+export function createTemplateRefSetter(): setRefFn {
+  const instance = currentInstance as VaporComponentInstance
+  return (...args) => setRef(instance, ...args)
+}
+
+/**
+ * Function for handling a template ref
+ */
+export function setRef(
+  instance: VaporComponentInstance,
+  el: RefEl,
+  ref: NodeRef,
+  oldRef?: NodeRef,
+  refFor = false,
+): NodeRef | undefined {
+  if (!instance || instance.isUnmounted) return
+
+  const setupState: any = __DEV__ ? instance.setupState || {} : null
+  const refValue = getRefValue(el)
+
+  const refs =
+    instance.refs === EMPTY_OBJ ? (instance.refs = {}) : instance.refs
+
+  // dynamic ref changed. unset old ref
+  if (oldRef != null && oldRef !== ref) {
+    if (isString(oldRef)) {
+      refs[oldRef] = null
+      if (__DEV__ && hasOwn(setupState, oldRef)) {
+        setupState[oldRef] = null
+      }
+    } else if (isRef(oldRef)) {
+      oldRef.value = null
+    }
+  }
+
+  if (isFunction(ref)) {
+    const invokeRefSetter = (value?: Element | Record<string, any>) => {
+      callWithErrorHandling(ref, currentInstance, ErrorCodes.FUNCTION_REF, [
+        value,
+        refs,
+      ])
+    }
+
+    invokeRefSetter(refValue)
+    // TODO this gets called repeatedly in renderEffect when it's dynamic ref?
+    onScopeDispose(() => invokeRefSetter())
+  } else {
+    const _isString = isString(ref)
+    const _isRef = isRef(ref)
+    let existing: unknown
+
+    if (_isString || _isRef) {
+      const doSet: SchedulerJob = () => {
+        if (refFor) {
+          existing = _isString
+            ? __DEV__ && hasOwn(setupState, ref)
+              ? setupState[ref]
+              : refs[ref]
+            : ref.value
+
+          if (!isArray(existing)) {
+            existing = [refValue]
+            if (_isString) {
+              refs[ref] = existing
+              if (__DEV__ && hasOwn(setupState, ref)) {
+                setupState[ref] = refs[ref]
+                // if setupState[ref] is a reactivity ref,
+                // the existing will also become reactivity too
+                // need to get the Proxy object by resetting
+                existing = setupState[ref]
+              }
+            } else {
+              ref.value = existing
+            }
+          } else if (!existing.includes(refValue)) {
+            existing.push(refValue)
+          }
+        } else if (_isString) {
+          refs[ref] = refValue
+          if (__DEV__ && hasOwn(setupState, ref)) {
+            setupState[ref] = refValue
+          }
+        } else if (_isRef) {
+          ref.value = refValue
+        } else if (__DEV__) {
+          warn('Invalid template ref type:', ref, `(${typeof ref})`)
+        }
+      }
+      queuePostFlushCb(doSet, -1)
+
+      // TODO this gets called repeatedly in renderEffect when it's dynamic ref?
+      onScopeDispose(() => {
+        queuePostFlushCb(() => {
+          if (isArray(existing)) {
+            remove(existing, refValue)
+          } else if (_isString) {
+            refs[ref] = null
+            if (__DEV__ && hasOwn(setupState, ref)) {
+              setupState[ref] = null
+            }
+          } else if (_isRef) {
+            ref.value = null
+          }
+        })
+      })
+    } else if (__DEV__) {
+      warn('Invalid template ref type:', ref, `(${typeof ref})`)
+    }
+  }
+  return ref
+}
+
+const getRefValue = (el: RefEl) => {
+  if (isVaporComponent(el)) {
+    return getExposed(el) || el
+  } else if (el instanceof DynamicFragment) {
+    return getRefValue(el.nodes as RefEl)
+  }
+  return el
+}
diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts
new file mode 100644
index 00000000000..e021ce84b05
--- /dev/null
+++ b/packages/runtime-vapor/src/block.ts
@@ -0,0 +1,189 @@
+import { isArray } from '@vue/shared'
+import {
+  type VaporComponentInstance,
+  isVaporComponent,
+  mountComponent,
+  unmountComponent,
+} from './component'
+import { createComment, createTextNode } from './dom/node'
+import { EffectScope, setActiveSub } from '@vue/reactivity'
+import { isHydrating } from './dom/hydration'
+
+export type Block =
+  | Node
+  | VaporFragment
+  | DynamicFragment
+  | VaporComponentInstance
+  | Block[]
+
+export type BlockFn = (...args: any[]) => Block
+
+export class VaporFragment {
+  nodes: Block
+  anchor?: Node
+  insert?: (parent: ParentNode, anchor: Node | null) => void
+  remove?: (parent?: ParentNode) => void
+
+  constructor(nodes: Block) {
+    this.nodes = nodes
+  }
+}
+
+export class DynamicFragment extends VaporFragment {
+  anchor: Node
+  scope: EffectScope | undefined
+  current?: BlockFn
+  fallback?: BlockFn
+
+  constructor(anchorLabel?: string) {
+    super([])
+    this.anchor =
+      __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
+  }
+
+  update(render?: BlockFn, key: any = render): void {
+    if (key === this.current) {
+      return
+    }
+    this.current = key
+
+    const prevSub = setActiveSub()
+    const parent = this.anchor.parentNode
+
+    // teardown previous branch
+    if (this.scope) {
+      this.scope.stop()
+      parent && remove(this.nodes, parent)
+    }
+
+    if (render) {
+      this.scope = new EffectScope()
+      this.nodes = this.scope.run(render) || []
+      if (parent) insert(this.nodes, parent, this.anchor)
+    } else {
+      this.scope = undefined
+      this.nodes = []
+    }
+
+    if (this.fallback && !isValidBlock(this.nodes)) {
+      parent && remove(this.nodes, parent)
+      this.nodes =
+        (this.scope || (this.scope = new EffectScope())).run(this.fallback) ||
+        []
+      parent && insert(this.nodes, parent, this.anchor)
+    }
+
+    setActiveSub(prevSub)
+  }
+}
+
+export function isFragment(val: NonNullable<unknown>): val is VaporFragment {
+  return val instanceof VaporFragment
+}
+
+export function isBlock(val: NonNullable<unknown>): val is Block {
+  return (
+    val instanceof Node ||
+    isArray(val) ||
+    isVaporComponent(val) ||
+    isFragment(val)
+  )
+}
+
+export function isValidBlock(block: Block): boolean {
+  if (block instanceof Node) {
+    return !(block instanceof Comment)
+  } else if (isVaporComponent(block)) {
+    return isValidBlock(block.block)
+  } else if (isArray(block)) {
+    return block.length > 0 && block.every(isValidBlock)
+  } else {
+    // fragment
+    return isValidBlock(block.nodes)
+  }
+}
+
+export function insert(
+  block: Block,
+  parent: ParentNode,
+  anchor: Node | null | 0 = null, // 0 means prepend
+): void {
+  anchor = anchor === 0 ? parent.firstChild : anchor
+  if (block instanceof Node) {
+    if (!isHydrating) {
+      parent.insertBefore(block, anchor)
+    }
+  } else if (isVaporComponent(block)) {
+    if (block.isMounted) {
+      insert(block.block!, parent, anchor)
+    } else {
+      mountComponent(block, parent, anchor)
+    }
+  } else if (isArray(block)) {
+    for (const b of block) {
+      insert(b, parent, anchor)
+    }
+  } else {
+    // fragment
+    if (block.insert) {
+      // TODO handle hydration for vdom interop
+      block.insert(parent, anchor)
+    } else {
+      insert(block.nodes, parent, anchor)
+    }
+    if (block.anchor) insert(block.anchor, parent, anchor)
+  }
+}
+
+export type InsertFn = typeof insert
+
+export function prepend(parent: ParentNode, ...blocks: Block[]): void {
+  let i = blocks.length
+  while (i--) insert(blocks[i], parent, 0)
+}
+
+export function remove(block: Block, parent?: ParentNode): void {
+  if (block instanceof Node) {
+    parent && parent.removeChild(block)
+  } else if (isVaporComponent(block)) {
+    unmountComponent(block, parent)
+  } else if (isArray(block)) {
+    for (let i = 0; i < block.length; i++) {
+      remove(block[i], parent)
+    }
+  } else {
+    // fragment
+    if (block.remove) {
+      block.remove(parent)
+    } else {
+      remove(block.nodes, parent)
+    }
+    if (block.anchor) remove(block.anchor, parent)
+    if ((block as DynamicFragment).scope) {
+      ;(block as DynamicFragment).scope!.stop()
+    }
+  }
+}
+
+/**
+ * dev / test only
+ */
+export function normalizeBlock(block: Block): Node[] {
+  if (!__DEV__ && !__TEST__) {
+    throw new Error(
+      'normalizeBlock should not be used in production code paths',
+    )
+  }
+  const nodes: Node[] = []
+  if (block instanceof Node) {
+    nodes.push(block)
+  } else if (isArray(block)) {
+    block.forEach(child => nodes.push(...normalizeBlock(child)))
+  } else if (isVaporComponent(block)) {
+    nodes.push(...normalizeBlock(block.block!))
+  } else {
+    nodes.push(...normalizeBlock(block.nodes))
+    block.anchor && nodes.push(block.anchor)
+  }
+  return nodes
+}
diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts
new file mode 100644
index 00000000000..da57882c49d
--- /dev/null
+++ b/packages/runtime-vapor/src/component.ts
@@ -0,0 +1,595 @@
+import {
+  type ComponentInternalOptions,
+  type ComponentPropsOptions,
+  EffectScope,
+  type EmitFn,
+  type EmitsOptions,
+  ErrorCodes,
+  type GenericAppContext,
+  type GenericComponentInstance,
+  type LifecycleHook,
+  type NormalizedPropsOptions,
+  type ObjectEmitsOptions,
+  type SuspenseBoundary,
+  callWithErrorHandling,
+  currentInstance,
+  endMeasure,
+  expose,
+  nextUid,
+  popWarningContext,
+  pushWarningContext,
+  queuePostFlushCb,
+  registerHMR,
+  setCurrentInstance,
+  startMeasure,
+  unregisterHMR,
+  warn,
+} from '@vue/runtime-dom'
+import { type Block, DynamicFragment, insert, isBlock, remove } from './block'
+import {
+  type ShallowRef,
+  markRaw,
+  onScopeDispose,
+  proxyRefs,
+  setActiveSub,
+  unref,
+} from '@vue/reactivity'
+import { EMPTY_OBJ, invokeArrayFns, isFunction, isString } from '@vue/shared'
+import {
+  type DynamicPropsSource,
+  type RawProps,
+  getKeysFromRawProps,
+  getPropsProxyHandlers,
+  hasFallthroughAttrs,
+  normalizePropsOptions,
+  resolveDynamicProps,
+  setupPropsValidation,
+} from './componentProps'
+import { renderEffect } from './renderEffect'
+import { emit, normalizeEmitsOptions } from './componentEmits'
+import { setDynamicProps } from './dom/prop'
+import {
+  type DynamicSlotSource,
+  type RawSlots,
+  type StaticSlots,
+  type VaporSlot,
+  dynamicSlotsProxyHandlers,
+  getSlot,
+} from './componentSlots'
+import { hmrReload, hmrRerender } from './hmr'
+import { isHydrating, locateHydrationNode } from './dom/hydration'
+import {
+  insertionAnchor,
+  insertionParent,
+  resetInsertionState,
+} from './insertionState'
+
+export { currentInstance } from '@vue/runtime-dom'
+
+export type VaporComponent = FunctionalVaporComponent | ObjectVaporComponent
+
+export type VaporSetupFn = (
+  props: any,
+  ctx: Pick<VaporComponentInstance, 'slots' | 'attrs' | 'emit' | 'expose'>,
+) => Block | Record<string, any> | undefined
+
+export type FunctionalVaporComponent = VaporSetupFn &
+  Omit<ObjectVaporComponent, 'setup'> & {
+    displayName?: string
+  } & SharedInternalOptions
+
+export interface ObjectVaporComponent
+  extends ComponentInternalOptions,
+    SharedInternalOptions {
+  setup?: VaporSetupFn
+  inheritAttrs?: boolean
+  props?: ComponentPropsOptions
+  emits?: EmitsOptions
+  render?(
+    ctx: any,
+    props?: any,
+    emit?: EmitFn,
+    attrs?: any,
+    slots?: Record<string, VaporSlot>,
+  ): Block
+
+  name?: string
+  vapor?: boolean
+}
+
+interface SharedInternalOptions {
+  /**
+   * Cached normalized props options.
+   * In vapor mode there are no mixins so normalized options can be cached
+   * directly on the component
+   */
+  __propsOptions?: NormalizedPropsOptions
+  /**
+   * Cached normalized props proxy handlers.
+   */
+  __propsHandlers?: [ProxyHandler<any> | null, ProxyHandler<any>]
+  /**
+   * Cached normalized emits options.
+   */
+  __emitsOptions?: ObjectEmitsOptions
+}
+
+// In TypeScript, it is actually impossible to have a record type with only
+// specific properties that have a different type from the indexed type.
+// This makes our rawProps / rawSlots shape difficult to satisfy when calling
+// `createComponent` - luckily this is not user-facing, so we don't need to be
+// 100% strict. Here we use intentionally wider types to make `createComponent`
+// more ergonomic in tests and internal call sites, where we immediately cast
+// them into the stricter types.
+export type LooseRawProps = Record<
+  string,
+  (() => unknown) | DynamicPropsSource[]
+> & {
+  $?: DynamicPropsSource[]
+}
+
+export type LooseRawSlots = Record<string, VaporSlot | DynamicSlotSource[]> & {
+  $?: DynamicSlotSource[]
+}
+
+export function createComponent(
+  component: VaporComponent,
+  rawProps?: LooseRawProps | null,
+  rawSlots?: LooseRawSlots | null,
+  isSingleRoot?: boolean,
+  appContext: GenericAppContext = (currentInstance &&
+    currentInstance.appContext) ||
+    emptyContext,
+): VaporComponentInstance {
+  const _insertionParent = insertionParent
+  const _insertionAnchor = insertionAnchor
+  if (isHydrating) {
+    locateHydrationNode()
+  } else {
+    resetInsertionState()
+  }
+
+  // vdom interop enabled and component is not an explicit vapor component
+  if (appContext.vapor && !component.__vapor) {
+    const frag = appContext.vapor.vdomMount(
+      component as any,
+      rawProps,
+      rawSlots,
+    )
+    if (!isHydrating && _insertionParent) {
+      insert(frag, _insertionParent, _insertionAnchor)
+    }
+    return frag
+  }
+
+  if (
+    isSingleRoot &&
+    component.inheritAttrs !== false &&
+    isVaporComponent(currentInstance) &&
+    currentInstance.hasFallthrough
+  ) {
+    // check if we are the single root of the parent
+    // if yes, inject parent attrs as dynamic props source
+    const attrs = currentInstance.attrs
+    if (rawProps) {
+      ;((rawProps as RawProps).$ || ((rawProps as RawProps).$ = [])).push(
+        () => attrs,
+      )
+    } else {
+      rawProps = { $: [() => attrs] } as RawProps
+    }
+  }
+
+  const instance = new VaporComponentInstance(
+    component,
+    rawProps as RawProps,
+    rawSlots as RawSlots,
+    appContext,
+  )
+
+  // HMR
+  if (__DEV__ && component.__hmrId) {
+    registerHMR(instance)
+    instance.isSingleRoot = isSingleRoot
+    instance.hmrRerender = hmrRerender.bind(null, instance)
+    instance.hmrReload = hmrReload.bind(null, instance)
+  }
+
+  if (__DEV__) {
+    pushWarningContext(instance)
+    startMeasure(instance, `init`)
+
+    // cache normalized options for dev only emit check
+    instance.propsOptions = normalizePropsOptions(component)
+    instance.emitsOptions = normalizeEmitsOptions(component)
+  }
+
+  const prevInstance = setCurrentInstance(instance)
+  const prevSub = setActiveSub()
+
+  if (__DEV__) {
+    setupPropsValidation(instance)
+  }
+
+  const setupFn = isFunction(component) ? component : component.setup
+  const setupResult = setupFn
+    ? callWithErrorHandling(setupFn, instance, ErrorCodes.SETUP_FUNCTION, [
+        instance.props,
+        instance,
+      ]) || EMPTY_OBJ
+    : EMPTY_OBJ
+
+  if (__DEV__ && !isBlock(setupResult)) {
+    if (isFunction(component)) {
+      warn(`Functional vapor component must return a block directly.`)
+      instance.block = []
+    } else if (!component.render) {
+      warn(
+        `Vapor component setup() returned non-block value, and has no render function.`,
+      )
+      instance.block = []
+    } else {
+      instance.devtoolsRawSetupState = setupResult
+      // TODO make the proxy warn non-existent property access during dev
+      instance.setupState = proxyRefs(setupResult)
+      devRender(instance)
+    }
+  } else {
+    // component has a render function but no setup function
+    // (typically components with only a template and no state)
+    if (!setupFn && component.render) {
+      instance.block = callWithErrorHandling(
+        component.render,
+        instance,
+        ErrorCodes.RENDER_FUNCTION,
+      )
+    } else {
+      // in prod result can only be block
+      instance.block = setupResult as Block
+    }
+  }
+
+  // single root, inherit attrs
+  if (
+    instance.hasFallthrough &&
+    component.inheritAttrs !== false &&
+    Object.keys(instance.attrs).length
+  ) {
+    const el = getRootElement(instance)
+    if (el) {
+      renderEffect(() => {
+        isApplyingFallthroughProps = true
+        setDynamicProps(el, [instance.attrs])
+        isApplyingFallthroughProps = false
+      })
+    }
+  }
+
+  setActiveSub(prevSub)
+  setCurrentInstance(...prevInstance)
+
+  if (__DEV__) {
+    popWarningContext()
+    endMeasure(instance, 'init')
+  }
+
+  onScopeDispose(() => unmountComponent(instance), true)
+
+  if (!isHydrating && _insertionParent) {
+    mountComponent(instance, _insertionParent, _insertionAnchor)
+  }
+
+  return instance
+}
+
+export let isApplyingFallthroughProps = false
+
+/**
+ * dev only
+ */
+export function devRender(instance: VaporComponentInstance): void {
+  instance.block =
+    (instance.type.render
+      ? callWithErrorHandling(
+          instance.type.render,
+          instance,
+          ErrorCodes.RENDER_FUNCTION,
+          [
+            instance.setupState,
+            instance.props,
+            instance.emit,
+            instance.attrs,
+            instance.slots,
+          ],
+        )
+      : callWithErrorHandling(
+          isFunction(instance.type) ? instance.type : instance.type.setup!,
+          instance,
+          ErrorCodes.SETUP_FUNCTION,
+          [
+            instance.props,
+            {
+              slots: instance.slots,
+              attrs: instance.attrs,
+              emit: instance.emit,
+              expose: instance.expose,
+            },
+          ],
+        )) || []
+}
+
+const emptyContext: GenericAppContext = {
+  app: null as any,
+  config: {},
+  provides: /*@__PURE__*/ Object.create(null),
+}
+
+export class VaporComponentInstance implements GenericComponentInstance {
+  vapor: true
+  uid: number
+  type: VaporComponent
+  root: GenericComponentInstance | null
+  parent: GenericComponentInstance | null
+  appContext: GenericAppContext
+
+  block: Block
+  scope: EffectScope
+
+  rawProps: RawProps
+  rawSlots: RawSlots
+
+  props: Record<string, any>
+  attrs: Record<string, any>
+  propsDefaults: Record<string, any> | null
+
+  slots: StaticSlots
+
+  // to hold vnode props / slots in vdom interop mode
+  rawPropsRef?: ShallowRef<any>
+  rawSlotsRef?: ShallowRef<any>
+
+  emit: EmitFn
+  emitted: Record<string, boolean> | null
+
+  expose: (exposed: Record<string, any>) => void
+  exposed: Record<string, any> | null
+  exposeProxy: Record<string, any> | null
+
+  // for useTemplateRef()
+  refs: Record<string, any>
+  // for provide / inject
+  provides: Record<string, any>
+  // for useId
+  ids: [string, number, number]
+  // for suspense
+  suspense: SuspenseBoundary | null
+
+  hasFallthrough: boolean
+
+  // lifecycle hooks
+  isMounted: boolean
+  isUnmounted: boolean
+  isDeactivated: boolean
+  isUpdating: boolean
+
+  bc?: LifecycleHook // LifecycleHooks.BEFORE_CREATE
+  c?: LifecycleHook // LifecycleHooks.CREATED
+  bm?: LifecycleHook // LifecycleHooks.BEFORE_MOUNT
+  m?: LifecycleHook // LifecycleHooks.MOUNTED
+  bu?: LifecycleHook // LifecycleHooks.BEFORE_UPDATE
+  u?: LifecycleHook // LifecycleHooks.UPDATED
+  um?: LifecycleHook // LifecycleHooks.BEFORE_UNMOUNT
+  bum?: LifecycleHook // LifecycleHooks.UNMOUNTED
+  da?: LifecycleHook // LifecycleHooks.DEACTIVATED
+  a?: LifecycleHook // LifecycleHooks.ACTIVATED
+  rtg?: LifecycleHook // LifecycleHooks.RENDER_TRACKED
+  rtc?: LifecycleHook // LifecycleHooks.RENDER_TRIGGERED
+  ec?: LifecycleHook // LifecycleHooks.ERROR_CAPTURED
+  sp?: LifecycleHook<() => Promise<unknown>> // LifecycleHooks.SERVER_PREFETCH
+
+  // dev only
+  setupState?: Record<string, any>
+  devtoolsRawSetupState?: any
+  hmrRerender?: () => void
+  hmrReload?: (newComp: VaporComponent) => void
+  propsOptions?: NormalizedPropsOptions
+  emitsOptions?: ObjectEmitsOptions | null
+  isSingleRoot?: boolean
+
+  constructor(
+    comp: VaporComponent,
+    rawProps?: RawProps | null,
+    rawSlots?: RawSlots | null,
+    appContext?: GenericAppContext,
+  ) {
+    this.vapor = true
+    this.uid = nextUid()
+    this.type = comp
+    this.parent = currentInstance
+    this.root = currentInstance ? currentInstance.root : this
+
+    if (currentInstance) {
+      this.appContext = currentInstance.appContext
+      this.provides = currentInstance.provides
+      this.ids = currentInstance.ids
+    } else {
+      this.appContext = appContext || emptyContext
+      this.provides = Object.create(this.appContext.provides)
+      this.ids = ['', 0, 0]
+    }
+
+    this.block = null! // to be set
+    this.scope = new EffectScope(true)
+
+    this.emit = emit.bind(null, this)
+    this.expose = expose.bind(null, this)
+    this.refs = EMPTY_OBJ
+    this.emitted =
+      this.exposed =
+      this.exposeProxy =
+      this.propsDefaults =
+      this.suspense =
+        null
+
+    this.isMounted =
+      this.isUnmounted =
+      this.isUpdating =
+      this.isDeactivated =
+        false
+
+    // init props
+    this.rawProps = rawProps || EMPTY_OBJ
+    this.hasFallthrough = hasFallthroughAttrs(comp, rawProps)
+    if (rawProps || comp.props) {
+      const [propsHandlers, attrsHandlers] = getPropsProxyHandlers(comp)
+      this.attrs = new Proxy(this, attrsHandlers)
+      this.props = comp.props
+        ? new Proxy(this, propsHandlers!)
+        : isFunction(comp)
+          ? this.attrs
+          : EMPTY_OBJ
+    } else {
+      this.props = this.attrs = EMPTY_OBJ
+    }
+
+    // init slots
+    this.rawSlots = rawSlots || EMPTY_OBJ
+    this.slots = rawSlots
+      ? rawSlots.$
+        ? new Proxy(rawSlots, dynamicSlotsProxyHandlers)
+        : rawSlots
+      : EMPTY_OBJ
+  }
+
+  /**
+   * Expose `getKeysFromRawProps` on the instance so it can be used in code
+   * paths where it's needed, e.g. `useModel`
+   */
+  rawKeys(): string[] {
+    return getKeysFromRawProps(this.rawProps)
+  }
+}
+
+export function isVaporComponent(
+  value: unknown,
+): value is VaporComponentInstance {
+  return value instanceof VaporComponentInstance
+}
+
+/**
+ * Used when a component cannot be resolved at compile time
+ * and needs rely on runtime resolution - where it might fallback to a plain
+ * element if the resolution fails.
+ */
+export function createComponentWithFallback(
+  comp: VaporComponent | string,
+  rawProps?: LooseRawProps | null,
+  rawSlots?: LooseRawSlots | null,
+  isSingleRoot?: boolean,
+): HTMLElement | VaporComponentInstance {
+  if (!isString(comp)) {
+    return createComponent(comp, rawProps, rawSlots, isSingleRoot)
+  }
+
+  const _insertionParent = insertionParent
+  const _insertionAnchor = insertionAnchor
+  if (isHydrating) {
+    locateHydrationNode()
+  } else {
+    resetInsertionState()
+  }
+
+  const el = document.createElement(comp)
+  // mark single root
+  ;(el as any).$root = isSingleRoot
+
+  if (rawProps) {
+    renderEffect(() => {
+      setDynamicProps(el, [resolveDynamicProps(rawProps as RawProps)])
+    })
+  }
+
+  if (rawSlots) {
+    if (rawSlots.$) {
+      // TODO dynamic slot fragment
+    } else {
+      insert(getSlot(rawSlots as RawSlots, 'default')!(), el)
+    }
+  }
+
+  if (!isHydrating && _insertionParent) {
+    insert(el, _insertionParent, _insertionAnchor)
+  }
+
+  return el
+}
+
+export function mountComponent(
+  instance: VaporComponentInstance,
+  parent: ParentNode,
+  anchor?: Node | null | 0,
+): void {
+  if (__DEV__) {
+    startMeasure(instance, `mount`)
+  }
+  if (instance.bm) invokeArrayFns(instance.bm)
+  insert(instance.block, parent, anchor)
+  if (instance.m) queuePostFlushCb(() => invokeArrayFns(instance.m!))
+  instance.isMounted = true
+  if (__DEV__) {
+    endMeasure(instance, `mount`)
+  }
+}
+
+export function unmountComponent(
+  instance: VaporComponentInstance,
+  parentNode?: ParentNode,
+): void {
+  if (instance.isMounted && !instance.isUnmounted) {
+    if (__DEV__ && instance.type.__hmrId) {
+      unregisterHMR(instance)
+    }
+    if (instance.bum) {
+      invokeArrayFns(instance.bum)
+    }
+
+    instance.scope.stop()
+
+    if (instance.um) {
+      queuePostFlushCb(() => invokeArrayFns(instance.um!))
+    }
+    instance.isUnmounted = true
+  }
+
+  if (parentNode) {
+    remove(instance.block, parentNode)
+  }
+}
+
+export function getExposed(
+  instance: GenericComponentInstance,
+): Record<string, any> | undefined {
+  if (instance.exposed) {
+    return (
+      instance.exposeProxy ||
+      (instance.exposeProxy = new Proxy(markRaw(instance.exposed), {
+        get: (target, key) => unref(target[key as any]),
+      }))
+    )
+  }
+}
+
+function getRootElement({
+  block,
+}: VaporComponentInstance): Element | undefined {
+  if (block instanceof Element) {
+    return block
+  }
+
+  if (block instanceof DynamicFragment) {
+    const { nodes } = block
+    if (nodes instanceof Element && (nodes as any).$root) {
+      return nodes
+    }
+  }
+}
diff --git a/packages/runtime-vapor/src/componentEmits.ts b/packages/runtime-vapor/src/componentEmits.ts
new file mode 100644
index 00000000000..daee4d59fe7
--- /dev/null
+++ b/packages/runtime-vapor/src/componentEmits.ts
@@ -0,0 +1,58 @@
+import { type ObjectEmitsOptions, baseEmit } from '@vue/runtime-dom'
+import type { VaporComponent, VaporComponentInstance } from './component'
+import { EMPTY_OBJ, hasOwn, isArray } from '@vue/shared'
+import { type RawProps, resolveSource } from './componentProps'
+import { interopKey } from './vdomInterop'
+
+/**
+ * The logic from core isn't too reusable so it's better to duplicate here
+ */
+export function normalizeEmitsOptions(
+  comp: VaporComponent,
+): ObjectEmitsOptions | null {
+  const cached = comp.__emitsOptions
+  if (cached) return cached
+
+  const raw = comp.emits
+  if (!raw) return null
+
+  let normalized: ObjectEmitsOptions
+  if (isArray(raw)) {
+    normalized = {}
+    for (const key of raw) normalized[key] = null
+  } else {
+    normalized = raw
+  }
+
+  return (comp.__emitsOptions = normalized)
+}
+
+export function emit(
+  instance: VaporComponentInstance,
+  event: string,
+  ...rawArgs: any[]
+): void {
+  baseEmit(
+    instance,
+    instance.rawProps || EMPTY_OBJ,
+    propGetter,
+    event,
+    ...rawArgs,
+  )
+}
+
+function propGetter(rawProps: RawProps, key: string) {
+  const dynamicSources = rawProps.$
+  if (dynamicSources) {
+    let i = dynamicSources.length
+    while (i--) {
+      const source = resolveSource(dynamicSources[i])
+      if (hasOwn(source, key))
+        // for props passed from VDOM component, no need to resolve
+        return dynamicSources[interopKey]
+          ? source[key]
+          : resolveSource(source[key])
+    }
+  }
+  return rawProps[key] && resolveSource(rawProps[key])
+}
diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts
new file mode 100644
index 00000000000..55eadb98046
--- /dev/null
+++ b/packages/runtime-vapor/src/componentProps.ts
@@ -0,0 +1,365 @@
+import {
+  EMPTY_ARR,
+  NO,
+  YES,
+  camelize,
+  hasOwn,
+  isArray,
+  isFunction,
+  isString,
+} from '@vue/shared'
+import type { VaporComponent, VaporComponentInstance } from './component'
+import {
+  type NormalizedPropsOptions,
+  baseNormalizePropsOptions,
+  isEmitListener,
+  popWarningContext,
+  pushWarningContext,
+  resolvePropValue,
+  setCurrentInstance,
+  validateProps,
+  warn,
+} from '@vue/runtime-dom'
+import { ReactiveFlags } from '@vue/reactivity'
+import { normalizeEmitsOptions } from './componentEmits'
+import { renderEffect } from './renderEffect'
+import type { interopKey } from './vdomInterop'
+
+export type RawProps = Record<string, () => unknown> & {
+  // generated by compiler for :[key]="x" or v-bind="x"
+  $?: DynamicPropsSource[] & { [interopKey]?: boolean }
+}
+
+export type DynamicPropsSource =
+  | (() => Record<string, unknown>)
+  | Record<string, () => unknown>
+
+// TODO optimization: maybe convert functions into computeds
+export function resolveSource(
+  source: Record<string, any> | (() => Record<string, any>),
+): Record<string, any> {
+  return isFunction(source) ? source() : source
+}
+
+export function getPropsProxyHandlers(
+  comp: VaporComponent,
+): [
+  ProxyHandler<VaporComponentInstance> | null,
+  ProxyHandler<VaporComponentInstance>,
+] {
+  if (comp.__propsHandlers) {
+    return comp.__propsHandlers
+  }
+  const propsOptions = normalizePropsOptions(comp)[0]
+  const emitsOptions = normalizeEmitsOptions(comp)
+  const isProp = (
+    propsOptions
+      ? (key: string | symbol) =>
+          isString(key) && hasOwn(propsOptions, camelize(key))
+      : NO
+  ) as (key: string | symbol) => key is string
+  const isAttr = propsOptions
+    ? (key: string) =>
+        key !== '$' && !isProp(key) && !isEmitListener(emitsOptions, key)
+    : YES
+
+  const getProp = (instance: VaporComponentInstance, key: string | symbol) => {
+    // this enables direct watching of props and prevents `Invalid watch source` DEV warnings.
+    if (key === ReactiveFlags.IS_REACTIVE) return true
+
+    if (!isProp(key)) return
+    const rawProps = instance.rawProps
+    const dynamicSources = rawProps.$
+    if (dynamicSources) {
+      let i = dynamicSources.length
+      let source, isDynamic, rawKey
+      while (i--) {
+        source = dynamicSources[i]
+        isDynamic = isFunction(source)
+        source = isDynamic ? (source as Function)() : source
+        for (rawKey in source) {
+          if (camelize(rawKey) === key) {
+            return resolvePropValue(
+              propsOptions!,
+              key,
+              isDynamic ? source[rawKey] : source[rawKey](),
+              instance,
+              resolveDefault,
+            )
+          }
+        }
+      }
+    }
+    for (const rawKey in rawProps) {
+      if (camelize(rawKey) === key) {
+        return resolvePropValue(
+          propsOptions!,
+          key,
+          rawProps[rawKey](),
+          instance,
+          resolveDefault,
+        )
+      }
+    }
+    return resolvePropValue(
+      propsOptions!,
+      key,
+      undefined,
+      instance,
+      resolveDefault,
+      true,
+    )
+  }
+
+  const propsHandlers = propsOptions
+    ? ({
+        get: (target, key) => getProp(target, key),
+        has: (_, key) => isProp(key),
+        ownKeys: () => Object.keys(propsOptions),
+        getOwnPropertyDescriptor(target, key) {
+          if (isProp(key)) {
+            return {
+              configurable: true,
+              enumerable: true,
+              get: () => getProp(target, key),
+            }
+          }
+        },
+      } satisfies ProxyHandler<VaporComponentInstance>)
+    : null
+
+  if (__DEV__ && propsOptions) {
+    Object.assign(propsHandlers!, {
+      set: propsSetDevTrap,
+      deleteProperty: propsDeleteDevTrap,
+    })
+  }
+
+  const getAttr = (target: RawProps, key: string) => {
+    if (!isProp(key) && !isEmitListener(emitsOptions, key)) {
+      return getAttrFromRawProps(target, key)
+    }
+  }
+
+  const hasAttr = (target: RawProps, key: string) => {
+    if (isAttr(key)) {
+      return hasAttrFromRawProps(target, key)
+    } else {
+      return false
+    }
+  }
+
+  const attrsHandlers = {
+    get: (target, key: string) => getAttr(target.rawProps, key),
+    has: (target, key: string) => hasAttr(target.rawProps, key),
+    ownKeys: target => getKeysFromRawProps(target.rawProps).filter(isAttr),
+    getOwnPropertyDescriptor(target, key: string) {
+      if (hasAttr(target.rawProps, key)) {
+        return {
+          configurable: true,
+          enumerable: true,
+          get: () => getAttr(target.rawProps, key),
+        }
+      }
+    },
+  } satisfies ProxyHandler<VaporComponentInstance>
+
+  if (__DEV__) {
+    Object.assign(attrsHandlers, {
+      set: propsSetDevTrap,
+      deleteProperty: propsDeleteDevTrap,
+    })
+  }
+
+  return (comp.__propsHandlers = [propsHandlers, attrsHandlers])
+}
+
+export function getAttrFromRawProps(rawProps: RawProps, key: string): unknown {
+  if (key === '$') return
+  // need special merging behavior for class & style
+  const merged = key === 'class' || key === 'style' ? ([] as any[]) : undefined
+  const dynamicSources = rawProps.$
+  if (dynamicSources) {
+    let i = dynamicSources.length
+    let source, isDynamic
+    while (i--) {
+      source = dynamicSources[i]
+      isDynamic = isFunction(source)
+      source = isDynamic ? (source as Function)() : source
+      if (source && hasOwn(source, key)) {
+        const value = isDynamic ? source[key] : source[key]()
+        if (merged) {
+          merged.push(value)
+        } else {
+          return value
+        }
+      }
+    }
+  }
+  if (hasOwn(rawProps, key)) {
+    if (merged) {
+      merged.push(rawProps[key]())
+    } else {
+      return rawProps[key]()
+    }
+  }
+  if (merged && merged.length) {
+    return merged
+  }
+}
+
+export function hasAttrFromRawProps(rawProps: RawProps, key: string): boolean {
+  if (key === '$') return false
+  const dynamicSources = rawProps.$
+  if (dynamicSources) {
+    let i = dynamicSources.length
+    while (i--) {
+      const source = resolveSource(dynamicSources[i])
+      if (source && hasOwn(source, key)) {
+        return true
+      }
+    }
+  }
+  return hasOwn(rawProps, key)
+}
+
+export function getKeysFromRawProps(rawProps: RawProps): string[] {
+  const keys: string[] = []
+  for (const key in rawProps) {
+    if (key !== '$') keys.push(key)
+  }
+  const dynamicSources = rawProps.$
+  if (dynamicSources) {
+    let i = dynamicSources.length
+    let source
+    while (i--) {
+      source = resolveSource(dynamicSources[i])
+      for (const key in source) {
+        keys.push(key)
+      }
+    }
+  }
+  return Array.from(new Set(keys))
+}
+
+export function normalizePropsOptions(
+  comp: VaporComponent,
+): NormalizedPropsOptions {
+  const cached = comp.__propsOptions
+  if (cached) return cached
+
+  const raw = comp.props
+  if (!raw) return EMPTY_ARR as []
+
+  const normalized: NormalizedPropsOptions[0] = {}
+  const needCastKeys: NormalizedPropsOptions[1] = []
+  baseNormalizePropsOptions(raw, normalized, needCastKeys)
+
+  return (comp.__propsOptions = [normalized, needCastKeys])
+}
+
+function resolveDefault(
+  factory: (props: Record<string, any>) => unknown,
+  instance: VaporComponentInstance,
+) {
+  const prev = setCurrentInstance(instance)
+  const res = factory.call(null, instance.props)
+  setCurrentInstance(...prev)
+  return res
+}
+
+export function hasFallthroughAttrs(
+  comp: VaporComponent,
+  rawProps: RawProps | null | undefined,
+): boolean {
+  if (rawProps) {
+    // determine fallthrough
+    if (rawProps.$ || !comp.props) {
+      return true
+    } else {
+      // check if rawProps contains any keys not declared
+      const propsOptions = normalizePropsOptions(comp)[0]!
+      for (const key in rawProps) {
+        if (!hasOwn(propsOptions, camelize(key))) {
+          return true
+        }
+      }
+    }
+  }
+  return false
+}
+
+/**
+ * dev only
+ */
+export function setupPropsValidation(instance: VaporComponentInstance): void {
+  const rawProps = instance.rawProps
+  if (!rawProps) return
+  renderEffect(() => {
+    pushWarningContext(instance)
+    validateProps(
+      resolveDynamicProps(rawProps),
+      instance.props,
+      normalizePropsOptions(instance.type)[0]!,
+    )
+    popWarningContext()
+  }, true /* noLifecycle */)
+}
+
+export function resolveDynamicProps(props: RawProps): Record<string, unknown> {
+  const mergedRawProps: Record<string, any> = {}
+  for (const key in props) {
+    if (key !== '$') {
+      mergedRawProps[key] = props[key]()
+    }
+  }
+  if (props.$) {
+    for (const source of props.$) {
+      const isDynamic = isFunction(source)
+      const resolved = isDynamic ? source() : source
+      for (const key in resolved) {
+        const value = isDynamic ? resolved[key] : (resolved[key] as Function)()
+        if (key === 'class' || key === 'style') {
+          const existing = mergedRawProps[key]
+          if (isArray(existing)) {
+            existing.push(value)
+          } else {
+            mergedRawProps[key] = [existing, value]
+          }
+        } else {
+          mergedRawProps[key] = value
+        }
+      }
+    }
+  }
+  return mergedRawProps
+}
+
+function propsSetDevTrap(_: any, key: string | symbol) {
+  warn(
+    `Attempt to mutate prop ${JSON.stringify(key)} failed. Props are readonly.`,
+  )
+  return true
+}
+
+function propsDeleteDevTrap(_: any, key: string | symbol) {
+  warn(
+    `Attempt to delete prop ${JSON.stringify(key)} failed. Props are readonly.`,
+  )
+  return true
+}
+
+export const rawPropsProxyHandlers: ProxyHandler<RawProps> = {
+  get: getAttrFromRawProps,
+  has: hasAttrFromRawProps,
+  ownKeys: getKeysFromRawProps,
+  getOwnPropertyDescriptor(target, key: string) {
+    if (hasAttrFromRawProps(target, key)) {
+      return {
+        configurable: true,
+        enumerable: true,
+        get: () => getAttrFromRawProps(target, key),
+      }
+    }
+  },
+}
diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts
new file mode 100644
index 00000000000..100c99cdb8a
--- /dev/null
+++ b/packages/runtime-vapor/src/componentSlots.ts
@@ -0,0 +1,159 @@
+import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared'
+import { type Block, type BlockFn, DynamicFragment, insert } from './block'
+import { rawPropsProxyHandlers } from './componentProps'
+import { currentInstance, isRef } from '@vue/runtime-dom'
+import type { LooseRawProps, VaporComponentInstance } from './component'
+import { renderEffect } from './renderEffect'
+import {
+  insertionAnchor,
+  insertionParent,
+  resetInsertionState,
+} from './insertionState'
+import { isHydrating, locateHydrationNode } from './dom/hydration'
+
+export type RawSlots = Record<string, VaporSlot> & {
+  $?: DynamicSlotSource[]
+}
+
+export type StaticSlots = Record<string, VaporSlot>
+
+export type VaporSlot = BlockFn
+export type DynamicSlot = { name: string; fn: VaporSlot }
+export type DynamicSlotFn = () => DynamicSlot | DynamicSlot[]
+export type DynamicSlotSource = StaticSlots | DynamicSlotFn
+
+export const dynamicSlotsProxyHandlers: ProxyHandler<RawSlots> = {
+  get: getSlot,
+  has: (target, key: string) => !!getSlot(target, key),
+  getOwnPropertyDescriptor(target, key: string) {
+    const slot = getSlot(target, key)
+    if (slot) {
+      return {
+        configurable: true,
+        enumerable: true,
+        value: slot,
+      }
+    }
+  },
+  ownKeys(target) {
+    let keys = Object.keys(target)
+    const dynamicSources = target.$
+    if (dynamicSources) {
+      keys = keys.filter(k => k !== '$')
+      for (const source of dynamicSources) {
+        if (isFunction(source)) {
+          const slot = source()
+          if (isArray(slot)) {
+            for (const s of slot) keys.push(String(s.name))
+          } else {
+            keys.push(String(slot.name))
+          }
+        } else {
+          keys.push(...Object.keys(source))
+        }
+      }
+    }
+    return keys
+  },
+  set: NO,
+  deleteProperty: NO,
+}
+
+export function getSlot(
+  target: RawSlots,
+  key: string,
+): (VaporSlot & { _bound?: VaporSlot }) | undefined {
+  if (key === '$') return
+  const dynamicSources = target.$
+  if (dynamicSources) {
+    let i = dynamicSources.length
+    let source
+    while (i--) {
+      source = dynamicSources[i]
+      if (isFunction(source)) {
+        const slot = source()
+        if (slot) {
+          if (isArray(slot)) {
+            for (const s of slot) {
+              if (String(s.name) === key) return s.fn
+            }
+          } else if (String(slot.name) === key) {
+            return slot.fn
+          }
+        }
+      } else if (hasOwn(source, key)) {
+        return source[key]
+      }
+    }
+  }
+  if (hasOwn(target, key)) {
+    return target[key]
+  }
+}
+
+export function createSlot(
+  name: string | (() => string),
+  rawProps?: LooseRawProps | null,
+  fallback?: VaporSlot,
+): Block {
+  const _insertionParent = insertionParent
+  const _insertionAnchor = insertionAnchor
+  if (isHydrating) {
+    locateHydrationNode()
+  } else {
+    resetInsertionState()
+  }
+
+  const instance = currentInstance as VaporComponentInstance
+  const rawSlots = instance.rawSlots
+  const slotProps = rawProps
+    ? new Proxy(rawProps, rawPropsProxyHandlers)
+    : EMPTY_OBJ
+
+  let fragment: DynamicFragment
+
+  if (isRef(rawSlots._)) {
+    fragment = instance.appContext.vapor!.vdomSlot(
+      rawSlots._,
+      name,
+      slotProps,
+      instance,
+      fallback,
+    )
+  } else {
+    fragment = __DEV__ ? new DynamicFragment('slot') : new DynamicFragment()
+    const isDynamicName = isFunction(name)
+    const renderSlot = () => {
+      const slot = getSlot(rawSlots, isFunction(name) ? name() : name)
+      if (slot) {
+        // create and cache bound version of the slot to make it stable
+        // so that we avoid unnecessary updates if it resolves to the same slot
+        fragment.update(
+          slot._bound ||
+            (slot._bound = () => {
+              const slotContent = slot(slotProps)
+              if (slotContent instanceof DynamicFragment) {
+                slotContent.fallback = fallback
+              }
+              return slotContent
+            }),
+        )
+      } else {
+        fragment.update(fallback)
+      }
+    }
+
+    // dynamic slot name or has dynamicSlots
+    if (isDynamicName || rawSlots.$) {
+      renderEffect(renderSlot)
+    } else {
+      renderSlot()
+    }
+  }
+
+  if (!isHydrating && _insertionParent) {
+    insert(fragment, _insertionParent, _insertionAnchor)
+  }
+
+  return fragment
+}
diff --git a/packages/runtime-vapor/src/directives/custom.ts b/packages/runtime-vapor/src/directives/custom.ts
new file mode 100644
index 00000000000..32cfe968b50
--- /dev/null
+++ b/packages/runtime-vapor/src/directives/custom.ts
@@ -0,0 +1,35 @@
+import { type DirectiveModifiers, onScopeDispose } from '@vue/runtime-dom'
+import type { VaporComponentInstance } from '../component'
+
+// !! vapor directive is different from vdom directives
+export type VaporDirective = (
+  node: Element | VaporComponentInstance,
+  value?: () => any,
+  argument?: string,
+  modifiers?: DirectiveModifiers,
+) => (() => void) | void
+
+type VaporDirectiveArguments = Array<
+  | [VaporDirective | undefined]
+  | [VaporDirective | undefined, () => any]
+  | [VaporDirective | undefined, (() => any) | undefined, argument: string]
+  | [
+      VaporDirective | undefined,
+      value: (() => any) | undefined,
+      argument: string | undefined,
+      modifiers: DirectiveModifiers,
+    ]
+>
+
+export function withVaporDirectives(
+  node: Element | VaporComponentInstance,
+  dirs: VaporDirectiveArguments,
+): void {
+  // TODO handle custom directive on component
+  for (const [dir, value, argument, modifiers] of dirs) {
+    if (dir) {
+      const ret = dir(node, value, argument, modifiers)
+      if (ret) onScopeDispose(ret)
+    }
+  }
+}
diff --git a/packages/runtime-vapor/src/directives/vModel.ts b/packages/runtime-vapor/src/directives/vModel.ts
new file mode 100644
index 00000000000..0c32b24bf4a
--- /dev/null
+++ b/packages/runtime-vapor/src/directives/vModel.ts
@@ -0,0 +1,113 @@
+import {
+  currentInstance,
+  onMounted,
+  vModelCheckboxInit,
+  vModelCheckboxUpdate,
+  vModelGetValue,
+  vModelSelectInit,
+  vModelSetSelected,
+  vModelTextInit,
+  vModelTextUpdate,
+} from '@vue/runtime-dom'
+import { renderEffect } from '../renderEffect'
+import { looseEqual } from '@vue/shared'
+import { addEventListener } from '../dom/event'
+import { traverse } from '@vue/reactivity'
+
+type VaporModelDirective<
+  T extends HTMLElement =
+    | HTMLInputElement
+    | HTMLTextAreaElement
+    | HTMLSelectElement,
+  Modifiers extends string = string,
+> = (
+  el: T,
+  get: () => any,
+  set: (v: any) => void,
+  modifiers?: { [key in Modifiers]?: true },
+) => void
+
+function ensureMounted(cb: () => void) {
+  if (currentInstance!.isMounted) {
+    cb()
+  } else {
+    onMounted(cb)
+  }
+}
+
+export const applyTextModel: VaporModelDirective<
+  HTMLInputElement | HTMLTextAreaElement,
+  'trim' | 'number' | 'lazy'
+> = (el, get, set, { trim, number, lazy } = {}) => {
+  vModelTextInit(el, trim, number, lazy, set)
+  ensureMounted(() => {
+    let value: any
+    renderEffect(() => {
+      vModelTextUpdate(el, value, (value = get()), trim, number, lazy)
+    })
+  })
+}
+
+export const applyCheckboxModel: VaporModelDirective<HTMLInputElement> = (
+  el,
+  get,
+  set,
+) => {
+  vModelCheckboxInit(el, set)
+  ensureMounted(() => {
+    let value: any
+    renderEffect(() => {
+      vModelCheckboxUpdate(
+        el,
+        value,
+        // #4096 array checkboxes need to be deep traversed
+        traverse((value = get())),
+      )
+    })
+  })
+}
+
+export const applyRadioModel: VaporModelDirective<HTMLInputElement> = (
+  el,
+  get,
+  set,
+) => {
+  addEventListener(el, 'change', () => set(vModelGetValue(el)))
+  ensureMounted(() => {
+    let value: any
+    renderEffect(() => {
+      if (value !== (value = get())) {
+        el.checked = looseEqual(value, vModelGetValue(el))
+      }
+    })
+  })
+}
+
+export const applySelectModel: VaporModelDirective<
+  HTMLSelectElement,
+  'number'
+> = (el, get, set, modifiers) => {
+  vModelSelectInit(el, get(), modifiers && modifiers.number, set)
+  ensureMounted(() => {
+    renderEffect(() => vModelSetSelected(el, traverse(get())))
+  })
+}
+
+export const applyDynamicModel: VaporModelDirective = (
+  el,
+  get,
+  set,
+  modifiers,
+) => {
+  let apply: VaporModelDirective<any> = applyTextModel
+  if (el.tagName === 'SELECT') {
+    apply = applySelectModel
+  } else if (el.tagName === 'TEXTAREA') {
+    apply = applyTextModel
+  } else if ((el as HTMLInputElement).type === 'checkbox') {
+    apply = applyCheckboxModel
+  } else if ((el as HTMLInputElement).type === 'radio') {
+    apply = applyRadioModel
+  }
+  apply(el, get, set, modifiers)
+}
diff --git a/packages/runtime-vapor/src/directives/vShow.ts b/packages/runtime-vapor/src/directives/vShow.ts
new file mode 100644
index 00000000000..ac4c066b71d
--- /dev/null
+++ b/packages/runtime-vapor/src/directives/vShow.ts
@@ -0,0 +1,56 @@
+import {
+  type VShowElement,
+  vShowHidden,
+  vShowOriginalDisplay,
+  warn,
+} from '@vue/runtime-dom'
+import { renderEffect } from '../renderEffect'
+import { isVaporComponent } from '../component'
+import { type Block, DynamicFragment } from '../block'
+import { isArray } from '@vue/shared'
+
+export function applyVShow(target: Block, source: () => any): void {
+  if (isVaporComponent(target)) {
+    return applyVShow(target.block, source)
+  }
+
+  if (isArray(target) && target.length === 1) {
+    return applyVShow(target[0], source)
+  }
+
+  if (target instanceof DynamicFragment) {
+    const update = target.update
+    target.update = (render, key) => {
+      update.call(target, render, key)
+      setDisplay(target, source())
+    }
+  }
+
+  renderEffect(() => setDisplay(target, source()))
+}
+
+function setDisplay(target: Block, value: unknown): void {
+  if (isVaporComponent(target)) {
+    return setDisplay(target, value)
+  }
+  if (isArray(target) && target.length === 1) {
+    return setDisplay(target[0], value)
+  }
+  if (target instanceof DynamicFragment) {
+    return setDisplay(target.nodes, value)
+  }
+  if (target instanceof Element) {
+    const el = target as VShowElement
+    if (!(vShowOriginalDisplay in el)) {
+      el[vShowOriginalDisplay] =
+        el.style.display === 'none' ? '' : el.style.display
+    }
+    el.style.display = value ? el[vShowOriginalDisplay]! : 'none'
+    el[vShowHidden] = !value
+  } else if (__DEV__) {
+    warn(
+      `v-show used on component with non-single-element root node ` +
+        `and will be ignored.`,
+    )
+  }
+}
diff --git a/packages/runtime-vapor/src/dom/event.ts b/packages/runtime-vapor/src/dom/event.ts
new file mode 100644
index 00000000000..4987ecfc0d8
--- /dev/null
+++ b/packages/runtime-vapor/src/dom/event.ts
@@ -0,0 +1,109 @@
+import { onEffectCleanup } from '@vue/reactivity'
+import { isArray } from '@vue/shared'
+
+export function addEventListener(
+  el: Element,
+  event: string,
+  handler: (...args: any) => any,
+  options?: AddEventListenerOptions,
+) {
+  el.addEventListener(event, handler, options)
+  return (): void => el.removeEventListener(event, handler, options)
+}
+
+export function on(
+  el: Element,
+  event: string,
+  handler: (e: Event) => any,
+  options: AddEventListenerOptions & { effect?: boolean } = {},
+): void {
+  addEventListener(el, event, handler, options)
+  if (options.effect) {
+    onEffectCleanup(() => {
+      el.removeEventListener(event, handler, options)
+    })
+  }
+}
+
+export function delegate(
+  el: any,
+  event: string,
+  handler: (e: Event) => any,
+): void {
+  const key = `$evt${event}`
+  const existing = el[key]
+  if (existing) {
+    if (isArray(existing)) {
+      existing.push(handler)
+    } else {
+      el[key] = [existing, handler]
+    }
+  } else {
+    el[key] = handler
+  }
+}
+
+type DelegatedHandler = {
+  (...args: any[]): any
+}
+
+/**
+ * Event delegation borrowed from solid
+ */
+const delegatedEvents = Object.create(null)
+
+export const delegateEvents = (...names: string[]): void => {
+  for (const name of names) {
+    if (!delegatedEvents[name]) {
+      delegatedEvents[name] = true
+      document.addEventListener(name, delegatedEventHandler)
+    }
+  }
+}
+
+const delegatedEventHandler = (e: Event) => {
+  let node = ((e.composedPath && e.composedPath()[0]) || e.target) as any
+  if (e.target !== node) {
+    Object.defineProperty(e, 'target', {
+      configurable: true,
+      value: node,
+    })
+  }
+  Object.defineProperty(e, 'currentTarget', {
+    configurable: true,
+    get() {
+      return node || document
+    },
+  })
+  while (node !== null) {
+    const handlers = node[`$evt${e.type}`] as
+      | DelegatedHandler
+      | DelegatedHandler[]
+    if (handlers) {
+      if (isArray(handlers)) {
+        for (const handler of handlers) {
+          if (!node.disabled) {
+            handler(e)
+            if (e.cancelBubble) return
+          }
+        }
+      } else {
+        handlers(e)
+        if (e.cancelBubble) return
+      }
+    }
+    node =
+      node.host && node.host !== node && node.host instanceof Node
+        ? node.host
+        : node.parentNode
+  }
+}
+
+export function setDynamicEvents(
+  el: HTMLElement,
+  events: Record<string, (...args: any[]) => any>,
+): void {
+  for (const name in events) {
+    on(el, name, events[name], { effect: true })
+  }
+}
diff --git a/packages/runtime-vapor/src/dom/hydration.ts b/packages/runtime-vapor/src/dom/hydration.ts
new file mode 100644
index 00000000000..d34d9db7da5
--- /dev/null
+++ b/packages/runtime-vapor/src/dom/hydration.ts
@@ -0,0 +1,129 @@
+import { warn } from '@vue/runtime-dom'
+import {
+  insertionAnchor,
+  insertionParent,
+  resetInsertionState,
+  setInsertionState,
+} from '../insertionState'
+import { child, next } from './node'
+
+export let isHydrating = false
+export let currentHydrationNode: Node | null = null
+
+export function setCurrentHydrationNode(node: Node | null): void {
+  currentHydrationNode = node
+}
+
+let isOptimized = false
+
+export function withHydration(container: ParentNode, fn: () => void): void {
+  adoptTemplate = adoptTemplateImpl
+  locateHydrationNode = locateHydrationNodeImpl
+  if (!isOptimized) {
+    // optimize anchor cache lookup
+    ;(Comment.prototype as any).$fs = undefined
+    isOptimized = true
+  }
+  isHydrating = true
+  setInsertionState(container, 0)
+  const res = fn()
+  resetInsertionState()
+  currentHydrationNode = null
+  isHydrating = false
+  return res
+}
+
+export let adoptTemplate: (node: Node, template: string) => Node | null
+export let locateHydrationNode: () => void
+
+type Anchor = Comment & {
+  // cached matching fragment start to avoid repeated traversal
+  // on nested fragments
+  $fs?: Anchor
+}
+
+const isComment = (node: Node, data: string): node is Anchor =>
+  node.nodeType === 8 && (node as Comment).data === data
+
+/**
+ * Locate the first non-fragment-comment node and locate the next node
+ * while handling potential fragments.
+ */
+function adoptTemplateImpl(node: Node, template: string): Node | null {
+  if (!(template[0] === '<' && template[1] === '!')) {
+    while (node.nodeType === 8) node = next(node)
+  }
+
+  if (__DEV__) {
+    const type = node.nodeType
+    if (
+      (type === 8 && !template.startsWith('<!')) ||
+      (type === 1 &&
+        !template.startsWith(`<` + (node as Element).tagName.toLowerCase())) ||
+      (type === 3 &&
+        template.trim() &&
+        !template.startsWith((node as Text).data))
+    ) {
+      // TODO recover and provide more info
+      warn(`adopted: `, node)
+      warn(`template: ${template}`)
+      warn('hydration mismatch!')
+    }
+  }
+
+  currentHydrationNode = next(node)
+  return node
+}
+
+function locateHydrationNodeImpl() {
+  let node: Node | null
+
+  // prepend / firstChild
+  if (insertionAnchor === 0) {
+    node = child(insertionParent!)
+  } else {
+    node = insertionAnchor
+      ? insertionAnchor.previousSibling
+      : insertionParent
+        ? insertionParent.lastChild
+        : currentHydrationNode
+
+    if (node && isComment(node, ']')) {
+      // fragment backward search
+      if (node.$fs) {
+        // already cached matching fragment start
+        node = node.$fs
+      } else {
+        let cur: Node | null = node
+        let curFragEnd = node
+        let fragDepth = 0
+        node = null
+        while (cur) {
+          cur = cur.previousSibling
+          if (cur) {
+            if (isComment(cur, '[')) {
+              curFragEnd.$fs = cur
+              if (!fragDepth) {
+                node = cur
+                break
+              } else {
+                fragDepth--
+              }
+            } else if (isComment(cur, ']')) {
+              curFragEnd = cur
+              fragDepth++
+            }
+          }
+        }
+      }
+    }
+  }
+
+  if (__DEV__ && !node) {
+    // TODO more info
+    warn('Hydration mismatch in ', insertionParent)
+  }
+
+  resetInsertionState()
+  currentHydrationNode = node
+}
diff --git a/packages/runtime-vapor/src/dom/node.ts b/packages/runtime-vapor/src/dom/node.ts
new file mode 100644
index 00000000000..83bc32c57f0
--- /dev/null
+++ b/packages/runtime-vapor/src/dom/node.ts
@@ -0,0 +1,29 @@
+/*! #__NO_SIDE_EFFECTS__ */
+export function createTextNode(value = ''): Text {
+  return document.createTextNode(value)
+}
+
+/*! #__NO_SIDE_EFFECTS__ */
+export function createComment(data: string): Comment {
+  return document.createComment(data)
+}
+
+/*! #__NO_SIDE_EFFECTS__ */
+export function querySelector(selectors: string): Element | null {
+  return document.querySelector(selectors)
+}
+
+/*! #__NO_SIDE_EFFECTS__ */
+export function child(node: ParentNode): Node {
+  return node.firstChild!
+}
+
+/*! #__NO_SIDE_EFFECTS__ */
+export function nthChild(node: Node, i: number): Node {
+  return node.childNodes[i]
+}
+
+/*! #__NO_SIDE_EFFECTS__ */
+export function next(node: Node): Node {
+  return node.nextSibling!
+}
diff --git a/packages/runtime-vapor/src/dom/prop.ts b/packages/runtime-vapor/src/dom/prop.ts
new file mode 100644
index 00000000000..8c42ad766a5
--- /dev/null
+++ b/packages/runtime-vapor/src/dom/prop.ts
@@ -0,0 +1,280 @@
+import {
+  type NormalizedStyle,
+  canSetValueDirectly,
+  isOn,
+  isString,
+  normalizeClass,
+  normalizeStyle,
+  parseStringStyle,
+  toDisplayString,
+} from '@vue/shared'
+import { on } from './event'
+import {
+  currentInstance,
+  mergeProps,
+  patchStyle,
+  shouldSetAsProp,
+  warn,
+} from '@vue/runtime-dom'
+import {
+  type VaporComponentInstance,
+  isApplyingFallthroughProps,
+} from '../component'
+
+type TargetElement = Element & {
+  $root?: true
+  $html?: string
+  $cls?: string
+  $sty?: NormalizedStyle | string | undefined
+  value?: string
+  _value?: any
+}
+
+const hasFallthroughKey = (key: string) =>
+  (currentInstance as VaporComponentInstance).hasFallthrough &&
+  key in currentInstance!.attrs
+
+export function setProp(el: any, key: string, value: any): void {
+  if (key in el) {
+    setDOMProp(el, key, value)
+  } else {
+    setAttr(el, key, value)
+  }
+}
+
+export function setAttr(el: any, key: string, value: any): void {
+  if (!isApplyingFallthroughProps && el.$root && hasFallthroughKey(key)) {
+    return
+  }
+
+  // special case for <input v-model type="checkbox"> with
+  // :true-value & :false-value
+  // store value as dom properties since non-string values will be
+  // stringified.
+  if (key === 'true-value') {
+    ;(el as any)._trueValue = value
+  } else if (key === 'false-value') {
+    ;(el as any)._falseValue = value
+  }
+
+  if (value !== el[`$${key}`]) {
+    el[`$${key}`] = value
+    if (value != null) {
+      el.setAttribute(key, value)
+    } else {
+      el.removeAttribute(key)
+    }
+  }
+}
+
+export function setDOMProp(el: any, key: string, value: any): void {
+  if (!isApplyingFallthroughProps && el.$root && hasFallthroughKey(key)) {
+    return
+  }
+
+  const prev = el[key]
+  if (value === prev) {
+    return
+  }
+
+  let needRemove = false
+  if (value === '' || value == null) {
+    const type = typeof prev
+    if (value == null && type === 'string') {
+      // e.g. <div :id="null">
+      value = ''
+      needRemove = true
+    } else if (type === 'number') {
+      // e.g. <img :width="null">
+      value = 0
+      needRemove = true
+    }
+  }
+
+  // some properties perform value validation and throw,
+  // some properties has getter, no setter, will error in 'use strict'
+  // eg. <select :type="null"></select> <select :willValidate="null"></select>
+  try {
+    el[key] = value
+  } catch (e: any) {
+    // do not warn if value is auto-coerced from nullish values
+    if (__DEV__ && !needRemove) {
+      warn(
+        `Failed setting prop "${key}" on <${el.tagName.toLowerCase()}>: ` +
+          `value ${value} is invalid.`,
+        e,
+      )
+    }
+  }
+  needRemove && el.removeAttribute(key)
+}
+
+export function setClass(el: TargetElement, value: any): void {
+  if (el.$root) {
+    setClassIncremental(el, value)
+  } else if ((value = normalizeClass(value)) !== el.$cls) {
+    el.className = el.$cls = value
+  }
+}
+
+function setClassIncremental(el: any, value: any): void {
+  const cacheKey = `$clsi${isApplyingFallthroughProps ? '$' : ''}`
+  const prev = el[cacheKey]
+  if ((value = el[cacheKey] = normalizeClass(value)) !== prev) {
+    const nextList = value.split(/\s+/)
+    if (value) {
+      el.classList.add(...nextList)
+    }
+    if (prev) {
+      for (const cls of prev.split(/\s+/)) {
+        if (!nextList.includes(cls)) el.classList.remove(cls)
+      }
+    }
+  }
+}
+
+export function setStyle(el: TargetElement, value: any): void {
+  if (el.$root) {
+    setStyleIncremental(el, value)
+  } else {
+    const prev = el.$sty
+    value = el.$sty = normalizeStyle(value)
+    patchStyle(el, prev, value)
+  }
+}
+
+function setStyleIncremental(el: any, value: any): NormalizedStyle | undefined {
+  const cacheKey = `$styi${isApplyingFallthroughProps ? '$' : ''}`
+  const prev = el[cacheKey]
+  value = el[cacheKey] = isString(value)
+    ? parseStringStyle(value)
+    : (normalizeStyle(value) as NormalizedStyle | undefined)
+  patchStyle(el, prev, value)
+  return value
+}
+
+export function setValue(el: TargetElement, value: any): void {
+  if (!isApplyingFallthroughProps && el.$root && hasFallthroughKey('value')) {
+    return
+  }
+
+  // store value as _value as well since
+  // non-string values will be stringified.
+  el._value = value
+  // #4956: <option> value will fallback to its text content so we need to
+  // compare against its attribute value instead.
+  const oldValue = el.tagName === 'OPTION' ? el.getAttribute('value') : el.value
+  const newValue = value == null ? '' : value
+  if (oldValue !== newValue) {
+    el.value = newValue
+  }
+  if (value == null) {
+    el.removeAttribute('value')
+  }
+}
+
+/**
+ * Only called on text nodes!
+ * Compiler should also ensure value passed here is already converted by
+ * `toDisplayString`
+ */
+export function setText(el: Text & { $txt?: string }, value: string): void {
+  if (el.$txt !== value) {
+    el.nodeValue = el.$txt = value
+  }
+}
+
+/**
+ * Used by setDynamicProps only, so need to guard with `toDisplayString`
+ */
+export function setElementText(
+  el: Node & { $txt?: string },
+  value: unknown,
+): void {
+  if (el.$txt !== (value = toDisplayString(value))) {
+    el.textContent = el.$txt = value as string
+  }
+}
+
+export function setHtml(el: TargetElement, value: any): void {
+  value = value == null ? '' : value
+  if (el.$html !== value) {
+    el.innerHTML = el.$html = value
+  }
+}
+
+export function setDynamicProps(el: any, args: any[]): void {
+  const props = args.length > 1 ? mergeProps(...args) : args[0]
+  const cacheKey = `$dprops${isApplyingFallthroughProps ? '$' : ''}`
+  const prevKeys = el[cacheKey] as string[]
+
+  if (prevKeys) {
+    for (const key of prevKeys) {
+      if (!(key in props)) {
+        setDynamicProp(el, key, null)
+      }
+    }
+  }
+
+  for (const key of (el[cacheKey] = Object.keys(props))) {
+    setDynamicProp(el, key, props[key])
+  }
+}
+
+/**
+ * @internal
+ */
+export function setDynamicProp(
+  el: TargetElement,
+  key: string,
+  value: any,
+): void {
+  // TODO
+  const isSVG = false
+  if (key === 'class') {
+    setClass(el, value)
+  } else if (key === 'style') {
+    setStyle(el, value)
+  } else if (isOn(key)) {
+    on(el, key[2].toLowerCase() + key.slice(3), value, { effect: true })
+  } else if (
+    key[0] === '.'
+      ? ((key = key.slice(1)), true)
+      : key[0] === '^'
+        ? ((key = key.slice(1)), false)
+        : shouldSetAsProp(el, key, value, isSVG)
+  ) {
+    if (key === 'innerHTML') {
+      setHtml(el, value)
+    } else if (key === 'textContent') {
+      setElementText(el, value)
+    } else if (key === 'value' && canSetValueDirectly(el.tagName)) {
+      setValue(el, value)
+    } else {
+      setDOMProp(el, key, value)
+    }
+  } else {
+    // TODO special case for <input v-model type="checkbox">
+    setAttr(el, key, value)
+  }
+  return value
+}
+
+let isOptimized = false
+
+/**
+ * Optimize property lookup for cache properties on Element and Text nodes
+ */
+export function optimizePropertyLookup(): void {
+  if (isOptimized) return
+  isOptimized = true
+  const proto = Element.prototype as any
+  proto.$evtclick = undefined
+  proto.$root = false
+  proto.$html =
+    proto.$txt =
+    proto.$cls =
+    proto.$sty =
+    (Text.prototype as any).$txt =
+      ''
+}
diff --git a/packages/runtime-vapor/src/dom/template.ts b/packages/runtime-vapor/src/dom/template.ts
new file mode 100644
index 00000000000..b78ca4e52cf
--- /dev/null
+++ b/packages/runtime-vapor/src/dom/template.ts
@@ -0,0 +1,30 @@
+import { adoptTemplate, currentHydrationNode, isHydrating } from './hydration'
+import { child, createTextNode } from './node'
+
+let t: HTMLTemplateElement
+
+/*! #__NO_SIDE_EFFECTS__ */
+export function template(html: string, root?: boolean) {
+  let node: Node
+  return (): Node & { $root?: true } => {
+    if (isHydrating) {
+      if (__DEV__ && !currentHydrationNode) {
+        // TODO this should not happen
+        throw new Error('No current hydration node')
+      }
+      return adoptTemplate(currentHydrationNode!, html)!
+    }
+    // fast path for text nodes
+    if (html[0] !== '<') {
+      return createTextNode(html)
+    }
+    if (!node) {
+      t = t || document.createElement('template')
+      t.innerHTML = html
+      node = child(t.content)
+    }
+    const ret = node.cloneNode(true)
+    if (root) (ret as any).$root = true
+    return ret
+  }
+}
diff --git a/packages/runtime-vapor/src/hmr.ts b/packages/runtime-vapor/src/hmr.ts
new file mode 100644
index 00000000000..c96c1afa130
--- /dev/null
+++ b/packages/runtime-vapor/src/hmr.ts
@@ -0,0 +1,46 @@
+import {
+  popWarningContext,
+  pushWarningContext,
+  setCurrentInstance,
+} from '@vue/runtime-dom'
+import { insert, normalizeBlock, remove } from './block'
+import {
+  type VaporComponent,
+  type VaporComponentInstance,
+  createComponent,
+  devRender,
+  mountComponent,
+  unmountComponent,
+} from './component'
+
+export function hmrRerender(instance: VaporComponentInstance): void {
+  const normalized = normalizeBlock(instance.block)
+  const parent = normalized[0].parentNode!
+  const anchor = normalized[normalized.length - 1].nextSibling
+  remove(instance.block, parent)
+  const prev = setCurrentInstance(instance)
+  pushWarningContext(instance)
+  devRender(instance)
+  popWarningContext()
+  setCurrentInstance(...prev)
+  insert(instance.block, parent, anchor)
+}
+
+export function hmrReload(
+  instance: VaporComponentInstance,
+  newComp: VaporComponent,
+): void {
+  const normalized = normalizeBlock(instance.block)
+  const parent = normalized[0].parentNode!
+  const anchor = normalized[normalized.length - 1].nextSibling
+  unmountComponent(instance, parent)
+  const prev = setCurrentInstance(instance.parent)
+  const newInstance = createComponent(
+    newComp,
+    instance.rawProps,
+    instance.rawSlots,
+    instance.isSingleRoot,
+  )
+  setCurrentInstance(...prev)
+  mountComponent(newInstance, parent, anchor)
+}
diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts
new file mode 100644
index 00000000000..7a8aea5a0d7
--- /dev/null
+++ b/packages/runtime-vapor/src/index.ts
@@ -0,0 +1,48 @@
+// public APIs
+export { createVaporApp, createVaporSSRApp } from './apiCreateApp'
+export { defineVaporComponent } from './apiDefineComponent'
+export { vaporInteropPlugin } from './vdomInterop'
+export type { VaporDirective } from './directives/custom'
+
+// compiler-use only
+export { insert, prepend, remove, isFragment, VaporFragment } from './block'
+export { setInsertionState } from './insertionState'
+export {
+  createComponent,
+  createComponentWithFallback,
+  isVaporComponent,
+} from './component'
+export { renderEffect } from './renderEffect'
+export { createSlot } from './componentSlots'
+export { template } from './dom/template'
+export { createTextNode, child, nthChild, next } from './dom/node'
+export {
+  setText,
+  setHtml,
+  setClass,
+  setStyle,
+  setAttr,
+  setValue,
+  setProp,
+  setDOMProp,
+  setDynamicProps,
+} from './dom/prop'
+export { on, delegate, delegateEvents, setDynamicEvents } from './dom/event'
+export { createIf } from './apiCreateIf'
+export {
+  createFor,
+  createForSlots,
+  getRestElement,
+  getDefaultValue,
+} from './apiCreateFor'
+export { createTemplateRefSetter } from './apiTemplateRef'
+export { createDynamicComponent } from './apiCreateDynamicComponent'
+export { applyVShow } from './directives/vShow'
+export {
+  applyTextModel,
+  applyRadioModel,
+  applyCheckboxModel,
+  applySelectModel,
+  applyDynamicModel,
+} from './directives/vModel'
+export { withVaporDirectives } from './directives/custom'
diff --git a/packages/runtime-vapor/src/insertionState.ts b/packages/runtime-vapor/src/insertionState.ts
new file mode 100644
index 00000000000..c8c7ffbcd1d
--- /dev/null
+++ b/packages/runtime-vapor/src/insertionState.ts
@@ -0,0 +1,16 @@
+export let insertionParent: ParentNode | undefined
+export let insertionAnchor: Node | 0 | undefined
+
+/**
+ * This function is called before a block type that requires insertion
+ * (component, slot outlet, if, for) is created. The state is used for actual
+ * insertion on client-side render, and used for node adoption during hydration.
+ */
+export function setInsertionState(parent: ParentNode, anchor?: Node | 0): void {
+  insertionParent = parent
+  insertionAnchor = anchor
+}
+
+export function resetInsertionState(): void {
+  insertionParent = insertionAnchor = undefined
+}
diff --git a/packages/runtime-vapor/src/renderEffect.ts b/packages/runtime-vapor/src/renderEffect.ts
new file mode 100644
index 00000000000..ac34e8863d2
--- /dev/null
+++ b/packages/runtime-vapor/src/renderEffect.ts
@@ -0,0 +1,91 @@
+import { EffectFlags, type EffectScope, ReactiveEffect } from '@vue/reactivity'
+import {
+  type SchedulerJob,
+  currentInstance,
+  queueJob,
+  queuePostFlushCb,
+  setCurrentInstance,
+  startMeasure,
+  warn,
+} from '@vue/runtime-dom'
+import { type VaporComponentInstance, isVaporComponent } from './component'
+import { invokeArrayFns } from '@vue/shared'
+
+class RenderEffect extends ReactiveEffect {
+  i: VaporComponentInstance | null
+  job: SchedulerJob
+  updateJob: SchedulerJob
+
+  constructor(public render: () => void) {
+    super()
+    const instance = currentInstance as VaporComponentInstance | null
+    if (__DEV__ && !__TEST__ && !this.subs && !isVaporComponent(instance)) {
+      warn('renderEffect called without active EffectScope or Vapor instance.')
+    }
+
+    const job: SchedulerJob = () => {
+      if (this.dirty) {
+        this.run()
+      }
+    }
+    this.updateJob = () => {
+      instance!.isUpdating = false
+      instance!.u && invokeArrayFns(instance!.u)
+    }
+
+    if (instance) {
+      if (__DEV__) {
+        this.onTrack = instance.rtc
+          ? e => invokeArrayFns(instance.rtc!, e)
+          : void 0
+        this.onTrigger = instance.rtg
+          ? e => invokeArrayFns(instance.rtg!, e)
+          : void 0
+      }
+      job.i = instance
+    }
+
+    this.job = job
+    this.i = instance
+
+    // TODO recurse handling
+  }
+
+  fn(): void {
+    const instance = this.i
+    const scope = this.subs ? (this.subs.sub as EffectScope) : undefined
+    // renderEffect is always called after user has registered all hooks
+    const hasUpdateHooks = instance && (instance.bu || instance.u)
+    if (__DEV__ && instance) {
+      startMeasure(instance, `renderEffect`)
+    }
+    const prev = setCurrentInstance(instance, scope)
+    if (hasUpdateHooks && instance.isMounted && !instance.isUpdating) {
+      instance.isUpdating = true
+      instance.bu && invokeArrayFns(instance.bu)
+      this.render()
+      queuePostFlushCb(this.updateJob)
+    } else {
+      this.render()
+    }
+    setCurrentInstance(...prev)
+    if (__DEV__ && instance) {
+      startMeasure(instance, `renderEffect`)
+    }
+  }
+
+  notify(): void {
+    const flags = this.flags
+    if (!(flags & EffectFlags.PAUSED)) {
+      queueJob(this.job, this.i ? this.i.uid : undefined)
+    }
+  }
+}
+
+export function renderEffect(fn: () => void, noLifecycle = false): void {
+  const effect = new RenderEffect(fn)
+  if (noLifecycle) {
+    effect.fn = fn
+  }
+  effect.run()
+}
diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts
new file mode 100644
index 00000000000..1573a306922
--- /dev/null
+++ b/packages/runtime-vapor/src/vdomInterop.ts
@@ -0,0 +1,311 @@
+import {
+  type App,
+  type ComponentInternalInstance,
+  type ConcreteComponent,
+  MoveType,
+  type Plugin,
+  type RendererInternals,
+  type ShallowRef,
+  type Slots,
+  type VNode,
+  type VaporInteropInterface,
+  createInternalObject,
+  createVNode,
+  currentInstance,
+  ensureRenderer,
+  isEmitListener,
+  onScopeDispose,
+  renderSlot,
+  shallowReactive,
+  shallowRef,
+  simpleSetCurrentInstance,
+} from '@vue/runtime-dom'
+import {
+  type LooseRawProps,
+  type LooseRawSlots,
+  type VaporComponent,
+  VaporComponentInstance,
+  createComponent,
+  mountComponent,
+  unmountComponent,
+} from './component'
+import { type Block, VaporFragment, insert, remove } from './block'
+import { EMPTY_OBJ, extend, isFunction } from '@vue/shared'
+import { type RawProps, rawPropsProxyHandlers } from './componentProps'
+import type { RawSlots, VaporSlot } from './componentSlots'
+import { renderEffect } from './renderEffect'
+import { createTextNode } from './dom/node'
+import { optimizePropertyLookup } from './dom/prop'
+
+export const interopKey: unique symbol = Symbol(`interop`)
+
+// mounting vapor components and slots in vdom
+const vaporInteropImpl: Omit<
+  VaporInteropInterface,
+  'vdomMount' | 'vdomUnmount' | 'vdomSlot'
+> = {
+  mount(vnode, container, anchor, parentComponent) {
+    const selfAnchor = (vnode.el = vnode.anchor = createTextNode())
+    container.insertBefore(selfAnchor, anchor)
+    const prev = currentInstance
+    simpleSetCurrentInstance(parentComponent)
+
+    const propsRef = shallowRef(vnode.props)
+    const slotsRef = shallowRef(vnode.children)
+
+    const dynamicPropSource: (() => any)[] & { [interopKey]?: boolean } = [
+      () => propsRef.value,
+    ]
+    // mark as interop props
+    dynamicPropSource[interopKey] = true
+    // @ts-expect-error
+    const instance = (vnode.component = createComponent(
+      vnode.type as any as VaporComponent,
+      {
+        $: dynamicPropSource,
+      } as RawProps,
+      {
+        _: slotsRef, // pass the slots ref
+      } as any as RawSlots,
+    ))
+    instance.rawPropsRef = propsRef
+    instance.rawSlotsRef = slotsRef
+    mountComponent(instance, container, selfAnchor)
+    simpleSetCurrentInstance(prev)
+    return instance
+  },
+
+  update(n1, n2, shouldUpdate) {
+    n2.component = n1.component
+    n2.el = n2.anchor = n1.anchor
+    if (shouldUpdate) {
+      const instance = n2.component as any as VaporComponentInstance
+      instance.rawPropsRef!.value = n2.props
+      instance.rawSlotsRef!.value = n2.children
+    }
+  },
+
+  unmount(vnode, doRemove) {
+    const container = doRemove ? vnode.anchor!.parentNode : undefined
+    if (vnode.component) {
+      unmountComponent(vnode.component as any, container)
+    } else if (vnode.vb) {
+      remove(vnode.vb, container)
+    }
+    remove(vnode.anchor as Node, container)
+  },
+
+  /**
+   * vapor slot in vdom
+   */
+  slot(n1: VNode, n2: VNode, container, anchor) {
+    if (!n1) {
+      // mount
+      const selfAnchor = (n2.el = n2.anchor = createTextNode())
+      insert(selfAnchor, container, anchor)
+      const { slot, fallback } = n2.vs!
+      const propsRef = (n2.vs!.ref = shallowRef(n2.props))
+      const slotBlock = slot(new Proxy(propsRef, vaporSlotPropsProxyHandler))
+      // TODO fallback for slot with v-if content
+      // fallback is a vnode slot function here, and slotBlock, if a DynamicFragment,
+      // expects a Vapor BlockFn as fallback
+      fallback
+      insert((n2.vb = slotBlock), container, selfAnchor)
+    } else {
+      // update
+      n2.el = n2.anchor = n1.anchor
+      n2.vb = n1.vb
+      ;(n2.vs!.ref = n1.vs!.ref)!.value = n2.props
+    }
+  },
+
+  move(vnode, container, anchor) {
+    insert(vnode.vb || (vnode.component as any), container, anchor)
+    insert(vnode.anchor as any, container, anchor)
+  },
+}
+
+const vaporSlotPropsProxyHandler: ProxyHandler<
+  ShallowRef<Record<string, any>>
+> = {
+  get(target, key: any) {
+    return target.value[key]
+  },
+  has(target, key: any) {
+    return target.value[key]
+  },
+  ownKeys(target) {
+    return Object.keys(target.value)
+  },
+}
+
+const vaporSlotsProxyHandler: ProxyHandler<any> = {
+  get(target, key) {
+    const slot = target[key]
+    if (isFunction(slot)) {
+      slot.__vapor = true
+    }
+    return slot
+  },
+}
+
+/**
+ * Mount vdom component in vapor
+ */
+function createVDOMComponent(
+  internals: RendererInternals,
+  component: ConcreteComponent,
+  rawProps?: LooseRawProps | null,
+  rawSlots?: LooseRawSlots | null,
+): VaporFragment {
+  const frag = new VaporFragment([])
+  const vnode = createVNode(
+    component,
+    rawProps && new Proxy(rawProps, rawPropsProxyHandlers),
+  )
+  const wrapper = new VaporComponentInstance(
+    { props: component.props },
+    rawProps as RawProps,
+    rawSlots as RawSlots,
+  )
+
+  // overwrite how the vdom instance handles props
+  vnode.vi = (instance: ComponentInternalInstance) => {
+    // ensure props are shallow reactive to align with VDOM behavior.
+    instance.props = shallowReactive(wrapper.props)
+
+    const attrs = (instance.attrs = createInternalObject())
+    for (const key in wrapper.attrs) {
+      if (!isEmitListener(instance.emitsOptions, key)) {
+        attrs[key] = wrapper.attrs[key]
+      }
+    }
+
+    instance.slots =
+      wrapper.slots === EMPTY_OBJ
+        ? EMPTY_OBJ
+        : new Proxy(wrapper.slots, vaporSlotsProxyHandler)
+  }
+
+  let isMounted = false
+  const parentInstance = currentInstance as VaporComponentInstance
+  const unmount = (parentNode?: ParentNode) => {
+    internals.umt(vnode.component!, null, !!parentNode)
+  }
+
+  frag.insert = (parentNode, anchor) => {
+    if (!isMounted) {
+      internals.mt(
+        vnode,
+        parentNode,
+        anchor,
+        parentInstance as any,
+        null,
+        undefined,
+        false,
+      )
+      onScopeDispose(unmount, true)
+      isMounted = true
+    } else {
+      // move
+      internals.m(
+        vnode,
+        parentNode,
+        anchor,
+        MoveType.REORDER,
+        parentInstance as any,
+      )
+    }
+  }
+
+  frag.remove = unmount
+
+  return frag
+}
+
+/**
+ * Mount vdom slot in vapor
+ */
+function renderVDOMSlot(
+  internals: RendererInternals,
+  slotsRef: ShallowRef<Slots>,
+  name: string | (() => string),
+  props: Record<string, any>,
+  parentComponent: VaporComponentInstance,
+  fallback?: VaporSlot,
+): VaporFragment {
+  const frag = new VaporFragment([])
+
+  let isMounted = false
+  let fallbackNodes: Block | undefined
+  let oldVNode: VNode | null = null
+
+  frag.insert = (parentNode, anchor) => {
+    if (!isMounted) {
+      renderEffect(() => {
+        const vnode = renderSlot(
+          slotsRef.value,
+          isFunction(name) ? name() : name,
+          props,
+        )
+        if ((vnode.children as any[]).length) {
+          if (fallbackNodes) {
+            remove(fallbackNodes, parentNode)
+            fallbackNodes = undefined
+          }
+          internals.p(
+            oldVNode,
+            vnode,
+            parentNode,
+            anchor,
+            parentComponent as any,
+          )
+          oldVNode = vnode
+        } else {
+          if (fallback && !fallbackNodes) {
+            // mount fallback
+            if (oldVNode) {
+              internals.um(oldVNode, parentComponent as any, null, true)
+            }
+            insert((fallbackNodes = fallback(props)), parentNode, anchor)
+          }
+          oldVNode = null
+        }
+      })
+      isMounted = true
+    } else {
+      // move
+      internals.m(
+        oldVNode!,
+        parentNode,
+        anchor,
+        MoveType.REORDER,
+        parentComponent as any,
+      )
+    }
+
+    frag.remove = parentNode => {
+      if (fallbackNodes) {
+        remove(fallbackNodes, parentNode)
+      } else if (oldVNode) {
+        internals.um(oldVNode, parentComponent as any, null)
+      }
+    }
+  }
+
+  return frag
+}
+
+export const vaporInteropPlugin: Plugin = app => {
+  const internals = ensureRenderer().internals
+  app._context.vapor = extend(vaporInteropImpl, {
+    vdomMount: createVDOMComponent.bind(null, internals),
+    vdomUnmount: internals.umt,
+    vdomSlot: renderVDOMSlot.bind(null, internals),
+  })
+  const mount = app.mount
+  app.mount = ((...args) => {
+    optimizePropertyLookup()
+    return mount(...args)
+  }) satisfies App['mount']
+}
diff --git a/packages/server-renderer/package.json b/packages/server-renderer/package.json
index 18ede162578..cb4da84e4e2 100644
--- a/packages/server-renderer/package.json
+++ b/packages/server-renderer/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/server-renderer",
-  "version": "3.5.17",
+  "version": "3.6.0-alpha.2",
   "description": "@vue/server-renderer",
   "main": "index.js",
   "module": "dist/server-renderer.esm-bundler.js",
diff --git a/packages/server-renderer/src/render.ts b/packages/server-renderer/src/render.ts
index f04080b9c31..221d3895e22 100644
--- a/packages/server-renderer/src/render.ts
+++ b/packages/server-renderer/src/render.ts
@@ -172,7 +172,7 @@ function renderComponentSubTree(
         if (parent && parent.subTree && parent.subTree === cur.vnode) {
           // parent is a non-SSR compiled component and is rendering this
           // component as root. inherit its scopeId if present.
-          cur = parent
+          cur = parent as ComponentInternalInstance
         } else {
           break
         }
@@ -314,7 +314,7 @@ function renderElementVNode(
     if (curVnode.scopeId) {
       openTag += ` ${curVnode.scopeId}`
     }
-    curParent = curParent.parent
+    curParent = curParent.parent as ComponentInternalInstance
   }
   if (slotScopeId) {
     openTag += ` ${slotScopeId}`
diff --git a/packages/shared/package.json b/packages/shared/package.json
index 6481797689c..b4703addd88 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/shared",
-  "version": "3.5.17",
+  "version": "3.6.0-alpha.2",
   "description": "internal utils shared across @vue packages",
   "main": "index.js",
   "module": "dist/shared.esm-bundler.js",
diff --git a/packages/shared/src/domAttrConfig.ts b/packages/shared/src/domAttrConfig.ts
index b5f0166327f..80fb80f9d9c 100644
--- a/packages/shared/src/domAttrConfig.ts
+++ b/packages/shared/src/domAttrConfig.ts
@@ -152,3 +152,52 @@ export function isRenderableAttrValue(value: unknown): boolean {
   const type = typeof value
   return type === 'string' || type === 'number' || type === 'boolean'
 }
+
+/*
+ * The following attributes must be set as attribute
+ */
+export function shouldSetAsAttr(tagName: string, key: string): boolean {
+  // these are enumerated attrs, however their corresponding DOM properties
+  // are actually booleans - this leads to setting it with a string "false"
+  // value leading it to be coerced to `true`, so we need to always treat
+  // them as attributes.
+  // Note that `contentEditable` doesn't have this problem: its DOM
+  // property is also enumerated string values.
+  if (
+    key === 'spellcheck' ||
+    key === 'draggable' ||
+    key === 'translate' ||
+    key === 'autocorrect'
+  ) {
+    return true
+  }
+
+  // #1787, #2840 form property on form elements is readonly and must be set as
+  // attribute.
+  if (key === 'form') {
+    return true
+  }
+
+  // #1526 <input list> must be set as attribute
+  if (key === 'list' && tagName === 'INPUT') {
+    return true
+  }
+
+  // #2766 <textarea type> must be set as attribute
+  if (key === 'type' && tagName === 'TEXTAREA') {
+    return true
+  }
+
+  // #8780 the width or height of embedded tags must be set as attribute
+  if (
+    (key === 'width' || key === 'height') &&
+    (tagName === 'IMG' ||
+      tagName === 'VIDEO' ||
+      tagName === 'CANVAS' ||
+      tagName === 'SOURCE')
+  ) {
+    return true
+  }
+
+  return false
+}
diff --git a/packages/shared/src/general.ts b/packages/shared/src/general.ts
index 9c6a2313240..bf11ba7a293 100644
--- a/packages/shared/src/general.ts
+++ b/packages/shared/src/general.ts
@@ -7,6 +7,11 @@ export const EMPTY_ARR: readonly never[] = __DEV__ ? Object.freeze([]) : []
 
 export const NOOP = (): void => {}
 
+/**
+ * Always return true.
+ */
+export const YES = () => true
+
 /**
  * Always return false.
  */
@@ -18,7 +23,14 @@ export const isOn = (key: string): boolean =>
   // uppercase letter
   (key.charCodeAt(2) > 122 || key.charCodeAt(2) < 97)
 
-export const isModelListener = (key: string): key is `onUpdate:${string}` =>
+export const isNativeOn = (key: string): boolean =>
+  key.charCodeAt(0) === 111 /* o */ &&
+  key.charCodeAt(1) === 110 /* n */ &&
+  // lowercase letter
+  key.charCodeAt(2) > 96 &&
+  key.charCodeAt(2) < 123
+
+export const isModelListener = (key: string): boolean =>
   key.startsWith('onUpdate:')
 
 export const extend: typeof Object.assign = Object.assign
@@ -88,6 +100,9 @@ export const isReservedProp: (key: string) => boolean = /*@__PURE__*/ makeMap(
     'onVnodeBeforeUnmount,onVnodeUnmounted',
 )
 
+export const isBuiltInTag: (key: string) => boolean =
+  /*#__PURE__*/ makeMap('slot,component')
+
 export const isBuiltInDirective: (key: string) => boolean =
   /*@__PURE__*/ makeMap(
     'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo',
@@ -102,13 +117,12 @@ const cacheStringFunction = <T extends (str: string) => string>(fn: T): T => {
 }
 
 const camelizeRE = /-(\w)/g
+const camelizeReplacer = (_: any, c: string) => (c ? c.toUpperCase() : '')
 /**
  * @private
  */
 export const camelize: (str: string) => string = cacheStringFunction(
-  (str: string): string => {
-    return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''))
-  },
+  (str: string): string => str.replace(camelizeRE, camelizeReplacer),
 )
 
 const hyphenateRE = /\B([A-Z])/g
@@ -217,3 +231,11 @@ export function genCacheKey(source: string, options: any): string {
     )
   )
 }
+
+export function canSetValueDirectly(tagName: string): boolean {
+  return (
+    tagName !== 'PROGRESS' &&
+    // custom elements may use _value internally
+    !tagName.includes('-')
+  )
+}
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts
index 08d4b07af8e..0c38d640ba0 100644
--- a/packages/shared/src/index.ts
+++ b/packages/shared/src/index.ts
@@ -12,4 +12,5 @@ export * from './escapeHtml'
 export * from './looseEqual'
 export * from './toDisplayString'
 export * from './typeUtils'
+export * from './subSequence'
 export * from './cssVars'
diff --git a/packages/shared/src/normalizeProp.ts b/packages/shared/src/normalizeProp.ts
index ef598a03ced..eec620480de 100644
--- a/packages/shared/src/normalizeProp.ts
+++ b/packages/shared/src/normalizeProp.ts
@@ -1,6 +1,6 @@
 import { hyphenate, isArray, isObject, isString } from './general'
 
-export type NormalizedStyle = Record<string, string | number>
+export type NormalizedStyle = Record<string, unknown>
 
 export function normalizeStyle(
   value: unknown,
diff --git a/packages/shared/src/subSequence.ts b/packages/shared/src/subSequence.ts
new file mode 100644
index 00000000000..8edde79b7e6
--- /dev/null
+++ b/packages/shared/src/subSequence.ts
@@ -0,0 +1,41 @@
+// https://en.wikipedia.org/wiki/Longest_increasing_subsequence
+export function getSequence(arr: number[]): number[] {
+  const p = arr.slice()
+  const result = [0]
+  let i, j, u, v, c
+  const len = arr.length
+  for (i = 0; i < len; i++) {
+    const arrI = arr[i]
+    if (arrI !== 0) {
+      j = result[result.length - 1]
+      if (arr[j] < arrI) {
+        p[i] = j
+        result.push(i)
+        continue
+      }
+      u = 0
+      v = result.length - 1
+      while (u < v) {
+        c = (u + v) >> 1
+        if (arr[result[c]] < arrI) {
+          u = c + 1
+        } else {
+          v = c
+        }
+      }
+      if (arrI < arr[result[u]]) {
+        if (u > 0) {
+          p[i] = result[u - 1]
+        }
+        result[u] = i
+      }
+    }
+  }
+  u = result.length
+  v = result[u - 1]
+  while (u-- > 0) {
+    result[u] = v
+    v = p[v]
+  }
+  return result
+}
diff --git a/packages/shared/src/toDisplayString.ts b/packages/shared/src/toDisplayString.ts
index b23cf766a5c..d2799667927 100644
--- a/packages/shared/src/toDisplayString.ts
+++ b/packages/shared/src/toDisplayString.ts
@@ -7,7 +7,6 @@ import {
   isObject,
   isPlainObject,
   isSet,
-  isString,
   isSymbol,
   objectToString,
 } from './general'
@@ -22,17 +21,24 @@ const isRef = (val: any): val is { value: unknown } => {
  * @private
  */
 export const toDisplayString = (val: unknown): string => {
-  return isString(val)
-    ? val
-    : val == null
-      ? ''
-      : isArray(val) ||
-          (isObject(val) &&
-            (val.toString === objectToString || !isFunction(val.toString)))
-        ? isRef(val)
-          ? toDisplayString(val.value)
-          : JSON.stringify(val, replacer, 2)
-        : String(val)
+  switch (typeof val) {
+    case 'string':
+      return val
+    case 'object':
+      if (val) {
+        if (isRef(val)) {
+          return toDisplayString(val.value)
+        } else if (
+          isArray(val) ||
+          val.toString === objectToString ||
+          !isFunction(val.toString)
+        ) {
+          return JSON.stringify(val, replacer, 2)
+        }
+      }
+    default:
+      return val == null ? '' : String(val)
+  }
 }
 
 const replacer = (_key: string, val: unknown): any => {
diff --git a/packages/shared/src/vaporFlags.ts b/packages/shared/src/vaporFlags.ts
new file mode 100644
index 00000000000..2be48df4d92
--- /dev/null
+++ b/packages/shared/src/vaporFlags.ts
@@ -0,0 +1,20 @@
+/**
+ * Flags to optimize vapor `createFor` runtime behavior, shared between the
+ * compiler and the runtime
+ */
+export enum VaporVForFlags {
+  /**
+   * v-for is the only child of a parent container, so it can take the fast
+   * path with textContent = '' when the whole list is emptied
+   */
+  FAST_REMOVE = 1,
+  /**
+   * v-for used on component - we can skip creating child scopes for each block
+   * because the component itself already has a scope.
+   */
+  IS_COMPONENT = 1 << 1,
+  /**
+   * v-for inside v-ince
+   */
+  ONCE = 1 << 2,
+}
diff --git a/packages/vue-compat/README.md b/packages/vue-compat/README.md
index 9428d6f6c7b..97fe9a1cf19 100644
--- a/packages/vue-compat/README.md
+++ b/packages/vue-compat/README.md
@@ -41,7 +41,6 @@ The following workflow walks through the steps of migrating an actual Vue 2 app
 ### Installation
 
 1. Upgrade tooling if applicable.
-
    - If using custom webpack setup: Upgrade `vue-loader` to `^16.0.0`.
    - If using `vue-cli`: upgrade to the latest `@vue/cli-service` with `vue upgrade`
    - (Alternative) migrate to [Vite](https://vitejs.dev/) + [vite-plugin-vue2](https://github.com/underfin/vite-plugin-vue2). [[Example commit](https://github.com/vuejs/vue-hackernews-2.0/commit/565b948919eb58f22a32afca7e321b490cb3b074)]
@@ -160,7 +159,6 @@ The following workflow walks through the steps of migrating an actual Vue 2 app
 5. After fixing the errors, the app should be able to run if it is not subject to the [limitations](#known-limitations) mentioned above.
 
    You will likely see a LOT of warnings from both the command line and the browser console. Here are some general tips:
-
    - You can filter for specific warnings in the browser console. It's a good idea to use the filter and focus on fixing one item at a time. You can also use negated filters like `-GLOBAL_MOUNT`.
 
    - You can suppress specific deprecations via [compat configuration](#compat-configuration).
diff --git a/packages/vue-compat/package.json b/packages/vue-compat/package.json
index 091dc10303e..cd2917e6b44 100644
--- a/packages/vue-compat/package.json
+++ b/packages/vue-compat/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/compat",
-  "version": "3.5.17",
+  "version": "3.6.0-alpha.2",
   "description": "Vue 3 compatibility build for Vue 2",
   "main": "index.js",
   "module": "dist/vue.runtime.esm-bundler.js",
diff --git a/packages/vue/README.md b/packages/vue/README.md
index 757ea82f5d7..30e08cfd2ab 100644
--- a/packages/vue/README.md
+++ b/packages/vue/README.md
@@ -5,7 +5,6 @@
 ### From CDN or without a Bundler
 
 - **`vue(.runtime).global(.prod).js`**:
-
   - For direct use via `<script src="...">` in the browser. Exposes the `Vue` global.
   - Note that global builds are not [UMD](https://github.com/umdjs/umd) builds. They are built as [IIFEs](https://developer.mozilla.org/en-US/docs/Glossary/IIFE) and is only meant for direct use via `<script src="...">`.
   - In-browser template compilation:
@@ -21,7 +20,6 @@
 ### With a Bundler
 
 - **`vue(.runtime).esm-bundler.js`**:
-
   - For use with bundlers like `webpack`, `rollup` and `parcel`.
   - Leaves prod/dev branches with `process.env.NODE_ENV` guards (must be replaced by bundler)
   - Does not ship minified builds (to be done together with the rest of the code after bundling)
@@ -39,12 +37,10 @@
 `esm-bundler` builds of Vue expose global feature flags that can be overwritten at compile time:
 
 - `__VUE_OPTIONS_API__`
-
   - Default: `true`
   - Enable / disable Options API support
 
 - `__VUE_PROD_DEVTOOLS__`
-
   - Default: `false`
   - Enable / disable devtools support in production
 
diff --git a/packages/vue/__tests__/e2e/Transition.spec.ts b/packages/vue/__tests__/e2e/Transition.spec.ts
index 45597521791..3df50caf9e3 100644
--- a/packages/vue/__tests__/e2e/Transition.spec.ts
+++ b/packages/vue/__tests__/e2e/Transition.spec.ts
@@ -1,3 +1,5 @@
+// @vitest-environment jsdom
+
 import type { ElementHandle } from 'puppeteer'
 import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
 import path from 'node:path'
diff --git a/packages/vue/__tests__/e2e/TransitionGroup.spec.ts b/packages/vue/__tests__/e2e/TransitionGroup.spec.ts
index 62d89db4e31..92efc228e80 100644
--- a/packages/vue/__tests__/e2e/TransitionGroup.spec.ts
+++ b/packages/vue/__tests__/e2e/TransitionGroup.spec.ts
@@ -1,3 +1,5 @@
+// @vitest-environment jsdom
+
 import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
 import path from 'node:path'
 import { createApp, ref } from 'vue'
diff --git a/packages/vue/__tests__/e2e/e2eUtils.ts b/packages/vue/__tests__/e2e/e2eUtils.ts
index 1ed5322d1f0..2ffebeb5950 100644
--- a/packages/vue/__tests__/e2e/e2eUtils.ts
+++ b/packages/vue/__tests__/e2e/e2eUtils.ts
@@ -76,8 +76,7 @@ export function setupPuppeteer(args?: string[]): PuppeteerUtils {
 
     page.on('console', e => {
       if (e.type() === 'error') {
-        const err = e.args()[0]
-        console.error(`Error from Puppeteer-loaded page:\n`, err.remoteObject())
+        console.error(`Error from Puppeteer-loaded page:\n`, e.text())
       }
     })
   })
diff --git a/packages/vue/package.json b/packages/vue/package.json
index 5d5704cb3f5..ab734ee7a08 100644
--- a/packages/vue/package.json
+++ b/packages/vue/package.json
@@ -1,6 +1,6 @@
 {
   "name": "vue",
-  "version": "3.5.17",
+  "version": "3.6.0-alpha.2",
   "description": "The progressive JavaScript framework for building modern web UI.",
   "main": "index.js",
   "module": "dist/vue.runtime.esm-bundler.js",
@@ -78,7 +78,8 @@
       "global",
       "global-runtime",
       "esm-browser",
-      "esm-browser-runtime"
+      "esm-browser-runtime",
+      "esm-browser-vapor"
     ]
   },
   "repository": {
@@ -98,6 +99,7 @@
     "@vue/shared": "workspace:*",
     "@vue/compiler-dom": "workspace:*",
     "@vue/runtime-dom": "workspace:*",
+    "@vue/runtime-vapor": "workspace:*",
     "@vue/compiler-sfc": "workspace:*",
     "@vue/server-renderer": "workspace:*"
   },
diff --git a/packages/vue/src/index-with-vapor.ts b/packages/vue/src/index-with-vapor.ts
new file mode 100644
index 00000000000..21f4c8073cf
--- /dev/null
+++ b/packages/vue/src/index-with-vapor.ts
@@ -0,0 +1,3 @@
+// for type generation only
+export * from './index'
+export * from '@vue/runtime-vapor'
diff --git a/packages/vue/src/runtime-with-vapor.ts b/packages/vue/src/runtime-with-vapor.ts
new file mode 100644
index 00000000000..4f03329ede4
--- /dev/null
+++ b/packages/vue/src/runtime-with-vapor.ts
@@ -0,0 +1,2 @@
+export * from './runtime'
+export * from '@vue/runtime-vapor'
diff --git a/packages/vue/src/runtime.ts b/packages/vue/src/runtime.ts
index 76f5fc74e25..af1ffe7a12a 100644
--- a/packages/vue/src/runtime.ts
+++ b/packages/vue/src/runtime.ts
@@ -1,7 +1,8 @@
 // This entry exports the runtime only, and is built as
 // `dist/vue.esm-bundler.js` which is used by default for bundlers.
+import { NOOP } from '@vue/shared'
 import { initDev } from './dev'
-import { warn } from '@vue/runtime-dom'
+import { type RenderFunction, warn } from '@vue/runtime-dom'
 
 if (__DEV__) {
   initDev()
@@ -9,7 +10,7 @@ if (__DEV__) {
 
 export * from '@vue/runtime-dom'
 
-export const compile = (): void => {
+export const compile = (_template: string): RenderFunction => {
   if (__DEV__) {
     warn(
       `Runtime compilation is not supported in this build of Vue.` +
@@ -22,4 +23,5 @@ export const compile = (): void => {
               : ``) /* should not happen */,
     )
   }
+  return NOOP
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 898a9fe6178..c777d8238f3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,10 +8,10 @@ catalogs:
   default:
     '@babel/parser':
       specifier: ^7.27.5
-      version: 7.27.5
+      version: 7.28.0
     '@babel/types':
       specifier: ^7.27.6
-      version: 7.27.6
+      version: 7.28.0
     '@vitejs/plugin-vue':
       specifier: ^5.2.4
       version: 5.2.4
@@ -25,8 +25,8 @@ catalogs:
       specifier: ^1.2.1
       version: 1.2.1
     vite:
-      specifier: ^5.4.15
-      version: 5.4.15
+      specifier: ^6.1.0
+      version: 6.3.5
 
 importers:
 
@@ -34,10 +34,10 @@ importers:
     devDependencies:
       '@babel/parser':
         specifier: 'catalog:'
-        version: 7.27.5
+        version: 7.28.0
       '@babel/types':
         specifier: 'catalog:'
-        version: 7.27.6
+        version: 7.28.0
       '@rollup/plugin-alias':
         specifier: ^5.1.1
         version: 5.1.1(rollup@4.44.1)
@@ -70,10 +70,13 @@ importers:
         version: 6.1.4
       '@vitest/coverage-v8':
         specifier: ^3.1.4
-        version: 3.1.4(vitest@3.1.4(@types/node@22.16.0)(jsdom@26.1.0)(sass@1.89.2))
+        version: 3.2.4(vitest@3.2.4)
       '@vitest/eslint-plugin':
         specifier: ^1.2.1
-        version: 1.2.1(eslint@9.27.0)(typescript@5.6.3)(vitest@3.1.4(@types/node@22.16.0)(jsdom@26.1.0)(sass@1.89.2))
+        version: 1.3.4(eslint@9.30.1)(typescript@5.6.3)(vitest@3.2.4)
+      '@vitest/ui':
+        specifier: ^3.0.2
+        version: 3.2.4(vitest@3.2.4)
       '@vue/consolidate':
         specifier: 1.0.0
         version: 1.0.0
@@ -91,10 +94,10 @@ importers:
         version: 0.3.0(esbuild@0.25.5)
       eslint:
         specifier: ^9.27.0
-        version: 9.27.0
+        version: 9.30.1
       eslint-plugin-import-x:
         specifier: ^4.13.1
-        version: 4.13.1(eslint@9.27.0)(typescript@5.6.3)
+        version: 4.16.1(@typescript-eslint/utils@8.35.1(eslint@9.30.1)(typescript@5.6.3))(eslint@9.30.1)
       estree-walker:
         specifier: 'catalog:'
         version: 2.0.2
@@ -103,7 +106,7 @@ importers:
         version: 26.1.0
       lint-staged:
         specifier: ^16.0.0
-        version: 16.0.0
+        version: 16.1.2
       lodash:
         specifier: ^4.17.21
         version: 4.17.21
@@ -124,7 +127,7 @@ importers:
         version: 1.1.1
       prettier:
         specifier: ^3.5.3
-        version: 3.5.3
+        version: 3.6.2
       pretty-bytes:
         specifier: ^6.1.1
         version: 6.1.1
@@ -172,13 +175,32 @@ importers:
         version: 5.6.3
       typescript-eslint:
         specifier: ^8.32.1
-        version: 8.32.1(eslint@9.27.0)(typescript@5.6.3)
+        version: 8.35.1(eslint@9.30.1)(typescript@5.6.3)
       vite:
         specifier: 'catalog:'
-        version: 5.4.15(@types/node@22.16.0)(sass@1.89.2)
+        version: 6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0)
       vitest:
         specifier: ^3.1.4
-        version: 3.1.4(@types/node@22.16.0)(jsdom@26.1.0)(sass@1.89.2)
+        version: 3.2.4(@types/node@22.16.0)(@vitest/ui@3.2.4)(jsdom@26.1.0)(sass@1.89.2)(yaml@2.8.0)
+
+  packages-private/benchmark:
+    dependencies:
+      '@vitejs/plugin-vue':
+        specifier: 'catalog:'
+        version: 5.2.4(vite@6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0))(vue@3.5.17(typescript@5.6.3))
+      connect:
+        specifier: ^3.7.0
+        version: 3.7.0
+      sirv:
+        specifier: ^2.0.4
+        version: 2.0.4
+      vite:
+        specifier: 'catalog:'
+        version: 6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0)
+    devDependencies:
+      '@types/connect':
+        specifier: ^3.4.38
+        version: 3.4.38
 
   packages-private/dts-built-test:
     dependencies:
@@ -201,6 +223,31 @@ importers:
         specifier: workspace:*
         version: link:../../packages/vue
 
+  packages-private/local-playground:
+    dependencies:
+      '@vueuse/core':
+        specifier: ^11.1.0
+        version: 11.3.0(vue@packages+vue)
+      vue:
+        specifier: workspace:*
+        version: link:../../packages/vue
+    devDependencies:
+      '@vitejs/plugin-vue':
+        specifier: 'catalog:'
+        version: 5.2.4(vite@6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0))(vue@packages+vue)
+      '@vue/compiler-sfc':
+        specifier: workspace:*
+        version: link:../../packages/compiler-sfc
+      vite:
+        specifier: 'catalog:'
+        version: 6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0)
+      vite-hyper-config:
+        specifier: ^0.4.0
+        version: 0.4.1(@types/node@22.16.0)(sass@1.89.2)(vite@6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0))
+      vite-plugin-inspect:
+        specifier: ^0.8.7
+        version: 0.8.9(rollup@4.44.1)(vite@6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0))
+
   packages-private/sfc-playground:
     dependencies:
       '@vue/repl':
@@ -218,13 +265,16 @@ importers:
     devDependencies:
       '@vitejs/plugin-vue':
         specifier: 'catalog:'
-        version: 5.2.4(vite@5.4.15(@types/node@22.16.0)(sass@1.89.2))(vue@packages+vue)
+        version: 5.2.4(vite@6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0))(vue@packages+vue)
       vite:
         specifier: 'catalog:'
-        version: 5.4.15(@types/node@22.16.0)(sass@1.89.2)
+        version: 6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0)
 
   packages-private/template-explorer:
     dependencies:
+      '@vue/compiler-vapor':
+        specifier: workspace:^
+        version: link:../../packages/compiler-vapor
       monaco-editor:
         specifier: ^0.52.2
         version: 0.52.2
@@ -232,14 +282,35 @@ importers:
         specifier: ^1.2.1
         version: 1.2.1
 
+  packages-private/vapor-e2e-test:
+    devDependencies:
+      '@types/connect':
+        specifier: ^3.4.38
+        version: 3.4.38
+      '@vitejs/plugin-vue':
+        specifier: 'catalog:'
+        version: 5.2.4(vite@6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0))(vue@packages+vue)
+      connect:
+        specifier: ^3.7.0
+        version: 3.7.0
+      sirv:
+        specifier: ^2.0.4
+        version: 2.0.4
+      vite:
+        specifier: 'catalog:'
+        version: 6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0)
+      vue:
+        specifier: workspace:*
+        version: link:../../packages/vue
+
   packages-private/vite-debug:
     devDependencies:
       '@vitejs/plugin-vue':
         specifier: 'catalog:'
-        version: 5.2.4(vite@5.4.15(@types/node@22.16.0)(sass@1.89.2))(vue@packages+vue)
+        version: 5.2.4(vite@6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0))(vue@packages+vue)
       vite:
         specifier: 'catalog:'
-        version: 5.4.15(@types/node@22.16.0)(sass@1.89.2)
+        version: 6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0)
       vue:
         specifier: workspace:*
         version: link:../../packages/vue
@@ -248,7 +319,7 @@ importers:
     dependencies:
       '@babel/parser':
         specifier: 'catalog:'
-        version: 7.27.5
+        version: 7.28.0
       '@vue/shared':
         specifier: workspace:*
         version: link:../shared
@@ -264,7 +335,7 @@ importers:
     devDependencies:
       '@babel/types':
         specifier: 'catalog:'
-        version: 7.27.6
+        version: 7.28.0
 
   packages/compiler-dom:
     dependencies:
@@ -279,7 +350,7 @@ importers:
     dependencies:
       '@babel/parser':
         specifier: 'catalog:'
-        version: 7.27.5
+        version: 7.28.0
       '@vue/compiler-core':
         specifier: workspace:*
         version: link:../compiler-core
@@ -289,6 +360,9 @@ importers:
       '@vue/compiler-ssr':
         specifier: workspace:*
         version: link:../compiler-ssr
+      '@vue/compiler-vapor':
+        specifier: workspace:*
+        version: link:../compiler-vapor
       '@vue/shared':
         specifier: workspace:*
         version: link:../shared
@@ -307,7 +381,7 @@ importers:
     devDependencies:
       '@babel/types':
         specifier: 'catalog:'
-        version: 7.27.6
+        version: 7.28.0
       '@vue/consolidate':
         specifier: ^1.0.0
         version: 1.0.0
@@ -345,6 +419,24 @@ importers:
         specifier: workspace:*
         version: link:../shared
 
+  packages/compiler-vapor:
+    dependencies:
+      '@babel/parser':
+        specifier: 'catalog:'
+        version: 7.28.0
+      '@vue/compiler-dom':
+        specifier: workspace:*
+        version: link:../compiler-dom
+      '@vue/shared':
+        specifier: workspace:*
+        version: link:../shared
+      estree-walker:
+        specifier: 'catalog:'
+        version: 2.0.2
+      source-map-js:
+        specifier: 'catalog:'
+        version: 1.2.1
+
   packages/reactivity:
     dependencies:
       '@vue/shared':
@@ -388,6 +480,18 @@ importers:
         specifier: workspace:*
         version: link:../shared
 
+  packages/runtime-vapor:
+    dependencies:
+      '@vue/reactivity':
+        specifier: workspace:*
+        version: link:../reactivity
+      '@vue/runtime-dom':
+        specifier: workspace:*
+        version: link:../runtime-dom
+      '@vue/shared':
+        specifier: workspace:*
+        version: link:../shared
+
   packages/server-renderer:
     dependencies:
       '@vue/compiler-ssr':
@@ -413,6 +517,9 @@ importers:
       '@vue/runtime-dom':
         specifier: workspace:*
         version: link:../runtime-dom
+      '@vue/runtime-vapor':
+        specifier: workspace:*
+        version: link:../runtime-vapor
       '@vue/server-renderer':
         specifier: workspace:*
         version: link:../server-renderer
@@ -427,7 +534,7 @@ importers:
     dependencies:
       '@babel/parser':
         specifier: 'catalog:'
-        version: 7.27.5
+        version: 7.28.0
       estree-walker:
         specifier: 'catalog:'
         version: 2.0.2
@@ -444,11 +551,14 @@ packages:
     resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
     engines: {node: '>=6.0.0'}
 
-  '@asamuzakjp/css-color@2.8.2':
-    resolution: {integrity: sha512-RtWv9jFN2/bLExuZgFFZ0I3pWWeezAHGgrmjqGGWclATl1aDe3yhCUaI0Ilkp6OCk9zX7+FjvDasEX8Q9Rxc5w==}
+  '@antfu/utils@0.7.10':
+    resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
 
-  '@babel/code-frame@7.26.2':
-    resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==}
+  '@asamuzakjp/css-color@3.2.0':
+    resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
+
+  '@babel/code-frame@7.27.1':
+    resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
     engines: {node: '>=6.9.0'}
 
   '@babel/helper-string-parser@7.27.1':
@@ -459,13 +569,13 @@ packages:
     resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/parser@7.27.5':
-    resolution: {integrity: sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==}
+  '@babel/parser@7.28.0':
+    resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==}
     engines: {node: '>=6.0.0'}
     hasBin: true
 
-  '@babel/types@7.27.6':
-    resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==}
+  '@babel/types@7.28.0':
+    resolution: {integrity: sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==}
     engines: {node: '>=6.9.0'}
 
   '@bcoe/v8-coverage@1.0.2':
@@ -484,32 +594,32 @@ packages:
       conventional-commits-parser:
         optional: true
 
-  '@csstools/color-helpers@5.0.1':
-    resolution: {integrity: sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA==}
+  '@csstools/color-helpers@5.0.2':
+    resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==}
     engines: {node: '>=18'}
 
-  '@csstools/css-calc@2.1.1':
-    resolution: {integrity: sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag==}
+  '@csstools/css-calc@2.1.4':
+    resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==}
     engines: {node: '>=18'}
     peerDependencies:
-      '@csstools/css-parser-algorithms': ^3.0.4
-      '@csstools/css-tokenizer': ^3.0.3
+      '@csstools/css-parser-algorithms': ^3.0.5
+      '@csstools/css-tokenizer': ^3.0.4
 
-  '@csstools/css-color-parser@3.0.7':
-    resolution: {integrity: sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA==}
+  '@csstools/css-color-parser@3.0.10':
+    resolution: {integrity: sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==}
     engines: {node: '>=18'}
     peerDependencies:
-      '@csstools/css-parser-algorithms': ^3.0.4
-      '@csstools/css-tokenizer': ^3.0.3
+      '@csstools/css-parser-algorithms': ^3.0.5
+      '@csstools/css-tokenizer': ^3.0.4
 
-  '@csstools/css-parser-algorithms@3.0.4':
-    resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==}
+  '@csstools/css-parser-algorithms@3.0.5':
+    resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
     engines: {node: '>=18'}
     peerDependencies:
-      '@csstools/css-tokenizer': ^3.0.3
+      '@csstools/css-tokenizer': ^3.0.4
 
-  '@csstools/css-tokenizer@3.0.3':
-    resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==}
+  '@csstools/css-tokenizer@3.0.4':
+    resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
     engines: {node: '>=18'}
 
   '@emnapi/core@1.4.3':
@@ -809,12 +919,6 @@ packages:
     cpu: [x64]
     os: [win32]
 
-  '@eslint-community/eslint-utils@4.6.1':
-    resolution: {integrity: sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-    peerDependencies:
-      eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
-
   '@eslint-community/eslint-utils@4.7.0':
     resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -825,32 +929,36 @@ packages:
     resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
     engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
 
-  '@eslint/config-array@0.20.0':
-    resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==}
+  '@eslint/config-array@0.21.0':
+    resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@eslint/config-helpers@0.2.1':
-    resolution: {integrity: sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==}
+  '@eslint/config-helpers@0.3.0':
+    resolution: {integrity: sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
   '@eslint/core@0.14.0':
     resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
+  '@eslint/core@0.15.1':
+    resolution: {integrity: sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
   '@eslint/eslintrc@3.3.1':
     resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@eslint/js@9.27.0':
-    resolution: {integrity: sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==}
+  '@eslint/js@9.30.1':
+    resolution: {integrity: sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
   '@eslint/object-schema@2.1.6':
     resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@eslint/plugin-kit@0.3.1':
-    resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==}
+  '@eslint/plugin-kit@0.3.3':
+    resolution: {integrity: sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
   '@humanfs/core@0.19.1':
@@ -869,8 +977,8 @@ packages:
     resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==}
     engines: {node: '>=18.18'}
 
-  '@humanwhocodes/retry@0.4.2':
-    resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==}
+  '@humanwhocodes/retry@0.4.3':
+    resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
     engines: {node: '>=18.18'}
 
   '@hutson/parse-repository-url@5.0.0':
@@ -893,29 +1001,24 @@ packages:
     resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
     engines: {node: '>=8'}
 
-  '@jridgewell/gen-mapping@0.3.5':
-    resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
-    engines: {node: '>=6.0.0'}
+  '@jridgewell/gen-mapping@0.3.12':
+    resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==}
 
   '@jridgewell/resolve-uri@3.1.2':
     resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
     engines: {node: '>=6.0.0'}
 
-  '@jridgewell/set-array@1.2.1':
-    resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
-    engines: {node: '>=6.0.0'}
-
-  '@jridgewell/sourcemap-codec@1.5.0':
-    resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
+  '@jridgewell/sourcemap-codec@1.5.4':
+    resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==}
 
-  '@jridgewell/trace-mapping@0.3.25':
-    resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
+  '@jridgewell/trace-mapping@0.3.29':
+    resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
 
-  '@jspm/core@2.0.1':
-    resolution: {integrity: sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==}
+  '@jspm/core@2.1.0':
+    resolution: {integrity: sha512-3sRl+pkyFY/kLmHl0cgHiFp2xEqErA8N3ECjMs7serSUBmoJ70lBa0PG5t0IM6WJgdZNyyI0R8YFfi5wM8+mzg==}
 
-  '@napi-rs/wasm-runtime@0.2.9':
-    resolution: {integrity: sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==}
+  '@napi-rs/wasm-runtime@0.2.11':
+    resolution: {integrity: sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==}
 
   '@nodelib/fs.scandir@2.1.5':
     resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
@@ -929,86 +1032,95 @@ packages:
     resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
     engines: {node: '>= 8'}
 
-  '@parcel/watcher-android-arm64@2.4.1':
-    resolution: {integrity: sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==}
+  '@parcel/watcher-android-arm64@2.5.1':
+    resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm64]
     os: [android]
 
-  '@parcel/watcher-darwin-arm64@2.4.1':
-    resolution: {integrity: sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==}
+  '@parcel/watcher-darwin-arm64@2.5.1':
+    resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm64]
     os: [darwin]
 
-  '@parcel/watcher-darwin-x64@2.4.1':
-    resolution: {integrity: sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==}
+  '@parcel/watcher-darwin-x64@2.5.1':
+    resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==}
     engines: {node: '>= 10.0.0'}
     cpu: [x64]
     os: [darwin]
 
-  '@parcel/watcher-freebsd-x64@2.4.1':
-    resolution: {integrity: sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==}
+  '@parcel/watcher-freebsd-x64@2.5.1':
+    resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==}
     engines: {node: '>= 10.0.0'}
     cpu: [x64]
     os: [freebsd]
 
-  '@parcel/watcher-linux-arm-glibc@2.4.1':
-    resolution: {integrity: sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==}
+  '@parcel/watcher-linux-arm-glibc@2.5.1':
+    resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [arm]
+    os: [linux]
+
+  '@parcel/watcher-linux-arm-musl@2.5.1':
+    resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm]
     os: [linux]
 
-  '@parcel/watcher-linux-arm64-glibc@2.4.1':
-    resolution: {integrity: sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==}
+  '@parcel/watcher-linux-arm64-glibc@2.5.1':
+    resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm64]
     os: [linux]
 
-  '@parcel/watcher-linux-arm64-musl@2.4.1':
-    resolution: {integrity: sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==}
+  '@parcel/watcher-linux-arm64-musl@2.5.1':
+    resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm64]
     os: [linux]
 
-  '@parcel/watcher-linux-x64-glibc@2.4.1':
-    resolution: {integrity: sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==}
+  '@parcel/watcher-linux-x64-glibc@2.5.1':
+    resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
     engines: {node: '>= 10.0.0'}
     cpu: [x64]
     os: [linux]
 
-  '@parcel/watcher-linux-x64-musl@2.4.1':
-    resolution: {integrity: sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==}
+  '@parcel/watcher-linux-x64-musl@2.5.1':
+    resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
     engines: {node: '>= 10.0.0'}
     cpu: [x64]
     os: [linux]
 
-  '@parcel/watcher-win32-arm64@2.4.1':
-    resolution: {integrity: sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==}
+  '@parcel/watcher-win32-arm64@2.5.1':
+    resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm64]
     os: [win32]
 
-  '@parcel/watcher-win32-ia32@2.4.1':
-    resolution: {integrity: sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==}
+  '@parcel/watcher-win32-ia32@2.5.1':
+    resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==}
     engines: {node: '>= 10.0.0'}
     cpu: [ia32]
     os: [win32]
 
-  '@parcel/watcher-win32-x64@2.4.1':
-    resolution: {integrity: sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==}
+  '@parcel/watcher-win32-x64@2.5.1':
+    resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==}
     engines: {node: '>= 10.0.0'}
     cpu: [x64]
     os: [win32]
 
-  '@parcel/watcher@2.4.1':
-    resolution: {integrity: sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==}
+  '@parcel/watcher@2.5.1':
+    resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
     engines: {node: '>= 10.0.0'}
 
   '@pkgjs/parseargs@0.11.0':
     resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
     engines: {node: '>=14'}
 
+  '@polka/url@1.0.0-next.29':
+    resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
+
   '@puppeteer/browsers@2.10.5':
     resolution: {integrity: sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==}
     engines: {node: '>=18'}
@@ -1068,8 +1180,8 @@ packages:
       rollup:
         optional: true
 
-  '@rollup/pluginutils@5.1.0':
-    resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==}
+  '@rollup/pluginutils@5.2.0':
+    resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==}
     engines: {node: '>=14.0.0'}
     peerDependencies:
       rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
@@ -1258,8 +1370,14 @@ packages:
   '@tybys/wasm-util@0.9.0':
     resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==}
 
-  '@types/estree@1.0.7':
-    resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
+  '@types/chai@5.2.2':
+    resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==}
+
+  '@types/connect@3.4.38':
+    resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
+
+  '@types/deep-eql@4.0.2':
+    resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
 
   '@types/estree@1.0.8':
     resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
@@ -1288,138 +1406,163 @@ packages:
   '@types/trusted-types@2.0.7':
     resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
 
+  '@types/web-bluetooth@0.0.20':
+    resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
+
   '@types/yauzl@2.10.3':
     resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
 
-  '@typescript-eslint/eslint-plugin@8.32.1':
-    resolution: {integrity: sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==}
+  '@typescript-eslint/eslint-plugin@8.35.1':
+    resolution: {integrity: sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
-      '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0
+      '@typescript-eslint/parser': ^8.35.1
       eslint: ^8.57.0 || ^9.0.0
       typescript: '>=4.8.4 <5.9.0'
 
-  '@typescript-eslint/parser@8.32.1':
-    resolution: {integrity: sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==}
+  '@typescript-eslint/parser@8.35.1':
+    resolution: {integrity: sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
       eslint: ^8.57.0 || ^9.0.0
       typescript: '>=4.8.4 <5.9.0'
 
-  '@typescript-eslint/scope-manager@8.32.1':
-    resolution: {integrity: sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==}
+  '@typescript-eslint/project-service@8.35.1':
+    resolution: {integrity: sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      typescript: '>=4.8.4 <5.9.0'
+
+  '@typescript-eslint/scope-manager@8.35.1':
+    resolution: {integrity: sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@typescript-eslint/tsconfig-utils@8.35.1':
+    resolution: {integrity: sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      typescript: '>=4.8.4 <5.9.0'
 
-  '@typescript-eslint/type-utils@8.32.1':
-    resolution: {integrity: sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==}
+  '@typescript-eslint/type-utils@8.35.1':
+    resolution: {integrity: sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
       eslint: ^8.57.0 || ^9.0.0
       typescript: '>=4.8.4 <5.9.0'
 
-  '@typescript-eslint/types@8.32.1':
-    resolution: {integrity: sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==}
+  '@typescript-eslint/types@8.35.1':
+    resolution: {integrity: sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@typescript-eslint/typescript-estree@8.32.1':
-    resolution: {integrity: sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==}
+  '@typescript-eslint/typescript-estree@8.35.1':
+    resolution: {integrity: sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
       typescript: '>=4.8.4 <5.9.0'
 
-  '@typescript-eslint/utils@8.32.1':
-    resolution: {integrity: sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==}
+  '@typescript-eslint/utils@8.35.1':
+    resolution: {integrity: sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
       eslint: ^8.57.0 || ^9.0.0
       typescript: '>=4.8.4 <5.9.0'
 
-  '@typescript-eslint/visitor-keys@8.32.1':
-    resolution: {integrity: sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==}
+  '@typescript-eslint/visitor-keys@8.35.1':
+    resolution: {integrity: sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@unrs/resolver-binding-darwin-arm64@1.7.2':
-    resolution: {integrity: sha512-vxtBno4xvowwNmO/ASL0Y45TpHqmNkAaDtz4Jqb+clmcVSSl8XCG/PNFFkGsXXXS6AMjP+ja/TtNCFFa1QwLRg==}
+  '@unrs/resolver-binding-android-arm-eabi@1.10.1':
+    resolution: {integrity: sha512-zohDKXT1Ok0yhbVGff4YAg9HUs5ietG5GpvJBPFSApZnGe7uf2cd26DRhKZbn0Be6xHUZrSzP+RAgMmzyc71EA==}
+    cpu: [arm]
+    os: [android]
+
+  '@unrs/resolver-binding-android-arm64@1.10.1':
+    resolution: {integrity: sha512-tAN6k5UrTd4nicpA7s2PbjR/jagpDzAmvXFjbpTazUe5FRsFxVcBlS1F5Lzp5jtWU6bdiqRhSvd4X8rdpCffeA==}
+    cpu: [arm64]
+    os: [android]
+
+  '@unrs/resolver-binding-darwin-arm64@1.10.1':
+    resolution: {integrity: sha512-+FCsag8WkauI4dQ50XumCXdfvDCZEpMUnvZDsKMxfOisnEklpDFXc6ThY0WqybBYZbiwR5tWcFaZmI0G6b4vrg==}
     cpu: [arm64]
     os: [darwin]
 
-  '@unrs/resolver-binding-darwin-x64@1.7.2':
-    resolution: {integrity: sha512-qhVa8ozu92C23Hsmv0BF4+5Dyyd5STT1FolV4whNgbY6mj3kA0qsrGPe35zNR3wAN7eFict3s4Rc2dDTPBTuFQ==}
+  '@unrs/resolver-binding-darwin-x64@1.10.1':
+    resolution: {integrity: sha512-qYKGGm5wk71ONcXTMZ0+J11qQeOAPz3nw6VtqrBUUELRyXFyvK8cHhHsLBFR4GHnilc2pgY1HTB2TvdW9wO26Q==}
     cpu: [x64]
     os: [darwin]
 
-  '@unrs/resolver-binding-freebsd-x64@1.7.2':
-    resolution: {integrity: sha512-zKKdm2uMXqLFX6Ac7K5ElnnG5VIXbDlFWzg4WJ8CGUedJryM5A3cTgHuGMw1+P5ziV8CRhnSEgOnurTI4vpHpg==}
+  '@unrs/resolver-binding-freebsd-x64@1.10.1':
+    resolution: {integrity: sha512-hOHMAhbvIQ63gkpgeNsXcWPSyvXH7ZEyeg254hY0Lp/hX8NdW+FsUWq73g9946Pc/BrcVI/I3C1cmZ4RCX9bNw==}
     cpu: [x64]
     os: [freebsd]
 
-  '@unrs/resolver-binding-linux-arm-gnueabihf@1.7.2':
-    resolution: {integrity: sha512-8N1z1TbPnHH+iDS/42GJ0bMPLiGK+cUqOhNbMKtWJ4oFGzqSJk/zoXFzcQkgtI63qMcUI7wW1tq2usZQSb2jxw==}
+  '@unrs/resolver-binding-linux-arm-gnueabihf@1.10.1':
+    resolution: {integrity: sha512-6ds7+zzHJgTDmpe0gmFcOTvSUhG5oZukkt+cCsSb3k4Uiz2yEQB4iCRITX2hBwSW+p8gAieAfecITjgqCkswXw==}
     cpu: [arm]
     os: [linux]
 
-  '@unrs/resolver-binding-linux-arm-musleabihf@1.7.2':
-    resolution: {integrity: sha512-tjYzI9LcAXR9MYd9rO45m1s0B/6bJNuZ6jeOxo1pq1K6OBuRMMmfyvJYval3s9FPPGmrldYA3mi4gWDlWuTFGA==}
+  '@unrs/resolver-binding-linux-arm-musleabihf@1.10.1':
+    resolution: {integrity: sha512-P7A0G2/jW00diNJyFeq4W9/nxovD62Ay8CMP4UK9OymC7qO7rG1a8Upad68/bdfpIOn7KSp7Aj/6lEW3yyznAA==}
     cpu: [arm]
     os: [linux]
 
-  '@unrs/resolver-binding-linux-arm64-gnu@1.7.2':
-    resolution: {integrity: sha512-jon9M7DKRLGZ9VYSkFMflvNqu9hDtOCEnO2QAryFWgT6o6AXU8du56V7YqnaLKr6rAbZBWYsYpikF226v423QA==}
+  '@unrs/resolver-binding-linux-arm64-gnu@1.10.1':
+    resolution: {integrity: sha512-Cg6xzdkrpltcTPO4At+A79zkC7gPDQIgosJmVV8M104ImB6KZi1MrNXgDYIAfkhUYjPzjNooEDFRAwwPadS7ZA==}
     cpu: [arm64]
     os: [linux]
 
-  '@unrs/resolver-binding-linux-arm64-musl@1.7.2':
-    resolution: {integrity: sha512-c8Cg4/h+kQ63pL43wBNaVMmOjXI/X62wQmru51qjfTvI7kmCy5uHTJvK/9LrF0G8Jdx8r34d019P1DVJmhXQpA==}
+  '@unrs/resolver-binding-linux-arm64-musl@1.10.1':
+    resolution: {integrity: sha512-aNeg99bVkXa4lt+oZbjNRPC8ZpjJTKxijg/wILrJdzNyAymO2UC/HUK1UfDjt6T7U5p/mK24T3CYOi3/+YEQSA==}
     cpu: [arm64]
     os: [linux]
 
-  '@unrs/resolver-binding-linux-ppc64-gnu@1.7.2':
-    resolution: {integrity: sha512-A+lcwRFyrjeJmv3JJvhz5NbcCkLQL6Mk16kHTNm6/aGNc4FwPHPE4DR9DwuCvCnVHvF5IAd9U4VIs/VvVir5lg==}
+  '@unrs/resolver-binding-linux-ppc64-gnu@1.10.1':
+    resolution: {integrity: sha512-ylz5ojeXrkPrtnzVhpCO+YegG63/aKhkoTlY8PfMfBfLaUG8v6m6iqrL7sBUKdVBgOB4kSTUPt9efQdA/Y3Z/w==}
     cpu: [ppc64]
     os: [linux]
 
-  '@unrs/resolver-binding-linux-riscv64-gnu@1.7.2':
-    resolution: {integrity: sha512-hQQ4TJQrSQW8JlPm7tRpXN8OCNP9ez7PajJNjRD1ZTHQAy685OYqPrKjfaMw/8LiHCt8AZ74rfUVHP9vn0N69Q==}
+  '@unrs/resolver-binding-linux-riscv64-gnu@1.10.1':
+    resolution: {integrity: sha512-xcWyhmJfXXOxK7lvE4+rLwBq+on83svlc0AIypfe6x4sMJR+S4oD7n9OynaQShfj2SufPw2KJAotnsNb+4nN2g==}
     cpu: [riscv64]
     os: [linux]
 
-  '@unrs/resolver-binding-linux-riscv64-musl@1.7.2':
-    resolution: {integrity: sha512-NoAGbiqrxtY8kVooZ24i70CjLDlUFI7nDj3I9y54U94p+3kPxwd2L692YsdLa+cqQ0VoqMWoehDFp21PKRUoIQ==}
+  '@unrs/resolver-binding-linux-riscv64-musl@1.10.1':
+    resolution: {integrity: sha512-mW9JZAdOCyorgi1eLJr4gX7xS67WNG9XNPYj5P8VuttK72XNsmdw9yhOO4tDANMgiLXFiSFaiL1gEpoNtRPw/A==}
     cpu: [riscv64]
     os: [linux]
 
-  '@unrs/resolver-binding-linux-s390x-gnu@1.7.2':
-    resolution: {integrity: sha512-KaZByo8xuQZbUhhreBTW+yUnOIHUsv04P8lKjQ5otiGoSJ17ISGYArc+4vKdLEpGaLbemGzr4ZeUbYQQsLWFjA==}
+  '@unrs/resolver-binding-linux-s390x-gnu@1.10.1':
+    resolution: {integrity: sha512-NZGKhBy6xkJ0k09cWNZz4DnhBcGlhDd3W+j7EYoNvf5TSwj2K6kbmfqTWITEgkvjsMUjm1wsrc4IJaH6VtjyHQ==}
     cpu: [s390x]
     os: [linux]
 
-  '@unrs/resolver-binding-linux-x64-gnu@1.7.2':
-    resolution: {integrity: sha512-dEidzJDubxxhUCBJ/SHSMJD/9q7JkyfBMT77Px1npl4xpg9t0POLvnWywSk66BgZS/b2Hy9Y1yFaoMTFJUe9yg==}
+  '@unrs/resolver-binding-linux-x64-gnu@1.10.1':
+    resolution: {integrity: sha512-VsjgckJ0gNMw7p0d8In6uPYr+s0p16yrT2rvG4v2jUpEMYkpnfnCiALa9SWshbvlGjKQ98Q2x19agm3iFk8w8Q==}
     cpu: [x64]
     os: [linux]
 
-  '@unrs/resolver-binding-linux-x64-musl@1.7.2':
-    resolution: {integrity: sha512-RvP+Ux3wDjmnZDT4XWFfNBRVG0fMsc+yVzNFUqOflnDfZ9OYujv6nkh+GOr+watwrW4wdp6ASfG/e7bkDradsw==}
+  '@unrs/resolver-binding-linux-x64-musl@1.10.1':
+    resolution: {integrity: sha512-idMnajMeejnaFi0Mx9UTLSYFDAOTfAEP7VjXNgxKApso3Eu2Njs0p2V95nNIyFi4oQVGFmIuCkoznAXtF/Zbmw==}
     cpu: [x64]
     os: [linux]
 
-  '@unrs/resolver-binding-wasm32-wasi@1.7.2':
-    resolution: {integrity: sha512-y797JBmO9IsvXVRCKDXOxjyAE4+CcZpla2GSoBQ33TVb3ILXuFnMrbR/QQZoauBYeOFuu4w3ifWLw52sdHGz6g==}
+  '@unrs/resolver-binding-wasm32-wasi@1.10.1':
+    resolution: {integrity: sha512-7jyhjIRNFjzlr8x5pth6Oi9hv3a7ubcVYm2GBFinkBQKcFhw4nIs5BtauSNtDW1dPIGrxF0ciynCZqzxMrYMsg==}
     engines: {node: '>=14.0.0'}
     cpu: [wasm32]
 
-  '@unrs/resolver-binding-win32-arm64-msvc@1.7.2':
-    resolution: {integrity: sha512-gtYTh4/VREVSLA+gHrfbWxaMO/00y+34htY7XpioBTy56YN2eBjkPrY1ML1Zys89X3RJDKVaogzwxlM1qU7egg==}
+  '@unrs/resolver-binding-win32-arm64-msvc@1.10.1':
+    resolution: {integrity: sha512-TY79+N+Gkoo7E99K+zmsKNeiuNJYlclZJtKqsHSls8We2iGhgxtletVsiBYie93MSTDRDMI8pkBZJlIJSZPrdA==}
     cpu: [arm64]
     os: [win32]
 
-  '@unrs/resolver-binding-win32-ia32-msvc@1.7.2':
-    resolution: {integrity: sha512-Ywv20XHvHTDRQs12jd3MY8X5C8KLjDbg/jyaal/QLKx3fAShhJyD4blEANInsjxW3P7isHx1Blt56iUDDJO3jg==}
+  '@unrs/resolver-binding-win32-ia32-msvc@1.10.1':
+    resolution: {integrity: sha512-BAJN5PEPlEV+1m8+PCtFoKm3LQ1P57B4Z+0+efU0NzmCaGk7pUaOxuPgl+m3eufVeeNBKiPDltG0sSB9qEfCxw==}
     cpu: [ia32]
     os: [win32]
 
-  '@unrs/resolver-binding-win32-x64-msvc@1.7.2':
-    resolution: {integrity: sha512-friS8NEQfHaDbkThxopGk+LuE5v3iY0StruifjQEt7SLbA46OnfgMO15sOTkbpJkol6RB+1l1TYPXh0sCddpvA==}
+  '@unrs/resolver-binding-win32-x64-msvc@1.10.1':
+    resolution: {integrity: sha512-2v3erKKmmCyIVvvhI2nF15qEbdBpISTq44m9pyd5gfIJB1PN94oePTLWEd82XUbIbvKhv76xTSeUQSCOGesLeg==}
     cpu: [x64]
     os: [win32]
 
@@ -1430,17 +1573,17 @@ packages:
       vite: ^5.0.0 || ^6.0.0
       vue: ^3.2.25
 
-  '@vitest/coverage-v8@3.1.4':
-    resolution: {integrity: sha512-G4p6OtioySL+hPV7Y6JHlhpsODbJzt1ndwHAFkyk6vVjpK03PFsKnauZIzcd0PrK4zAbc5lc+jeZ+eNGiMA+iw==}
+  '@vitest/coverage-v8@3.2.4':
+    resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==}
     peerDependencies:
-      '@vitest/browser': 3.1.4
-      vitest: 3.1.4
+      '@vitest/browser': 3.2.4
+      vitest: 3.2.4
     peerDependenciesMeta:
       '@vitest/browser':
         optional: true
 
-  '@vitest/eslint-plugin@1.2.1':
-    resolution: {integrity: sha512-JQr1jdVcrsoS7Sdzn83h9sq4DvREf9Q/onTZbJCqTVlv/76qb+TZrLv/9VhjnjSMHweQH5FdpMDeCR6aDe2fgw==}
+  '@vitest/eslint-plugin@1.3.4':
+    resolution: {integrity: sha512-EOg8d0jn3BAiKnR55WkFxmxfWA3nmzrbIIuOXyTe6A72duryNgyU+bdBEauA97Aab3ho9kLmAwgPX63Ckj4QEg==}
     peerDependencies:
       eslint: '>= 8.57.0'
       typescript: '>= 5.0.0'
@@ -1451,42 +1594,85 @@ packages:
       vitest:
         optional: true
 
-  '@vitest/expect@3.1.4':
-    resolution: {integrity: sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==}
+  '@vitest/expect@3.2.4':
+    resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
 
-  '@vitest/mocker@3.1.4':
-    resolution: {integrity: sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==}
+  '@vitest/mocker@3.2.4':
+    resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==}
     peerDependencies:
       msw: ^2.4.9
-      vite: ^5.0.0 || ^6.0.0
+      vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
     peerDependenciesMeta:
       msw:
         optional: true
       vite:
         optional: true
 
-  '@vitest/pretty-format@3.1.4':
-    resolution: {integrity: sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==}
+  '@vitest/pretty-format@3.2.4':
+    resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
 
-  '@vitest/runner@3.1.4':
-    resolution: {integrity: sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==}
+  '@vitest/runner@3.2.4':
+    resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==}
+
+  '@vitest/snapshot@3.2.4':
+    resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==}
+
+  '@vitest/spy@3.2.4':
+    resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
+
+  '@vitest/ui@3.2.4':
+    resolution: {integrity: sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==}
+    peerDependencies:
+      vitest: 3.2.4
 
-  '@vitest/snapshot@3.1.4':
-    resolution: {integrity: sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==}
+  '@vitest/utils@3.2.4':
+    resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
 
-  '@vitest/spy@3.1.4':
-    resolution: {integrity: sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==}
+  '@vue/compiler-core@3.5.17':
+    resolution: {integrity: sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA==}
 
-  '@vitest/utils@3.1.4':
-    resolution: {integrity: sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==}
+  '@vue/compiler-dom@3.5.17':
+    resolution: {integrity: sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ==}
+
+  '@vue/compiler-sfc@3.5.17':
+    resolution: {integrity: sha512-rQQxbRJMgTqwRugtjw0cnyQv9cP4/4BxWfTdRBkqsTfLOHWykLzbOc3C4GGzAmdMDxhzU/1Ija5bTjMVrddqww==}
+
+  '@vue/compiler-ssr@3.5.17':
+    resolution: {integrity: sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ==}
 
   '@vue/consolidate@1.0.0':
     resolution: {integrity: sha512-oTyUE+QHIzLw2PpV14GD/c7EohDyP64xCniWTcqcEmTd699eFqTIwOmtDYjcO1j3QgdXoJEoWv1/cCdLrRoOfg==}
     engines: {node: '>= 0.12.0'}
 
+  '@vue/reactivity@3.5.17':
+    resolution: {integrity: sha512-l/rmw2STIscWi7SNJp708FK4Kofs97zc/5aEPQh4bOsReD/8ICuBcEmS7KGwDj5ODQLYWVN2lNibKJL1z5b+Lw==}
+
   '@vue/repl@4.6.1':
     resolution: {integrity: sha512-tgeEa+QXzqbFsAIbq/dCXzOJxIW2Nq1F79KXRjbKyPt1ODpCx86bDbFgNzFcBEK3In2/mjPTMpN7fSD6Ig0Qsw==}
 
+  '@vue/runtime-core@3.5.17':
+    resolution: {integrity: sha512-QQLXa20dHg1R0ri4bjKeGFKEkJA7MMBxrKo2G+gJikmumRS7PTD4BOU9FKrDQWMKowz7frJJGqBffYMgQYS96Q==}
+
+  '@vue/runtime-dom@3.5.17':
+    resolution: {integrity: sha512-8El0M60TcwZ1QMz4/os2MdlQECgGoVHPuLnQBU3m9h3gdNRW9xRmI8iLS4t/22OQlOE6aJvNNlBiCzPHur4H9g==}
+
+  '@vue/server-renderer@3.5.17':
+    resolution: {integrity: sha512-BOHhm8HalujY6lmC3DbqF6uXN/K00uWiEeF22LfEsm9Q93XeJ/plHTepGwf6tqFcF7GA5oGSSAAUock3VvzaCA==}
+    peerDependencies:
+      vue: 3.5.17
+
+  '@vue/shared@3.5.17':
+    resolution: {integrity: sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg==}
+
+  '@vueuse/core@11.3.0':
+    resolution: {integrity: sha512-7OC4Rl1f9G8IT6rUfi9JrKiXy4bfmHhZ5x2Ceojy0jnd3mHNEvV4JaRygH362ror6/NZ+Nl+n13LPzGiPN8cKA==}
+
+  '@vueuse/metadata@11.3.0':
+    resolution: {integrity: sha512-pwDnDspTqtTo2HwfLw4Rp6yywuuBdYnPYDq+mO38ZYKGebCUQC/nVj/PXSiK9HX5otxLz8Fn7ECPbjiRz2CC3g==}
+
+  '@vueuse/shared@11.3.0':
+    resolution: {integrity: sha512-P8gSSWQeucH5821ek2mn/ciCk+MS/zoRKqdQIM3bHq6p7GXDAJLmnRRKmF5F65sAVJIfzQlwR3aDzwCn10s8hA==}
+
   '@zeit/schemas@2.36.0':
     resolution: {integrity: sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==}
 
@@ -1504,8 +1690,8 @@ packages:
     engines: {node: '>=0.4.0'}
     hasBin: true
 
-  acorn@8.14.0:
-    resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==}
+  acorn@8.15.0:
+    resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
     engines: {node: '>=0.4.0'}
     hasBin: true
 
@@ -1537,8 +1723,8 @@ packages:
     resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
     engines: {node: '>=8'}
 
-  ansi-regex@6.0.1:
-    resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
+  ansi-regex@6.1.0:
+    resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
     engines: {node: '>=12'}
 
   ansi-styles@4.3.0:
@@ -1564,8 +1750,8 @@ packages:
   asap@2.0.6:
     resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
 
-  assert-never@1.3.0:
-    resolution: {integrity: sha512-9Z3vxQ+berkL/JJo0dK+EY3Lp0s3NtSnP3VCLsh5HDcZPrh0M+KQRK5sWhUeyPPH+/RCxZqOxLMR+YC6vlviEQ==}
+  assert-never@1.4.0:
+    resolution: {integrity: sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==}
 
   assertion-error@2.0.1:
     resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
@@ -1575,8 +1761,11 @@ packages:
     resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==}
     engines: {node: '>=4'}
 
-  b4a@1.6.6:
-    resolution: {integrity: sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==}
+  ast-v8-to-istanbul@0.3.3:
+    resolution: {integrity: sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==}
+
+  b4a@1.6.7:
+    resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==}
 
   babel-walk@3.0.0-canary-5:
     resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==}
@@ -1585,22 +1774,35 @@ packages:
   balanced-match@1.0.2:
     resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
 
-  bare-events@2.4.2:
-    resolution: {integrity: sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==}
+  bare-events@2.5.4:
+    resolution: {integrity: sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==}
 
-  bare-fs@4.0.1:
-    resolution: {integrity: sha512-ilQs4fm/l9eMfWY2dY0WCIUplSUp7U0CT1vrqMg1MUdeZl4fypu5UP0XcDBK5WBQPJAKP1b7XEodISmekH/CEg==}
-    engines: {bare: '>=1.7.0'}
+  bare-fs@4.1.5:
+    resolution: {integrity: sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA==}
+    engines: {bare: '>=1.16.0'}
+    peerDependencies:
+      bare-buffer: '*'
+    peerDependenciesMeta:
+      bare-buffer:
+        optional: true
 
-  bare-os@3.4.0:
-    resolution: {integrity: sha512-9Ous7UlnKbe3fMi7Y+qh0DwAup6A1JkYgPnjvMDNOlmnxNRQvQ/7Nst+OnUQKzk0iAT0m9BisbDVp9gCv8+ETA==}
-    engines: {bare: '>=1.6.0'}
+  bare-os@3.6.1:
+    resolution: {integrity: sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==}
+    engines: {bare: '>=1.14.0'}
 
   bare-path@3.0.0:
     resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==}
 
-  bare-stream@2.1.3:
-    resolution: {integrity: sha512-tiDAH9H/kP+tvNO5sczyn9ZAA7utrSMobyDchsnyyXBuUe2FSQWbxhtuHB8jwpHYYevVo2UJpcmvvjrbHboUUQ==}
+  bare-stream@2.6.5:
+    resolution: {integrity: sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==}
+    peerDependencies:
+      bare-buffer: '*'
+      bare-events: '*'
+    peerDependenciesMeta:
+      bare-buffer:
+        optional: true
+      bare-events:
+        optional: true
 
   basic-ftp@5.0.5:
     resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==}
@@ -1610,11 +1812,11 @@ packages:
     resolution: {integrity: sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==}
     engines: {node: '>=14.16'}
 
-  brace-expansion@1.1.11:
-    resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
+  brace-expansion@1.1.12:
+    resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
 
-  brace-expansion@2.0.1:
-    resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
+  brace-expansion@2.0.2:
+    resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
 
   braces@3.0.3:
     resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
@@ -1623,6 +1825,10 @@ packages:
   buffer-crc32@0.2.13:
     resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
 
+  bundle-name@4.1.0:
+    resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
+    engines: {node: '>=18'}
+
   bytes@3.0.0:
     resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==}
     engines: {node: '>= 0.8'}
@@ -1631,8 +1837,12 @@ packages:
     resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
     engines: {node: '>=8'}
 
-  call-bind@1.0.7:
-    resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==}
+  call-bind-apply-helpers@1.0.2:
+    resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+    engines: {node: '>= 0.4'}
+
+  call-bound@1.0.4:
+    resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
     engines: {node: '>= 0.4'}
 
   callsites@3.1.0:
@@ -1670,8 +1880,8 @@ packages:
     resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
     engines: {node: '>= 16'}
 
-  chokidar@4.0.1:
-    resolution: {integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==}
+  chokidar@4.0.3:
+    resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
     engines: {node: '>= 14.16.0'}
 
   chromium-bidi@5.1.0:
@@ -1709,9 +1919,9 @@ packages:
   colorette@2.0.20:
     resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
 
-  commander@13.1.0:
-    resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
-    engines: {node: '>=18'}
+  commander@14.0.0:
+    resolution: {integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==}
+    engines: {node: '>=20'}
 
   comment-parser@1.4.1:
     resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==}
@@ -1734,6 +1944,10 @@ packages:
   concat-map@0.0.1:
     resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
 
+  connect@3.7.0:
+    resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==}
+    engines: {node: '>= 0.10.0'}
+
   constantinople@4.0.1:
     resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==}
 
@@ -1790,8 +2004,8 @@ packages:
     resolution: {integrity: sha512-SetDSntXLk8Jh1NOAl1Gu5uLiCNSYenB5tm0YVeZKePRIgDW9lQImromTwLa3c/Gae298tsgOM+/CYT9XAl0NA==}
     engines: {node: '>=18'}
 
-  conventional-changelog-writer@8.0.0:
-    resolution: {integrity: sha512-TQcoYGRatlAnT2qEWDON/XSfnVG38JzA7E0wcGScu7RElQBkg9WWgZd1peCWFcWDh1xfb2CfsrcvOn1bbSzztA==}
+  conventional-changelog-writer@8.1.0:
+    resolution: {integrity: sha512-dpC440QnORNCO81XYuRRFOLCsjKj4W7tMkUIn3lR6F/FAaJcWLi7iCj6IcEvSQY2zw6VUgwUKd5DEHKEWrpmEQ==}
     engines: {node: '>=18'}
     hasBin: true
 
@@ -1803,8 +2017,8 @@ packages:
     resolution: {integrity: sha512-tQMagCOC59EVgNZcC5zl7XqO30Wki9i9J3acbUvkaosCT6JX3EeFwJD7Qqp4MCikRnzS18WXV3BLIQ66ytu6+Q==}
     engines: {node: '>=18'}
 
-  conventional-commits-parser@6.0.0:
-    resolution: {integrity: sha512-TbsINLp48XeMXR8EvGjTnKGsZqBemisPoyWESlpRyR8lif0lcwzqz+NMtYSj1ooF/WYjSuu7wX0CtdeeMEQAmA==}
+  conventional-commits-parser@6.2.0:
+    resolution: {integrity: sha512-uLnoLeIW4XaoFtH37qEcg/SXMJmKF4vi7V0H2rnPueg+VEtFGA/asSCNTcq4M/GQ6QmlzchAEtOoDTtKqWeHag==}
     engines: {node: '>=18'}
     hasBin: true
 
@@ -1829,8 +2043,8 @@ packages:
     engines: {node: '>=4'}
     hasBin: true
 
-  cssstyle@4.2.1:
-    resolution: {integrity: sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==}
+  cssstyle@4.6.0:
+    resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==}
     engines: {node: '>=18'}
 
   csstype@3.1.3:
@@ -1852,23 +2066,6 @@ packages:
       supports-color:
         optional: true
 
-  debug@3.2.7:
-    resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
-    peerDependencies:
-      supports-color: '*'
-    peerDependenciesMeta:
-      supports-color:
-        optional: true
-
-  debug@4.4.0:
-    resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
-    engines: {node: '>=6.0'}
-    peerDependencies:
-      supports-color: '*'
-    peerDependenciesMeta:
-      supports-color:
-        optional: true
-
   debug@4.4.1:
     resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
     engines: {node: '>=6.0'}
@@ -1896,9 +2093,17 @@ packages:
     resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
     engines: {node: '>=0.10.0'}
 
-  define-data-property@1.1.4:
-    resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
-    engines: {node: '>= 0.4'}
+  default-browser-id@5.0.0:
+    resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==}
+    engines: {node: '>=18'}
+
+  default-browser@5.2.1:
+    resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==}
+    engines: {node: '>=18'}
+
+  define-lazy-prop@3.0.0:
+    resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
+    engines: {node: '>=12'}
 
   degenerator@5.0.1:
     resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==}
@@ -1919,11 +2124,18 @@ packages:
     resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==}
     engines: {node: '>=8'}
 
+  dunder-proto@1.0.1:
+    resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+    engines: {node: '>= 0.4'}
+
   eastasianwidth@0.2.0:
     resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
 
-  emoji-regex@10.3.0:
-    resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==}
+  ee-first@1.1.1:
+    resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
+
+  emoji-regex@10.4.0:
+    resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==}
 
   emoji-regex@8.0.0:
     resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -1931,8 +2143,12 @@ packages:
   emoji-regex@9.2.2:
     resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
 
-  end-of-stream@1.4.4:
-    resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
+  encodeurl@1.0.2:
+    resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
+    engines: {node: '>= 0.8'}
+
+  end-of-stream@1.4.5:
+    resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
 
   enquirer@2.4.1:
     resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==}
@@ -1942,6 +2158,10 @@ packages:
     resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
     engines: {node: '>=0.12'}
 
+  entities@6.0.1:
+    resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
+    engines: {node: '>=0.12'}
+
   env-paths@2.2.1:
     resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
     engines: {node: '>=6'}
@@ -1953,20 +2173,24 @@ packages:
   error-ex@1.3.2:
     resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
 
-  es-define-property@1.0.0:
-    resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==}
+  error-stack-parser-es@0.1.5:
+    resolution: {integrity: sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg==}
+
+  es-define-property@1.0.1:
+    resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
     engines: {node: '>= 0.4'}
 
   es-errors@1.3.0:
     resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
     engines: {node: '>= 0.4'}
 
-  es-module-lexer@1.6.0:
-    resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==}
-
   es-module-lexer@1.7.0:
     resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
 
+  es-object-atoms@1.1.1:
+    resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
+    engines: {node: '>= 0.4'}
+
   esbuild-plugin-polyfill-node@0.3.0:
     resolution: {integrity: sha512-SHG6CKUfWfYyYXGpW143NEZtcVVn8S/WHcEOxk62LuDXnY4Zpmc+WmxJKN6GMTgTClXJXhEM5KQlxKY6YjbucQ==}
     peerDependencies:
@@ -1982,10 +2206,13 @@ packages:
     engines: {node: '>=18'}
     hasBin: true
 
-  escalade@3.1.2:
-    resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==}
+  escalade@3.2.0:
+    resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
     engines: {node: '>=6'}
 
+  escape-html@1.0.3:
+    resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
+
   escape-string-regexp@4.0.0:
     resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
     engines: {node: '>=10'}
@@ -1995,8 +2222,8 @@ packages:
     engines: {node: '>=6.0'}
     hasBin: true
 
-  eslint-import-context@0.1.4:
-    resolution: {integrity: sha512-x3+etvB5TPxjFIq2m4tTnpt/9Ekp5GZKzXNp5ExLaS7Qv9E5BVs/Td7jxSnRtSzrgTCExXZlc0MuOdSuDLURiQ==}
+  eslint-import-context@0.1.9:
+    resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==}
     engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
     peerDependencies:
       unrs-resolver: ^1.0.0
@@ -2004,29 +2231,33 @@ packages:
       unrs-resolver:
         optional: true
 
-  eslint-import-resolver-node@0.3.9:
-    resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
-
-  eslint-plugin-import-x@4.13.1:
-    resolution: {integrity: sha512-Ua4HZBmG5TNr18q3Is+nT6mKCzNNpycqtv/+TkIK7E3w4LBlPlZI6vLwmDjXdIZtJPP2Z1Oh5+wksWwlcCjMpA==}
+  eslint-plugin-import-x@4.16.1:
+    resolution: {integrity: sha512-vPZZsiOKaBAIATpFE2uMI4w5IRwdv/FpQ+qZZMR4E+PeOcM4OeoEbqxRMnywdxP19TyB/3h6QBB0EWon7letSQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
+      '@typescript-eslint/utils': ^8.0.0
       eslint: ^8.57.0 || ^9.0.0
+      eslint-import-resolver-node: '*'
+    peerDependenciesMeta:
+      '@typescript-eslint/utils':
+        optional: true
+      eslint-import-resolver-node:
+        optional: true
 
-  eslint-scope@8.3.0:
-    resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==}
+  eslint-scope@8.4.0:
+    resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
   eslint-visitor-keys@3.4.3:
     resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
 
-  eslint-visitor-keys@4.2.0:
-    resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
+  eslint-visitor-keys@4.2.1:
+    resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  eslint@9.27.0:
-    resolution: {integrity: sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==}
+  eslint@9.30.1:
+    resolution: {integrity: sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     hasBin: true
     peerDependencies:
@@ -2035,8 +2266,8 @@ packages:
       jiti:
         optional: true
 
-  espree@10.3.0:
-    resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==}
+  espree@10.4.0:
+    resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
   esprima@4.0.1:
@@ -2104,14 +2335,17 @@ packages:
   fd-slicer@1.1.0:
     resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
 
-  fdir@6.4.4:
-    resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==}
+  fdir@6.4.6:
+    resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==}
     peerDependencies:
       picomatch: ^3 || ^4
     peerDependenciesMeta:
       picomatch:
         optional: true
 
+  fflate@0.8.2:
+    resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
+
   file-entry-cache@8.0.0:
     resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
     engines: {node: '>=16.0.0'}
@@ -2123,8 +2357,12 @@ packages:
     resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
     engines: {node: '>=8'}
 
-  find-up-simple@1.0.0:
-    resolution: {integrity: sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==}
+  finalhandler@1.1.2:
+    resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==}
+    engines: {node: '>= 0.8'}
+
+  find-up-simple@1.0.1:
+    resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==}
     engines: {node: '>=18'}
 
   find-up@5.0.0:
@@ -2135,15 +2373,15 @@ packages:
     resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
     engines: {node: '>=16'}
 
-  flatted@3.3.1:
-    resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
+  flatted@3.3.3:
+    resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
 
-  foreground-child@3.3.0:
-    resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
+  foreground-child@3.3.1:
+    resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
     engines: {node: '>=14'}
 
-  fs-extra@11.2.0:
-    resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==}
+  fs-extra@11.3.0:
+    resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==}
     engines: {node: '>=14.14'}
 
   fsevents@2.3.3:
@@ -2161,12 +2399,16 @@ packages:
     resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
     engines: {node: 6.* || 8.* || >= 10.*}
 
-  get-east-asian-width@1.2.0:
-    resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==}
+  get-east-asian-width@1.3.0:
+    resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
     engines: {node: '>=18'}
 
-  get-intrinsic@1.2.4:
-    resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
+  get-intrinsic@1.3.0:
+    resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+    engines: {node: '>= 0.4'}
+
+  get-proto@1.0.1:
+    resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
     engines: {node: '>= 0.4'}
 
   get-stream@5.2.0:
@@ -2177,14 +2419,11 @@ packages:
     resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
     engines: {node: '>=10'}
 
-  get-tsconfig@4.10.0:
-    resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==}
-
   get-tsconfig@4.10.1:
     resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
 
-  get-uri@6.0.3:
-    resolution: {integrity: sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==}
+  get-uri@6.0.4:
+    resolution: {integrity: sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==}
     engines: {node: '>= 14'}
 
   git-raw-commits@5.0.0:
@@ -2209,8 +2448,8 @@ packages:
     resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
     hasBin: true
 
-  glob@11.0.0:
-    resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==}
+  glob@11.0.3:
+    resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==}
     engines: {node: 20 || >=22}
     hasBin: true
 
@@ -2218,8 +2457,9 @@ packages:
     resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
     engines: {node: '>=18'}
 
-  gopd@1.0.1:
-    resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
+  gopd@1.2.0:
+    resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+    engines: {node: '>= 0.4'}
 
   graceful-fs@4.2.11:
     resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
@@ -2236,15 +2476,8 @@ packages:
     resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
     engines: {node: '>=8'}
 
-  has-property-descriptors@1.0.2:
-    resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
-
-  has-proto@1.0.3:
-    resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==}
-    engines: {node: '>= 0.4'}
-
-  has-symbols@1.0.3:
-    resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
+  has-symbols@1.1.0:
+    resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
     engines: {node: '>= 0.4'}
 
   has-tostringtag@1.0.2:
@@ -2295,18 +2528,18 @@ packages:
     resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
     engines: {node: '>= 4'}
 
-  ignore@7.0.4:
-    resolution: {integrity: sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==}
+  ignore@7.0.5:
+    resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
     engines: {node: '>= 4'}
 
   immediate@3.0.6:
     resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
 
-  immutable@5.0.2:
-    resolution: {integrity: sha512-1NU7hWZDkV7hJ4PJ9dur9gTNQ4ePNPN4k9/0YhwjzykTi/+3Q5pF93YU5QoVj8BuOnhLgaY8gs0U2pj4kSYVcw==}
+  immutable@5.1.3:
+    resolution: {integrity: sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==}
 
-  import-fresh@3.3.0:
-    resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
+  import-fresh@3.3.1:
+    resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
     engines: {node: '>=6'}
 
   import-meta-resolve@3.1.1:
@@ -2316,8 +2549,8 @@ packages:
     resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
     engines: {node: '>=0.8.19'}
 
-  index-to-position@0.1.2:
-    resolution: {integrity: sha512-MWDKS3AS1bGCHLBA2VLImJz42f7bJh8wQsTGCzI3j519/CASStoDONUBVz2I/VID0MpiX3SGSnbOD2xUalbE5g==}
+  index-to-position@1.1.0:
+    resolution: {integrity: sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==}
     engines: {node: '>=18'}
 
   inherits@2.0.4:
@@ -2333,8 +2566,8 @@ packages:
   is-arrayish@0.2.1:
     resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
 
-  is-core-module@2.15.0:
-    resolution: {integrity: sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==}
+  is-core-module@2.16.1:
+    resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
     engines: {node: '>= 0.4'}
 
   is-docker@2.2.1:
@@ -2342,6 +2575,11 @@ packages:
     engines: {node: '>=8'}
     hasBin: true
 
+  is-docker@3.0.0:
+    resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+    hasBin: true
+
   is-expression@4.0.0:
     resolution: {integrity: sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==}
 
@@ -2365,6 +2603,11 @@ packages:
     resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
     engines: {node: '>=0.10.0'}
 
+  is-inside-container@1.0.0:
+    resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
+    engines: {node: '>=14.16'}
+    hasBin: true
+
   is-module@1.0.0:
     resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==}
 
@@ -2389,8 +2632,8 @@ packages:
   is-reference@1.2.1:
     resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==}
 
-  is-regex@1.1.4:
-    resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==}
+  is-regex@1.2.1:
+    resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
     engines: {node: '>= 0.4'}
 
   is-stream@2.0.1:
@@ -2401,6 +2644,10 @@ packages:
     resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
     engines: {node: '>=8'}
 
+  is-wsl@3.1.0:
+    resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
+    engines: {node: '>=16'}
+
   isarray@1.0.0:
     resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
 
@@ -2430,8 +2677,8 @@ packages:
   jackspeak@3.4.3:
     resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
 
-  jackspeak@4.0.1:
-    resolution: {integrity: sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==}
+  jackspeak@4.1.1:
+    resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
     engines: {node: 20 || >=22}
 
   js-stringify@1.0.2:
@@ -2440,6 +2687,9 @@ packages:
   js-tokens@4.0.0:
     resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
 
+  js-tokens@9.0.1:
+    resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
+
   js-yaml@4.1.0:
     resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
     hasBin: true
@@ -2501,9 +2751,9 @@ packages:
   lines-and-columns@1.2.4:
     resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
 
-  lint-staged@16.0.0:
-    resolution: {integrity: sha512-sUCprePs6/rbx4vKC60Hez6X10HPkpDJaGcy3D1NdwR7g1RcNkWL8q9mJMreOqmHBTs+1sNFp+wOiX9fr+hoOQ==}
-    engines: {node: '>=20.18'}
+  lint-staged@16.1.2:
+    resolution: {integrity: sha512-sQKw2Si2g9KUZNY3XNvRuDq4UJqpHwF0/FQzZR2M7I5MvtpWvibikCjUVJzZdGE0ByurEl3KQNvsGetd1ty1/Q==}
+    engines: {node: '>=20.17'}
     hasBin: true
 
   listr2@8.3.3:
@@ -2531,8 +2781,8 @@ packages:
     resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
     engines: {node: '>=18'}
 
-  loupe@3.1.3:
-    resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==}
+  loupe@3.1.4:
+    resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==}
 
   lru-cache@10.1.0:
     resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==}
@@ -2541,12 +2791,8 @@ packages:
   lru-cache@10.4.3:
     resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
 
-  lru-cache@11.0.0:
-    resolution: {integrity: sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==}
-    engines: {node: 20 || >=22}
-
-  lru-cache@11.0.2:
-    resolution: {integrity: sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==}
+  lru-cache@11.1.0:
+    resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==}
     engines: {node: 20 || >=22}
 
   lru-cache@7.18.3:
@@ -2571,6 +2817,10 @@ packages:
     engines: {node: '>= 18'}
     hasBin: true
 
+  math-intrinsics@1.1.0:
+    resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+    engines: {node: '>= 0.4'}
+
   memorystream@0.3.1:
     resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
     engines: {node: '>= 0.10.0'}
@@ -2601,8 +2851,8 @@ packages:
     resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
     engines: {node: '>= 0.6'}
 
-  mime-db@1.53.0:
-    resolution: {integrity: sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==}
+  mime-db@1.54.0:
+    resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
     engines: {node: '>= 0.6'}
 
   mime-types@2.1.18:
@@ -2621,10 +2871,6 @@ packages:
     resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
     engines: {node: '>=18'}
 
-  minimatch@10.0.1:
-    resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==}
-    engines: {node: 20 || >=22}
-
   minimatch@10.0.3:
     resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==}
     engines: {node: 20 || >=22}
@@ -2649,6 +2895,10 @@ packages:
   monaco-editor@0.52.2:
     resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
 
+  mrmime@2.0.1:
+    resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
+    engines: {node: '>=10'}
+
   ms@2.0.0:
     resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
 
@@ -2664,8 +2914,8 @@ packages:
     engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
     hasBin: true
 
-  napi-postinstall@0.2.3:
-    resolution: {integrity: sha512-Mi7JISo/4Ij2tDZ2xBE2WH+/KvVlkhA6juEjpEeRAVPNCpN3nxJo/5FhDNKgBcdmcmhaH6JjgST4xY/23ZYK0w==}
+  napi-postinstall@0.3.0:
+    resolution: {integrity: sha512-M7NqKyhODKV1gRLdkwE7pDsZP2/SC2a2vHkOYh9MCpKMbWVfyVfUw5MaH83Fv6XMjxr5jryUp3IDDL9rlxsTeA==}
     engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
     hasBin: true
 
@@ -2703,13 +2953,17 @@ packages:
     resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
     engines: {node: '>=8'}
 
-  nwsapi@2.2.16:
-    resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==}
+  nwsapi@2.2.20:
+    resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==}
 
   object-assign@4.1.1:
     resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
     engines: {node: '>=0.10.0'}
 
+  on-finished@2.3.0:
+    resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==}
+    engines: {node: '>= 0.8'}
+
   on-headers@1.0.2:
     resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==}
     engines: {node: '>= 0.8'}
@@ -2725,6 +2979,10 @@ packages:
     resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
     engines: {node: '>=18'}
 
+  open@10.1.2:
+    resolution: {integrity: sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==}
+    engines: {node: '>=18'}
+
   optionator@0.9.4:
     resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
     engines: {node: '>= 0.8.0'}
@@ -2737,16 +2995,16 @@ packages:
     resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
     engines: {node: '>=10'}
 
-  pac-proxy-agent@7.1.0:
-    resolution: {integrity: sha512-Z5FnLVVZSnX7WjBg0mhDtydeRZ1xMcATZThjySQUHqr+0ksP8kqaw23fNKkaaN/Z8gwLUs/W7xdl0I75eP2Xyw==}
+  pac-proxy-agent@7.2.0:
+    resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==}
     engines: {node: '>= 14'}
 
   pac-resolver@7.0.1:
     resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==}
     engines: {node: '>= 14'}
 
-  package-json-from-dist@1.0.0:
-    resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==}
+  package-json-from-dist@1.0.1:
+    resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
 
   pako@1.0.11:
     resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
@@ -2759,12 +3017,16 @@ packages:
     resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
     engines: {node: '>=8'}
 
-  parse-json@8.1.0:
-    resolution: {integrity: sha512-rum1bPifK5SSar35Z6EKZuYPJx85pkNaFrxBK3mwdfSJ1/WKbYrjoW/zTPSjRRamfmVX1ACBIdFAO0VRErW/EA==}
+  parse-json@8.3.0:
+    resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==}
     engines: {node: '>=18'}
 
-  parse5@7.2.1:
-    resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==}
+  parse5@7.3.0:
+    resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
+
+  parseurl@1.3.3:
+    resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
+    engines: {node: '>= 0.8'}
 
   path-exists@4.0.0:
     resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
@@ -2791,16 +3053,22 @@ packages:
   path-to-regexp@3.3.0:
     resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==}
 
+  pathe@1.1.2:
+    resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
+
   pathe@2.0.3:
     resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
 
-  pathval@2.0.0:
-    resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==}
+  pathval@2.0.1:
+    resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
     engines: {node: '>= 14.16'}
 
   pend@1.2.0:
     resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
 
+  perfect-debounce@1.0.0:
+    resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
+
   picocolors@1.1.1:
     resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
 
@@ -2823,14 +3091,14 @@ packages:
     peerDependencies:
       postcss: ^8.1.0
 
-  postcss-modules-local-by-default@4.0.5:
-    resolution: {integrity: sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==}
+  postcss-modules-local-by-default@4.2.0:
+    resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==}
     engines: {node: ^10 || ^12 || >= 14}
     peerDependencies:
       postcss: ^8.1.0
 
-  postcss-modules-scope@3.2.0:
-    resolution: {integrity: sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==}
+  postcss-modules-scope@3.2.1:
+    resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==}
     engines: {node: ^10 || ^12 || >= 14}
     peerDependencies:
       postcss: ^8.1.0
@@ -2846,10 +3114,6 @@ packages:
     peerDependencies:
       postcss: ^8.0.0
 
-  postcss-selector-parser@6.1.2:
-    resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
-    engines: {node: '>=4'}
-
   postcss-selector-parser@7.1.0:
     resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==}
     engines: {node: '>=4'}
@@ -2865,8 +3129,8 @@ packages:
     resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
     engines: {node: '>= 0.8.0'}
 
-  prettier@3.5.3:
-    resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==}
+  prettier@3.6.2:
+    resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
     engines: {node: '>=14'}
     hasBin: true
 
@@ -2927,8 +3191,8 @@ packages:
   pug@3.0.3:
     resolution: {integrity: sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==}
 
-  pump@3.0.0:
-    resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
+  pump@3.0.3:
+    resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
 
   punycode@2.3.1:
     resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
@@ -2946,9 +3210,6 @@ packages:
   queue-microtask@1.2.3:
     resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
 
-  queue-tick@1.0.1:
-    resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==}
-
   range-parser@1.2.0:
     resolution: {integrity: sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==}
     engines: {node: '>= 0.6'}
@@ -2972,9 +3233,9 @@ packages:
   readable-stream@2.3.8:
     resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
 
-  readdirp@4.0.1:
-    resolution: {integrity: sha512-GkMg9uOTpIWWKbSsgwb5fA4EavTR+SG/PMPoAY8hkhHfEEY0/vqljY+XHqtDf2cr2IJtoNRDbrrEpZUiZCkYRw==}
-    engines: {node: '>= 14.16.0'}
+  readdirp@4.1.2:
+    resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
+    engines: {node: '>= 14.18.0'}
 
   registry-auth-token@3.3.2:
     resolution: {integrity: sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==}
@@ -2998,8 +3259,9 @@ packages:
   resolve-pkg-maps@1.0.0:
     resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
 
-  resolve@1.22.8:
-    resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
+  resolve@1.22.10:
+    resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
+    engines: {node: '>= 0.4'}
     hasBin: true
 
   restore-cursor@5.1.0:
@@ -3045,6 +3307,10 @@ packages:
   rrweb-cssom@0.8.0:
     resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
 
+  run-applescript@7.0.0:
+    resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==}
+    engines: {node: '>=18'}
+
   run-parallel@1.2.0:
     resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
 
@@ -3079,10 +3345,6 @@ packages:
     engines: {node: '>= 14'}
     hasBin: true
 
-  set-function-length@1.2.2:
-    resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
-    engines: {node: '>= 0.4'}
-
   setimmediate@1.0.5:
     resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
 
@@ -3094,8 +3356,9 @@ packages:
     resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
     engines: {node: '>=8'}
 
-  shell-quote@1.8.1:
-    resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==}
+  shell-quote@1.8.3:
+    resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
+    engines: {node: '>= 0.4'}
 
   siginfo@2.0.0:
     resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
@@ -3111,6 +3374,14 @@ packages:
     resolution: {integrity: sha512-N+goiLxlkHJlyaYEglFypzVNMaNplPAk5syu0+OPp/Bk6dwVoXF6FfOw2vO0Dp+JHsBaI+w6cm8TnFl2Hw6tDA==}
     hasBin: true
 
+  sirv@2.0.4:
+    resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
+    engines: {node: '>= 10'}
+
+  sirv@3.0.1:
+    resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==}
+    engines: {node: '>=18'}
+
   slice-ansi@5.0.0:
     resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==}
     engines: {node: '>=12'}
@@ -3127,8 +3398,8 @@ packages:
     resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==}
     engines: {node: '>= 14'}
 
-  socks@2.8.3:
-    resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==}
+  socks@2.8.5:
+    resolution: {integrity: sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==}
     engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
 
   source-map-js@1.2.1:
@@ -3148,23 +3419,28 @@ packages:
   spdx-expression-parse@3.0.1:
     resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==}
 
-  spdx-license-ids@3.0.18:
-    resolution: {integrity: sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==}
+  spdx-license-ids@3.0.21:
+    resolution: {integrity: sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==}
 
   sprintf-js@1.1.3:
     resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
 
-  stable-hash@0.0.5:
-    resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
+  stable-hash-x@0.2.0:
+    resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==}
+    engines: {node: '>=12.0.0'}
 
   stackback@0.0.2:
     resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
 
+  statuses@1.5.0:
+    resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
+    engines: {node: '>= 0.6'}
+
   std-env@3.9.0:
     resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==}
 
-  streamx@2.18.0:
-    resolution: {integrity: sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==}
+  streamx@2.22.1:
+    resolution: {integrity: sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==}
 
   string-argv@0.3.2:
     resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
@@ -3208,6 +3484,9 @@ packages:
     resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
     engines: {node: '>=8'}
 
+  strip-literal@3.0.0:
+    resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==}
+
   supports-color@7.2.0:
     resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
     engines: {node: '>=8'}
@@ -3219,8 +3498,8 @@ packages:
   symbol-tree@3.2.4:
     resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
 
-  tar-fs@3.0.8:
-    resolution: {integrity: sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==}
+  tar-fs@3.1.0:
+    resolution: {integrity: sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==}
 
   tar-stream@3.1.7:
     resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}
@@ -3237,8 +3516,8 @@ packages:
     resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
     engines: {node: '>=18'}
 
-  text-decoder@1.1.1:
-    resolution: {integrity: sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==}
+  text-decoder@1.2.3:
+    resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==}
 
   tinybench@2.9.0:
     resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@@ -3246,20 +3525,20 @@ packages:
   tinyexec@0.3.2:
     resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
 
-  tinyglobby@0.2.13:
-    resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==}
+  tinyglobby@0.2.14:
+    resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
     engines: {node: '>=12.0.0'}
 
-  tinypool@1.0.2:
-    resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==}
+  tinypool@1.1.1:
+    resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
     engines: {node: ^18.0.0 || >=20.0.0}
 
   tinyrainbow@2.0.0:
     resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
     engines: {node: '>=14.0.0'}
 
-  tinyspy@3.0.2:
-    resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
+  tinyspy@4.0.3:
+    resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==}
     engines: {node: '>=14.0.0'}
 
   tldts-core@6.1.86:
@@ -3280,6 +3559,10 @@ packages:
   token-stream@1.0.0:
     resolution: {integrity: sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==}
 
+  totalist@3.0.1:
+    resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
+    engines: {node: '>=6'}
+
   tough-cookie@5.1.2:
     resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
     engines: {node: '>=16'}
@@ -3305,15 +3588,15 @@ packages:
     resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
     engines: {node: '>=12.20'}
 
-  type-fest@4.24.0:
-    resolution: {integrity: sha512-spAaHzc6qre0TlZQQ2aA/nGMe+2Z/wyGk5Z+Ru2VUfdNwT6kWO6TjevOlpebsATEG1EIQ2sOiDszud3lO5mt/Q==}
+  type-fest@4.41.0:
+    resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
     engines: {node: '>=16'}
 
   typed-query-selector@2.12.0:
     resolution: {integrity: sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==}
 
-  typescript-eslint@8.32.1:
-    resolution: {integrity: sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==}
+  typescript-eslint@8.35.1:
+    resolution: {integrity: sha512-xslJjFzhOmHYQzSB/QTeASAHbjmxOGEP6Coh93TXmUBFQoJ1VU35UHIDmG06Jd6taf3wqqC1ntBnCMeymy5Ovw==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
       eslint: ^8.57.0 || ^9.0.0
@@ -3324,8 +3607,8 @@ packages:
     engines: {node: '>=14.17'}
     hasBin: true
 
-  uglify-js@3.19.1:
-    resolution: {integrity: sha512-y/2wiW+ceTYR2TSSptAhfnEtpLaQ4Ups5zrjB2d3kuVxHj16j/QJwPl5PvuGy9uARb39J0+iKxcRPvtpsx4A4A==}
+  uglify-js@3.19.3:
+    resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
     engines: {node: '>=0.8.0'}
     hasBin: true
 
@@ -3340,12 +3623,16 @@ packages:
     resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
     engines: {node: '>= 10.0.0'}
 
+  unpipe@1.0.0:
+    resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
+    engines: {node: '>= 0.8'}
+
   unplugin-utils@0.2.4:
     resolution: {integrity: sha512-8U/MtpkPkkk3Atewj1+RcKIjb5WBimZ/WSLhhR3w6SsIj8XJuKTacSP8g+2JhfSGw0Cb125Y+2zA/IzJZDVbhA==}
     engines: {node: '>=18.12.0'}
 
-  unrs-resolver@1.7.2:
-    resolution: {integrity: sha512-BBKpaylOW8KbHsu378Zky/dGh4ckT/4NW/0SHRABdqRLcQJ2dAOjDo9g97p04sWflm0kqPqpUatxReNV/dqI5A==}
+  unrs-resolver@1.10.1:
+    resolution: {integrity: sha512-EFrL7Hw4kmhZdwWO3dwwFJo6hO3FXuQ6Bg8BK/faHZ9m1YxqBS31BNSTxklIQkxK/4LlV8zTYnPsIRLBzTzjCA==}
 
   update-check@1.5.4:
     resolution: {integrity: sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==}
@@ -3356,6 +3643,10 @@ packages:
   util-deprecate@1.0.2:
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
 
+  utils-merge@1.0.1:
+    resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
+    engines: {node: '>= 0.4.0'}
+
   validate-npm-package-license@3.0.4:
     resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
 
@@ -3363,13 +3654,34 @@ packages:
     resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
     engines: {node: '>= 0.8'}
 
-  vite-node@3.1.4:
-    resolution: {integrity: sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==}
+  vite-hyper-config@0.4.1:
+    resolution: {integrity: sha512-w9D4g0+5Km8XCgkBY/BZrXZAl8FF2q1UpDXT/Fsm6VLEU5tkkzDCko8fjLPOaSbvirUJgbY5OsD5wuuZ6581Fg==}
+    engines: {node: '>=18.0.0'}
+    peerDependencies:
+      vite: ^4.0.0 || ^5.0.0 || ^6.0.0
+
+  vite-node@2.1.9:
+    resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==}
+    engines: {node: ^18.0.0 || >=20.0.0}
+    hasBin: true
+
+  vite-node@3.2.4:
+    resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
     engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
     hasBin: true
 
-  vite@5.4.15:
-    resolution: {integrity: sha512-6ANcZRivqL/4WtwPGTKNaosuNJr5tWiftOC7liM7G9+rMb8+oeJeyzymDu4rTN93seySBmbjSfsS3Vzr19KNtA==}
+  vite-plugin-inspect@0.8.9:
+    resolution: {integrity: sha512-22/8qn+LYonzibb1VeFZmISdVao5kC22jmEKm24vfFE8siEn47EpVcCLYMv6iKOYMJfjSvSJfueOwcFCkUnV3A==}
+    engines: {node: '>=14'}
+    peerDependencies:
+      '@nuxt/kit': '*'
+      vite: ^3.1.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.1
+    peerDependenciesMeta:
+      '@nuxt/kit':
+        optional: true
+
+  vite@5.4.19:
+    resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==}
     engines: {node: ^18.0.0 || >=20.0.0}
     hasBin: true
     peerDependencies:
@@ -3399,22 +3711,27 @@ packages:
       terser:
         optional: true
 
-  vite@5.4.19:
-    resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==}
-    engines: {node: ^18.0.0 || >=20.0.0}
+  vite@6.3.5:
+    resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==}
+    engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
     hasBin: true
     peerDependencies:
-      '@types/node': ^18.0.0 || >=20.0.0
+      '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
+      jiti: '>=1.21.0'
       less: '*'
       lightningcss: ^1.21.0
       sass: '*'
       sass-embedded: '*'
       stylus: '*'
       sugarss: '*'
-      terser: ^5.4.0
+      terser: ^5.16.0
+      tsx: ^4.8.1
+      yaml: ^2.4.2
     peerDependenciesMeta:
       '@types/node':
         optional: true
+      jiti:
+        optional: true
       less:
         optional: true
       lightningcss:
@@ -3429,17 +3746,21 @@ packages:
         optional: true
       terser:
         optional: true
+      tsx:
+        optional: true
+      yaml:
+        optional: true
 
-  vitest@3.1.4:
-    resolution: {integrity: sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==}
+  vitest@3.2.4:
+    resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==}
     engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
     hasBin: true
     peerDependencies:
       '@edge-runtime/vm': '*'
       '@types/debug': ^4.1.12
       '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
-      '@vitest/browser': 3.1.4
-      '@vitest/ui': 3.1.4
+      '@vitest/browser': 3.2.4
+      '@vitest/ui': 3.2.4
       happy-dom: '*'
       jsdom: '*'
     peerDependenciesMeta:
@@ -3462,6 +3783,25 @@ packages:
     resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
     engines: {node: '>=0.10.0'}
 
+  vue-demi@0.14.10:
+    resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
+    engines: {node: '>=12'}
+    hasBin: true
+    peerDependencies:
+      '@vue/composition-api': ^1.0.0-rc.1
+      vue: ^3.0.0-0 || ^2.6.0
+    peerDependenciesMeta:
+      '@vue/composition-api':
+        optional: true
+
+  vue@3.5.17:
+    resolution: {integrity: sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g==}
+    peerDependencies:
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
   w3c-xmlserializer@5.0.0:
     resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
     engines: {node: '>=18'}
@@ -3527,20 +3867,8 @@ packages:
   wrappy@1.0.2:
     resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
 
-  ws@8.18.1:
-    resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==}
-    engines: {node: '>=10.0.0'}
-    peerDependencies:
-      bufferutil: ^4.0.1
-      utf-8-validate: '>=5.0.2'
-    peerDependenciesMeta:
-      bufferutil:
-        optional: true
-      utf-8-validate:
-        optional: true
-
-  ws@8.18.2:
-    resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==}
+  ws@8.18.3:
+    resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
     engines: {node: '>=10.0.0'}
     peerDependencies:
       bufferutil: ^4.0.1
@@ -3582,25 +3910,27 @@ packages:
     resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
     engines: {node: '>=10'}
 
-  zod@3.24.1:
-    resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==}
+  zod@3.25.72:
+    resolution: {integrity: sha512-Cl+fe4dNL4XumOBNBsr0lHfA80PQiZXHI4xEMTEr8gt6aGz92t3lBA32e71j9+JeF/VAYvdfBnuwJs+BMx/BrA==}
 
 snapshots:
 
   '@ampproject/remapping@2.3.0':
     dependencies:
-      '@jridgewell/gen-mapping': 0.3.5
-      '@jridgewell/trace-mapping': 0.3.25
+      '@jridgewell/gen-mapping': 0.3.12
+      '@jridgewell/trace-mapping': 0.3.29
 
-  '@asamuzakjp/css-color@2.8.2':
+  '@antfu/utils@0.7.10': {}
+
+  '@asamuzakjp/css-color@3.2.0':
     dependencies:
-      '@csstools/css-calc': 2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)
-      '@csstools/css-color-parser': 3.0.7(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)
-      '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3)
-      '@csstools/css-tokenizer': 3.0.3
-      lru-cache: 11.0.2
+      '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+      '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+      '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+      '@csstools/css-tokenizer': 3.0.4
+      lru-cache: 10.4.3
 
-  '@babel/code-frame@7.26.2':
+  '@babel/code-frame@7.27.1':
     dependencies:
       '@babel/helper-validator-identifier': 7.27.1
       js-tokens: 4.0.0
@@ -3610,44 +3940,44 @@ snapshots:
 
   '@babel/helper-validator-identifier@7.27.1': {}
 
-  '@babel/parser@7.27.5':
+  '@babel/parser@7.28.0':
     dependencies:
-      '@babel/types': 7.27.6
+      '@babel/types': 7.28.0
 
-  '@babel/types@7.27.6':
+  '@babel/types@7.28.0':
     dependencies:
       '@babel/helper-string-parser': 7.27.1
       '@babel/helper-validator-identifier': 7.27.1
 
   '@bcoe/v8-coverage@1.0.2': {}
 
-  '@conventional-changelog/git-client@1.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.0.0)':
+  '@conventional-changelog/git-client@1.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.2.0)':
     dependencies:
       '@types/semver': 7.7.0
       semver: 7.7.2
     optionalDependencies:
       conventional-commits-filter: 5.0.0
-      conventional-commits-parser: 6.0.0
+      conventional-commits-parser: 6.2.0
 
-  '@csstools/color-helpers@5.0.1': {}
+  '@csstools/color-helpers@5.0.2': {}
 
-  '@csstools/css-calc@2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)':
+  '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
     dependencies:
-      '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3)
-      '@csstools/css-tokenizer': 3.0.3
+      '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+      '@csstools/css-tokenizer': 3.0.4
 
-  '@csstools/css-color-parser@3.0.7(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)':
+  '@csstools/css-color-parser@3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
     dependencies:
-      '@csstools/color-helpers': 5.0.1
-      '@csstools/css-calc': 2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)
-      '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3)
-      '@csstools/css-tokenizer': 3.0.3
+      '@csstools/color-helpers': 5.0.2
+      '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+      '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+      '@csstools/css-tokenizer': 3.0.4
 
-  '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)':
+  '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
     dependencies:
-      '@csstools/css-tokenizer': 3.0.3
+      '@csstools/css-tokenizer': 3.0.4
 
-  '@csstools/css-tokenizer@3.0.3': {}
+  '@csstools/css-tokenizer@3.0.4': {}
 
   '@emnapi/core@1.4.3':
     dependencies:
@@ -3809,53 +4139,52 @@ snapshots:
   '@esbuild/win32-x64@0.25.5':
     optional: true
 
-  '@eslint-community/eslint-utils@4.6.1(eslint@9.27.0)':
-    dependencies:
-      eslint: 9.27.0
-      eslint-visitor-keys: 3.4.3
-
-  '@eslint-community/eslint-utils@4.7.0(eslint@9.27.0)':
+  '@eslint-community/eslint-utils@4.7.0(eslint@9.30.1)':
     dependencies:
-      eslint: 9.27.0
+      eslint: 9.30.1
       eslint-visitor-keys: 3.4.3
 
   '@eslint-community/regexpp@4.12.1': {}
 
-  '@eslint/config-array@0.20.0':
+  '@eslint/config-array@0.21.0':
     dependencies:
       '@eslint/object-schema': 2.1.6
-      debug: 4.4.0
+      debug: 4.4.1
       minimatch: 3.1.2
     transitivePeerDependencies:
       - supports-color
 
-  '@eslint/config-helpers@0.2.1': {}
+  '@eslint/config-helpers@0.3.0': {}
 
   '@eslint/core@0.14.0':
     dependencies:
       '@types/json-schema': 7.0.15
 
+  '@eslint/core@0.15.1':
+    dependencies:
+      '@types/json-schema': 7.0.15
+
   '@eslint/eslintrc@3.3.1':
     dependencies:
       ajv: 6.12.6
-      debug: 4.4.0
-      espree: 10.3.0
+      debug: 4.4.1
+      espree: 10.4.0
       globals: 14.0.0
       ignore: 5.3.2
-      import-fresh: 3.3.0
+      import-fresh: 3.3.1
       js-yaml: 4.1.0
       minimatch: 3.1.2
       strip-json-comments: 3.1.1
     transitivePeerDependencies:
       - supports-color
 
-  '@eslint/js@9.27.0': {}
+  '@eslint/js@9.30.1': {}
 
   '@eslint/object-schema@2.1.6': {}
 
-  '@eslint/plugin-kit@0.3.1':
+  '@eslint/plugin-kit@0.3.3':
     dependencies:
-      '@eslint/core': 0.14.0
+      '@eslint/core': 0.15.1
       levn: 0.4.1
 
   '@humanfs/core@0.19.1': {}
@@ -3869,7 +4198,7 @@ snapshots:
 
   '@humanwhocodes/retry@0.3.1': {}
 
-  '@humanwhocodes/retry@0.4.2': {}
+  '@humanwhocodes/retry@0.4.3': {}
 
   '@hutson/parse-repository-url@5.0.0': {}
 
@@ -3890,26 +4219,23 @@ snapshots:
 
   '@istanbuljs/schema@0.1.3': {}
 
-  '@jridgewell/gen-mapping@0.3.5':
+  '@jridgewell/gen-mapping@0.3.12':
     dependencies:
-      '@jridgewell/set-array': 1.2.1
-      '@jridgewell/sourcemap-codec': 1.5.0
-      '@jridgewell/trace-mapping': 0.3.25
+      '@jridgewell/sourcemap-codec': 1.5.4
+      '@jridgewell/trace-mapping': 0.3.29
 
   '@jridgewell/resolve-uri@3.1.2': {}
 
-  '@jridgewell/set-array@1.2.1': {}
-
-  '@jridgewell/sourcemap-codec@1.5.0': {}
+  '@jridgewell/sourcemap-codec@1.5.4': {}
 
-  '@jridgewell/trace-mapping@0.3.25':
+  '@jridgewell/trace-mapping@0.3.29':
     dependencies:
       '@jridgewell/resolve-uri': 3.1.2
-      '@jridgewell/sourcemap-codec': 1.5.0
+      '@jridgewell/sourcemap-codec': 1.5.4
 
-  '@jspm/core@2.0.1': {}
+  '@jspm/core@2.1.0': {}
 
-  '@napi-rs/wasm-runtime@0.2.9':
+  '@napi-rs/wasm-runtime@0.2.11':
     dependencies:
       '@emnapi/core': 1.4.3
       '@emnapi/runtime': 1.4.3
@@ -3928,66 +4254,72 @@ snapshots:
       '@nodelib/fs.scandir': 2.1.5
       fastq: 1.19.1
 
-  '@parcel/watcher-android-arm64@2.4.1':
+  '@parcel/watcher-android-arm64@2.5.1':
     optional: true
 
-  '@parcel/watcher-darwin-arm64@2.4.1':
+  '@parcel/watcher-darwin-arm64@2.5.1':
     optional: true
 
-  '@parcel/watcher-darwin-x64@2.4.1':
+  '@parcel/watcher-darwin-x64@2.5.1':
     optional: true
 
-  '@parcel/watcher-freebsd-x64@2.4.1':
+  '@parcel/watcher-freebsd-x64@2.5.1':
     optional: true
 
-  '@parcel/watcher-linux-arm-glibc@2.4.1':
+  '@parcel/watcher-linux-arm-glibc@2.5.1':
     optional: true
 
-  '@parcel/watcher-linux-arm64-glibc@2.4.1':
+  '@parcel/watcher-linux-arm-musl@2.5.1':
     optional: true
 
-  '@parcel/watcher-linux-arm64-musl@2.4.1':
+  '@parcel/watcher-linux-arm64-glibc@2.5.1':
     optional: true
 
-  '@parcel/watcher-linux-x64-glibc@2.4.1':
+  '@parcel/watcher-linux-arm64-musl@2.5.1':
     optional: true
 
-  '@parcel/watcher-linux-x64-musl@2.4.1':
+  '@parcel/watcher-linux-x64-glibc@2.5.1':
     optional: true
 
-  '@parcel/watcher-win32-arm64@2.4.1':
+  '@parcel/watcher-linux-x64-musl@2.5.1':
     optional: true
 
-  '@parcel/watcher-win32-ia32@2.4.1':
+  '@parcel/watcher-win32-arm64@2.5.1':
     optional: true
 
-  '@parcel/watcher-win32-x64@2.4.1':
+  '@parcel/watcher-win32-ia32@2.5.1':
     optional: true
 
-  '@parcel/watcher@2.4.1':
+  '@parcel/watcher-win32-x64@2.5.1':
+    optional: true
+
+  '@parcel/watcher@2.5.1':
     dependencies:
       detect-libc: 1.0.3
       is-glob: 4.0.3
       micromatch: 4.0.8
       node-addon-api: 7.1.1
     optionalDependencies:
-      '@parcel/watcher-android-arm64': 2.4.1
-      '@parcel/watcher-darwin-arm64': 2.4.1
-      '@parcel/watcher-darwin-x64': 2.4.1
-      '@parcel/watcher-freebsd-x64': 2.4.1
-      '@parcel/watcher-linux-arm-glibc': 2.4.1
-      '@parcel/watcher-linux-arm64-glibc': 2.4.1
-      '@parcel/watcher-linux-arm64-musl': 2.4.1
-      '@parcel/watcher-linux-x64-glibc': 2.4.1
-      '@parcel/watcher-linux-x64-musl': 2.4.1
-      '@parcel/watcher-win32-arm64': 2.4.1
-      '@parcel/watcher-win32-ia32': 2.4.1
-      '@parcel/watcher-win32-x64': 2.4.1
+      '@parcel/watcher-android-arm64': 2.5.1
+      '@parcel/watcher-darwin-arm64': 2.5.1
+      '@parcel/watcher-darwin-x64': 2.5.1
+      '@parcel/watcher-freebsd-x64': 2.5.1
+      '@parcel/watcher-linux-arm-glibc': 2.5.1
+      '@parcel/watcher-linux-arm-musl': 2.5.1
+      '@parcel/watcher-linux-arm64-glibc': 2.5.1
+      '@parcel/watcher-linux-arm64-musl': 2.5.1
+      '@parcel/watcher-linux-x64-glibc': 2.5.1
+      '@parcel/watcher-linux-x64-musl': 2.5.1
+      '@parcel/watcher-win32-arm64': 2.5.1
+      '@parcel/watcher-win32-ia32': 2.5.1
+      '@parcel/watcher-win32-x64': 2.5.1
     optional: true
 
   '@pkgjs/parseargs@0.11.0':
     optional: true
 
+  '@polka/url@1.0.0-next.29': {}
+
   '@puppeteer/browsers@2.10.5':
     dependencies:
       debug: 4.4.1
@@ -3995,9 +4327,10 @@ snapshots:
       progress: 2.0.3
       proxy-agent: 6.5.0
       semver: 7.7.2
-      tar-fs: 3.0.8
+      tar-fs: 3.1.0
       yargs: 17.7.2
     transitivePeerDependencies:
+      - bare-buffer
       - supports-color
 
   '@rollup/plugin-alias@5.1.1(rollup@4.44.1)':
@@ -4006,10 +4339,10 @@ snapshots:
 
   '@rollup/plugin-commonjs@28.0.6(rollup@4.44.1)':
     dependencies:
-      '@rollup/pluginutils': 5.1.0(rollup@4.44.1)
+      '@rollup/pluginutils': 5.2.0(rollup@4.44.1)
       commondir: 1.0.1
       estree-walker: 2.0.2
-      fdir: 6.4.4(picomatch@4.0.2)
+      fdir: 6.4.6(picomatch@4.0.2)
       is-reference: 1.2.1
       magic-string: 0.30.17
       picomatch: 4.0.2
@@ -4018,7 +4351,7 @@ snapshots:
 
   '@rollup/plugin-inject@5.0.5(rollup@4.44.1)':
     dependencies:
-      '@rollup/pluginutils': 5.1.0(rollup@4.44.1)
+      '@rollup/pluginutils': 5.2.0(rollup@4.44.1)
       estree-walker: 2.0.2
       magic-string: 0.30.17
     optionalDependencies:
@@ -4026,32 +4359,32 @@ snapshots:
 
   '@rollup/plugin-json@6.1.0(rollup@4.44.1)':
     dependencies:
-      '@rollup/pluginutils': 5.1.0(rollup@4.44.1)
+      '@rollup/pluginutils': 5.2.0(rollup@4.44.1)
     optionalDependencies:
       rollup: 4.44.1
 
   '@rollup/plugin-node-resolve@16.0.1(rollup@4.44.1)':
     dependencies:
-      '@rollup/pluginutils': 5.1.0(rollup@4.44.1)
+      '@rollup/pluginutils': 5.2.0(rollup@4.44.1)
       '@types/resolve': 1.20.2
       deepmerge: 4.3.1
       is-module: 1.0.0
-      resolve: 1.22.8
+      resolve: 1.22.10
     optionalDependencies:
       rollup: 4.44.1
 
   '@rollup/plugin-replace@5.0.4(rollup@4.44.1)':
     dependencies:
-      '@rollup/pluginutils': 5.1.0(rollup@4.44.1)
+      '@rollup/pluginutils': 5.2.0(rollup@4.44.1)
       magic-string: 0.30.17
     optionalDependencies:
       rollup: 4.44.1
 
-  '@rollup/pluginutils@5.1.0(rollup@4.44.1)':
+  '@rollup/pluginutils@5.2.0(rollup@4.44.1)':
     dependencies:
-      '@types/estree': 1.0.7
+      '@types/estree': 1.0.8
       estree-walker: 2.0.2
-      picomatch: 2.3.1
+      picomatch: 4.0.2
     optionalDependencies:
       rollup: 4.44.1
 
@@ -4174,7 +4507,15 @@ snapshots:
       tslib: 2.8.1
     optional: true
 
-  '@types/estree@1.0.7': {}
+  '@types/chai@5.2.2':
+    dependencies:
+      '@types/deep-eql': 4.0.2
+
+  '@types/connect@3.4.38':
+    dependencies:
+      '@types/node': 22.16.0
+
+  '@types/deep-eql@4.0.2': {}
 
   '@types/estree@1.0.8': {}
 
@@ -4198,62 +4539,79 @@ snapshots:
 
   '@types/trusted-types@2.0.7': {}
 
+  '@types/web-bluetooth@0.0.20': {}
+
   '@types/yauzl@2.10.3':
     dependencies:
       '@types/node': 22.16.0
     optional: true
 
-  '@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0)(typescript@5.6.3))(eslint@9.27.0)(typescript@5.6.3)':
+  '@typescript-eslint/eslint-plugin@8.35.1(@typescript-eslint/parser@8.35.1(eslint@9.30.1)(typescript@5.6.3))(eslint@9.30.1)(typescript@5.6.3)':
     dependencies:
       '@eslint-community/regexpp': 4.12.1
-      '@typescript-eslint/parser': 8.32.1(eslint@9.27.0)(typescript@5.6.3)
-      '@typescript-eslint/scope-manager': 8.32.1
-      '@typescript-eslint/type-utils': 8.32.1(eslint@9.27.0)(typescript@5.6.3)
-      '@typescript-eslint/utils': 8.32.1(eslint@9.27.0)(typescript@5.6.3)
-      '@typescript-eslint/visitor-keys': 8.32.1
-      eslint: 9.27.0
+      '@typescript-eslint/parser': 8.35.1(eslint@9.30.1)(typescript@5.6.3)
+      '@typescript-eslint/scope-manager': 8.35.1
+      '@typescript-eslint/type-utils': 8.35.1(eslint@9.30.1)(typescript@5.6.3)
+      '@typescript-eslint/utils': 8.35.1(eslint@9.30.1)(typescript@5.6.3)
+      '@typescript-eslint/visitor-keys': 8.35.1
+      eslint: 9.30.1
       graphemer: 1.4.0
-      ignore: 7.0.4
+      ignore: 7.0.5
       natural-compare: 1.4.0
       ts-api-utils: 2.1.0(typescript@5.6.3)
       typescript: 5.6.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/parser@8.32.1(eslint@9.27.0)(typescript@5.6.3)':
+  '@typescript-eslint/parser@8.35.1(eslint@9.30.1)(typescript@5.6.3)':
     dependencies:
-      '@typescript-eslint/scope-manager': 8.32.1
-      '@typescript-eslint/types': 8.32.1
-      '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.6.3)
-      '@typescript-eslint/visitor-keys': 8.32.1
+      '@typescript-eslint/scope-manager': 8.35.1
+      '@typescript-eslint/types': 8.35.1
+      '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.6.3)
+      '@typescript-eslint/visitor-keys': 8.35.1
       debug: 4.4.1
-      eslint: 9.27.0
+      eslint: 9.30.1
       typescript: 5.6.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/scope-manager@8.32.1':
+  '@typescript-eslint/project-service@8.35.1(typescript@5.6.3)':
     dependencies:
-      '@typescript-eslint/types': 8.32.1
-      '@typescript-eslint/visitor-keys': 8.32.1
+      '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.6.3)
+      '@typescript-eslint/types': 8.35.1
+      debug: 4.4.1
+      typescript: 5.6.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/scope-manager@8.35.1':
+    dependencies:
+      '@typescript-eslint/types': 8.35.1
+      '@typescript-eslint/visitor-keys': 8.35.1
+
+  '@typescript-eslint/tsconfig-utils@8.35.1(typescript@5.6.3)':
+    dependencies:
+      typescript: 5.6.3
 
-  '@typescript-eslint/type-utils@8.32.1(eslint@9.27.0)(typescript@5.6.3)':
+  '@typescript-eslint/type-utils@8.35.1(eslint@9.30.1)(typescript@5.6.3)':
     dependencies:
-      '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.6.3)
-      '@typescript-eslint/utils': 8.32.1(eslint@9.27.0)(typescript@5.6.3)
+      '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.6.3)
+      '@typescript-eslint/utils': 8.35.1(eslint@9.30.1)(typescript@5.6.3)
       debug: 4.4.1
-      eslint: 9.27.0
+      eslint: 9.30.1
       ts-api-utils: 2.1.0(typescript@5.6.3)
       typescript: 5.6.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/types@8.32.1': {}
+  '@typescript-eslint/types@8.35.1': {}
 
-  '@typescript-eslint/typescript-estree@8.32.1(typescript@5.6.3)':
+  '@typescript-eslint/typescript-estree@8.35.1(typescript@5.6.3)':
     dependencies:
-      '@typescript-eslint/types': 8.32.1
-      '@typescript-eslint/visitor-keys': 8.32.1
+      '@typescript-eslint/project-service': 8.35.1(typescript@5.6.3)
+      '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.6.3)
+      '@typescript-eslint/types': 8.35.1
+      '@typescript-eslint/visitor-keys': 8.35.1
       debug: 4.4.1
       fast-glob: 3.3.3
       is-glob: 4.0.3
@@ -4264,85 +4622,97 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/utils@8.32.1(eslint@9.27.0)(typescript@5.6.3)':
+  '@typescript-eslint/utils@8.35.1(eslint@9.30.1)(typescript@5.6.3)':
     dependencies:
-      '@eslint-community/eslint-utils': 4.7.0(eslint@9.27.0)
-      '@typescript-eslint/scope-manager': 8.32.1
-      '@typescript-eslint/types': 8.32.1
-      '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.6.3)
-      eslint: 9.27.0
+      '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1)
+      '@typescript-eslint/scope-manager': 8.35.1
+      '@typescript-eslint/types': 8.35.1
+      '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.6.3)
+      eslint: 9.30.1
       typescript: 5.6.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/visitor-keys@8.32.1':
+  '@typescript-eslint/visitor-keys@8.35.1':
     dependencies:
-      '@typescript-eslint/types': 8.32.1
-      eslint-visitor-keys: 4.2.0
+      '@typescript-eslint/types': 8.35.1
+      eslint-visitor-keys: 4.2.1
+
+  '@unrs/resolver-binding-android-arm-eabi@1.10.1':
+    optional: true
+
+  '@unrs/resolver-binding-android-arm64@1.10.1':
+    optional: true
 
-  '@unrs/resolver-binding-darwin-arm64@1.7.2':
+  '@unrs/resolver-binding-darwin-arm64@1.10.1':
     optional: true
 
-  '@unrs/resolver-binding-darwin-x64@1.7.2':
+  '@unrs/resolver-binding-darwin-x64@1.10.1':
     optional: true
 
-  '@unrs/resolver-binding-freebsd-x64@1.7.2':
+  '@unrs/resolver-binding-freebsd-x64@1.10.1':
     optional: true
 
-  '@unrs/resolver-binding-linux-arm-gnueabihf@1.7.2':
+  '@unrs/resolver-binding-linux-arm-gnueabihf@1.10.1':
     optional: true
 
-  '@unrs/resolver-binding-linux-arm-musleabihf@1.7.2':
+  '@unrs/resolver-binding-linux-arm-musleabihf@1.10.1':
     optional: true
 
-  '@unrs/resolver-binding-linux-arm64-gnu@1.7.2':
+  '@unrs/resolver-binding-linux-arm64-gnu@1.10.1':
     optional: true
 
-  '@unrs/resolver-binding-linux-arm64-musl@1.7.2':
+  '@unrs/resolver-binding-linux-arm64-musl@1.10.1':
     optional: true
 
-  '@unrs/resolver-binding-linux-ppc64-gnu@1.7.2':
+  '@unrs/resolver-binding-linux-ppc64-gnu@1.10.1':
     optional: true
 
-  '@unrs/resolver-binding-linux-riscv64-gnu@1.7.2':
+  '@unrs/resolver-binding-linux-riscv64-gnu@1.10.1':
     optional: true
 
-  '@unrs/resolver-binding-linux-riscv64-musl@1.7.2':
+  '@unrs/resolver-binding-linux-riscv64-musl@1.10.1':
     optional: true
 
-  '@unrs/resolver-binding-linux-s390x-gnu@1.7.2':
+  '@unrs/resolver-binding-linux-s390x-gnu@1.10.1':
     optional: true
 
-  '@unrs/resolver-binding-linux-x64-gnu@1.7.2':
+  '@unrs/resolver-binding-linux-x64-gnu@1.10.1':
     optional: true
 
-  '@unrs/resolver-binding-linux-x64-musl@1.7.2':
+  '@unrs/resolver-binding-linux-x64-musl@1.10.1':
     optional: true
 
-  '@unrs/resolver-binding-wasm32-wasi@1.7.2':
+  '@unrs/resolver-binding-wasm32-wasi@1.10.1':
     dependencies:
-      '@napi-rs/wasm-runtime': 0.2.9
+      '@napi-rs/wasm-runtime': 0.2.11
     optional: true
 
-  '@unrs/resolver-binding-win32-arm64-msvc@1.7.2':
+  '@unrs/resolver-binding-win32-arm64-msvc@1.10.1':
     optional: true
 
-  '@unrs/resolver-binding-win32-ia32-msvc@1.7.2':
+  '@unrs/resolver-binding-win32-ia32-msvc@1.10.1':
     optional: true
 
-  '@unrs/resolver-binding-win32-x64-msvc@1.7.2':
+  '@unrs/resolver-binding-win32-x64-msvc@1.10.1':
     optional: true
 
-  '@vitejs/plugin-vue@5.2.4(vite@5.4.15(@types/node@22.16.0)(sass@1.89.2))(vue@packages+vue)':
+  '@vitejs/plugin-vue@5.2.4(vite@6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0))(vue@3.5.17(typescript@5.6.3))':
+    dependencies:
+      vite: 6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0)
+      vue: 3.5.17(typescript@5.6.3)
+
+  '@vitejs/plugin-vue@5.2.4(vite@6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0))(vue@packages+vue)':
     dependencies:
-      vite: 5.4.15(@types/node@22.16.0)(sass@1.89.2)
+      vite: 6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0)
       vue: link:packages/vue
 
-  '@vitest/coverage-v8@3.1.4(vitest@3.1.4(@types/node@22.16.0)(jsdom@26.1.0)(sass@1.89.2))':
+  '@vitest/coverage-v8@3.2.4(vitest@3.2.4)':
     dependencies:
       '@ampproject/remapping': 2.3.0
       '@bcoe/v8-coverage': 1.0.2
-      debug: 4.4.0
+      ast-v8-to-istanbul: 0.3.3
+      debug: 4.4.1
       istanbul-lib-coverage: 3.2.2
       istanbul-lib-report: 3.0.1
       istanbul-lib-source-maps: 5.0.6
@@ -4352,64 +4722,150 @@ snapshots:
       std-env: 3.9.0
       test-exclude: 7.0.1
       tinyrainbow: 2.0.0
-      vitest: 3.1.4(@types/node@22.16.0)(jsdom@26.1.0)(sass@1.89.2)
+      vitest: 3.2.4(@types/node@22.16.0)(@vitest/ui@3.2.4)(jsdom@26.1.0)(sass@1.89.2)(yaml@2.8.0)
     transitivePeerDependencies:
       - supports-color
 
-  '@vitest/eslint-plugin@1.2.1(eslint@9.27.0)(typescript@5.6.3)(vitest@3.1.4(@types/node@22.16.0)(jsdom@26.1.0)(sass@1.89.2))':
+  '@vitest/eslint-plugin@1.3.4(eslint@9.30.1)(typescript@5.6.3)(vitest@3.2.4)':
     dependencies:
-      '@typescript-eslint/utils': 8.32.1(eslint@9.27.0)(typescript@5.6.3)
-      eslint: 9.27.0
+      '@typescript-eslint/utils': 8.35.1(eslint@9.30.1)(typescript@5.6.3)
+      eslint: 9.30.1
     optionalDependencies:
       typescript: 5.6.3
-      vitest: 3.1.4(@types/node@22.16.0)(jsdom@26.1.0)(sass@1.89.2)
+      vitest: 3.2.4(@types/node@22.16.0)(@vitest/ui@3.2.4)(jsdom@26.1.0)(sass@1.89.2)(yaml@2.8.0)
     transitivePeerDependencies:
       - supports-color
 
-  '@vitest/expect@3.1.4':
+  '@vitest/expect@3.2.4':
     dependencies:
-      '@vitest/spy': 3.1.4
-      '@vitest/utils': 3.1.4
+      '@types/chai': 5.2.2
+      '@vitest/spy': 3.2.4
+      '@vitest/utils': 3.2.4
       chai: 5.2.0
       tinyrainbow: 2.0.0
 
-  '@vitest/mocker@3.1.4(vite@5.4.19(@types/node@22.16.0)(sass@1.89.2))':
+  '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0))':
     dependencies:
-      '@vitest/spy': 3.1.4
+      '@vitest/spy': 3.2.4
       estree-walker: 3.0.3
       magic-string: 0.30.17
     optionalDependencies:
-      vite: 5.4.19(@types/node@22.16.0)(sass@1.89.2)
+      vite: 6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0)
 
-  '@vitest/pretty-format@3.1.4':
+  '@vitest/pretty-format@3.2.4':
     dependencies:
       tinyrainbow: 2.0.0
 
-  '@vitest/runner@3.1.4':
+  '@vitest/runner@3.2.4':
     dependencies:
-      '@vitest/utils': 3.1.4
+      '@vitest/utils': 3.2.4
       pathe: 2.0.3
+      strip-literal: 3.0.0
 
-  '@vitest/snapshot@3.1.4':
+  '@vitest/snapshot@3.2.4':
     dependencies:
-      '@vitest/pretty-format': 3.1.4
+      '@vitest/pretty-format': 3.2.4
       magic-string: 0.30.17
       pathe: 2.0.3
 
-  '@vitest/spy@3.1.4':
+  '@vitest/spy@3.2.4':
+    dependencies:
+      tinyspy: 4.0.3
+
+  '@vitest/ui@3.2.4(vitest@3.2.4)':
     dependencies:
-      tinyspy: 3.0.2
+      '@vitest/utils': 3.2.4
+      fflate: 0.8.2
+      flatted: 3.3.3
+      pathe: 2.0.3
+      sirv: 3.0.1
+      tinyglobby: 0.2.14
+      tinyrainbow: 2.0.0
+      vitest: 3.2.4(@types/node@22.16.0)(@vitest/ui@3.2.4)(jsdom@26.1.0)(sass@1.89.2)(yaml@2.8.0)
 
-  '@vitest/utils@3.1.4':
+  '@vitest/utils@3.2.4':
     dependencies:
-      '@vitest/pretty-format': 3.1.4
-      loupe: 3.1.3
+      '@vitest/pretty-format': 3.2.4
+      loupe: 3.1.4
       tinyrainbow: 2.0.0
 
+  '@vue/compiler-core@3.5.17':
+    dependencies:
+      '@babel/parser': 7.28.0
+      '@vue/shared': 3.5.17
+      entities: 4.5.0
+      estree-walker: 2.0.2
+      source-map-js: 1.2.1
+
+  '@vue/compiler-dom@3.5.17':
+    dependencies:
+      '@vue/compiler-core': 3.5.17
+      '@vue/shared': 3.5.17
+
+  '@vue/compiler-sfc@3.5.17':
+    dependencies:
+      '@babel/parser': 7.28.0
+      '@vue/compiler-core': 3.5.17
+      '@vue/compiler-dom': 3.5.17
+      '@vue/compiler-ssr': 3.5.17
+      '@vue/shared': 3.5.17
+      estree-walker: 2.0.2
+      magic-string: 0.30.17
+      postcss: 8.5.6
+      source-map-js: 1.2.1
+
+  '@vue/compiler-ssr@3.5.17':
+    dependencies:
+      '@vue/compiler-dom': 3.5.17
+      '@vue/shared': 3.5.17
+
   '@vue/consolidate@1.0.0': {}
 
+  '@vue/reactivity@3.5.17':
+    dependencies:
+      '@vue/shared': 3.5.17
+
   '@vue/repl@4.6.1': {}
 
+  '@vue/runtime-core@3.5.17':
+    dependencies:
+      '@vue/reactivity': 3.5.17
+      '@vue/shared': 3.5.17
+
+  '@vue/runtime-dom@3.5.17':
+    dependencies:
+      '@vue/reactivity': 3.5.17
+      '@vue/runtime-core': 3.5.17
+      '@vue/shared': 3.5.17
+      csstype: 3.1.3
+
+  '@vue/server-renderer@3.5.17(vue@3.5.17(typescript@5.6.3))':
+    dependencies:
+      '@vue/compiler-ssr': 3.5.17
+      '@vue/shared': 3.5.17
+      vue: 3.5.17(typescript@5.6.3)
+
+  '@vue/shared@3.5.17': {}
+
+  '@vueuse/core@11.3.0(vue@packages+vue)':
+    dependencies:
+      '@types/web-bluetooth': 0.0.20
+      '@vueuse/metadata': 11.3.0
+      '@vueuse/shared': 11.3.0(vue@packages+vue)
+      vue-demi: 0.14.10(vue@packages+vue)
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+      - vue
+
+  '@vueuse/metadata@11.3.0': {}
+
+  '@vueuse/shared@11.3.0(vue@packages+vue)':
+    dependencies:
+      vue-demi: 0.14.10(vue@packages+vue)
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+      - vue
+
   '@zeit/schemas@2.36.0': {}
 
   accepts@1.3.8:
@@ -4417,13 +4873,13 @@ snapshots:
       mime-types: 2.1.35
       negotiator: 0.6.3
 
-  acorn-jsx@5.3.2(acorn@8.14.0):
+  acorn-jsx@5.3.2(acorn@8.15.0):
     dependencies:
-      acorn: 8.14.0
+      acorn: 8.15.0
 
   acorn@7.4.1: {}
 
-  acorn@8.14.0: {}
+  acorn@8.15.0: {}
 
   add-stream@1.0.0: {}
 
@@ -4455,7 +4911,7 @@ snapshots:
 
   ansi-regex@5.0.1: {}
 
-  ansi-regex@6.0.1: {}
+  ansi-regex@6.1.0: {}
 
   ansi-styles@4.3.0:
     dependencies:
@@ -4473,7 +4929,7 @@ snapshots:
 
   asap@2.0.6: {}
 
-  assert-never@1.3.0: {}
+  assert-never@1.4.0: {}
 
   assertion-error@2.0.1: {}
 
@@ -4481,35 +4937,43 @@ snapshots:
     dependencies:
       tslib: 2.8.1
 
-  b4a@1.6.6: {}
+  ast-v8-to-istanbul@0.3.3:
+    dependencies:
+      '@jridgewell/trace-mapping': 0.3.29
+      estree-walker: 3.0.3
+      js-tokens: 9.0.1
+
+  b4a@1.6.7: {}
 
   babel-walk@3.0.0-canary-5:
     dependencies:
-      '@babel/types': 7.27.6
+      '@babel/types': 7.28.0
 
   balanced-match@1.0.2: {}
 
-  bare-events@2.4.2:
+  bare-events@2.5.4:
     optional: true
 
-  bare-fs@4.0.1:
+  bare-fs@4.1.5:
     dependencies:
-      bare-events: 2.4.2
+      bare-events: 2.5.4
       bare-path: 3.0.0
-      bare-stream: 2.1.3
+      bare-stream: 2.6.5(bare-events@2.5.4)
     optional: true
 
-  bare-os@3.4.0:
+  bare-os@3.6.1:
     optional: true
 
   bare-path@3.0.0:
     dependencies:
-      bare-os: 3.4.0
+      bare-os: 3.6.1
     optional: true
 
-  bare-stream@2.1.3:
+  bare-stream@2.6.5(bare-events@2.5.4):
     dependencies:
-      streamx: 2.18.0
+      streamx: 2.22.1
+    optionalDependencies:
+      bare-events: 2.5.4
     optional: true
 
   basic-ftp@5.0.5: {}
@@ -4518,19 +4982,19 @@ snapshots:
     dependencies:
       ansi-align: 3.0.1
       camelcase: 7.0.1
-      chalk: 5.4.1
+      chalk: 5.0.1
       cli-boxes: 3.0.0
       string-width: 5.1.2
       type-fest: 2.19.0
       widest-line: 4.0.1
       wrap-ansi: 8.1.0
 
-  brace-expansion@1.1.11:
+  brace-expansion@1.1.12:
     dependencies:
       balanced-match: 1.0.2
       concat-map: 0.0.1
 
-  brace-expansion@2.0.1:
+  brace-expansion@2.0.2:
     dependencies:
       balanced-match: 1.0.2
 
@@ -4540,17 +5004,23 @@ snapshots:
 
   buffer-crc32@0.2.13: {}
 
+  bundle-name@4.1.0:
+    dependencies:
+      run-applescript: 7.0.0
+
   bytes@3.0.0: {}
 
   cac@6.7.14: {}
 
-  call-bind@1.0.7:
+  call-bind-apply-helpers@1.0.2:
     dependencies:
-      es-define-property: 1.0.0
       es-errors: 1.3.0
       function-bind: 1.1.2
-      get-intrinsic: 1.2.4
-      set-function-length: 1.2.2
+
+  call-bound@1.0.4:
+    dependencies:
+      call-bind-apply-helpers: 1.0.2
+      get-intrinsic: 1.3.0
 
   callsites@3.1.0: {}
 
@@ -4561,8 +5031,8 @@ snapshots:
       assertion-error: 2.0.1
       check-error: 2.1.1
       deep-eql: 5.0.2
-      loupe: 3.1.3
-      pathval: 2.0.0
+      loupe: 3.1.4
+      pathval: 2.0.1
 
   chalk-template@0.4.0:
     dependencies:
@@ -4579,19 +5049,19 @@ snapshots:
 
   character-parser@2.2.0:
     dependencies:
-      is-regex: 1.1.4
+      is-regex: 1.2.1
 
   check-error@2.1.1: {}
 
-  chokidar@4.0.1:
+  chokidar@4.0.3:
     dependencies:
-      readdirp: 4.0.1
+      readdirp: 4.1.2
 
   chromium-bidi@5.1.0(devtools-protocol@0.0.1439962):
     dependencies:
       devtools-protocol: 0.0.1439962
       mitt: 3.0.1
-      zod: 3.24.1
+      zod: 3.25.72
 
   cli-boxes@3.0.0: {}
 
@@ -4624,7 +5094,7 @@ snapshots:
 
   colorette@2.0.20: {}
 
-  commander@13.1.0: {}
+  commander@14.0.0: {}
 
   comment-parser@1.4.1: {}
 
@@ -4637,7 +5107,7 @@ snapshots:
 
   compressible@2.0.18:
     dependencies:
-      mime-db: 1.53.0
+      mime-db: 1.54.0
 
   compression@1.7.4:
     dependencies:
@@ -4653,10 +5123,19 @@ snapshots:
 
   concat-map@0.0.1: {}
 
+  connect@3.7.0:
+    dependencies:
+      debug: 2.6.9
+      finalhandler: 1.1.2
+      parseurl: 1.3.3
+      utils-merge: 1.0.1
+    transitivePeerDependencies:
+      - supports-color
+
   constantinople@4.0.1:
     dependencies:
-      '@babel/parser': 7.27.5
-      '@babel/types': 7.27.6
+      '@babel/parser': 7.28.0
+      '@babel/types': 7.28.0
 
   content-disposition@0.5.2: {}
 
@@ -4685,10 +5164,10 @@ snapshots:
     dependencies:
       '@hutson/parse-repository-url': 5.0.0
       add-stream: 1.0.0
-      conventional-changelog-writer: 8.0.0
-      conventional-commits-parser: 6.0.0
-      git-raw-commits: 5.0.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.0.0)
-      git-semver-tags: 8.0.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.0.0)
+      conventional-changelog-writer: 8.1.0
+      conventional-commits-parser: 6.2.0
+      git-raw-commits: 5.0.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.2.0)
+      git-semver-tags: 8.0.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.2.0)
       hosted-git-info: 7.0.2
       normalize-package-data: 6.0.2
       read-package-up: 11.0.0
@@ -4710,9 +5189,8 @@ snapshots:
 
   conventional-changelog-preset-loader@5.0.0: {}
 
-  conventional-changelog-writer@8.0.0:
+  conventional-changelog-writer@8.1.0:
     dependencies:
-      '@types/semver': 7.7.0
       conventional-commits-filter: 5.0.0
       handlebars: 4.7.8
       meow: 13.2.0
@@ -4736,7 +5214,7 @@ snapshots:
 
   conventional-commits-filter@5.0.0: {}
 
-  conventional-commits-parser@6.0.0:
+  conventional-commits-parser@6.2.0:
     dependencies:
       meow: 13.2.0
 
@@ -4745,7 +5223,7 @@ snapshots:
   cosmiconfig@9.0.0(typescript@5.6.3):
     dependencies:
       env-paths: 2.2.1
-      import-fresh: 3.3.0
+      import-fresh: 3.3.1
       js-yaml: 4.1.0
       parse-json: 5.2.0
     optionalDependencies:
@@ -4759,9 +5237,9 @@ snapshots:
 
   cssesc@3.0.0: {}
 
-  cssstyle@4.2.1:
+  cssstyle@4.6.0:
     dependencies:
-      '@asamuzakjp/css-color': 2.8.2
+      '@asamuzakjp/css-color': 3.2.0
       rrweb-cssom: 0.8.0
 
   csstype@3.1.3: {}
@@ -4777,14 +5255,6 @@ snapshots:
     dependencies:
       ms: 2.0.0
 
-  debug@3.2.7:
-    dependencies:
-      ms: 2.1.3
-
-  debug@4.4.0:
-    dependencies:
-      ms: 2.1.3
-
   debug@4.4.1:
     dependencies:
       ms: 2.1.3
@@ -4799,11 +5269,14 @@ snapshots:
 
   deepmerge@4.3.1: {}
 
-  define-data-property@1.1.4:
+  default-browser-id@5.0.0: {}
+
+  default-browser@5.2.1:
     dependencies:
-      es-define-property: 1.0.0
-      es-errors: 1.3.0
-      gopd: 1.0.1
+      bundle-name: 4.1.0
+      default-browser-id: 5.0.0
+
+  define-lazy-prop@3.0.0: {}
 
   degenerator@5.0.1:
     dependencies:
@@ -4822,15 +5295,25 @@ snapshots:
     dependencies:
       is-obj: 2.0.0
 
+  dunder-proto@1.0.1:
+    dependencies:
+      call-bind-apply-helpers: 1.0.2
+      es-errors: 1.3.0
+      gopd: 1.2.0
+
   eastasianwidth@0.2.0: {}
 
-  emoji-regex@10.3.0: {}
+  ee-first@1.1.1: {}
+
+  emoji-regex@10.4.0: {}
 
   emoji-regex@8.0.0: {}
 
   emoji-regex@9.2.2: {}
 
-  end-of-stream@1.4.4:
+  encodeurl@1.0.2: {}
+
+  end-of-stream@1.4.5:
     dependencies:
       once: 1.4.0
 
@@ -4841,6 +5324,8 @@ snapshots:
 
   entities@4.5.0: {}
 
+  entities@6.0.1: {}
+
   env-paths@2.2.1: {}
 
   environment@1.1.0: {}
@@ -4849,19 +5334,21 @@ snapshots:
     dependencies:
       is-arrayish: 0.2.1
 
-  es-define-property@1.0.0:
-    dependencies:
-      get-intrinsic: 1.2.4
+  error-stack-parser-es@0.1.5: {}
 
-  es-errors@1.3.0: {}
+  es-define-property@1.0.1: {}
 
-  es-module-lexer@1.6.0: {}
+  es-errors@1.3.0: {}
 
   es-module-lexer@1.7.0: {}
 
+  es-object-atoms@1.1.1:
+    dependencies:
+      es-errors: 1.3.0
+
   esbuild-plugin-polyfill-node@0.3.0(esbuild@0.25.5):
     dependencies:
-      '@jspm/core': 2.0.1
+      '@jspm/core': 2.1.0
       esbuild: 0.25.5
       import-meta-resolve: 3.1.1
 
@@ -4919,7 +5406,9 @@ snapshots:
       '@esbuild/win32-ia32': 0.25.5
       '@esbuild/win32-x64': 0.25.5
 
-  escalade@3.1.2: {}
+  escalade@3.2.0: {}
+
+  escape-html@1.0.3: {}
 
   escape-string-regexp@4.0.0: {}
 
@@ -4931,71 +5420,62 @@ snapshots:
     optionalDependencies:
       source-map: 0.6.1
 
-  eslint-import-context@0.1.4(unrs-resolver@1.7.2):
+  eslint-import-context@0.1.9(unrs-resolver@1.10.1):
     dependencies:
       get-tsconfig: 4.10.1
-      stable-hash: 0.0.5
+      stable-hash-x: 0.2.0
     optionalDependencies:
-      unrs-resolver: 1.7.2
-
-  eslint-import-resolver-node@0.3.9:
-    dependencies:
-      debug: 3.2.7
-      is-core-module: 2.15.0
-      resolve: 1.22.8
-    transitivePeerDependencies:
-      - supports-color
+      unrs-resolver: 1.10.1
 
-  eslint-plugin-import-x@4.13.1(eslint@9.27.0)(typescript@5.6.3):
+  eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.35.1(eslint@9.30.1)(typescript@5.6.3))(eslint@9.30.1):
     dependencies:
-      '@typescript-eslint/utils': 8.32.1(eslint@9.27.0)(typescript@5.6.3)
+      '@typescript-eslint/types': 8.35.1
       comment-parser: 1.4.1
       debug: 4.4.1
-      eslint: 9.27.0
-      eslint-import-context: 0.1.4(unrs-resolver@1.7.2)
-      eslint-import-resolver-node: 0.3.9
+      eslint: 9.30.1
+      eslint-import-context: 0.1.9(unrs-resolver@1.10.1)
       is-glob: 4.0.3
-      minimatch: 10.0.1
+      minimatch: 10.0.3
       semver: 7.7.2
-      stable-hash: 0.0.5
-      tslib: 2.8.1
-      unrs-resolver: 1.7.2
+      stable-hash-x: 0.2.0
+      unrs-resolver: 1.10.1
+    optionalDependencies:
+      '@typescript-eslint/utils': 8.35.1(eslint@9.30.1)(typescript@5.6.3)
     transitivePeerDependencies:
       - supports-color
-      - typescript
 
-  eslint-scope@8.3.0:
+  eslint-scope@8.4.0:
     dependencies:
       esrecurse: 4.3.0
       estraverse: 5.3.0
 
   eslint-visitor-keys@3.4.3: {}
 
-  eslint-visitor-keys@4.2.0: {}
+  eslint-visitor-keys@4.2.1: {}
 
-  eslint@9.27.0:
+  eslint@9.30.1:
     dependencies:
-      '@eslint-community/eslint-utils': 4.6.1(eslint@9.27.0)
+      '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1)
       '@eslint-community/regexpp': 4.12.1
-      '@eslint/config-array': 0.20.0
-      '@eslint/config-helpers': 0.2.1
+      '@eslint/config-array': 0.21.0
+      '@eslint/config-helpers': 0.3.0
       '@eslint/core': 0.14.0
       '@eslint/eslintrc': 3.3.1
-      '@eslint/js': 9.27.0
-      '@eslint/plugin-kit': 0.3.1
+      '@eslint/js': 9.30.1
+      '@eslint/plugin-kit': 0.3.3
       '@humanfs/node': 0.16.6
       '@humanwhocodes/module-importer': 1.0.1
-      '@humanwhocodes/retry': 0.4.2
-      '@types/estree': 1.0.7
+      '@humanwhocodes/retry': 0.4.3
+      '@types/estree': 1.0.8
       '@types/json-schema': 7.0.15
       ajv: 6.12.6
       chalk: 4.1.2
       cross-spawn: 7.0.6
-      debug: 4.4.0
+      debug: 4.4.1
       escape-string-regexp: 4.0.0
-      eslint-scope: 8.3.0
-      eslint-visitor-keys: 4.2.0
-      espree: 10.3.0
+      eslint-scope: 8.4.0
+      eslint-visitor-keys: 4.2.1
+      espree: 10.4.0
       esquery: 1.6.0
       esutils: 2.0.3
       fast-deep-equal: 3.1.3
@@ -5013,11 +5493,11 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  espree@10.3.0:
+  espree@10.4.0:
     dependencies:
-      acorn: 8.14.0
-      acorn-jsx: 5.3.2(acorn@8.14.0)
-      eslint-visitor-keys: 4.2.0
+      acorn: 8.15.0
+      acorn-jsx: 5.3.2(acorn@8.15.0)
+      eslint-visitor-keys: 4.2.1
 
   esprima@4.0.1: {}
 
@@ -5089,10 +5569,12 @@ snapshots:
     dependencies:
       pend: 1.2.0
 
-  fdir@6.4.4(picomatch@4.0.2):
+  fdir@6.4.6(picomatch@4.0.2):
     optionalDependencies:
       picomatch: 4.0.2
 
+  fflate@0.8.2: {}
+
   file-entry-cache@8.0.0:
     dependencies:
       flat-cache: 4.0.1
@@ -5103,7 +5585,19 @@ snapshots:
     dependencies:
       to-regex-range: 5.0.1
 
-  find-up-simple@1.0.0: {}
+  finalhandler@1.1.2:
+    dependencies:
+      debug: 2.6.9
+      encodeurl: 1.0.2
+      escape-html: 1.0.3
+      on-finished: 2.3.0
+      parseurl: 1.3.3
+      statuses: 1.5.0
+      unpipe: 1.0.0
+    transitivePeerDependencies:
+      - supports-color
+
+  find-up-simple@1.0.1: {}
 
   find-up@5.0.0:
     dependencies:
@@ -5112,17 +5606,17 @@ snapshots:
 
   flat-cache@4.0.1:
     dependencies:
-      flatted: 3.3.1
+      flatted: 3.3.3
       keyv: 4.5.4
 
-  flatted@3.3.1: {}
+  flatted@3.3.3: {}
 
-  foreground-child@3.3.0:
+  foreground-child@3.3.1:
     dependencies:
       cross-spawn: 7.0.6
       signal-exit: 4.1.0
 
-  fs-extra@11.2.0:
+  fs-extra@11.3.0:
     dependencies:
       graceful-fs: 4.2.11
       jsonfile: 6.1.0
@@ -5139,50 +5633,55 @@ snapshots:
 
   get-caller-file@2.0.5: {}
 
-  get-east-asian-width@1.2.0: {}
+  get-east-asian-width@1.3.0: {}
 
-  get-intrinsic@1.2.4:
+  get-intrinsic@1.3.0:
     dependencies:
+      call-bind-apply-helpers: 1.0.2
+      es-define-property: 1.0.1
       es-errors: 1.3.0
+      es-object-atoms: 1.1.1
       function-bind: 1.1.2
-      has-proto: 1.0.3
-      has-symbols: 1.0.3
+      get-proto: 1.0.1
+      gopd: 1.2.0
+      has-symbols: 1.1.0
       hasown: 2.0.2
+      math-intrinsics: 1.1.0
+
+  get-proto@1.0.1:
+    dependencies:
+      dunder-proto: 1.0.1
+      es-object-atoms: 1.1.1
 
   get-stream@5.2.0:
     dependencies:
-      pump: 3.0.0
+      pump: 3.0.3
 
   get-stream@6.0.1: {}
 
-  get-tsconfig@4.10.0:
-    dependencies:
-      resolve-pkg-maps: 1.0.0
-
   get-tsconfig@4.10.1:
     dependencies:
       resolve-pkg-maps: 1.0.0
 
-  get-uri@6.0.3:
+  get-uri@6.0.4:
     dependencies:
       basic-ftp: 5.0.5
       data-uri-to-buffer: 6.0.2
       debug: 4.4.1
-      fs-extra: 11.2.0
     transitivePeerDependencies:
       - supports-color
 
-  git-raw-commits@5.0.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.0.0):
+  git-raw-commits@5.0.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.2.0):
     dependencies:
-      '@conventional-changelog/git-client': 1.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.0.0)
+      '@conventional-changelog/git-client': 1.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.2.0)
       meow: 13.2.0
     transitivePeerDependencies:
       - conventional-commits-filter
       - conventional-commits-parser
 
-  git-semver-tags@8.0.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.0.0):
+  git-semver-tags@8.0.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.2.0):
     dependencies:
-      '@conventional-changelog/git-client': 1.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.0.0)
+      '@conventional-changelog/git-client': 1.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.2.0)
       meow: 13.2.0
     transitivePeerDependencies:
       - conventional-commits-filter
@@ -5198,27 +5697,25 @@ snapshots:
 
   glob@10.4.5:
     dependencies:
-      foreground-child: 3.3.0
+      foreground-child: 3.3.1
       jackspeak: 3.4.3
       minimatch: 9.0.5
       minipass: 7.1.2
-      package-json-from-dist: 1.0.0
+      package-json-from-dist: 1.0.1
       path-scurry: 1.11.1
 
-  glob@11.0.0:
+  glob@11.0.3:
     dependencies:
-      foreground-child: 3.3.0
-      jackspeak: 4.0.1
+      foreground-child: 3.3.1
+      jackspeak: 4.1.1
       minimatch: 10.0.3
       minipass: 7.1.2
-      package-json-from-dist: 1.0.0
+      package-json-from-dist: 1.0.1
       path-scurry: 2.0.0
 
   globals@14.0.0: {}
 
-  gopd@1.0.1:
-    dependencies:
-      get-intrinsic: 1.2.4
+  gopd@1.2.0: {}
 
   graceful-fs@4.2.11: {}
 
@@ -5231,21 +5728,15 @@ snapshots:
       source-map: 0.6.1
       wordwrap: 1.0.0
     optionalDependencies:
-      uglify-js: 3.19.1
+      uglify-js: 3.19.3
 
   has-flag@4.0.0: {}
 
-  has-property-descriptors@1.0.2:
-    dependencies:
-      es-define-property: 1.0.0
-
-  has-proto@1.0.3: {}
-
-  has-symbols@1.0.3: {}
+  has-symbols@1.1.0: {}
 
   has-tostringtag@1.0.2:
     dependencies:
-      has-symbols: 1.0.3
+      has-symbols: 1.1.0
 
   hash-sum@2.0.0: {}
 
@@ -5266,14 +5757,14 @@ snapshots:
   http-proxy-agent@7.0.2:
     dependencies:
       agent-base: 7.1.3
-      debug: 4.4.0
+      debug: 4.4.1
     transitivePeerDependencies:
       - supports-color
 
   https-proxy-agent@7.0.6:
     dependencies:
       agent-base: 7.1.3
-      debug: 4.4.0
+      debug: 4.4.1
     transitivePeerDependencies:
       - supports-color
 
@@ -5289,13 +5780,13 @@ snapshots:
 
   ignore@5.3.2: {}
 
-  ignore@7.0.4: {}
+  ignore@7.0.5: {}
 
   immediate@3.0.6: {}
 
-  immutable@5.0.2: {}
+  immutable@5.1.3: {}
 
-  import-fresh@3.3.0:
+  import-fresh@3.3.1:
     dependencies:
       parent-module: 1.0.1
       resolve-from: 4.0.0
@@ -5304,7 +5795,7 @@ snapshots:
 
   imurmurhash@0.1.4: {}
 
-  index-to-position@0.1.2: {}
+  index-to-position@1.1.0: {}
 
   inherits@2.0.4: {}
 
@@ -5317,12 +5808,14 @@ snapshots:
 
   is-arrayish@0.2.1: {}
 
-  is-core-module@2.15.0:
+  is-core-module@2.16.1:
     dependencies:
       hasown: 2.0.2
 
   is-docker@2.2.1: {}
 
+  is-docker@3.0.0: {}
+
   is-expression@4.0.0:
     dependencies:
       acorn: 7.4.1
@@ -5336,12 +5829,16 @@ snapshots:
 
   is-fullwidth-code-point@5.0.0:
     dependencies:
-      get-east-asian-width: 1.2.0
+      get-east-asian-width: 1.3.0
 
   is-glob@4.0.3:
     dependencies:
       is-extglob: 2.1.1
 
+  is-inside-container@1.0.0:
+    dependencies:
+      is-docker: 3.0.0
+
   is-module@1.0.0: {}
 
   is-number@7.0.0: {}
@@ -5356,12 +5853,14 @@ snapshots:
 
   is-reference@1.2.1:
     dependencies:
-      '@types/estree': 1.0.7
+      '@types/estree': 1.0.8
 
-  is-regex@1.1.4:
+  is-regex@1.2.1:
     dependencies:
-      call-bind: 1.0.7
+      call-bound: 1.0.4
+      gopd: 1.2.0
       has-tostringtag: 1.0.2
+      hasown: 2.0.2
 
   is-stream@2.0.1: {}
 
@@ -5369,6 +5868,10 @@ snapshots:
     dependencies:
       is-docker: 2.2.1
 
+  is-wsl@3.1.0:
+    dependencies:
+      is-inside-container: 1.0.0
+
   isarray@1.0.0: {}
 
   isexe@2.0.0: {}
@@ -5385,8 +5888,8 @@ snapshots:
 
   istanbul-lib-source-maps@5.0.6:
     dependencies:
-      '@jridgewell/trace-mapping': 0.3.25
-      debug: 4.4.0
+      '@jridgewell/trace-mapping': 0.3.29
+      debug: 4.4.1
       istanbul-lib-coverage: 3.2.2
     transitivePeerDependencies:
       - supports-color
@@ -5402,16 +5905,16 @@ snapshots:
     optionalDependencies:
       '@pkgjs/parseargs': 0.11.0
 
-  jackspeak@4.0.1:
+  jackspeak@4.1.1:
     dependencies:
       '@isaacs/cliui': 8.0.2
-    optionalDependencies:
-      '@pkgjs/parseargs': 0.11.0
 
   js-stringify@1.0.2: {}
 
   js-tokens@4.0.0: {}
 
+  js-tokens@9.0.1: {}
+
   js-yaml@4.1.0:
     dependencies:
       argparse: 2.0.1
@@ -5420,15 +5923,15 @@ snapshots:
 
   jsdom@26.1.0:
     dependencies:
-      cssstyle: 4.2.1
+      cssstyle: 4.6.0
       data-urls: 5.0.0
       decimal.js: 10.5.0
       html-encoding-sniffer: 4.0.0
       http-proxy-agent: 7.0.2
       https-proxy-agent: 7.0.6
       is-potential-custom-element-name: 1.0.1
-      nwsapi: 2.2.16
-      parse5: 7.2.1
+      nwsapi: 2.2.20
+      parse5: 7.3.0
       rrweb-cssom: 0.8.0
       saxes: 6.0.0
       symbol-tree: 3.2.4
@@ -5438,7 +5941,7 @@ snapshots:
       whatwg-encoding: 3.1.1
       whatwg-mimetype: 4.0.0
       whatwg-url: 14.2.0
-      ws: 8.18.1
+      ws: 8.18.3
       xml-name-validator: 5.0.0
     transitivePeerDependencies:
       - bufferutil
@@ -5492,10 +5995,10 @@ snapshots:
 
   lines-and-columns@1.2.4: {}
 
-  lint-staged@16.0.0:
+  lint-staged@16.1.2:
     dependencies:
       chalk: 5.4.1
-      commander: 13.1.0
+      commander: 14.0.0
       debug: 4.4.1
       lilconfig: 3.1.3
       listr2: 8.3.3
@@ -5536,26 +6039,24 @@ snapshots:
       strip-ansi: 7.1.0
       wrap-ansi: 9.0.0
 
-  loupe@3.1.3: {}
+  loupe@3.1.4: {}
 
   lru-cache@10.1.0: {}
 
   lru-cache@10.4.3: {}
 
-  lru-cache@11.0.0: {}
-
-  lru-cache@11.0.2: {}
+  lru-cache@11.1.0: {}
 
   lru-cache@7.18.3: {}
 
   magic-string@0.30.17:
     dependencies:
-      '@jridgewell/sourcemap-codec': 1.5.0
+      '@jridgewell/sourcemap-codec': 1.5.4
 
   magicast@0.3.5:
     dependencies:
-      '@babel/parser': 7.27.5
-      '@babel/types': 7.27.6
+      '@babel/parser': 7.28.0
+      '@babel/types': 7.28.0
       source-map-js: 1.2.1
 
   make-dir@4.0.0:
@@ -5566,6 +6067,8 @@ snapshots:
 
   marked@13.0.3: {}
 
+  math-intrinsics@1.1.0: {}
+
   memorystream@0.3.1: {}
 
   meow@13.2.0: {}
@@ -5587,7 +6090,7 @@ snapshots:
 
   mime-db@1.52.0: {}
 
-  mime-db@1.53.0: {}
+  mime-db@1.54.0: {}
 
   mime-types@2.1.18:
     dependencies:
@@ -5601,21 +6104,17 @@ snapshots:
 
   mimic-function@5.0.1: {}
 
-  minimatch@10.0.1:
-    dependencies:
-      brace-expansion: 2.0.1
-
   minimatch@10.0.3:
     dependencies:
       '@isaacs/brace-expansion': 5.0.0
 
   minimatch@3.1.2:
     dependencies:
-      brace-expansion: 1.1.11
+      brace-expansion: 1.1.12
 
   minimatch@9.0.5:
     dependencies:
-      brace-expansion: 2.0.1
+      brace-expansion: 2.0.2
 
   minimist@1.2.8: {}
 
@@ -5625,6 +6124,8 @@ snapshots:
 
   monaco-editor@0.52.2: {}
 
+  mrmime@2.0.1: {}
+
   ms@2.0.0: {}
 
   ms@2.1.3: {}
@@ -5633,7 +6134,7 @@ snapshots:
 
   nanoid@3.3.11: {}
 
-  napi-postinstall@0.2.3: {}
+  napi-postinstall@0.3.0: {}
 
   natural-compare@1.4.0: {}
 
@@ -5662,17 +6163,21 @@ snapshots:
       minimatch: 9.0.5
       pidtree: 0.6.0
       read-package-json-fast: 4.0.0
-      shell-quote: 1.8.1
+      shell-quote: 1.8.3
       which: 5.0.0
 
   npm-run-path@4.0.1:
     dependencies:
       path-key: 3.1.1
 
-  nwsapi@2.2.16: {}
+  nwsapi@2.2.20: {}
 
   object-assign@4.1.1: {}
 
+  on-finished@2.3.0:
+    dependencies:
+      ee-first: 1.1.1
+
   on-headers@1.0.2: {}
 
   once@1.4.0:
@@ -5687,6 +6192,13 @@ snapshots:
     dependencies:
       mimic-function: 5.0.1
 
+  open@10.1.2:
+    dependencies:
+      default-browser: 5.2.1
+      define-lazy-prop: 3.0.0
+      is-inside-container: 1.0.0
+      is-wsl: 3.1.0
+
   optionator@0.9.4:
     dependencies:
       deep-is: 0.1.4
@@ -5704,12 +6216,12 @@ snapshots:
     dependencies:
       p-limit: 3.1.0
 
-  pac-proxy-agent@7.1.0:
+  pac-proxy-agent@7.2.0:
     dependencies:
       '@tootallnate/quickjs-emscripten': 0.23.0
       agent-base: 7.1.3
       debug: 4.4.1
-      get-uri: 6.0.3
+      get-uri: 6.0.4
       http-proxy-agent: 7.0.2
       https-proxy-agent: 7.0.6
       pac-resolver: 7.0.1
@@ -5722,7 +6234,7 @@ snapshots:
       degenerator: 5.0.1
       netmask: 2.0.2
 
-  package-json-from-dist@1.0.0: {}
+  package-json-from-dist@1.0.1: {}
 
   pako@1.0.11: {}
 
@@ -5732,20 +6244,22 @@ snapshots:
 
   parse-json@5.2.0:
     dependencies:
-      '@babel/code-frame': 7.26.2
+      '@babel/code-frame': 7.27.1
       error-ex: 1.3.2
       json-parse-even-better-errors: 2.3.1
       lines-and-columns: 1.2.4
 
-  parse-json@8.1.0:
+  parse-json@8.3.0:
     dependencies:
-      '@babel/code-frame': 7.26.2
-      index-to-position: 0.1.2
-      type-fest: 4.24.0
+      '@babel/code-frame': 7.27.1
+      index-to-position: 1.1.0
+      type-fest: 4.41.0
 
-  parse5@7.2.1:
+  parse5@7.3.0:
     dependencies:
-      entities: 4.5.0
+      entities: 6.0.1
+
+  parseurl@1.3.3: {}
 
   path-exists@4.0.0: {}
 
@@ -5762,17 +6276,21 @@ snapshots:
 
   path-scurry@2.0.0:
     dependencies:
-      lru-cache: 11.0.0
+      lru-cache: 11.1.0
       minipass: 7.1.2
 
   path-to-regexp@3.3.0: {}
 
+  pathe@1.1.2: {}
+
   pathe@2.0.3: {}
 
-  pathval@2.0.0: {}
+  pathval@2.0.1: {}
 
   pend@1.2.0: {}
 
+  perfect-debounce@1.0.0: {}
+
   picocolors@1.1.1: {}
 
   picomatch@2.3.1: {}
@@ -5785,17 +6303,17 @@ snapshots:
     dependencies:
       postcss: 8.5.6
 
-  postcss-modules-local-by-default@4.0.5(postcss@8.5.6):
+  postcss-modules-local-by-default@4.2.0(postcss@8.5.6):
     dependencies:
       icss-utils: 5.1.0(postcss@8.5.6)
       postcss: 8.5.6
-      postcss-selector-parser: 6.1.2
+      postcss-selector-parser: 7.1.0
       postcss-value-parser: 4.2.0
 
-  postcss-modules-scope@3.2.0(postcss@8.5.6):
+  postcss-modules-scope@3.2.1(postcss@8.5.6):
     dependencies:
       postcss: 8.5.6
-      postcss-selector-parser: 6.1.2
+      postcss-selector-parser: 7.1.0
 
   postcss-modules-values@4.0.0(postcss@8.5.6):
     dependencies:
@@ -5809,16 +6327,11 @@ snapshots:
       lodash.camelcase: 4.3.0
       postcss: 8.5.6
       postcss-modules-extract-imports: 3.1.0(postcss@8.5.6)
-      postcss-modules-local-by-default: 4.0.5(postcss@8.5.6)
-      postcss-modules-scope: 3.2.0(postcss@8.5.6)
+      postcss-modules-local-by-default: 4.2.0(postcss@8.5.6)
+      postcss-modules-scope: 3.2.1(postcss@8.5.6)
       postcss-modules-values: 4.0.0(postcss@8.5.6)
       string-hash: 1.1.3
 
-  postcss-selector-parser@6.1.2:
-    dependencies:
-      cssesc: 3.0.0
-      util-deprecate: 1.0.2
-
   postcss-selector-parser@7.1.0:
     dependencies:
       cssesc: 3.0.0
@@ -5834,7 +6347,7 @@ snapshots:
 
   prelude-ls@1.2.1: {}
 
-  prettier@3.5.3: {}
+  prettier@3.6.2: {}
 
   pretty-bytes@6.1.1: {}
 
@@ -5853,7 +6366,7 @@ snapshots:
       http-proxy-agent: 7.0.2
       https-proxy-agent: 7.0.6
       lru-cache: 7.18.3
-      pac-proxy-agent: 7.1.0
+      pac-proxy-agent: 7.2.0
       proxy-from-env: 1.1.0
       socks-proxy-agent: 8.0.5
     transitivePeerDependencies:
@@ -5886,7 +6399,7 @@ snapshots:
       jstransformer: 1.0.0
       pug-error: 2.1.0
       pug-walk: 2.0.0
-      resolve: 1.22.8
+      resolve: 1.22.10
 
   pug-lexer@5.0.1:
     dependencies:
@@ -5928,9 +6441,9 @@ snapshots:
       pug-runtime: 3.0.1
       pug-strip-comments: 2.0.0
 
-  pump@3.0.0:
+  pump@3.0.3:
     dependencies:
-      end-of-stream: 1.4.4
+      end-of-stream: 1.4.5
       once: 1.4.0
 
   punycode@2.3.1: {}
@@ -5942,8 +6455,9 @@ snapshots:
       debug: 4.4.1
       devtools-protocol: 0.0.1439962
       typed-query-selector: 2.12.0
-      ws: 8.18.2
+      ws: 8.18.3
     transitivePeerDependencies:
+      - bare-buffer
       - bufferutil
       - supports-color
       - utf-8-validate
@@ -5957,6 +6471,7 @@ snapshots:
       puppeteer-core: 24.9.0
       typed-query-selector: 2.12.0
     transitivePeerDependencies:
+      - bare-buffer
       - bufferutil
       - supports-color
       - typescript
@@ -5964,8 +6479,6 @@ snapshots:
 
   queue-microtask@1.2.3: {}
 
-  queue-tick@1.0.1: {}
-
   range-parser@1.2.0: {}
 
   rc@1.2.8:
@@ -5982,16 +6495,16 @@ snapshots:
 
   read-package-up@11.0.0:
     dependencies:
-      find-up-simple: 1.0.0
+      find-up-simple: 1.0.1
       read-pkg: 9.0.1
-      type-fest: 4.24.0
+      type-fest: 4.41.0
 
   read-pkg@9.0.1:
     dependencies:
       '@types/normalize-package-data': 2.4.4
       normalize-package-data: 6.0.2
-      parse-json: 8.1.0
-      type-fest: 4.24.0
+      parse-json: 8.3.0
+      type-fest: 4.41.0
       unicorn-magic: 0.1.0
 
   readable-stream@2.3.8:
@@ -6004,7 +6517,7 @@ snapshots:
       string_decoder: 1.1.1
       util-deprecate: 1.0.2
 
-  readdirp@4.0.1: {}
+  readdirp@4.1.2: {}
 
   registry-auth-token@3.3.2:
     dependencies:
@@ -6023,9 +6536,9 @@ snapshots:
 
   resolve-pkg-maps@1.0.0: {}
 
-  resolve@1.22.8:
+  resolve@1.22.10:
     dependencies:
-      is-core-module: 2.15.0
+      is-core-module: 2.16.1
       path-parse: 1.0.7
       supports-preserve-symlinks-flag: 1.0.0
 
@@ -6040,8 +6553,8 @@ snapshots:
 
   rimraf@6.0.1:
     dependencies:
-      glob: 11.0.0
-      package-json-from-dist: 1.0.0
+      glob: 11.0.3
+      package-json-from-dist: 1.0.1
 
   rollup-plugin-dts@6.2.1(rollup@4.44.1)(typescript@5.6.3):
     dependencies:
@@ -6049,14 +6562,14 @@ snapshots:
       rollup: 4.44.1
       typescript: 5.6.3
     optionalDependencies:
-      '@babel/code-frame': 7.26.2
+      '@babel/code-frame': 7.27.1
 
   rollup-plugin-esbuild@6.2.1(esbuild@0.25.5)(rollup@4.44.1):
     dependencies:
-      debug: 4.4.0
-      es-module-lexer: 1.6.0
+      debug: 4.4.1
+      es-module-lexer: 1.7.0
       esbuild: 0.25.5
-      get-tsconfig: 4.10.0
+      get-tsconfig: 4.10.1
       rollup: 4.44.1
       unplugin-utils: 0.2.4
     transitivePeerDependencies:
@@ -6095,6 +6608,8 @@ snapshots:
 
   rrweb-cssom@0.8.0: {}
 
+  run-applescript@7.0.0: {}
+
   run-parallel@1.2.0:
     dependencies:
       queue-microtask: 1.2.3
@@ -6107,11 +6622,11 @@ snapshots:
 
   sass@1.89.2:
     dependencies:
-      chokidar: 4.0.1
-      immutable: 5.0.2
+      chokidar: 4.0.3
+      immutable: 5.1.3
       source-map-js: 1.2.1
     optionalDependencies:
-      '@parcel/watcher': 2.4.1
+      '@parcel/watcher': 2.5.1
 
   saxes@6.0.0:
     dependencies:
@@ -6145,15 +6660,6 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  set-function-length@1.2.2:
-    dependencies:
-      define-data-property: 1.1.4
-      es-errors: 1.3.0
-      function-bind: 1.1.2
-      get-intrinsic: 1.2.4
-      gopd: 1.0.1
-      has-property-descriptors: 1.0.2
-
   setimmediate@1.0.5: {}
 
   shebang-command@2.0.0:
@@ -6162,7 +6668,7 @@ snapshots:
 
   shebang-regex@3.0.0: {}
 
-  shell-quote@1.8.1: {}
+  shell-quote@1.8.3: {}
 
   siginfo@2.0.0: {}
 
@@ -6172,6 +6678,18 @@ snapshots:
 
   simple-git-hooks@2.13.0: {}
 
+  sirv@2.0.4:
+    dependencies:
+      '@polka/url': 1.0.0-next.29
+      mrmime: 2.0.1
+      totalist: 3.0.1
+
+  sirv@3.0.1:
+    dependencies:
+      '@polka/url': 1.0.0-next.29
+      mrmime: 2.0.1
+      totalist: 3.0.1
+
   slice-ansi@5.0.0:
     dependencies:
       ansi-styles: 6.2.1
@@ -6188,11 +6706,11 @@ snapshots:
     dependencies:
       agent-base: 7.1.3
       debug: 4.4.1
-      socks: 2.8.3
+      socks: 2.8.5
     transitivePeerDependencies:
       - supports-color
 
-  socks@2.8.3:
+  socks@2.8.5:
     dependencies:
       ip-address: 9.0.5
       smart-buffer: 4.2.0
@@ -6204,32 +6722,33 @@ snapshots:
   spdx-correct@3.2.0:
     dependencies:
       spdx-expression-parse: 3.0.1
-      spdx-license-ids: 3.0.18
+      spdx-license-ids: 3.0.21
 
   spdx-exceptions@2.5.0: {}
 
   spdx-expression-parse@3.0.1:
     dependencies:
       spdx-exceptions: 2.5.0
-      spdx-license-ids: 3.0.18
+      spdx-license-ids: 3.0.21
 
-  spdx-license-ids@3.0.18: {}
+  spdx-license-ids@3.0.21: {}
 
   sprintf-js@1.1.3: {}
 
-  stable-hash@0.0.5: {}
+  stable-hash-x@0.2.0: {}
 
   stackback@0.0.2: {}
 
+  statuses@1.5.0: {}
+
   std-env@3.9.0: {}
 
-  streamx@2.18.0:
+  streamx@2.22.1:
     dependencies:
       fast-fifo: 1.3.2
-      queue-tick: 1.0.1
-      text-decoder: 1.1.1
+      text-decoder: 1.2.3
     optionalDependencies:
-      bare-events: 2.4.2
+      bare-events: 2.5.4
 
   string-argv@0.3.2: {}
 
@@ -6249,8 +6768,8 @@ snapshots:
 
   string-width@7.2.0:
     dependencies:
-      emoji-regex: 10.3.0
-      get-east-asian-width: 1.2.0
+      emoji-regex: 10.4.0
+      get-east-asian-width: 1.3.0
       strip-ansi: 7.1.0
 
   string_decoder@1.1.1:
@@ -6263,7 +6782,7 @@ snapshots:
 
   strip-ansi@7.1.0:
     dependencies:
-      ansi-regex: 6.0.1
+      ansi-regex: 6.1.0
 
   strip-final-newline@2.0.0: {}
 
@@ -6271,6 +6790,10 @@ snapshots:
 
   strip-json-comments@3.1.1: {}
 
+  strip-literal@3.0.0:
+    dependencies:
+      js-tokens: 9.0.1
+
   supports-color@7.2.0:
     dependencies:
       has-flag: 4.0.0
@@ -6279,19 +6802,21 @@ snapshots:
 
   symbol-tree@3.2.4: {}
 
-  tar-fs@3.0.8:
+  tar-fs@3.1.0:
     dependencies:
-      pump: 3.0.0
+      pump: 3.0.3
       tar-stream: 3.1.7
     optionalDependencies:
-      bare-fs: 4.0.1
+      bare-fs: 4.1.5
       bare-path: 3.0.0
+    transitivePeerDependencies:
+      - bare-buffer
 
   tar-stream@3.1.7:
     dependencies:
-      b4a: 1.6.6
+      b4a: 1.6.7
       fast-fifo: 1.3.2
-      streamx: 2.18.0
+      streamx: 2.22.1
 
   temp-dir@3.0.0: {}
 
@@ -6305,24 +6830,24 @@ snapshots:
       glob: 10.4.5
       minimatch: 9.0.5
 
-  text-decoder@1.1.1:
+  text-decoder@1.2.3:
     dependencies:
-      b4a: 1.6.6
+      b4a: 1.6.7
 
   tinybench@2.9.0: {}
 
   tinyexec@0.3.2: {}
 
-  tinyglobby@0.2.13:
+  tinyglobby@0.2.14:
     dependencies:
-      fdir: 6.4.4(picomatch@4.0.2)
+      fdir: 6.4.6(picomatch@4.0.2)
       picomatch: 4.0.2
 
-  tinypool@1.0.2: {}
+  tinypool@1.1.1: {}
 
   tinyrainbow@2.0.0: {}
 
-  tinyspy@3.0.2: {}
+  tinyspy@4.0.3: {}
 
   tldts-core@6.1.86: {}
 
@@ -6338,6 +6863,8 @@ snapshots:
 
   token-stream@1.0.0: {}
 
+  totalist@3.0.1: {}
+
   tough-cookie@5.1.2:
     dependencies:
       tldts: 6.1.86
@@ -6358,23 +6885,23 @@ snapshots:
 
   type-fest@2.19.0: {}
 
-  type-fest@4.24.0: {}
+  type-fest@4.41.0: {}
 
   typed-query-selector@2.12.0: {}
 
-  typescript-eslint@8.32.1(eslint@9.27.0)(typescript@5.6.3):
+  typescript-eslint@8.35.1(eslint@9.30.1)(typescript@5.6.3):
     dependencies:
-      '@typescript-eslint/eslint-plugin': 8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0)(typescript@5.6.3))(eslint@9.27.0)(typescript@5.6.3)
-      '@typescript-eslint/parser': 8.32.1(eslint@9.27.0)(typescript@5.6.3)
-      '@typescript-eslint/utils': 8.32.1(eslint@9.27.0)(typescript@5.6.3)
-      eslint: 9.27.0
+      '@typescript-eslint/eslint-plugin': 8.35.1(@typescript-eslint/parser@8.35.1(eslint@9.30.1)(typescript@5.6.3))(eslint@9.30.1)(typescript@5.6.3)
+      '@typescript-eslint/parser': 8.35.1(eslint@9.30.1)(typescript@5.6.3)
+      '@typescript-eslint/utils': 8.35.1(eslint@9.30.1)(typescript@5.6.3)
+      eslint: 9.30.1
       typescript: 5.6.3
     transitivePeerDependencies:
       - supports-color
 
   typescript@5.6.3: {}
 
-  uglify-js@3.19.1:
+  uglify-js@3.19.3:
     optional: true
 
   undici-types@6.21.0: {}
@@ -6383,32 +6910,36 @@ snapshots:
 
   universalify@2.0.1: {}
 
+  unpipe@1.0.0: {}
+
   unplugin-utils@0.2.4:
     dependencies:
       pathe: 2.0.3
       picomatch: 4.0.2
 
-  unrs-resolver@1.7.2:
+  unrs-resolver@1.10.1:
     dependencies:
-      napi-postinstall: 0.2.3
+      napi-postinstall: 0.3.0
     optionalDependencies:
-      '@unrs/resolver-binding-darwin-arm64': 1.7.2
-      '@unrs/resolver-binding-darwin-x64': 1.7.2
-      '@unrs/resolver-binding-freebsd-x64': 1.7.2
-      '@unrs/resolver-binding-linux-arm-gnueabihf': 1.7.2
-      '@unrs/resolver-binding-linux-arm-musleabihf': 1.7.2
-      '@unrs/resolver-binding-linux-arm64-gnu': 1.7.2
-      '@unrs/resolver-binding-linux-arm64-musl': 1.7.2
-      '@unrs/resolver-binding-linux-ppc64-gnu': 1.7.2
-      '@unrs/resolver-binding-linux-riscv64-gnu': 1.7.2
-      '@unrs/resolver-binding-linux-riscv64-musl': 1.7.2
-      '@unrs/resolver-binding-linux-s390x-gnu': 1.7.2
-      '@unrs/resolver-binding-linux-x64-gnu': 1.7.2
-      '@unrs/resolver-binding-linux-x64-musl': 1.7.2
-      '@unrs/resolver-binding-wasm32-wasi': 1.7.2
-      '@unrs/resolver-binding-win32-arm64-msvc': 1.7.2
-      '@unrs/resolver-binding-win32-ia32-msvc': 1.7.2
-      '@unrs/resolver-binding-win32-x64-msvc': 1.7.2
+      '@unrs/resolver-binding-android-arm-eabi': 1.10.1
+      '@unrs/resolver-binding-android-arm64': 1.10.1
+      '@unrs/resolver-binding-darwin-arm64': 1.10.1
+      '@unrs/resolver-binding-darwin-x64': 1.10.1
+      '@unrs/resolver-binding-freebsd-x64': 1.10.1
+      '@unrs/resolver-binding-linux-arm-gnueabihf': 1.10.1
+      '@unrs/resolver-binding-linux-arm-musleabihf': 1.10.1
+      '@unrs/resolver-binding-linux-arm64-gnu': 1.10.1
+      '@unrs/resolver-binding-linux-arm64-musl': 1.10.1
+      '@unrs/resolver-binding-linux-ppc64-gnu': 1.10.1
+      '@unrs/resolver-binding-linux-riscv64-gnu': 1.10.1
+      '@unrs/resolver-binding-linux-riscv64-musl': 1.10.1
+      '@unrs/resolver-binding-linux-s390x-gnu': 1.10.1
+      '@unrs/resolver-binding-linux-x64-gnu': 1.10.1
+      '@unrs/resolver-binding-linux-x64-musl': 1.10.1
+      '@unrs/resolver-binding-wasm32-wasi': 1.10.1
+      '@unrs/resolver-binding-win32-arm64-msvc': 1.10.1
+      '@unrs/resolver-binding-win32-ia32-msvc': 1.10.1
+      '@unrs/resolver-binding-win32-x64-msvc': 1.10.1
 
   update-check@1.5.4:
     dependencies:
@@ -6421,6 +6952,8 @@ snapshots:
 
   util-deprecate@1.0.2: {}
 
+  utils-merge@1.0.1: {}
+
   validate-npm-package-license@3.0.4:
     dependencies:
       spdx-correct: 3.2.0
@@ -6428,12 +6961,29 @@ snapshots:
 
   vary@1.1.2: {}
 
-  vite-node@3.1.4(@types/node@22.16.0)(sass@1.89.2):
+  vite-hyper-config@0.4.1(@types/node@22.16.0)(sass@1.89.2)(vite@6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0)):
+    dependencies:
+      cac: 6.7.14
+      picocolors: 1.1.1
+      vite: 6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0)
+      vite-node: 2.1.9(@types/node@22.16.0)(sass@1.89.2)
+    transitivePeerDependencies:
+      - '@types/node'
+      - less
+      - lightningcss
+      - sass
+      - sass-embedded
+      - stylus
+      - sugarss
+      - supports-color
+      - terser
+
+  vite-node@2.1.9(@types/node@22.16.0)(sass@1.89.2):
     dependencies:
       cac: 6.7.14
       debug: 4.4.1
       es-module-lexer: 1.7.0
-      pathe: 2.0.3
+      pathe: 1.1.2
       vite: 5.4.19(@types/node@22.16.0)(sass@1.89.2)
     transitivePeerDependencies:
       - '@types/node'
@@ -6446,7 +6996,44 @@ snapshots:
       - supports-color
       - terser
 
-  vite@5.4.15(@types/node@22.16.0)(sass@1.89.2):
+  vite-node@3.2.4(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0):
+    dependencies:
+      cac: 6.7.14
+      debug: 4.4.1
+      es-module-lexer: 1.7.0
+      pathe: 2.0.3
+      vite: 6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0)
+    transitivePeerDependencies:
+      - '@types/node'
+      - jiti
+      - less
+      - lightningcss
+      - sass
+      - sass-embedded
+      - stylus
+      - sugarss
+      - supports-color
+      - terser
+      - tsx
+      - yaml
+
+  vite-plugin-inspect@0.8.9(rollup@4.44.1)(vite@6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0)):
+    dependencies:
+      '@antfu/utils': 0.7.10
+      '@rollup/pluginutils': 5.2.0(rollup@4.44.1)
+      debug: 4.4.1
+      error-stack-parser-es: 0.1.5
+      fs-extra: 11.3.0
+      open: 10.1.2
+      perfect-debounce: 1.0.0
+      picocolors: 1.1.1
+      sirv: 3.0.1
+      vite: 6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0)
+    transitivePeerDependencies:
+      - rollup
+      - supports-color
+
+  vite@5.4.19(@types/node@22.16.0)(sass@1.89.2):
     dependencies:
       esbuild: 0.21.5
       postcss: 8.5.6
@@ -6456,43 +7043,51 @@ snapshots:
       fsevents: 2.3.3
       sass: 1.89.2
 
-  vite@5.4.19(@types/node@22.16.0)(sass@1.89.2):
+  vite@6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0):
     dependencies:
-      esbuild: 0.21.5
+      esbuild: 0.25.5
+      fdir: 6.4.6(picomatch@4.0.2)
+      picomatch: 4.0.2
       postcss: 8.5.6
       rollup: 4.44.1
+      tinyglobby: 0.2.14
     optionalDependencies:
       '@types/node': 22.16.0
       fsevents: 2.3.3
       sass: 1.89.2
+      yaml: 2.8.0
 
-  vitest@3.1.4(@types/node@22.16.0)(jsdom@26.1.0)(sass@1.89.2):
+  vitest@3.2.4(@types/node@22.16.0)(@vitest/ui@3.2.4)(jsdom@26.1.0)(sass@1.89.2)(yaml@2.8.0):
     dependencies:
-      '@vitest/expect': 3.1.4
-      '@vitest/mocker': 3.1.4(vite@5.4.19(@types/node@22.16.0)(sass@1.89.2))
-      '@vitest/pretty-format': 3.1.4
-      '@vitest/runner': 3.1.4
-      '@vitest/snapshot': 3.1.4
-      '@vitest/spy': 3.1.4
-      '@vitest/utils': 3.1.4
+      '@types/chai': 5.2.2
+      '@vitest/expect': 3.2.4
+      '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0))
+      '@vitest/pretty-format': 3.2.4
+      '@vitest/runner': 3.2.4
+      '@vitest/snapshot': 3.2.4
+      '@vitest/spy': 3.2.4
+      '@vitest/utils': 3.2.4
       chai: 5.2.0
-      debug: 4.4.0
+      debug: 4.4.1
       expect-type: 1.2.1
       magic-string: 0.30.17
       pathe: 2.0.3
+      picomatch: 4.0.2
       std-env: 3.9.0
       tinybench: 2.9.0
       tinyexec: 0.3.2
-      tinyglobby: 0.2.13
-      tinypool: 1.0.2
+      tinyglobby: 0.2.14
+      tinypool: 1.1.1
       tinyrainbow: 2.0.0
-      vite: 5.4.19(@types/node@22.16.0)(sass@1.89.2)
-      vite-node: 3.1.4(@types/node@22.16.0)(sass@1.89.2)
+      vite: 6.3.5(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0)
+      vite-node: 3.2.4(@types/node@22.16.0)(sass@1.89.2)(yaml@2.8.0)
       why-is-node-running: 2.3.0
     optionalDependencies:
       '@types/node': 22.16.0
+      '@vitest/ui': 3.2.4(vitest@3.2.4)
       jsdom: 26.1.0
     transitivePeerDependencies:
+      - jiti
       - less
       - lightningcss
       - msw
@@ -6502,9 +7097,25 @@ snapshots:
       - sugarss
       - supports-color
       - terser
+      - tsx
+      - yaml
 
   void-elements@3.1.0: {}
 
+  vue-demi@0.14.10(vue@packages+vue):
+    dependencies:
+      vue: link:packages/vue
+
+  vue@3.5.17(typescript@5.6.3):
+    dependencies:
+      '@vue/compiler-dom': 3.5.17
+      '@vue/compiler-sfc': 3.5.17
+      '@vue/runtime-dom': 3.5.17
+      '@vue/server-renderer': 3.5.17(vue@3.5.17(typescript@5.6.3))
+      '@vue/shared': 3.5.17
+    optionalDependencies:
+      typescript: 5.6.3
+
   w3c-xmlserializer@5.0.0:
     dependencies:
       xml-name-validator: 5.0.0
@@ -6541,9 +7152,9 @@ snapshots:
 
   with@7.0.2:
     dependencies:
-      '@babel/parser': 7.27.5
-      '@babel/types': 7.27.6
-      assert-never: 1.3.0
+      '@babel/parser': 7.28.0
+      '@babel/types': 7.28.0
+      assert-never: 1.4.0
       babel-walk: 3.0.0-canary-5
 
   word-wrap@1.2.5: {}
@@ -6570,9 +7181,7 @@ snapshots:
 
   wrappy@1.0.2: {}
 
-  ws@8.18.1: {}
-
-  ws@8.18.2: {}
+  ws@8.18.3: {}
 
   xml-name-validator@5.0.0: {}
 
@@ -6587,7 +7196,7 @@ snapshots:
   yargs@17.7.2:
     dependencies:
       cliui: 8.0.1
-      escalade: 3.1.2
+      escalade: 3.2.0
       get-caller-file: 2.0.5
       require-directory: 2.1.1
       string-width: 4.2.3
@@ -6601,4 +7210,4 @@ snapshots:
 
   yocto-queue@0.1.0: {}
 
-  zod@3.24.1: {}
+  zod@3.25.72: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 5113b10daed..51693b02d65 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -6,10 +6,10 @@ catalog:
   '@babel/parser': ^7.27.5
   '@babel/types': ^7.27.6
   'estree-walker': ^2.0.2
+  'vite': ^6.1.0
+  '@vitejs/plugin-vue': ^5.2.4
   'magic-string': ^0.30.17
   'source-map-js': ^1.2.1
-  'vite': ^5.4.15
-  '@vitejs/plugin-vue': ^5.2.4
 
 onlyBuiltDependencies:
   - '@swc/core'
diff --git a/rollup.config.js b/rollup.config.js
index da7de554b64..1fa345f87fc 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -15,13 +15,14 @@ import alias from '@rollup/plugin-alias'
 import { entries } from './scripts/aliases.js'
 import { inlineEnums } from './scripts/inline-enums.js'
 import { minify as minifySwc } from '@swc/core'
+import { trimVaporExportsPlugin } from './scripts/trim-vapor-exports.js'
 
 /**
  * @template T
  * @template {keyof T} K
  * @typedef { Omit<T, K> & Required<Pick<T, K>> } MarkRequired
  */
-/** @typedef {'cjs' | 'esm-bundler' | 'global' | 'global-runtime' | 'esm-browser' | 'esm-bundler-runtime' | 'esm-browser-runtime'} PackageFormat */
+/** @typedef {'cjs' | 'esm-bundler' | 'global' | 'global-runtime' | 'esm-browser' | 'esm-bundler-runtime' | 'esm-browser-runtime' | 'esm-browser-vapor'} PackageFormat */
 /** @typedef {MarkRequired<import('rollup').OutputOptions, 'file' | 'format'>} OutputOptions */
 
 if (!process.env.TARGET) {
@@ -85,13 +86,19 @@ const outputConfigs = {
     file: resolve(`dist/${name}.runtime.global.js`),
     format: 'iife',
   },
+  // The vapor format is a esm-browser + runtime only build that is meant for
+  // the SFC playground only.
+  'esm-browser-vapor': {
+    file: resolve(`dist/${name}.runtime-with-vapor.esm-browser.js`),
+    format: 'es',
+  },
 }
 
 /** @type {ReadonlyArray<PackageFormat>} */
 const defaultFormats = ['esm-bundler', 'cjs']
 /** @type {ReadonlyArray<PackageFormat>} */
 const inlineFormats = /** @type {any} */ (
-  process.env.FORMATS && process.env.FORMATS.split(',')
+  process.env.FORMATS && process.env.FORMATS.split('+')
 )
 /** @type {ReadonlyArray<PackageFormat>} */
 const packageFormats = inlineFormats || packageOptions.formats || defaultFormats
@@ -107,7 +114,10 @@ if (process.env.NODE_ENV === 'production') {
     if (format === 'cjs') {
       packageConfigs.push(createProductionConfig(format))
     }
-    if (/^(global|esm-browser)(-runtime)?/.test(format)) {
+    if (
+      format === 'esm-browser-vapor' ||
+      /^(global|esm-browser)(-runtime)?/.test(format)
+    ) {
       packageConfigs.push(createMinifiedConfig(format))
     }
   })
@@ -157,15 +167,59 @@ function createConfig(format, output, plugins = []) {
     output.name = packageOptions.name
   }
 
-  let entryFile = /runtime$/.test(format) ? `src/runtime.ts` : `src/index.ts`
+  let entryFile = 'index.ts'
+  if (pkg.name === 'vue') {
+    if (format === 'esm-browser-vapor' || format === 'esm-bundler-runtime') {
+      entryFile = 'runtime-with-vapor.ts'
+    } else if (format === 'esm-bundler') {
+      entryFile = 'index-with-vapor.ts'
+    } else if (format.includes('runtime')) {
+      entryFile = 'runtime.ts'
+    }
+  }
 
   // the compat build needs both default AND named exports. This will cause
   // Rollup to complain for non-ESM targets, so we use separate entries for
   // esm vs. non-esm builds.
   if (isCompatPackage && (isBrowserESMBuild || isBundlerESMBuild)) {
-    entryFile = /runtime$/.test(format)
-      ? `src/esm-runtime.ts`
-      : `src/esm-index.ts`
+    entryFile = `esm-${entryFile}`
+  }
+  entryFile = 'src/' + entryFile
+
+  return {
+    input: resolve(entryFile),
+    // Global and Browser ESM builds inlines everything so that they can be
+    // used alone.
+    external: resolveExternal(),
+    plugins: [
+      ...trimVaporExportsPlugin(format, pkg.name),
+      json({
+        namedExports: false,
+      }),
+      alias({
+        entries,
+      }),
+      enumPlugin,
+      ...resolveReplace(),
+      esbuild({
+        tsconfig: path.resolve(__dirname, 'tsconfig.json'),
+        sourceMap: output.sourcemap,
+        minify: false,
+        target: isServerRenderer || isCJSBuild ? 'es2019' : 'es2016',
+        define: resolveDefine(),
+      }),
+      ...resolveNodePlugins(),
+      ...plugins,
+    ],
+    output,
+    onwarn(msg, warn) {
+      if (msg.code !== 'CIRCULAR_DEPENDENCY') {
+        warn(msg)
+      }
+    },
+    treeshake: {
+      moduleSideEffects: false,
+    },
   }
 
   function resolveDefine() {
@@ -184,6 +238,7 @@ function createConfig(format, output, plugins = []) {
       __CJS__: String(isCJSBuild),
       // need SSR-specific branches?
       __SSR__: String(!isGlobalBuild),
+      __BENCHMARK__: process.env.BENCHMARK || 'false',
 
       // 2.x compat build
       __COMPAT__: String(isCompatBuild),
@@ -259,6 +314,7 @@ function createConfig(format, output, plugins = []) {
     const treeShakenDeps = [
       'source-map-js',
       '@babel/parser',
+      '@babel/types',
       'estree-walker',
       'entities/lib/decode.js',
     ]
@@ -319,47 +375,12 @@ function createConfig(format, output, plugins = []) {
 
     return nodePlugins
   }
-
-  return {
-    input: resolve(entryFile),
-    // Global and Browser ESM builds inlines everything so that they can be
-    // used alone.
-    external: resolveExternal(),
-    plugins: [
-      json({
-        namedExports: false,
-      }),
-      alias({
-        entries,
-      }),
-      enumPlugin,
-      ...resolveReplace(),
-      esbuild({
-        tsconfig: path.resolve(__dirname, 'tsconfig.json'),
-        sourceMap: output.sourcemap,
-        minify: false,
-        target: isServerRenderer || isCJSBuild ? 'es2019' : 'es2016',
-        define: resolveDefine(),
-      }),
-      ...resolveNodePlugins(),
-      ...plugins,
-    ],
-    output,
-    onwarn: (msg, warn) => {
-      if (msg.code !== 'CIRCULAR_DEPENDENCY') {
-        warn(msg)
-      }
-    },
-    treeshake: {
-      moduleSideEffects: false,
-    },
-  }
 }
 
 function createProductionConfig(/** @type {PackageFormat} */ format) {
   return createConfig(format, {
+    ...outputConfigs[format],
     file: resolve(`dist/${name}.${format}.prod.js`),
-    format: outputConfigs[format].format,
   })
 }
 
@@ -367,8 +388,8 @@ function createMinifiedConfig(/** @type {PackageFormat} */ format) {
   return createConfig(
     format,
     {
+      ...outputConfigs[format],
       file: outputConfigs[format].file.replace(/\.js$/, '.prod.js'),
-      format: outputConfigs[format].format,
     },
     [
       {
diff --git a/rollup.dts.config.js b/rollup.dts.config.js
index d9af98f1306..5ec8f321137 100644
--- a/rollup.dts.config.js
+++ b/rollup.dts.config.js
@@ -22,7 +22,7 @@ export default targetPackages.map(
   /** @returns {import('rollup').RollupOptions} */
   pkg => {
     return {
-      input: `./temp/packages/${pkg}/src/index.d.ts`,
+      input: `./temp/packages/${pkg}/src/index${pkg === 'vue' ? '-with-vapor' : ''}.d.ts`,
       output: {
         file: `packages/${pkg}/dist/${pkg}.d.ts`,
         format: 'es',
diff --git a/scripts/build.js b/scripts/build.js
index 02212a70e0c..5b7e9e96fc7 100644
--- a/scripts/build.js
+++ b/scripts/build.js
@@ -5,14 +5,17 @@ Produces production builds and stitches together d.ts files.
 
 To specify the package to build, simply pass its name and the desired build
 formats to output (defaults to `buildOptions.formats` specified in that package,
-or "esm,cjs"):
+or ["esm-bundler", "cjs"]):
 
 ```
 # name supports fuzzy match. will build all packages with name containing "dom":
 nr build dom
 
 # specify the format to output
-nr build core --formats cjs
+nr build vue -f cjs
+
+# to specify multiple formats, separate with "+":
+nr build vue -f esm-bundler+esm-browser
 ```
 */
 
@@ -169,6 +172,23 @@ async function build(target) {
     return
   }
 
+  let resolvedFormats
+  if (formats) {
+    const isNegation = formats.startsWith('~')
+    resolvedFormats = (isNegation ? formats.slice(1) : formats).split('+')
+    const pkgFormats = pkg.buildOptions?.formats
+    if (pkgFormats) {
+      if (isNegation) {
+        resolvedFormats = pkgFormats.filter(f => !resolvedFormats.includes(f))
+      } else {
+        resolvedFormats = resolvedFormats.filter(f => pkgFormats.includes(f))
+      }
+    }
+    if (!resolvedFormats.length) {
+      return
+    }
+  }
+
   // if building a specific format, do not remove dist.
   if (!formats && fs.existsSync(`${pkgDir}/dist`)) {
     fs.rmSync(`${pkgDir}/dist`, { recursive: true })
@@ -187,7 +207,7 @@ async function build(target) {
         `COMMIT:${commit}`,
         `NODE_ENV:${env}`,
         `TARGET:${target}`,
-        formats ? `FORMATS:${formats}` : ``,
+        resolvedFormats ? `FORMATS:${resolvedFormats.join('+')}` : ``,
         prodOnly ? `PROD_ONLY:true` : ``,
         sourceMap ? `SOURCE_MAP:true` : ``,
       ]
@@ -204,7 +224,10 @@ async function build(target) {
  * @returns {Promise<void>}
  */
 async function checkAllSizes(targets) {
-  if (devOnly || (formats && !formats.includes('global'))) {
+  if (
+    devOnly ||
+    (formats && (formats.startsWith('~') || !formats.includes('global')))
+  ) {
     return
   }
   console.log()
diff --git a/scripts/dev.js b/scripts/dev.js
index fb4d3873e8b..f8690749221 100644
--- a/scripts/dev.js
+++ b/scripts/dev.js
@@ -49,9 +49,12 @@ const outputFormat = format.startsWith('global')
     ? 'cjs'
     : 'esm'
 
-const postfix = format.endsWith('-runtime')
-  ? `runtime.${format.replace(/-runtime$/, '')}`
-  : format
+const postfix =
+  format === 'esm-browser-vapor'
+    ? 'runtime-with-vapor.esm-browser'
+    : format.endsWith('-runtime')
+      ? `runtime.${format.replace(/-runtime$/, '')}`
+      : format
 
 const privatePackages = fs.readdirSync('packages-private')
 
@@ -127,9 +130,16 @@ for (const target of targets) {
     plugins.push(polyfillNode())
   }
 
+  const entry =
+    format === 'esm-browser-vapor'
+      ? 'runtime-with-vapor.ts'
+      : format.endsWith('-runtime')
+        ? 'runtime.ts'
+        : 'index.ts'
+
   esbuild
     .context({
-      entryPoints: [resolve(__dirname, `${pkgBasePath}/src/index.ts`)],
+      entryPoints: [resolve(__dirname, `${pkgBasePath}/src/${entry}`)],
       outfile,
       bundle: true,
       external,
@@ -151,6 +161,7 @@ for (const target of targets) {
         __ESM_BROWSER__: String(format.includes('esm-browser')),
         __CJS__: String(format === 'cjs'),
         __SSR__: String(format !== 'global'),
+        __BENCHMARK__: process.env.BENCHMARK || 'false',
         __COMPAT__: String(target === 'vue-compat'),
         __FEATURE_SUSPENSE__: `true`,
         __FEATURE_OPTIONS_API__: `true`,
diff --git a/scripts/inline-enums.js b/scripts/inline-enums.js
index b1baaa6c5c3..4e582e25185 100644
--- a/scripts/inline-enums.js
+++ b/scripts/inline-enums.js
@@ -198,7 +198,9 @@ export function scanEnums() {
   }
 
   // 3. save cache
-  if (!existsSync('temp')) mkdirSync('temp')
+  try {
+    mkdirSync('temp')
+  } catch {}
 
   /** @type {EnumData} */
   const enumData = {
diff --git a/scripts/pre-dev-sfc.js b/scripts/prepare-cjs.js
similarity index 95%
rename from scripts/pre-dev-sfc.js
rename to scripts/prepare-cjs.js
index b28705f3464..3e427e2b77f 100644
--- a/scripts/pre-dev-sfc.js
+++ b/scripts/prepare-cjs.js
@@ -6,6 +6,7 @@ const packagesToCheck = [
   'compiler-core',
   'compiler-dom',
   'compiler-ssr',
+  'compiler-vapor',
   'shared',
 ]
 
diff --git a/scripts/release.js b/scripts/release.js
index cd287dfc12b..9872a1f3f37 100644
--- a/scripts/release.js
+++ b/scripts/release.js
@@ -6,7 +6,7 @@ import semver from 'semver'
 import enquirer from 'enquirer'
 import { createRequire } from 'node:module'
 import { fileURLToPath } from 'node:url'
-import { exec } from './utils.js'
+import { exec, getSha } from './utils.js'
 import { parseArgs } from 'node:util'
 
 /**
@@ -425,10 +425,6 @@ async function isInSyncWithRemote() {
   }
 }
 
-async function getSha() {
-  return (await exec('git', ['rev-parse', 'HEAD'])).stdout
-}
-
 async function getBranch() {
   return (await exec('git', ['rev-parse', '--abbrev-ref', 'HEAD'])).stdout
 }
diff --git a/scripts/trim-vapor-exports.js b/scripts/trim-vapor-exports.js
new file mode 100644
index 00000000000..313a1a9b8ea
--- /dev/null
+++ b/scripts/trim-vapor-exports.js
@@ -0,0 +1,36 @@
+// @ts-check
+
+/**
+ * runtime-core exports a number of internal helpers that are only used by
+ * runtime-vapor, which should be only preserved in vapor / esm-bundler builds.
+ * This plugin should be included as the first plugin for all other formats
+ * other than vapor / esm-bundler.
+ *
+ * @param {string} format
+ * @param {string} pkgName
+ * @returns {import('rollup').Plugin[]}
+ */
+export function trimVaporExportsPlugin(format, pkgName) {
+  if (
+    format.includes('vapor') ||
+    format.startsWith('esm-bundler') ||
+    pkgName === '@vue/runtime-vapor'
+  ) {
+    return []
+  } else {
+    return [
+      {
+        name: 'trim-vapor-exports',
+        transform(code, id) {
+          if (
+            id.endsWith('runtime-core/src/index.ts') ||
+            id.endsWith('runtime-dom/src/index.ts')
+          ) {
+            const index = code.lastIndexOf('// VAPOR ---')
+            return code.slice(0, index)
+          }
+        },
+      },
+    ]
+  }
+}
diff --git a/scripts/usage-size.js b/scripts/usage-size.js
index c68e3703b91..a1a9da69771 100644
--- a/scripts/usage-size.js
+++ b/scripts/usage-size.js
@@ -22,12 +22,12 @@ const {
 })
 
 const sizeDir = path.resolve('temp/size')
-const entry = path.resolve('./packages/vue/dist/vue.runtime.esm-bundler.js')
+const vuePath = path.resolve('./packages/vue/dist/vue.runtime.esm-bundler.js')
 
 /**
  * @typedef {Object} Preset
  * @property {string} name - The name of the preset
- * @property {string[]} imports - The imports that are part of this preset
+ * @property {'*' | string[]} imports - The imports that are part of this preset
  * @property {Record<string, string>} [replace]
  */
 
@@ -39,6 +39,11 @@ const presets = [
     replace: { __VUE_OPTIONS_API__: 'false' },
   },
   { name: 'createApp', imports: ['createApp'] },
+  {
+    name: 'createApp + vaporInteropPlugin',
+    imports: ['createApp', 'vaporInteropPlugin'],
+  },
+  { name: 'createVaporApp', imports: ['createVaporApp'] },
   { name: 'createSSRApp', imports: ['createSSRApp'] },
   { name: 'defineCustomElement', imports: ['defineCustomElement'] },
   {
@@ -93,8 +98,11 @@ async function main() {
  */
 async function generateBundle(preset) {
   const id = 'virtual:entry'
-  const content = `export { ${preset.imports.join(', ')} } from '${entry}'`
-
+  const exportSpecifiers =
+    preset.imports === '*'
+      ? `* as ${preset.name}`
+      : `{ ${preset.imports.join(', ')} }`
+  const content = `export ${exportSpecifiers} from '${vuePath}'`
   const result = await rollup({
     input: id,
     plugins: [
diff --git a/scripts/utils.js b/scripts/utils.js
index 3c92bf7bafb..cad185a4bc2 100644
--- a/scripts/utils.js
+++ b/scripts/utils.js
@@ -3,19 +3,22 @@ import fs from 'node:fs'
 import pico from 'picocolors'
 import { createRequire } from 'node:module'
 import { spawn } from 'node:child_process'
+import path from 'node:path'
 
 const require = createRequire(import.meta.url)
+const packagesPath = path.resolve(import.meta.dirname, '../packages')
 
 export const targets = fs
-  .readdirSync('packages')
+  .readdirSync(packagesPath)
   .filter(f => {
+    const folder = path.resolve(packagesPath, f)
     if (
-      !fs.statSync(`packages/${f}`).isDirectory() ||
-      !fs.existsSync(`packages/${f}/package.json`)
+      !fs.statSync(folder).isDirectory() ||
+      !fs.existsSync(`${folder}/package.json`)
     ) {
       return false
     }
-    const pkg = require(`../packages/${f}/package.json`)
+    const pkg = require(`${folder}/package.json`)
     if (pkg.private && !pkg.buildOptions) {
       return false
     }
@@ -32,6 +35,10 @@ export function fuzzyMatchTarget(partialTargets, includeAllMatching) {
   /** @type {Array<string>} */
   const matched = []
   partialTargets.forEach(partialTarget => {
+    if (!includeAllMatching && targets.includes(partialTarget)) {
+      matched.push(partialTarget)
+      return
+    }
     for (const target of targets) {
       if (target.match(partialTarget)) {
         matched.push(target)
@@ -60,6 +67,7 @@ export function fuzzyMatchTarget(partialTargets, includeAllMatching) {
  * @param {string} command
  * @param {ReadonlyArray<string>} args
  * @param {object} [options]
+ * @returns {Promise<{ ok: boolean, code: number | null, stderr: string, stdout: string }>}
  */
 export async function exec(command, args, options) {
   return new Promise((resolve, reject) => {
@@ -112,3 +120,12 @@ export async function exec(command, args, options) {
     })
   })
 }
+
+/**
+ * @param {boolean=} short
+ */
+export async function getSha(short) {
+  return (
+    await exec('git', ['rev-parse', ...(short ? ['--short'] : []), 'HEAD'])
+  ).stdout
+}
diff --git a/tsconfig.json b/tsconfig.json
index 9896083cf14..8973145f6f3 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -24,7 +24,7 @@
     "paths": {
       "@vue/compat": ["packages/vue-compat/src"],
       "@vue/*": ["packages/*/src"],
-      "vue": ["packages/vue/src"]
+      "vue": ["packages/vue/src/runtime-with-vapor.ts"]
     },
     "isolatedDeclarations": true,
     "composite": true
diff --git a/vitest.config.ts b/vitest.config.ts
index 3fce4ce87c2..8daa34f7ed3 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -1,9 +1,9 @@
-import { defineConfig } from 'vitest/config'
+import { configDefaults, defineConfig } from 'vitest/config'
 import { entries } from './scripts/aliases.js'
 
 export default defineConfig({
   define: {
-    __DEV__: true,
+    __DEV__: process.env.MODE !== 'benchmark',
     __TEST__: true,
     __VERSION__: '"test"',
     __BROWSER__: false,
@@ -12,6 +12,7 @@ export default defineConfig({
     __ESM_BROWSER__: false,
     __CJS__: true,
     __SSR__: true,
+    __BENCHMARK__: false,
     __FEATURE_OPTIONS_API__: true,
     __FEATURE_SUSPENSE__: true,
     __FEATURE_PROD_DEVTOOLS__: false,
@@ -24,10 +25,12 @@ export default defineConfig({
   test: {
     globals: true,
     pool: 'threads',
+    poolOptions: {
+      forks: {
+        execArgv: ['--expose-gc'],
+      },
+    },
     setupFiles: 'scripts/setup-vitest.ts',
-    environmentMatchGlobs: [
-      ['packages/{vue,vue-compat,runtime-dom}/**', 'jsdom'],
-    ],
     sequence: {
       hooks: 'list',
     },
@@ -49,5 +52,54 @@ export default defineConfig({
         'packages/runtime-dom/src/components/Transition*',
       ],
     },
+    workspace: [
+      {
+        extends: true,
+        test: {
+          name: 'unit',
+          exclude: [
+            ...configDefaults.exclude,
+            '**/e2e/**',
+            '**/vapor-e2e-test/**',
+            'packages/{vue,vue-compat,runtime-dom,runtime-vapor}/**',
+          ],
+        },
+      },
+      {
+        extends: true,
+        test: {
+          name: 'unit-jsdom',
+          environment: 'jsdom',
+          include: [
+            'packages/{vue,vue-compat,runtime-dom,runtime-vapor}/**/*.spec.ts',
+          ],
+          exclude: [...configDefaults.exclude, '**/e2e/**'],
+        },
+      },
+      {
+        extends: true,
+        test: {
+          name: 'e2e',
+          poolOptions: {
+            threads: {
+              singleThread: !!process.env.CI,
+            },
+          },
+          include: ['packages/vue/__tests__/e2e/*.spec.ts'],
+        },
+      },
+      {
+        extends: true,
+        test: {
+          name: 'e2e-vapor',
+          poolOptions: {
+            threads: {
+              singleThread: !!process.env.CI,
+            },
+          },
+          include: ['packages-private/vapor-e2e-test/__tests__/*.spec.ts'],
+        },
+      },
+    ],
   },
 })
diff --git a/vitest.e2e.config.ts b/vitest.e2e.config.ts
deleted file mode 100644
index 622bda0bd57..00000000000
--- a/vitest.e2e.config.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { mergeConfig } from 'vitest/config'
-import config from './vitest.config'
-
-export default mergeConfig(config, {
-  test: {
-    name: 'e2e',
-    poolOptions: {
-      threads: {
-        singleThread: !!process.env.CI,
-      },
-    },
-    include: ['packages/vue/__tests__/e2e/*.spec.ts'],
-  },
-})
diff --git a/vitest.unit.config.ts b/vitest.unit.config.ts
deleted file mode 100644
index 0082997e0d1..00000000000
--- a/vitest.unit.config.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { configDefaults, mergeConfig } from 'vitest/config'
-import config from './vitest.config'
-
-export default mergeConfig(config, {
-  test: {
-    name: 'unit',
-    exclude: [...configDefaults.exclude, '**/e2e/**'],
-  },
-})
diff --git a/vitest.workspace.ts b/vitest.workspace.ts
deleted file mode 100644
index a20586e7cfe..00000000000
--- a/vitest.workspace.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { defineWorkspace } from 'vitest/config'
-
-export default defineWorkspace([
-  './vitest.unit.config.ts',
-  './vitest.e2e.config.ts',
-])
