]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: Vapor mode (#12359)
authorEvan You <evan@vuejs.org>
Mon, 7 Jul 2025 00:06:20 +0000 (08:06 +0800)
committerGitHub <noreply@github.com>
Mon, 7 Jul 2025 00:06:20 +0000 (08:06 +0800)
303 files changed:
.github/contributing.md
.github/maintenance.md
.github/workflows/ci.yml
.github/workflows/test.yml
.gitignore
.vscode/settings.json
eslint.config.js
package.json
packages-private/benchmark/.gitignore [new file with mode: 0644]
packages-private/benchmark/client/App.vue [new file with mode: 0644]
packages-private/benchmark/client/AppVapor.vue [new file with mode: 0644]
packages-private/benchmark/client/data.ts [new file with mode: 0644]
packages-private/benchmark/client/index.html [new file with mode: 0644]
packages-private/benchmark/client/index.ts [new file with mode: 0644]
packages-private/benchmark/client/profiling.ts [new file with mode: 0644]
packages-private/benchmark/client/vapor.ts [new file with mode: 0644]
packages-private/benchmark/client/vdom.ts [new file with mode: 0644]
packages-private/benchmark/index.js [new file with mode: 0644]
packages-private/benchmark/package.json [new file with mode: 0644]
packages-private/benchmark/tsconfig.json [new file with mode: 0644]
packages-private/local-playground/index.html [new file with mode: 0644]
packages-private/local-playground/package.json [new file with mode: 0644]
packages-private/local-playground/setup/dev.js [new file with mode: 0644]
packages-private/local-playground/setup/vite.js [new file with mode: 0644]
packages-private/local-playground/src/.gitignore [new file with mode: 0644]
packages-private/local-playground/src/App.vue [new file with mode: 0644]
packages-private/local-playground/src/main.ts [new file with mode: 0644]
packages-private/local-playground/src/style.css [new file with mode: 0644]
packages-private/local-playground/tsconfig.json [new file with mode: 0644]
packages-private/local-playground/vite.config.ts [new file with mode: 0644]
packages-private/local-playground/vite.prod.config.ts [new file with mode: 0644]
packages-private/sfc-playground/src/App.vue
packages-private/sfc-playground/src/Header.vue
packages-private/sfc-playground/src/vue-dev-proxy-prod.ts
packages-private/sfc-playground/src/vue-dev-proxy.ts
packages-private/sfc-playground/src/welcome.vue [new file with mode: 0644]
packages-private/sfc-playground/vite.config.ts
packages-private/template-explorer/package.json
packages-private/template-explorer/src/index.ts
packages-private/template-explorer/src/options.ts
packages-private/tsconfig.json
packages-private/vapor-e2e-test/__tests__/todomvc.spec.ts [new file with mode: 0644]
packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts [new file with mode: 0644]
packages-private/vapor-e2e-test/index.html [new file with mode: 0644]
packages-private/vapor-e2e-test/interop/App.vue [new file with mode: 0644]
packages-private/vapor-e2e-test/interop/VaporComp.vue [new file with mode: 0644]
packages-private/vapor-e2e-test/interop/VdomComp.vue [new file with mode: 0644]
packages-private/vapor-e2e-test/interop/index.html [new file with mode: 0644]
packages-private/vapor-e2e-test/interop/main.ts [new file with mode: 0644]
packages-private/vapor-e2e-test/package.json [new file with mode: 0644]
packages-private/vapor-e2e-test/todomvc/App.vue [new file with mode: 0644]
packages-private/vapor-e2e-test/todomvc/index.html [new file with mode: 0644]
packages-private/vapor-e2e-test/todomvc/main.ts [new file with mode: 0644]
packages-private/vapor-e2e-test/vite.config.ts [new file with mode: 0644]
packages/compiler-core/src/ast.ts
packages/compiler-core/src/babelUtils.ts
packages/compiler-core/src/codegen.ts
packages/compiler-core/src/index.ts
packages/compiler-core/src/options.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/transformExpression.ts
packages/compiler-core/src/utils.ts
packages/compiler-dom/src/errors.ts
packages/compiler-dom/src/index.ts
packages/compiler-dom/src/transforms/vOn.ts
packages/compiler-sfc/README.md
packages/compiler-sfc/__tests__/compileScript/__snapshots__/definePropsDestructure.spec.ts.snap
packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts
packages/compiler-sfc/__tests__/parse.spec.ts
packages/compiler-sfc/package.json
packages/compiler-sfc/src/compileScript.ts
packages/compiler-sfc/src/compileTemplate.ts
packages/compiler-sfc/src/parse.ts
packages/compiler-sfc/src/script/defineProps.ts
packages/compiler-vapor/LICENSE [new file with mode: 0644]
packages/compiler-vapor/README.md [new file with mode: 0644]
packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap [new file with mode: 0644]
packages/compiler-vapor/__tests__/abbreviation.spec.ts [new file with mode: 0644]
packages/compiler-vapor/__tests__/compile.spec.ts [new file with mode: 0644]
packages/compiler-vapor/__tests__/scopeId.spec.ts [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/__snapshots__/expression.spec.ts.snap [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformChildren.spec.ts.snap [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformRef.spec.ts.snap [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformText.spec.ts.snap [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/__snapshots__/vHtml.spec.ts.snap [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/__snapshots__/vShow.spec.ts.snap [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/_utils.ts [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/expression.spec.ts [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/transformText.spec.ts [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/vBind.spec.ts [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/vFor.spec.ts [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/vHtml.spec.ts [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/vIf.spec.ts [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/vModel.spec.ts [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/vOn.spec.ts [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/vShow.spec.ts [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/vText.spec.ts [new file with mode: 0644]
packages/compiler-vapor/package.json [new file with mode: 0644]
packages/compiler-vapor/src/compile.ts [new file with mode: 0644]
packages/compiler-vapor/src/errors.ts [new file with mode: 0644]
packages/compiler-vapor/src/generate.ts [new file with mode: 0644]
packages/compiler-vapor/src/generators/block.ts [new file with mode: 0644]
packages/compiler-vapor/src/generators/component.ts [new file with mode: 0644]
packages/compiler-vapor/src/generators/directive.ts [new file with mode: 0644]
packages/compiler-vapor/src/generators/dom.ts [new file with mode: 0644]
packages/compiler-vapor/src/generators/event.ts [new file with mode: 0644]
packages/compiler-vapor/src/generators/expression.ts [new file with mode: 0644]
packages/compiler-vapor/src/generators/for.ts [new file with mode: 0644]
packages/compiler-vapor/src/generators/html.ts [new file with mode: 0644]
packages/compiler-vapor/src/generators/if.ts [new file with mode: 0644]
packages/compiler-vapor/src/generators/operation.ts [new file with mode: 0644]
packages/compiler-vapor/src/generators/prop.ts [new file with mode: 0644]
packages/compiler-vapor/src/generators/slotOutlet.ts [new file with mode: 0644]
packages/compiler-vapor/src/generators/template.ts [new file with mode: 0644]
packages/compiler-vapor/src/generators/templateRef.ts [new file with mode: 0644]
packages/compiler-vapor/src/generators/text.ts [new file with mode: 0644]
packages/compiler-vapor/src/generators/utils.ts [new file with mode: 0644]
packages/compiler-vapor/src/generators/vModel.ts [new file with mode: 0644]
packages/compiler-vapor/src/generators/vShow.ts [new file with mode: 0644]
packages/compiler-vapor/src/index.ts [new file with mode: 0644]
packages/compiler-vapor/src/ir/component.ts [new file with mode: 0644]
packages/compiler-vapor/src/ir/index.ts [new file with mode: 0644]
packages/compiler-vapor/src/transform.ts [new file with mode: 0644]
packages/compiler-vapor/src/transforms/transformChildren.ts [new file with mode: 0644]
packages/compiler-vapor/src/transforms/transformComment.ts [new file with mode: 0644]
packages/compiler-vapor/src/transforms/transformElement.ts [new file with mode: 0644]
packages/compiler-vapor/src/transforms/transformSlotOutlet.ts [new file with mode: 0644]
packages/compiler-vapor/src/transforms/transformTemplateRef.ts [new file with mode: 0644]
packages/compiler-vapor/src/transforms/transformText.ts [new file with mode: 0644]
packages/compiler-vapor/src/transforms/utils.ts [new file with mode: 0644]
packages/compiler-vapor/src/transforms/vBind.ts [new file with mode: 0644]
packages/compiler-vapor/src/transforms/vFor.ts [new file with mode: 0644]
packages/compiler-vapor/src/transforms/vHtml.ts [new file with mode: 0644]
packages/compiler-vapor/src/transforms/vIf.ts [new file with mode: 0644]
packages/compiler-vapor/src/transforms/vModel.ts [new file with mode: 0644]
packages/compiler-vapor/src/transforms/vOn.ts [new file with mode: 0644]
packages/compiler-vapor/src/transforms/vOnce.ts [new file with mode: 0644]
packages/compiler-vapor/src/transforms/vShow.ts [new file with mode: 0644]
packages/compiler-vapor/src/transforms/vSlot.ts [new file with mode: 0644]
packages/compiler-vapor/src/transforms/vText.ts [new file with mode: 0644]
packages/compiler-vapor/src/utils.ts [new file with mode: 0644]
packages/global.d.ts
packages/reactivity/__tests__/effectScope.spec.ts
packages/reactivity/__tests__/ref.spec.ts
packages/reactivity/src/debug.ts
packages/reactivity/src/effectScope.ts
packages/reactivity/src/ref.ts
packages/runtime-core/__tests__/componentProps.spec.ts
packages/runtime-core/__tests__/directives.spec.ts
packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts
packages/runtime-core/__tests__/rendererComponent.spec.ts
packages/runtime-core/__tests__/rendererFragment.spec.ts
packages/runtime-core/__tests__/scheduler.spec.ts
packages/runtime-core/__tests__/vnode.spec.ts
packages/runtime-core/src/apiAsyncComponent.ts
packages/runtime-core/src/apiCreateApp.ts
packages/runtime-core/src/apiInject.ts
packages/runtime-core/src/apiLifecycle.ts
packages/runtime-core/src/apiSetupHelpers.ts
packages/runtime-core/src/compat/componentAsync.ts
packages/runtime-core/src/compat/global.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentCurrentInstance.ts [new file with mode: 0644]
packages/runtime-core/src/componentEmits.ts
packages/runtime-core/src/componentOptions.ts
packages/runtime-core/src/componentProps.ts
packages/runtime-core/src/componentPublicInstance.ts
packages/runtime-core/src/componentRenderUtils.ts
packages/runtime-core/src/componentSlots.ts
packages/runtime-core/src/components/KeepAlive.ts
packages/runtime-core/src/components/Suspense.ts
packages/runtime-core/src/components/Teleport.ts
packages/runtime-core/src/devtools.ts
packages/runtime-core/src/errorHandling.ts
packages/runtime-core/src/featureFlags.ts
packages/runtime-core/src/helpers/renderSlot.ts
packages/runtime-core/src/helpers/resolveAssets.ts
packages/runtime-core/src/helpers/useId.ts
packages/runtime-core/src/helpers/useModel.ts
packages/runtime-core/src/helpers/useTemplateRef.ts
packages/runtime-core/src/hmr.ts
packages/runtime-core/src/hydration.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/profiling.ts
packages/runtime-core/src/renderer.ts
packages/runtime-core/src/scheduler.ts
packages/runtime-core/src/vnode.ts
packages/runtime-core/src/warning.ts
packages/runtime-dom/src/components/TransitionGroup.ts
packages/runtime-dom/src/directives/vModel.ts
packages/runtime-dom/src/directives/vShow.ts
packages/runtime-dom/src/index.ts
packages/runtime-dom/src/modules/props.ts
packages/runtime-dom/src/modules/style.ts
packages/runtime-dom/src/patchProp.ts
packages/runtime-vapor/LICENSE [new file with mode: 0644]
packages/runtime-vapor/README.md [new file with mode: 0644]
packages/runtime-vapor/__tests__/_utils.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/apiCreateDynamicComponent.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/apiCreateSelector.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/apiCreateVaporApp.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/apiExpose.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/apiInject.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/apiLifecycle.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/apiSetupContext.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/apiSetupHelpers.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/apiWatch.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/block.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/component.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/componentAttrs.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/componentEmits.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/componentProps.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/componentSlots.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/directives/customDirective.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/directives/vModel.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/directives/vShow.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/dom/event.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/dom/prop.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/dom/template.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/dom/templateRef.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/errorHandling.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/for.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/hydration.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/if.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/renderEffect.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/vdomInterop.spec.ts [new file with mode: 0644]
packages/runtime-vapor/package.json [new file with mode: 0644]
packages/runtime-vapor/src/.gitignore [new file with mode: 0644]
packages/runtime-vapor/src/apiCreateApp.ts [new file with mode: 0644]
packages/runtime-vapor/src/apiCreateDynamicComponent.ts [new file with mode: 0644]
packages/runtime-vapor/src/apiCreateFor.ts [new file with mode: 0644]
packages/runtime-vapor/src/apiCreateIf.ts [new file with mode: 0644]
packages/runtime-vapor/src/apiDefineComponent.ts [new file with mode: 0644]
packages/runtime-vapor/src/apiTemplateRef.ts [new file with mode: 0644]
packages/runtime-vapor/src/block.ts [new file with mode: 0644]
packages/runtime-vapor/src/component.ts [new file with mode: 0644]
packages/runtime-vapor/src/componentEmits.ts [new file with mode: 0644]
packages/runtime-vapor/src/componentProps.ts [new file with mode: 0644]
packages/runtime-vapor/src/componentSlots.ts [new file with mode: 0644]
packages/runtime-vapor/src/directives/custom.ts [new file with mode: 0644]
packages/runtime-vapor/src/directives/vModel.ts [new file with mode: 0644]
packages/runtime-vapor/src/directives/vShow.ts [new file with mode: 0644]
packages/runtime-vapor/src/dom/event.ts [new file with mode: 0644]
packages/runtime-vapor/src/dom/hydration.ts [new file with mode: 0644]
packages/runtime-vapor/src/dom/node.ts [new file with mode: 0644]
packages/runtime-vapor/src/dom/prop.ts [new file with mode: 0644]
packages/runtime-vapor/src/dom/template.ts [new file with mode: 0644]
packages/runtime-vapor/src/hmr.ts [new file with mode: 0644]
packages/runtime-vapor/src/index.ts [new file with mode: 0644]
packages/runtime-vapor/src/insertionState.ts [new file with mode: 0644]
packages/runtime-vapor/src/renderEffect.ts [new file with mode: 0644]
packages/runtime-vapor/src/vdomInterop.ts [new file with mode: 0644]
packages/server-renderer/src/render.ts
packages/shared/src/domAttrConfig.ts
packages/shared/src/general.ts
packages/shared/src/index.ts
packages/shared/src/normalizeProp.ts
packages/shared/src/subSequence.ts [new file with mode: 0644]
packages/shared/src/toDisplayString.ts
packages/shared/src/vaporFlags.ts [new file with mode: 0644]
packages/vue-compat/README.md
packages/vue/README.md
packages/vue/__tests__/e2e/Transition.spec.ts
packages/vue/__tests__/e2e/TransitionGroup.spec.ts
packages/vue/__tests__/e2e/e2eUtils.ts
packages/vue/package.json
packages/vue/src/index-with-vapor.ts [new file with mode: 0644]
packages/vue/src/runtime-with-vapor.ts [new file with mode: 0644]
packages/vue/src/runtime.ts
pnpm-lock.yaml
pnpm-workspace.yaml
rollup.config.js
rollup.dts.config.js
scripts/build.js
scripts/dev.js
scripts/inline-enums.js
scripts/prepare-cjs.js [moved from scripts/pre-dev-sfc.js with 95% similarity]
scripts/release.js
scripts/trim-vapor-exports.js [new file with mode: 0644]
scripts/usage-size.js
scripts/utils.js
tsconfig.json
vitest.config.ts
vitest.e2e.config.ts [deleted file]
vitest.unit.config.ts [deleted file]
vitest.workspace.ts [deleted file]

index 2554582b887f753e1e57f8d6f7b077138674ae8f..f0ec46ee7092cfcce5e8606028b19546a96c8c86 100644 (file)
@@ -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:
index b1fb550dd7a5ba93636388748b638511de85b2e1..7b0c2a336266148e58bbe4ffa1e5624c4f3223f7 100644 (file)
@@ -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
index c8c217f62c463c353ecb63785e6ecc599ae2cd8f..6b69e4727e454758e1dd06680ef477708400d769 100644 (file)
@@ -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
index 1122eb355733ba152fe078d20df556505d60b43f..1202ef9c8b4254c5f1cd9eaf84943dc6bce9c750 100644 (file)
@@ -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:
index 9dd21f59bf6fd8cfd5a5ada8add15c3bd2ca26c0..973c062daf78d37279b58fdf978167911dd4ee75 100644 (file)
@@ -11,3 +11,4 @@ TODOs.md
 dts-build/packages
 *.tsbuildinfo
 *.tgz
+packages-private/benchmark/reference
index 302428290b9aae64b68641a30fd64cea8c225eaf..7907859bb862835fed09072a399b3fab6958daa1 100644 (file)
@@ -14,5 +14,6 @@
   "[json]": {
     "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
-  "editor.formatOnSave": true
+  "editor.formatOnSave": true,
+  "vitest.disableWorkspaceWarning": true
 }
index b752b2e19f15ef4190fce5ddcf92d9da11375d90..1f5128ec72febc7eb5870445a18e669fff779777 100644 (file)
@@ -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',
index 08bdc5ac9084d95bfa33f8f3a9a2f944bd19e4d5..e05a475cb500bc634bd7de10789a9a3f44f4e33d 100644 (file)
@@ -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",
     "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 (file)
index 0000000..484ab7e
--- /dev/null
@@ -0,0 +1 @@
+results/*
diff --git a/packages-private/benchmark/client/App.vue b/packages-private/benchmark/client/App.vue
new file mode 100644 (file)
index 0000000..c85deea
--- /dev/null
@@ -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 (file)
index 0000000..0fd284d
--- /dev/null
@@ -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 (file)
index 0000000..ea5de14
--- /dev/null
@@ -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 (file)
index 0000000..c3ca4c5
--- /dev/null
@@ -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 (file)
index 0000000..a12f727
--- /dev/null
@@ -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 (file)
index 0000000..ee4f38b
--- /dev/null
@@ -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 (file)
index 0000000..2574da9
--- /dev/null
@@ -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 (file)
index 0000000..01433bc
--- /dev/null
@@ -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 (file)
index 0000000..3af47ea
--- /dev/null
@@ -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 (file)
index 0000000..e6eb08e
--- /dev/null
@@ -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 (file)
index 0000000..4a32149
--- /dev/null
@@ -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 (file)
index 0000000..a01ba96
--- /dev/null
@@ -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 (file)
index 0000000..37c9818
--- /dev/null
@@ -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 (file)
index 0000000..72a7ed4
--- /dev/null
@@ -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 (file)
index 0000000..e6b9a92
--- /dev/null
@@ -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 (file)
index 0000000..2e1b9cf
--- /dev/null
@@ -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 (file)
index 0000000..b6124e3
--- /dev/null
@@ -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 (file)
index 0000000..9d682d9
--- /dev/null
@@ -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 (file)
index 0000000..832e616
--- /dev/null
@@ -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 (file)
index 0000000..8ed9819
--- /dev/null
@@ -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 (file)
index 0000000..8b4b1a4
--- /dev/null
@@ -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 (file)
index 0000000..6bafb7a
--- /dev/null
@@ -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,
+      },
+    }),
+  ],
+})
index 455137ba6f61a1c53e2be3aac862f76215970776..be4b136012c951b65f771477e27e12b84b0745b6 100644 (file)
@@ -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()
index bf1c9bad6ebda90e123de73a803fae3f52122932..132453e986d4ba91a984e4a926c8ff0d84b17c59 100644 (file)
@@ -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;
index 3b2faf1953365c3d01b9abe0f09770e98ab3929e..6448f9d4c2f79017494797c8afa80af563c88d3c 100644 (file)
@@ -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'
index f254416d4fd239d48bc9fcb1c41a426b502afe01..427d41621c2dff9938827156737491a6d4617dc6 100644 (file)
@@ -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 (file)
index 0000000..a466ef8
--- /dev/null
@@ -0,0 +1,9 @@
+<script setup lang="ts">
+import { ref } from 'vue'
+
+const msg = ref('Hello World!')
+</script>
+
+<template>
+  <h1>{{ msg }}</h1>
+</template>
index 2e77f1970a7f3ac891ba7dcd958ecc27f9f645bd..c1a40fd1ca9367182a4d86afaaeaf218ed78c6dd 100644 (file)
@@ -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`)
     },
   }
index 08da34b173e3d11703bd796a1a08ff5731a64fa8..c546f4a298d302b25f7f70ccf69fbf9b3d034573 100644 (file)
@@ -11,6 +11,7 @@
     "enableNonBrowserBranches": true
   },
   "dependencies": {
+    "@vue/compiler-vapor": "workspace:^",
     "monaco-editor": "^0.52.2",
     "source-map-js": "^1.2.1"
   }
index 988712d623ca076144afcce0c4bc55c686d3c399..96619b5a311958a7b307be6276f619337285bcf2 100644 (file)
@@ -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 => {
index e3cc6173a8a3fc95860116a011aa7ed7d76c7a45..341e885c083ac10603c19bd0a23a17bf3b7fbd72 100644 (file)
@@ -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,11 +40,11 @@ const App = {
         compilerOptions.prefixIdentifiers || compilerOptions.mode === 'module'
 
       return [
-        h('h1', `Vue Template Explorer`),
+        h('h1', `Vue Template Explorer`),
         h(
           'a',
           {
-            href: `https://github.com/vuejs/core/tree/${__COMMIT__}`,
+            href: `https://github.com/vuejs/vue/tree/${__COMMIT__}`,
             target: `_blank`,
           },
           `@${__COMMIT__}`,
@@ -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'),
+            ]),
           ]),
         ]),
       ]
index 1c287a7500c5a9f813b9d6143f460071b134ba91..37a38f53fc63ecda8082c79d1b5a00712ea5b78d 100644 (file)
@@ -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 (file)
index 0000000..3de8392
--- /dev/null
@@ -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 (file)
index 0000000..360f480
--- /dev/null
@@ -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 (file)
index 0000000..7dc205e
--- /dev/null
@@ -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 (file)
index 0000000..772a698
--- /dev/null
@@ -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 (file)
index 0000000..88a60c7
--- /dev/null
@@ -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 (file)
index 0000000..30ec1b2
--- /dev/null
@@ -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 (file)
index 0000000..79052a0
--- /dev/null
@@ -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 (file)
index 0000000..d5d6d7d
--- /dev/null
@@ -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 (file)
index 0000000..66ea045
--- /dev/null
@@ -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 (file)
index 0000000..910ada5
--- /dev/null
@@ -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 (file)
index 0000000..79052a0
--- /dev/null
@@ -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 (file)
index 0000000..42497ab
--- /dev/null
@@ -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 (file)
index 0000000..1e29a4d
--- /dev/null
@@ -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'),
+      },
+    },
+  },
+})
index 2d6df9d90106941708f3398cda3daf64c667ba55..bae13372a98be3d205d15cc1c045edd0b0220197 100644 (file)
@@ -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
index 52fabeea896a39caaad8f40afd564d52ae5c48dc..6ede6bd0386ad0b1101c02ef46df7bcec3c356de 100644 (file)
@@ -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
+}
index 6b4559fabb20c7c56396ea2d139413e09ec56848..99020bcf1ae1f93eda6ab6a7f458cd539293b701 100644 (file)
@@ -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,
   }
 }
 
index 29e5f681300aadc918134985832e0c55ff31f925..e54b0c3a498b00c39ce5509b55204d7c5533a625 100644 (file)
@@ -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'
index 1de865f42ebaed3edcd63ab5500ce675b6a0cb9c..9983071609eaad3d1337543438771eed7a8d7c61 100644 (file)
@@ -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
    */
index 9d8fd842935ecd8198cc40da400e20c90f82ea31..10121fb5d5cb1fcfdb36e04d2371fea430bcd0ec 100644 (file)
@@ -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,
index 9ae8897e674866e821dc75d56d608fd5a0ec29cb..9012c2701f7493fbdeb4138df21b379e150acaa4 100644 (file)
@@ -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) {
index b49d70bb2fba1d207e9ef76ab5d35cefcaec91aa..b90a7018c8b192ffcd47ea6d898818f4c899b3d2 100644 (file)
@@ -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,
index b47624840abe1e34b0619ff1c04d661394ef7bd3..15641e531af717fd3a0c6238d74162c2055ea176 100644 (file)
@@ -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__]: ``,
 }
index 809f3708023cf01b9f449534e076611fb524dc34..950901e7bf91c1d6ad97f8526ff788f538658adc 100644 (file)
@@ -74,4 +74,6 @@ export {
   DOMErrorCodes,
   DOMErrorMessages,
 } from './errors'
+export { resolveModifiers } from './transforms/vOn'
+export { isValidHTMLNesting } from './htmlNesting'
 export * from '@vue/compiler-core'
index 1bb5763188bd0417c1cb17ed308f7a6b86c97fb8..723229cf34b42c6aeda6e34cc7f8c3690fa3c351 100644 (file)
@@ -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)
index 4f8ff3ac8b3aa5cc6ffaae4fe5f02ef6d7e04df7..4d2ae6e924827d3ac88cf7fecf8f5f217e8ca3b9 100644 (file)
@@ -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
 
index 9306d31da97401cd3c05d696349edef858fbd559..fd34e7a32dead15abe868e2208f052432a6fec2f 100644 (file)
@@ -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)
+}
 }
 
 })"
index 25dd817bbe5d4996808523051c1f3dd231bb7b40..5709cfbe5bc1667464968073a92ee973162a4b20 100644 (file)
@@ -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)
   })
 
index 265655e47efe54f17f401618e08e09e87694199c..82b8cf98f11ac85c33b99987eee19db201c3f661 100644 (file)
@@ -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)
index 45468177a942767b4fbd61ab8518a3f4edbd32b9..55e12af299db0ebcda029d906996d2fd67cddc3f 100644 (file)
@@ -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:",
index 2e1e0ec34de57dd1ad2804c9a9530746bcba8c87..54ca260bdd6f48d58686d3af24bf939dc9346372 100644 (file)
@@ -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,
index b043cf813d740392ed7a2f1d1eb856495415764c..29d1853d2d6233836ade13f34040de58ccf47fb8 100644 (file)
@@ -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 {
index c8be865508f4e04735e85bf84f2d092b22f7647f..98b08a208151687b0221ed4648b040b41002e63a 100644 (file)
@@ -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)
 }
 
 /**
index 9a4880a1a543f20f601b44c672df817d024aabc7..ac5226168e40bc3aa5a5f2f475b1c05db6d93a5f 100644 (file)
@@ -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-vapor/LICENSE b/packages/compiler-vapor/LICENSE
new file mode 100644 (file)
index 0000000..15f1f7e
--- /dev/null
@@ -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 (file)
index 0000000..c2f1132
--- /dev/null
@@ -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 (file)
index 0000000..b10a98d
--- /dev/null
@@ -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 (file)
index 0000000..6159c6c
--- /dev/null
@@ -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 (file)
index 0000000..178021d
--- /dev/null
@@ -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 (file)
index 0000000..da17ef5
--- /dev/null
@@ -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 (file)
index 0000000..7e15723
--- /dev/null
@@ -0,0 +1,55 @@
+// 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 > 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 (file)
index 0000000..4a8caa6
--- /dev/null
@@ -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 (file)
index 0000000..7aa56aa
--- /dev/null
@@ -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 (file)
index 0000000..a59b17d
--- /dev/null
@@ -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 (file)
index 0000000..6d62b9c
--- /dev/null
@@ -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 (file)
index 0000000..2d64e1f
--- /dev/null
@@ -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
+  }, null, 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 (file)
index 0000000..f18f852
--- /dev/null
@@ -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 (file)
index 0000000..4ea0db5
--- /dev/null
@@ -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 (file)
index 0000000..e3631f1
--- /dev/null
@@ -0,0 +1,159 @@
+// 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 > 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
+    }, null, 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 > 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 (file)
index 0000000..ecf886d
--- /dev/null
@@ -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 (file)
index 0000000..c41dc92
--- /dev/null
@@ -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 (file)
index 0000000..5ef0649
--- /dev/null
@@ -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 (file)
index 0000000..dd00e55
--- /dev/null
@@ -0,0 +1,472 @@
+// 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 > 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 (file)
index 0000000..ab3ade4
--- /dev/null
@@ -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
+  }, null, 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 (file)
index 0000000..f595da5
--- /dev/null
@@ -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 (file)
index 0000000..f296d72
--- /dev/null
@@ -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 (file)
index 0000000..9a3b88a
--- /dev/null
@@ -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 (file)
index 0000000..1b6e3f1
--- /dev/null
@@ -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 (file)
index 0000000..5983bde
--- /dev/null
@@ -0,0 +1,50 @@
+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++)`)
+  })
+})
diff --git a/packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts
new file mode 100644 (file)
index 0000000..2d8ae8c
--- /dev/null
@@ -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 (file)
index 0000000..a693db4
--- /dev/null
@@ -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 (file)
index 0000000..389c665
--- /dev/null
@@ -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 (file)
index 0000000..2c883d1
--- /dev/null
@@ -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 (file)
index 0000000..20fa6d1
--- /dev/null
@@ -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 (file)
index 0000000..e96186c
--- /dev/null
@@ -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 (file)
index 0000000..d22981c
--- /dev/null
@@ -0,0 +1,304 @@
+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('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 (file)
index 0000000..0de0b6a
--- /dev/null
@@ -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 (file)
index 0000000..e5fd61a
--- /dev/null
@@ -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 (file)
index 0000000..51eaa9e
--- /dev/null
@@ -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 (file)
index 0000000..aca88c7
--- /dev/null
@@ -0,0 +1,698 @@
+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)')
+  })
+})
diff --git a/packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts b/packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts
new file mode 100644 (file)
index 0000000..97f6ee6
--- /dev/null
@@ -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 (file)
index 0000000..800f64c
--- /dev/null
@@ -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 (file)
index 0000000..909162f
--- /dev/null
@@ -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 (file)
index 0000000..4f074fe
--- /dev/null
@@ -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 (file)
index 0000000..8b0cc89
--- /dev/null
@@ -0,0 +1,49 @@
+{
+  "name": "@vue/compiler-vapor",
+  "version": "3.5.13",
+  "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": {
+    "@vue/compiler-dom": "workspace:*",
+    "@vue/shared": "workspace:*",
+    "source-map-js": "catalog:"
+  }
+}
diff --git a/packages/compiler-vapor/src/compile.ts b/packages/compiler-vapor/src/compile.ts
new file mode 100644 (file)
index 0000000..c39037a
--- /dev/null
@@ -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 (file)
index 0000000..3ccd32c
--- /dev/null
@@ -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 (file)
index 0000000..193a0f5
--- /dev/null
@@ -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 (file)
index 0000000..ff240dd
--- /dev/null
@@ -0,0 +1,99 @@
+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,
+  customReturns?: (returns: CodeFragment[]) => CodeFragment[],
+): CodeFragment[] {
+  return [
+    '(',
+    ...args,
+    ') => {',
+    INDENT_START,
+    ...genBlockContent(oper, context, root, customReturns),
+    INDENT_END,
+    NEWLINE,
+    '}',
+  ]
+}
+
+export function genBlockContent(
+  block: BlockIRNode,
+  context: CodegenContext,
+  root?: boolean,
+  customReturns?: (returns: CodeFragment[]) => 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))
+
+  push(NEWLINE, `return `)
+
+  const returnNodes = returns.map(n => `n${n}`)
+  const returnsCode: CodeFragment[] =
+    returnNodes.length > 1
+      ? genMulti(DELIMITERS_ARRAY, ...returnNodes)
+      : [returnNodes[0] || 'null']
+  push(...(customReturns ? customReturns(returnsCode) : 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 (file)
index 0000000..10705a2
--- /dev/null
@@ -0,0 +1,434 @@
+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 } 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
+  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 (file)
index 0000000..7ed4624
--- /dev/null
@@ -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 (file)
index 0000000..5634f7e
--- /dev/null
@@ -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 (file)
index 0000000..cfb47b6
--- /dev/null
@@ -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 (file)
index 0000000..a8fbc8f
--- /dev/null
@@ -0,0 +1,674 @@
+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[]
+}
+type DeclarationValue = {
+  name: string
+  isIdentifier?: boolean
+  value: SimpleExpressionNode
+  rawName?: string
+  exps?: Set<SimpleExpressionNode>
+  seenCount?: number
+}
+
+export function processExpressions(
+  context: CodegenContext,
+  expressions: SimpleExpressionNode[],
+): 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)
+}
+
+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,
+): DeclarationResult {
+  const [frag, push] = buildCodeFragment()
+  const ids: Record<string, string> = Object.create(null)
+
+  // process identifiers first as expressions may rely on them
+  declarations.forEach(({ name, isIdentifier, value }) => {
+    if (isIdentifier) {
+      const varName = (ids[name] = `_${name}`)
+      push(`const ${varName} = `, ...genExpression(value, context), NEWLINE)
+    }
+  })
+
+  // process expressions
+  declarations.forEach(({ name, isIdentifier, value }) => {
+    if (!isIdentifier) {
+      const varName = (ids[name] = `_${name}`)
+      push(
+        `const ${varName} = `,
+        ...context.withId(() => genExpression(value, context), ids),
+        NEWLINE,
+      )
+    }
+  })
+
+  return { ids, frag }
+}
+
+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)
+}
+
+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 (file)
index 0000000..fbb72c6
--- /dev/null
@@ -0,0 +1,236 @@
+import {
+  type SimpleExpressionNode,
+  createSimpleExpression,
+  walkIdentifiers,
+} from '@vue/compiler-dom'
+import { genBlock } from './block'
+import { genExpression } from './expression'
+import type { CodegenContext } from '../generate'
+import type { ForIRNode } from '../ir'
+import { type CodeFragment, NEWLINE, genCall, genMulti } from './utils'
+import type { Identifier } from '@babel/types'
+import { parseExpression } from '@babel/parser'
+import { VaporVForFlags } from '../../../shared/src/vaporFlags'
+
+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 blockFn = context.withId(() => genBlock(render, context, args), idMap)
+  exitScope()
+
+  let flags = 0
+  if (onlyChild) {
+    flags |= VaporVForFlags.FAST_REMOVE
+  }
+  if (component) {
+    flags |= VaporVForFlags.IS_COMPONENT
+  }
+  if (once) {
+    flags |= VaporVForFlags.ONCE
+  }
+
+  return [
+    NEWLINE,
+    `const n${id} = `,
+    ...genCall(
+      helper('createFor'),
+      sourceExpr,
+      blockFn,
+      genCallback(keyProp),
+      flags ? String(flags) : 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
+  }
+}
diff --git a/packages/compiler-vapor/src/generators/html.ts b/packages/compiler-vapor/src/generators/html.ts
new file mode 100644 (file)
index 0000000..72af699
--- /dev/null
@@ -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 (file)
index 0000000..f4a3e59
--- /dev/null
@@ -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 (file)
index 0000000..563d72f
--- /dev/null
@@ -0,0 +1,169 @@
+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,
+): CodeFragment[] {
+  const { helper } = context
+  const expressions = effects.flatMap(effect => effect.expressions)
+  const [frag, push, unshift] = buildCodeFragment()
+  let operationsCount = 0
+  const { ids, frag: declarationFrags } = processExpressions(
+    context,
+    expressions,
+  )
+  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, `${helper('renderEffect')}(() => `)
+    push(`)`)
+  }
+
+  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 (file)
index 0000000..42f0633
--- /dev/null
@@ -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 (file)
index 0000000..3221cbb
--- /dev/null
@@ -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 (file)
index 0000000..5a066b0
--- /dev/null
@@ -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 (file)
index 0000000..af8facc
--- /dev/null
@@ -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 (file)
index 0000000..89e3167
--- /dev/null
@@ -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 (file)
index 0000000..904b3dc
--- /dev/null
@@ -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 (file)
index 0000000..a7082d9
--- /dev/null
@@ -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 (file)
index 0000000..9a6ccef
--- /dev/null
@@ -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 (file)
index 0000000..6eda102
--- /dev/null
@@ -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 (file)
index 0000000..884636c
--- /dev/null
@@ -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 (file)
index 0000000..18f0139
--- /dev/null
@@ -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 (file)
index 0000000..946c89b
--- /dev/null
@@ -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 (file)
index 0000000..790cd9d
--- /dev/null
@@ -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 (file)
index 0000000..23c4069
--- /dev/null
@@ -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 (file)
index 0000000..05153e7
--- /dev/null
@@ -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 (file)
index 0000000..e76fcdd
--- /dev/null
@@ -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 (file)
index 0000000..9fa7c09
--- /dev/null
@@ -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 (file)
index 0000000..5f85805
--- /dev/null
@@ -0,0 +1,163 @@
+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 children = context.parent!.node.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)
+  }
+
+  context.template += ' '
+  const id = context.reference()
+  const values = nodes.map(node => createTextLikeExpression(node, context))
+
+  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 = children.map(child => createTextLikeExpression(child, 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 createTextLikeExpression(node: TextLike, context: TransformContext) {
+  markNonTemplate(node, context)
+  if (node.type === NodeTypes.TEXT) {
+    return createSimpleExpression(node.content, true, node.loc)
+  } else {
+    return node.content as SimpleExpressionNode
+  }
+}
+
+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 (file)
index 0000000..f7d0594
--- /dev/null
@@ -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 (file)
index 0000000..dc56eb3
--- /dev/null
@@ -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 (file)
index 0000000..d4cf42d
--- /dev/null
@@ -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 (file)
index 0000000..6b9a269
--- /dev/null
@@ -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 (file)
index 0000000..bae9f1a
--- /dev/null
@@ -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 (file)
index 0000000..e92f1bc
--- /dev/null
@@ -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 (file)
index 0000000..fcbfc26
--- /dev/null
@@ -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 (file)
index 0000000..3ccfb23
--- /dev/null
@@ -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 (file)
index 0000000..f1135d6
--- /dev/null
@@ -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 (file)
index 0000000..3e78913
--- /dev/null
@@ -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 (file)
index 0000000..0832398
--- /dev/null
@@ -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 (file)
index 0000000..7282819
--- /dev/null
@@ -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
+}
index 8b627d2e556215791bc00065ff7257ecdad31577..e6acf0d1be876e4d85fdbea04fb4a9d80329fac7 100644 (file)
@@ -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
index debbdafb1e70bbef3529729f5061fb211490ccf0..84310b985f25faf1fa3c86b99bc9b01329f4bf81 100644 (file)
@@ -296,6 +296,19 @@ describe('reactivity/effect/scope', () => {
     })
   })
 
+  it('calling on() and off() multiple times inside an active scope should not break currentScope', () => {
+    const parentScope = effectScope()
+    parentScope.run(() => {
+      const childScope = effectScope(true)
+      childScope.on()
+      childScope.on()
+      childScope.off()
+      childScope.off()
+      childScope.off()
+      expect(getCurrentScope()).toBe(parentScope)
+    })
+  })
+
   it('should pause/resume EffectScope', async () => {
     const counter = reactive({ num: 0 })
     const fnSpy = vi.fn(() => counter.num)
index 7976a5373baf6edd4ad7f0f83618b203eee47a82..b6fd23ed39bf601ef2a4ade4dbda5ee90dab8e6e 100644 (file)
@@ -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)
index b830f5a46a75423fec81172d3ffa982c6f07712e..5503dc8a11b763d9cb7e81829d846cb94e9ba9e1 100644 (file)
@@ -55,7 +55,7 @@ export function setupOnTrigger(target: { new (...args: any[]): any }): void {
       return this._onTrigger
     },
     set(val) {
-      if (!this._onTrigger) setupFlagsHandler(this)
+      if (val && !this._onTrigger) setupFlagsHandler(this)
       this._onTrigger = val
     },
   })
index 2e741f92ffd0b09ac2d1c31611d963a920e2d80f..00fa403b02e184603cf2ddc35cce93031acad3e6 100644 (file)
@@ -45,13 +45,13 @@ export class EffectScope implements Subscriber {
    */
   private index: number | undefined
 
-  constructor(public detached = false) {
-    this.parent = activeEffectScope
-    if (!detached && activeEffectScope) {
-      this.index =
-        (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
-          this,
-        ) - 1
+  constructor(
+    public detached = false,
+    parent: EffectScope | undefined = activeEffectScope,
+  ) {
+    this.parent = parent
+    if (!detached && parent) {
+      this.index = (parent.scopes || (parent.scopes = [])).push(this) - 1
     }
   }
 
index 851683557bee184387fa85181419322f7b828396..5239f34bf3fc1a80d9118343ec7de332fe5fd6f0 100644 (file)
@@ -13,7 +13,6 @@ import { activeSub } from './effect'
 import {
   type Builtin,
   type ShallowReactiveMarker,
-  isProxy,
   isReactive,
   isReadonly,
   isShallow,
@@ -21,7 +20,6 @@ import {
   toReactive,
 } from './reactive'
 import { type Dependency, type Link, link, propagate } from './system'
-import { warn } from './warning'
 
 declare const RefSymbol: unique symbol
 export declare const RawSymbol: unique symbol
@@ -60,7 +58,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
@@ -95,14 +93,14 @@ 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)
 }
 
 /**
@@ -114,15 +112,17 @@ class RefImpl<T = any> implements Dependency {
   subsTail: Link | undefined = undefined
 
   _value: T
+  _wrap?: <T>(v: T) => T
   private _rawValue: T
 
   public readonly [ReactiveFlags.IS_REF] = true
   public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false
 
-  constructor(value: T, isShallow: boolean) {
-    this._rawValue = isShallow ? value : toRaw(value)
-    this._value = isShallow ? value : toReactive(value)
-    this[ReactiveFlags.IS_SHALLOW] = isShallow
+  constructor(value: T, wrap: (<T>(v: T) => T) | undefined) {
+    this._rawValue = wrap ? toRaw(value) : value
+    this._value = wrap ? wrap(value) : value
+    this._wrap = wrap
+    this[ReactiveFlags.IS_SHALLOW] = !wrap
   }
 
   get dep() {
@@ -143,7 +143,8 @@ class RefImpl<T = any> implements Dependency {
     newValue = useDirectValue ? newValue : toRaw(newValue)
     if (hasChanged(newValue, oldValue)) {
       this._rawValue = newValue
-      this._value = useDirectValue ? newValue : toReactive(newValue)
+      this._value =
+        this._wrap && !useDirectValue ? this._wrap(newValue) : newValue
       if (__DEV__) {
         triggerEventInfos.push({
           target: this,
@@ -353,9 +354,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)
index b8eb0e4720874764616e18a9eb73352a61fbc20d..ffae1085cd872122e5ab2571355b19d0be029024 100644 (file)
@@ -826,7 +826,7 @@ describe('component props', () => {
     )
   })
 
-  // #691ef
+  // #6915
   test('should not mutate original props long-form definition object', () => {
     const props = {
       msg: {
index 8d9982548b5b6c9a0ea1a0fca2b8e454bd4bcddd..96ca009e1a024228741e1b039119410f7ffc8adb 100644 (file)
@@ -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
 
index 45d00579feeb7149400517f2dc0f79ed0f01086c..73e67aab2a0d4aa6ef5153c5f0124a346ebf9c8c 100644 (file)
@@ -573,7 +573,7 @@ describe('attribute fallthrough', () => {
     const Child = {
       props: [],
       render() {
-        return openBlock(), createBlock('div')
+        return (openBlock(), createBlock('div'))
       },
     }
 
index fefc4137034f98d1407958330d63a65b596a4272..fa3c192e885f2052c933771a4bb1711fdc68a848 100644 (file)
@@ -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')
index 81cf7b8df31e98d5d0e0d423c2e4f2a1c2229b8d..3394a41b2a062a6e20c3fe7e47add4db05ca22e4 100644 (file)
@@ -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)
index 3e454aec02378a98ce4b8d301edea651f5b6e70a..f2de08a40328bbae49f2a1c2a029392da19d91a3 100644 (file)
@@ -844,4 +844,12 @@ describe('scheduler', () => {
     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()
+  })
 })
index a7f6a2d5684d5bcb1b80350b901dafce5c28ba44..b4d42d478737c91dfea4d4e874192d54999a974a 100644 (file)
@@ -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'
index cb675f06e432f808e0f45108251bba73db09f803..5c5c06c4891c67fc588a189d28b0cc19014fd8a3 100644 (file)
@@ -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 => {
index d26a14b741c2eeb5cd9c7794bd9358bafb6680b6..6f818cd16b495c9dabc0982b4dcffdcf70aec287 100644 (file)
@@ -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 '.'
@@ -95,11 +99,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 +124,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 +140,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,27 +170,61 @@ export interface AppConfig {
    * @deprecated use config.compilerOptions.isCustomElement
    */
   isCustomElement?: (tag: string) => 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
@@ -187,11 +243,6 @@ export interface AppContext {
    * @internal
    */
   emitsCache: WeakMap<ConcreteComponent, ObjectEmitsOptions | null>
-  /**
-   * HMR only
-   * @internal
-   */
-  reload?: () => void
   /**
    * v2 compat only
    * @internal
@@ -237,17 +288,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)
@@ -350,58 +417,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` +
@@ -429,7 +470,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)
@@ -473,7 +514,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
index 711c5d84de825b5e3087c602593c15053ff2ae6e..91ffed0e8dc015db5f85d998901c846544b86848 100644 (file)
@@ -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)
 }
index b79a6d38a06185be7d94406fb8dfd5616a92dc8c..93af3a2b01ce7f8f15289168a26f9928a3615bd1 100644 (file)
@@ -1,5 +1,5 @@
 import {
-  type ComponentInternalInstance,
+  type GenericComponentInstance,
   currentInstance,
   isInSSRComponentSetup,
   setCurrentInstance,
@@ -20,7 +20,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) {
@@ -38,10 +38,12 @@ export function injectHook(
         // 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
+        try {
+          return callWithAsyncErrorHandling(hook, target, type, args)
+        } finally {
+          reset()
+          resetTracking()
+        }
       })
     if (prepend) {
       hooks.unshift(wrappedHook)
@@ -67,7 +69,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 +81,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 +112,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)
 }
index 2ddaeb509ad7c2a418e6f852f97924ba7fddec3a..6a5532ad555d097cb25e15c154789a09a11e91d0 100644 (file)
@@ -9,9 +9,10 @@ import {
   isPromise,
 } from '@vue/shared'
 import {
+  type ComponentInternalInstance,
   type SetupContext,
   createSetupContext,
-  getCurrentInstance,
+  getCurrentGenericInstance,
   setCurrentInstance,
   unsetCurrentInstance,
 } from './component'
@@ -381,6 +382,7 @@ export function withDefaults<
   return null as any
 }
 
+// TODO return type for Vapor components
 export function useSlots(): SetupContext['slots'] {
   return getContext().slots
 }
@@ -390,11 +392,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 +503,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. ` +
index b6bd6dd4b94f6b0951d52f73a7d9f5c718a82146..fc6ae3b7939ea8bfb1a72eccba3ae0ff6f502e72 100644 (file)
@@ -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!)
index edc57436a56a8071d3b0faa277dee1bcb68d3f49..f998260c7dc65443f821c781684178e368ba2456 100644 (file)
@@ -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
 
index 60552d736d50aa9537c1cc250d09a95671e2cb3f..b9ae038a49edf7b44f6ba338415c8b4d7b41d7af 100644 (file)
@@ -39,6 +39,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 +66,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 +94,13 @@ import type { DefineComponent } from './apiDefineComponent'
 import { markAsyncBoundary } from './helpers/useId'
 import { isAsyncWrapper } from './apiAsyncComponent'
 import type { RendererElement } from './renderer'
+import {
+  setCurrentInstance,
+  setInSSRSetupState,
+  unsetCurrentInstance,
+} from './componentCurrentInstance'
+
+export * from './componentCurrentInstance'
 
 export type Data = Record<string, unknown>
 
@@ -189,6 +195,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 +249,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 +334,84 @@ 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
   /**
-   * Force update render effect
+   * @internal
    */
-  update: () => void
+  refs: Data
+  emit: EmitFn
   /**
-   * Render effect job to be passed to scheduler (checks if dirty)
+   * used for keeping track of .once event handlers on components
+   * @internal
    */
-  job: SchedulerJob
+  emitted: Record<string, boolean> | null
   /**
-   * The render function that returns vdom tree.
+   * used for caching the value returned from props default factory functions to
+   * avoid unnecessary watcher trigger
    * @internal
    */
-  render: InternalRenderFunction | null
+  propsDefaults: Data | null
   /**
-   * SSR render function
+   * used for getting the keys of a component's raw props, vapor only
    * @internal
    */
-  ssrRender?: Function | null
+  rawKeys?: () => string[]
+
+  // exposed properties via expose()
+  exposed: Record<string, any> | null
+  exposeProxy: Record<string, any> | null
+
   /**
-   * Object containing values this component provides for its descendants
+   * setup related
    * @internal
    */
-  provides: Data
+  setupState?: Data
+  /**
+   * devtools access to additional info
+   * @internal
+   */
+  devtoolsRawSetupState?: any
+
+  // lifecycle
+  isMounted: boolean
+  isUnmounted: boolean
+  isDeactivated: boolean
+
   /**
    * for tracking useId()
    * first element is the current boundary prefix
@@ -369,206 +419,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 +694,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 +748,7 @@ export function createComponentInstance(
     emitted: null,
 
     // props default value
-    propsDefaults: EMPTY_OBJ,
+    propsDefaults: null,
 
     // inheritAttrs
     inheritAttrs: type.inheritAttrs,
@@ -708,78 +805,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 +825,6 @@ export function isStatefulComponent(
   return instance.vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT
 }
 
-export let isInSSRComponentSetup = false
-
 export function setupComponent(
   instance: ComponentInternalInstance,
   isSSR = false,
@@ -806,10 +832,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)
@@ -1125,30 +1157,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 +1175,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 +1228,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 +1248,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 +1257,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 +1280,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 (file)
index 0000000..c091b9c
--- /dev/null
@@ -0,0 +1,107 @@
+import { getGlobalThis } from '@vue/shared'
+import type {
+  ComponentInternalInstance,
+  GenericComponentInstance,
+} from './component'
+import { currentRenderingInstance } from './componentRenderContext'
+
+/**
+ * @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
+
+let internalSetCurrentInstance: (
+  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)
+    }
+  }
+  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: GenericComponentInstance) => {
+  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)
+}
+
+/**
+ * Exposed for vapor only. Vapor never runs during SSR so we don't want to pay
+ * for the extra overhead
+ * @internal
+ */
+export const simpleSetCurrentInstance = (
+  i: GenericComponentInstance | null,
+  unset?: GenericComponentInstance | null,
+): void => {
+  currentInstance = i
+  if (unset) {
+    unset.scope.off()
+  } else if (i) {
+    i.scope.on()
+  }
+}
index c03bead3a9280cf1e7bbf4a286123b877e09011b..f055deae5f9d964d8e3b43060b4e2601ed5d3fcb 100644 (file)
@@ -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,
index 5db6a0a1760938f27db68cb13d37c14d0708079f..3422d3a4f4f55206a698623008f45bb2cfc8aa22 100644 (file)
@@ -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 (
index 775eb8b67281ae74708588a9101b7ba338542cbd..93be425b333467a6864c1004e662ba47d2d6ec80 100644 (file)
@@ -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 reset = setCurrentInstance(instance)
+  const props = toRaw(instance.props)
+  value = factory.call(
+    __COMPAT__ && isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance)
+      ? createPropsDefaultThis(instance, props, key)
+      : null,
+    props,
+  )
+  reset()
+  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 + '".')
   }
 }
 
index e9e7770ebd988ac2c8ac2b43979d5c9351916cf9..a43c99e2f45accdd137e689bd253f82f74890337 100644 (file)
@@ -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)
 }
 
index a1afae6201a57033b70829aba8f68f86537c3c4e..a62b5cf4a82a37a4f726f750c6baa363df3ca9c2 100644 (file)
@@ -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
index 6114f6c86cf183ce83b49f433740f44deead40a2..b2394f72c39c2e7534edcef512946227de17c60e 100644 (file)
@@ -106,6 +106,7 @@ const normalizeSlot = (
     if (
       __DEV__ &&
       currentInstance &&
+      !currentInstance.vapor &&
       !(ctx === null && currentRenderingInstance) &&
       !(ctx && ctx.root !== currentInstance.root)
     ) {
index f2b7bdf97386973e9b0cb4cc9e43af0e32574952..d18d5a48b8f76b221039dbd0183edca8c552aa5c 100644 (file)
@@ -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,
@@ -171,7 +178,14 @@ const KeepAliveImpl: ComponentOptions = {
       invalidateMount(instance.m)
       invalidateMount(instance.a)
 
-      move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
+      move(
+        vnode,
+        storageContainer,
+        null,
+        MoveType.LEAVE,
+        keepAliveInstance,
+        parentSuspense,
+      )
       queuePostRenderEffect(() => {
         if (instance.da) {
           invokeArrayFns(instance.da)
@@ -197,7 +211,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 +254,15 @@ 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)) {
+        if (isSuspense(keepAliveInstance.subTree.type)) {
           queuePostRenderEffect(() => {
-            cache.set(pendingCacheKey!, getInnerChild(instance.subTree))
-          }, instance.subTree.suspense)
+            cache.set(
+              pendingCacheKey!,
+              getInnerChild(keepAliveInstance.subTree),
+            )
+          }, keepAliveInstance.subTree.suspense)
         } else {
-          cache.set(pendingCacheKey, getInnerChild(instance.subTree))
+          cache.set(pendingCacheKey, getInnerChild(keepAliveInstance.subTree))
         }
       }
     }
@@ -254,7 +271,7 @@ 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
@@ -416,7 +433,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 +442,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 +459,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 +472,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.
index 85001f500cff8edc027cae4553dd8aa3eee4bed4..0f6f69c6526d9edd488b5d0586b0b9dd41020585 100644 (file)
@@ -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
     },
 
index c37356a7869bfa054eea5fa15e917171fb0266b2..21655f9d7511e858c8db81ae9775cdbfa473750a 100644 (file)
@@ -245,6 +245,7 @@ export const TeleportImpl = {
             container,
             mainAnchor,
             internals,
+            parentComponent,
             TeleportMoveTypes.TOGGLE,
           )
         } else {
@@ -268,6 +269,7 @@ export const TeleportImpl = {
               nextTarget,
               null,
               internals,
+              parentComponent,
               TeleportMoveTypes.TARGET_CHANGE,
             )
           } else if (__DEV__) {
@@ -285,6 +287,7 @@ export const TeleportImpl = {
             target,
             targetAnchor,
             internals,
+            parentComponent,
             TeleportMoveTypes.TOGGLE,
           )
         }
@@ -347,6 +350,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 +375,7 @@ function moveTeleport(
           container,
           parentAnchor,
           MoveType.REORDER,
+          parentComponent,
         )
       }
     }
index 9ac4c433ac162b16b5de10b09a52c3e16d2b43d7..cf7b1973c427f881eba22a3ef43e522a67273608 100644 (file)
@@ -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
@@ -49,7 +49,11 @@ function emit(event: string, ...args: any[]) {
   }
 }
 
+let queued = false
 export function setDevtoolsHook(hook: DevtoolsHook, target: any): void {
+  if (devtoolsNotInstalled || queued) {
+    return
+  }
   devtools = hook
   if (devtools) {
     devtools.enabled = true
@@ -66,6 +70,7 @@ export function setDevtoolsHook(hook: DevtoolsHook, target: any): void {
     // eslint-disable-next-line no-restricted-syntax
     !window.navigator?.userAgent?.includes('jsdom')
   ) {
+    queued = true
     const replay = (target.__VUE_DEVTOOLS_HOOK_REPLAY__ =
       target.__VUE_DEVTOOLS_HOOK_REPLAY__ || [])
     replay.push((newHook: DevtoolsHook) => {
@@ -111,7 +116,7 @@ const _devtoolsComponentRemoved = /*@__PURE__*/ createDevtoolsComponentHook(
 )
 
 export const devtoolsComponentRemoved = (
-  component: ComponentInternalInstance,
+  component: GenericComponentInstance,
 ): void => {
   if (
     devtools &&
@@ -123,13 +128,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 +152,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 {
index c4bdf0baccd6a15bbb20658e465cdb3021d9387f..f8048c5c0e7d32000c5e086479055d7a258d2d6b 100644 (file)
@@ -1,6 +1,5 @@
 import { pauseTracking, resetTracking } from '@vue/reactivity'
-import type { VNode } from './vnode'
-import type { ComponentInternalInstance } from './component'
+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]
@@ -151,23 +149,23 @@ export function handleError(
       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
index 8092e8d3574af062b9c3e72523228c08dd6b1ccb..149f67512de0c071037b66e05f95257cf834b276 100644 (file)
@@ -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
 }
index 92f7dab36b621f2be3cf2b7a49ea2e9b066f6123..152c5a4b81c13735825feb6e6d182ad08e2b0136 100644 (file)
@@ -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 && slots._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 ` +
index aa6532c28117f3aa37e0ebf9f6ff20c28a6d6189..910fab334242c78369c7b75b7196cf78b467131f 100644 (file)
@@ -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) {
index 4b51e0dda7f3580c9dddda226134a5a7a9df0c8a..0fcc1e435c021c0c779024e801fa79e0ac1b1598 100644 (file)
@@ -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]
 }
index c40938ead3cee5edb62eba241c7c4133cd9c867a..e85edc6e9a7b7c2bee08490aeba5b928a606fba8 100644 (file)
@@ -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`)
 }
index f516d14c9bd8c229a398d9e0f0f1a924a8fff934..8ba4409c52a61f4441f6877d35d71dadc568e995 100644 (file)
@@ -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
index 7aedf52dd3ea562045200e411947c7ca937aa90f..ed5d8b081a0ff8d9eddc149f21d37938501e9c59 100644 (file)
@@ -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.update()
+    }
+    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).update()
+          }
+          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()
index bdebed5960284d8bd708c9971eeff2bffe0d1410..640f26eb05cef0707faa16e161e9eeb987e54178 100644 (file)
@@ -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.
@@ -771,7 +779,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 +959,11 @@ function resolveCssVars(
     }
   }
   if (vnode === root && instance.parent) {
-    resolveCssVars(instance.parent, instance.vnode, expectedMap)
+    resolveCssVars(
+      instance.parent as ComponentInternalInstance,
+      instance.vnode,
+      expectedMap,
+    )
   }
 }
 
index 9910f82102b06daa9f132f6e15dd86d908dccb66..1ed6f21df7769a695a0643a6c27c38eaeaf30eef 100644 (file)
@@ -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,83 @@ 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,
+  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'
index 1984f5a21f2c10168c1f2692797d61dfcda85780..c14207f91e4ef686851f28793691f88644d0e04e 100644 (file)
@@ -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())
   }
 }
 
index 3550a2aefc37b51e69d8260b8c81061cade70573..39f652add7c3509abbb93717a322db7b2f7cadf8 100644 (file)
@@ -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,
@@ -57,7 +63,12 @@ import {
 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,
@@ -426,6 +447,9 @@ function baseCreateRenderer(
           optimized,
         )
         break
+      case VaporSlot:
+        getVaporInterface(parentComponent, n2).slot(n1, n2, container, anchor)
+        break
       default:
         if (shapeFlag & ShapeFlags.ELEMENT) {
           processElement(
@@ -733,7 +757,7 @@ function baseCreateRenderer(
     vnode: VNode,
     scopeId: string | null,
     slotScopeIds: string[] | null,
-    parentComponent: ComponentInternalInstance | null,
+    parentComponent: GenericComponentInstance | null,
   ) => {
     if (scopeId) {
       hostSetScopeId(el, scopeId)
@@ -743,8 +767,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 +782,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,
         )
       }
     }
@@ -1141,7 +1165,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,
@@ -1352,11 +1392,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__) {
@@ -1414,6 +1455,7 @@ function baseCreateRenderer(
         if (
           initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE ||
           (parent &&
+            parent.vnode &&
             isAsyncWrapper(parent.vnode) &&
             parent.vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE)
         ) {
@@ -2000,7 +2042,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--
           }
@@ -2014,11 +2062,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
     }
 
@@ -2028,14 +2087,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
@@ -2134,7 +2205,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)
@@ -2182,6 +2258,11 @@ function baseCreateRenderer(
         unmountChildren(children as VNode[], parentComponent, parentSuspense)
       }
 
+      if (type === VaporSlot) {
+        getVaporInterface(parentComponent, vnode).unmount(vnode, doRemove)
+        return
+      }
+
       if (doRemove) {
         remove(vnode)
       }
@@ -2264,10 +2345,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)
@@ -2295,7 +2376,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
       })
     }
 
@@ -2371,6 +2452,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) {
@@ -2384,7 +2468,6 @@ function baseCreateRenderer(
     return teleportEnd ? hostNextSibling(teleportEnd) : el
   }
 
-  let isFlushing = false
   const render: RootRenderFunction = (vnode, container, namespace) => {
     if (vnode == null) {
       if (container._vnode) {
@@ -2402,12 +2485,7 @@ function baseCreateRenderer(
       )
     }
     container._vnode = vnode
-    if (!isFlushing) {
-      isFlushing = true
-      flushPreFlushCbs()
-      flushPostFlushCbs()
-      isFlushing = false
-    }
+    flushOnAppMount()
   }
 
   const internals: RendererInternals = {
@@ -2416,6 +2494,7 @@ function baseCreateRenderer(
     m: move,
     r: remove,
     mt: mountComponent,
+    umt: unmountComponent,
     mc: mountChildren,
     pc: patchChildren,
     pbc: patchBlockChildren,
@@ -2431,10 +2510,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,
+    ),
   }
 }
 
@@ -2453,15 +2580,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
+    }
   }
 }
 
@@ -2525,48 +2654,6 @@ 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 {
@@ -2580,9 +2667,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!
+}
index b40c31d395211325bef0dcc965c1189ed8a58e24..a75eba300f780f4a62b3f5daa2334f3cfff0fdbb 100644 (file)
@@ -1,6 +1,6 @@
 import { ErrorCodes, callWithErrorHandling, handleError } from './errorHandling'
 import { NOOP, isArray } from '@vue/shared'
-import { type ComponentInternalInstance, getComponentName } from './component'
+import { type GenericComponentInstance, getComponentName } from './component'
 
 export enum SchedulerJobFlags {
   QUEUED = 1 << 0,
@@ -35,7 +35,7 @@ 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[]
@@ -91,6 +91,9 @@ function findInsertionIndex(id: number) {
   return start
 }
 
+/**
+ * @internal for runtime-vapor only
+ */
 export function queueJob(job: SchedulerJob): void {
   if (!(job.flags! & SchedulerJobFlags.QUEUED)) {
     const jobId = getId(job)
@@ -113,7 +116,10 @@ export function queueJob(job: SchedulerJob): void {
 
 function queueFlush() {
   if (!currentFlushPromise) {
-    currentFlushPromise = resolvedPromise.then(flushJobs)
+    currentFlushPromise = resolvedPromise.then(flushJobs).catch(e => {
+      currentFlushPromise = null
+      throw e
+    })
   }
 }
 
@@ -135,7 +141,7 @@ export function queuePostFlushCb(cb: SchedulerJobs): void {
 }
 
 export function flushPreFlushCbs(
-  instance?: ComponentInternalInstance,
+  instance?: GenericComponentInstance,
   seen?: CountMap,
   // skip the current job
   i: number = flushIndex + 1,
@@ -195,14 +201,32 @@ export function flushPostFlushCbs(seen?: CountMap): void {
       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
     postFlushIndex = 0
   }
 }
 
+let isFlushing = false
+/**
+ * @internal
+ */
+export function flushOnAppMount(): void {
+  if (!isFlushing) {
+    isFlushing = true
+    flushPreFlushCbs()
+    flushPostFlushCbs()
+    isFlushing = false
+  }
+}
+
 const getId = (job: SchedulerJob): number =>
   job.id == null ? (job.flags! & SchedulerJobFlags.PRE ? -1 : Infinity) : job.id
 
index a8c5340cd1fe167840f700e34a2a25b1909eb048..4b31151da228a0aa7e1f81e1f3e738f7aed434fa 100644 (file)
@@ -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 {
index 788e9372154fc30a36de05cc91926c2d2a7b64f6..361a2734ba4da81cbd79373d7a89e9cbebd8fef8 100644 (file)
@@ -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 { 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()
 }
@@ -40,7 +43,8 @@ export function warn(msg: string, ...args: any[]): void {
   // during patch, leading to infinite recursion.
   pauseTracking()
 
-  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,
@@ -79,8 +84,8 @@ export function warn(msg: string, ...args: any[]): void {
 }
 
 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[] {
index 7bb5ce0c6b13e6657928db879a455c897e30d116..72af535d385e0a837df6fccea72e9f0bae6e7213 100644 (file)
@@ -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'
index 5057e16d472bc59304b2bb386e96b53934e6cf92..ee6cfcbab7ab1101477ba3f8a62228fc99267474 100644 (file)
@@ -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<
index f8f41bb04d4d0ffbe8d39e68d3312d4848edafc2..fab85368025490ba04b808c260e5dde99d56a61c 100644 (file)
@@ -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
 }
 
index c69375983d81ee1cbbf0f1f023bf05cfbd25f052..b241458dba798cc31280d7b2f9fd981de42f979d 100644 (file)
@@ -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'
index 98608831a9a650a388dc994006269d254cacdb8c..a28290acf4a3b88fe9b06bbe160e9a39659ac00c 100644 (file)
@@ -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 =
index 383628a6ad0492d926d536fe9006a14dc9d9e717..2d1db4c0abdb6af07b149c196c7c31ba424b61d5 100644 (file)
@@ -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(
index 27174ddf62460238ef59cbbd2a1d7a3020b6fe28..dfa5974ebe94b7397f0753063521445cccb21689 100644 (file)
@@ -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 (file)
index 0000000..15f1f7e
--- /dev/null
@@ -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 (file)
index 0000000..ab8204d
--- /dev/null
@@ -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 (file)
index 0000000..d1ede2a
--- /dev/null
@@ -0,0 +1,137 @@
+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
+}
diff --git a/packages/runtime-vapor/__tests__/apiCreateDynamicComponent.spec.ts b/packages/runtime-vapor/__tests__/apiCreateDynamicComponent.spec.ts
new file mode 100644 (file)
index 0000000..e912af2
--- /dev/null
@@ -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 (file)
index 0000000..9b36a2c
--- /dev/null
@@ -0,0 +1,115 @@
+import { ref } from '@vue/reactivity'
+import { makeRender } from './_utils'
+// @ts-expect-error
+import { createFor, createSelector, renderEffect } from '../src'
+import { nextTick } from '@vue/runtime-dom'
+
+const define = makeRender()
+
+describe.todo('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(() => {
+      const isSleected = createSelector(index)
+      return createFor(
+        () => list.value,
+        item => {
+          const span = document.createElement('li')
+          renderEffect(() => {
+            calledTimes += 1
+            const { id } = item.value
+            span.textContent = `${id}.${isSleected(id) ? 't' : 'f'}`
+          })
+          return span
+        },
+        item => item.id,
+      )
+    }).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))
+  })
+
+  test('custom compare', async () => {
+    let calledTimes = 0
+    let expectedCalledTimes = 0
+
+    const list = ref([{ id: 1 }, { id: 2 }, { id: 3 }])
+    const index = ref(0)
+
+    const { host } = define(() => {
+      const isSleected = createSelector(
+        index,
+        // @ts-expect-error
+        (key, value) => key === value + 1,
+      )
+      return createFor(
+        () => list.value,
+        item => {
+          const span = document.createElement('li')
+          renderEffect(() => {
+            calledTimes += 1
+            const { id } = item.value
+            span.textContent = `${id}.${isSleected(id) ? 't' : 'f'}`
+          })
+          return span
+        },
+        item => item.id,
+      )
+    }).render()
+
+    expect(host.innerHTML).toBe(
+      '<li>1.t</li><li>2.f</li><li>3.f</li><!--for-->',
+    )
+    expect(calledTimes).toBe((expectedCalledTimes += 3))
+
+    index.value = 1
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>1.f</li><li>2.t</li><li>3.f</li><!--for-->',
+    )
+    expect(calledTimes).toBe((expectedCalledTimes += 2))
+
+    index.value = 2
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>1.f</li><li>2.f</li><li>3.t</li><!--for-->',
+    )
+    expect(calledTimes).toBe((expectedCalledTimes += 2))
+
+    list.value[2].id = 4
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>1.f</li><li>2.f</li><li>4.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 (file)
index 0000000..c82d432
--- /dev/null
@@ -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('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`,
+    ).toHaveBeenWarned()
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/apiExpose.spec.ts b/packages/runtime-vapor/__tests__/apiExpose.spec.ts
new file mode 100644 (file)
index 0000000..03a7e75
--- /dev/null
@@ -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 (file)
index 0000000..ea185e1
--- /dev/null
@@ -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 (file)
index 0000000..2ad7d2e
--- /dev/null
@@ -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 (file)
index 0000000..5e0d5d9
--- /dev/null
@@ -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 (file)
index 0000000..21edc87
--- /dev/null
@@ -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 (file)
index 0000000..068791b
--- /dev/null
@@ -0,0 +1,338 @@
+import {
+  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(instance!.scope.effects.length).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)
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/block.spec.ts b/packages/runtime-vapor/__tests__/block.spec.ts
new file mode 100644 (file)
index 0000000..9f76c7f
--- /dev/null
@@ -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 (file)
index 0000000..22294b1
--- /dev/null
@@ -0,0 +1,355 @@
+import {
+  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(i.scope.effects.length).toBe(3)
+    expect(host.innerHTML).toBe('<div>0</div>')
+
+    app.unmount()
+    expect(host.innerHTML).toBe('')
+    expect(i.scope.effects.length).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()
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/componentAttrs.spec.ts b/packages/runtime-vapor/__tests__/componentAttrs.spec.ts
new file mode 100644 (file)
index 0000000..1f43ebb
--- /dev/null
@@ -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 (file)
index 0000000..8c8a560
--- /dev/null
@@ -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 (file)
index 0000000..55b4924
--- /dev/null
@@ -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 (file)
index 0000000..58076ff
--- /dev/null
@@ -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 (file)
index 0000000..1fd5f95
--- /dev/null
@@ -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 (file)
index 0000000..5215421
--- /dev/null
@@ -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 (file)
index 0000000..da55378
--- /dev/null
@@ -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 (file)
index 0000000..4b59882
--- /dev/null
@@ -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 (file)
index 0000000..e879b71
--- /dev/null
@@ -0,0 +1,454 @@
+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 {
+  currentInstance,
+  ref,
+  simpleSetCurrentInstance,
+} from '@vue/runtime-dom'
+
+let removeComponentInstance = NOOP
+beforeEach(() => {
+  const instance = new VaporComponentInstance({}, {}, null)
+  const prev = currentInstance
+  simpleSetCurrentInstance(instance)
+  removeComponentInstance = () => {
+    simpleSetCurrentInstance(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 (file)
index 0000000..85de309
--- /dev/null
@@ -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 (file)
index 0000000..f1ce23a
--- /dev/null
@@ -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 (file)
index 0000000..87a7961
--- /dev/null
@@ -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 (file)
index 0000000..db91b6a
--- /dev/null
@@ -0,0 +1,737 @@
+import {
+  createFor,
+  getDefaultValue,
+  getRestElement,
+  renderEffect,
+} from '../src'
+import {
+  nextTick,
+  reactive,
+  readonly,
+  ref,
+  shallowRef,
+  triggerRef,
+} from '@vue/runtime-dom'
+import { makeRender } 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.name,
+      )
+      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-->')
+    })
+  })
+})
diff --git a/packages/runtime-vapor/__tests__/hydration.spec.ts b/packages/runtime-vapor/__tests__/hydration.spec.ts
new file mode 100644 (file)
index 0000000..6ba2bf8
--- /dev/null
@@ -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 (file)
index 0000000..eed0a95
--- /dev/null
@@ -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 (file)
index 0000000..425d849
--- /dev/null
@@ -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 (file)
index 0000000..08326d4
--- /dev/null
@@ -0,0 +1,58 @@
+import { defineComponent, h } from '@vue/runtime-dom'
+import { makeInteropRender } from './_utils'
+import { createComponent, defineVaporComponent } from '../src'
+
+const define = makeInteropRender()
+
+describe('vdomInterop', () => {
+  describe.todo('props', () => {})
+
+  describe.todo('emit', () => {})
+
+  describe.todo('slots', () => {})
+
+  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 (file)
index 0000000..3706e8e
--- /dev/null
@@ -0,0 +1,47 @@
+{
+  "name": "@vue/runtime-vapor",
+  "version": "3.5.13",
+  "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 (file)
index 0000000..a1f960b
--- /dev/null
@@ -0,0 +1 @@
+_old
diff --git a/packages/runtime-vapor/src/apiCreateApp.ts b/packages/runtime-vapor/src/apiCreateApp.ts
new file mode 100644 (file)
index 0000000..834437e
--- /dev/null
@@ -0,0 +1,131 @@
+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) {
+  if (__DEV__) {
+    app.config.globalProperties = new Proxy(
+      {},
+      {
+        set() {
+          warn(`app.config.globalProperties is not supported in vapor mode.`)
+          return false
+        },
+      },
+    )
+  }
+
+  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 (file)
index 0000000..945e0f3
--- /dev/null
@@ -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 (file)
index 0000000..6252914
--- /dev/null
@@ -0,0 +1,473 @@
+import {
+  EffectScope,
+  type ShallowRef,
+  isReactive,
+  isReadonly,
+  isShallow,
+  pauseTracking,
+  resetTracking,
+  shallowReadArray,
+  shallowRef,
+  toReactive,
+  toReadonly,
+} from '@vue/reactivity'
+import { getSequence, 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,
+): 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
+  // 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
+
+  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)
+
+    pauseTracking()
+
+    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
+        const doRemove = !canUseFastRemove
+        for (let i = 0; i < oldLength; i++) {
+          unmount(oldBlocks[i], doRemove)
+        }
+        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 {
+        let i = 0
+        let e1 = oldLength - 1 // prev ending index
+        let e2 = newLength - 1 // next ending index
+
+        // 1. sync from start
+        // (a b) c
+        // (a b) d e
+        while (i <= e1 && i <= e2) {
+          if (tryPatchIndex(source, i)) {
+            i++
+          } else {
+            break
+          }
+        }
+
+        // 2. sync from end
+        // a (b c)
+        // d e (b c)
+        while (i <= e1 && i <= e2) {
+          if (tryPatchIndex(source, i)) {
+            e1--
+            e2--
+          } else {
+            break
+          }
+        }
+
+        // 3. common sequence + mount
+        // (a b)
+        // (a b) c
+        // i = 2, e1 = 1, e2 = 2
+        // (a b)
+        // c (a b)
+        // i = 0, e1 = -1, e2 = 0
+        if (i > e1) {
+          if (i <= e2) {
+            const nextPos = e2 + 1
+            const anchor =
+              nextPos < newLength
+                ? normalizeAnchor(newBlocks[nextPos].nodes)
+                : parentAnchor
+            while (i <= e2) {
+              mount(source, i, anchor)
+              i++
+            }
+          }
+        }
+
+        // 4. common sequence + unmount
+        // (a b) c
+        // (a b)
+        // i = 2, e1 = 2, e2 = 1
+        // a (b c)
+        // (b c)
+        // i = 0, e1 = 0, e2 = -1
+        else if (i > e2) {
+          while (i <= e1) {
+            unmount(oldBlocks[i])
+            i++
+          }
+        }
+
+        // 5. unknown sequence
+        // [i ... e1 + 1]: a b [c d e] f g
+        // [i ... e2 + 1]: a b [e d c h] f g
+        // i = 2, e1 = 4, e2 = 5
+        else {
+          const s1 = i // prev starting index
+          const s2 = i // next starting index
+
+          // 5.1 build key:index map for newChildren
+          const keyToNewIndexMap = new Map()
+          for (i = s2; i <= e2; i++) {
+            keyToNewIndexMap.set(getKey(...getItem(source, i)), i)
+          }
+
+          // 5.2 loop through old children left to be patched and try to patch
+          // matching nodes & remove nodes that are no longer present
+          let j
+          let patched = 0
+          const toBePatched = e2 - s2 + 1
+          let moved = false
+          // used to track whether any node has moved
+          let maxNewIndexSoFar = 0
+          // works as Map<newIndex, oldIndex>
+          // Note that oldIndex is offset by +1
+          // and oldIndex = 0 is a special value indicating the new node has
+          // no corresponding old node.
+          // used for determining longest stable subsequence
+          const newIndexToOldIndexMap = new Array(toBePatched).fill(0)
+
+          for (i = s1; i <= e1; i++) {
+            const prevBlock = oldBlocks[i]
+            if (patched >= toBePatched) {
+              // all new children have been patched so this can only be a removal
+              unmount(prevBlock)
+            } else {
+              const newIndex = keyToNewIndexMap.get(prevBlock.key)
+              if (newIndex == null) {
+                unmount(prevBlock)
+              } else {
+                newIndexToOldIndexMap[newIndex - s2] = i + 1
+                if (newIndex >= maxNewIndexSoFar) {
+                  maxNewIndexSoFar = newIndex
+                } else {
+                  moved = true
+                }
+                update(
+                  (newBlocks[newIndex] = prevBlock),
+                  ...getItem(source, newIndex),
+                )
+                patched++
+              }
+            }
+          }
+
+          // 5.3 move and mount
+          // generate longest stable subsequence only when nodes have moved
+          const increasingNewIndexSequence = moved
+            ? getSequence(newIndexToOldIndexMap)
+            : []
+          j = increasingNewIndexSequence.length - 1
+          // looping backwards so that we can use last patched node as anchor
+          for (i = toBePatched - 1; i >= 0; i--) {
+            const nextIndex = s2 + i
+            const anchor =
+              nextIndex + 1 < newLength
+                ? normalizeAnchor(newBlocks[nextIndex + 1].nodes)
+                : parentAnchor
+            if (newIndexToOldIndexMap[i] === 0) {
+              // mount new
+              mount(source, nextIndex, anchor)
+            } else if (moved) {
+              // move if:
+              // There is no stable subsequence (e.g. a reverse)
+              // OR current node is not among the stable sequence
+              if (j < 0 || i !== increasingNewIndexSequence[j]) {
+                insert(newBlocks[nextIndex].nodes, parent!, anchor)
+              } else {
+                j--
+              }
+            }
+          }
+        }
+      }
+    }
+
+    frag.nodes = [(oldBlocks = newBlocks)]
+    if (parentAnchor) {
+      frag.nodes.push(parentAnchor)
+    }
+
+    resetTracking()
+  }
+
+  const needKey = renderItem.length > 1
+  const needIndex = renderItem.length > 2
+
+  const mount = (
+    source: ResolvedSource,
+    idx: number,
+    anchor: Node | undefined = parentAnchor,
+  ): ForBlock => {
+    const [item, key, index] = getItem(source, idx)
+    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
+
+    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,
+      getKey && getKey(item, key, index),
+    ))
+
+    if (parent) insert(block.nodes, parent, anchor)
+
+    return block
+  }
+
+  const tryPatchIndex = (source: any, idx: number) => {
+    const block = oldBlocks[idx]
+    const [item, key, index] = getItem(source, idx)
+    if (block.key === getKey!(item, key, index)) {
+      update((newBlocks[idx] = block), item)
+      return true
+    }
+  }
+
+  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 = ({ nodes, scope }: ForBlock, doRemove = true) => {
+    scope && scope.stop()
+    doRemove && removeBlock(nodes, parent!)
+  }
+
+  if (flags & VaporVForFlags.ONCE) {
+    renderList()
+  } else {
+    renderEffect(renderList)
+  }
+
+  if (!isHydrating && _insertionParent) {
+    insert(frag, _insertionParent, _insertionAnchor)
+  }
+
+  return frag
+}
+
+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 (file)
index 0000000..f573a61
--- /dev/null
@@ -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 (file)
index 0000000..430f87c
--- /dev/null
@@ -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 (file)
index 0000000..d3f3cf7
--- /dev/null
@@ -0,0 +1,155 @@
+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})`)
+        }
+      }
+      doSet.id = -1
+      queuePostFlushCb(doSet)
+
+      // 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 (file)
index 0000000..b782afd
--- /dev/null
@@ -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, pauseTracking, resetTracking } 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
+
+    pauseTracking()
+    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)
+    }
+
+    resetTracking()
+  }
+}
+
+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 (file)
index 0000000..af15133
--- /dev/null
@@ -0,0 +1,582 @@
+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,
+  simpleSetCurrentInstance,
+  startMeasure,
+  unregisterHMR,
+  warn,
+} from '@vue/runtime-dom'
+import { type Block, DynamicFragment, insert, isBlock, remove } from './block'
+import {
+  type ShallowRef,
+  markRaw,
+  onScopeDispose,
+  pauseTracking,
+  proxyRefs,
+  resetTracking,
+  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,
+  )
+
+  if (__DEV__) {
+    pushWarningContext(instance)
+    startMeasure(instance, `init`)
+
+    // cache normalized options for dev only emit check
+    instance.propsOptions = normalizePropsOptions(component)
+    instance.emitsOptions = normalizeEmitsOptions(component)
+  }
+
+  const prev = currentInstance
+  simpleSetCurrentInstance(instance)
+  pauseTracking()
+
+  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)
+
+      // HMR
+      if (component.__hmrId) {
+        registerHMR(instance)
+        instance.isSingleRoot = isSingleRoot
+        instance.hmrRerender = hmrRerender.bind(null, instance)
+        instance.hmrReload = hmrReload.bind(null, 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
+      })
+    }
+  }
+
+  resetTracking()
+  simpleSetCurrentInstance(prev, instance)
+
+  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 =
+    callWithErrorHandling(
+      instance.type.render!,
+      instance,
+      ErrorCodes.RENDER_FUNCTION,
+      [
+        instance.setupState,
+        instance.props,
+        instance.emit,
+        instance.attrs,
+        instance.slots,
+      ],
+    ) || []
+}
+
+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 (file)
index 0000000..68b7cfb
--- /dev/null
@@ -0,0 +1,53 @@
+import { type ObjectEmitsOptions, baseEmit } from '@vue/runtime-dom'
+import type { VaporComponent, VaporComponentInstance } from './component'
+import { EMPTY_OBJ, hasOwn, isArray } from '@vue/shared'
+import { resolveSource } from './componentProps'
+
+/**
+ * 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: Record<string, any>, key: string) {
+  const dynamicSources = rawProps.$
+  if (dynamicSources) {
+    let i = dynamicSources.length
+    while (i--) {
+      const source = resolveSource(dynamicSources[i])
+      if (hasOwn(source, key)) return 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 (file)
index 0000000..9cf65c5
--- /dev/null
@@ -0,0 +1,366 @@
+import {
+  EMPTY_ARR,
+  NO,
+  YES,
+  camelize,
+  hasOwn,
+  isArray,
+  isFunction,
+  isString,
+} from '@vue/shared'
+import type { VaporComponent, VaporComponentInstance } from './component'
+import {
+  type NormalizedPropsOptions,
+  baseNormalizePropsOptions,
+  currentInstance,
+  isEmitListener,
+  popWarningContext,
+  pushWarningContext,
+  resolvePropValue,
+  simpleSetCurrentInstance,
+  validateProps,
+  warn,
+} from '@vue/runtime-dom'
+import { ReactiveFlags } from '@vue/reactivity'
+import { normalizeEmitsOptions } from './componentEmits'
+import { renderEffect } from './renderEffect'
+
+export type RawProps = Record<string, () => unknown> & {
+  // generated by compiler for :[key]="x" or v-bind="x"
+  $?: DynamicPropsSource[]
+}
+
+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 (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 = currentInstance
+  simpleSetCurrentInstance(instance)
+  const res = factory.call(null, instance.props)
+  simpleSetCurrentInstance(prev, instance)
+  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 (file)
index 0000000..100c99c
--- /dev/null
@@ -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 (file)
index 0000000..32cfe96
--- /dev/null
@@ -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 (file)
index 0000000..0c32b24
--- /dev/null
@@ -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 (file)
index 0000000..ac4c066
--- /dev/null
@@ -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 (file)
index 0000000..4987ecf
--- /dev/null
@@ -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 (file)
index 0000000..d34d9db
--- /dev/null
@@ -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 (file)
index 0000000..83bc32c
--- /dev/null
@@ -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 (file)
index 0000000..8c42ad7
--- /dev/null
@@ -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 (file)
index 0000000..b78ca4e
--- /dev/null
@@ -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 (file)
index 0000000..741f385
--- /dev/null
@@ -0,0 +1,49 @@
+import {
+  currentInstance,
+  popWarningContext,
+  pushWarningContext,
+  simpleSetCurrentInstance,
+} 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 = currentInstance
+  simpleSetCurrentInstance(instance)
+  pushWarningContext(instance)
+  devRender(instance)
+  popWarningContext()
+  simpleSetCurrentInstance(prev, instance)
+  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 = currentInstance
+  simpleSetCurrentInstance(instance.parent)
+  const newInstance = createComponent(
+    newComp,
+    instance.rawProps,
+    instance.rawSlots,
+    instance.isSingleRoot,
+  )
+  simpleSetCurrentInstance(prev, instance.parent)
+  mountComponent(newInstance, parent, anchor)
+}
diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts
new file mode 100644 (file)
index 0000000..682532f
--- /dev/null
@@ -0,0 +1,44 @@
+// 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 } 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 (file)
index 0000000..c8c7ffb
--- /dev/null
@@ -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 (file)
index 0000000..a9fa9b3
--- /dev/null
@@ -0,0 +1,70 @@
+import { ReactiveEffect, getCurrentScope } from '@vue/reactivity'
+import {
+  type SchedulerJob,
+  currentInstance,
+  queueJob,
+  queuePostFlushCb,
+  simpleSetCurrentInstance,
+  startMeasure,
+  warn,
+} from '@vue/runtime-dom'
+import { type VaporComponentInstance, isVaporComponent } from './component'
+import { invokeArrayFns } from '@vue/shared'
+
+export function renderEffect(fn: () => void, noLifecycle = false): void {
+  const instance = currentInstance as VaporComponentInstance | null
+  const scope = getCurrentScope()
+  if (__DEV__ && !__TEST__ && !scope && !isVaporComponent(instance)) {
+    warn('renderEffect called without active EffectScope or Vapor instance.')
+  }
+
+  // renderEffect is always called after user has registered all hooks
+  const hasUpdateHooks = instance && (instance.bu || instance.u)
+  const renderEffectFn = noLifecycle
+    ? fn
+    : () => {
+        if (__DEV__ && instance) {
+          startMeasure(instance, `renderEffect`)
+        }
+        const prev = currentInstance
+        simpleSetCurrentInstance(instance)
+        if (scope) scope.on()
+        if (hasUpdateHooks && instance.isMounted && !instance.isUpdating) {
+          instance.isUpdating = true
+          instance.bu && invokeArrayFns(instance.bu)
+          fn()
+          queuePostFlushCb(() => {
+            instance.isUpdating = false
+            instance.u && invokeArrayFns(instance.u)
+          })
+        } else {
+          fn()
+        }
+        if (scope) scope.off()
+        simpleSetCurrentInstance(prev, instance)
+        if (__DEV__ && instance) {
+          startMeasure(instance, `renderEffect`)
+        }
+      }
+
+  const effect = new ReactiveEffect(renderEffectFn)
+  const job: SchedulerJob = () => effect.dirty && effect.run()
+
+  if (instance) {
+    if (__DEV__) {
+      effect.onTrack = instance.rtc
+        ? e => invokeArrayFns(instance.rtc!, e)
+        : void 0
+      effect.onTrigger = instance.rtg
+        ? e => invokeArrayFns(instance.rtg!, e)
+        : void 0
+    }
+    job.i = instance
+    job.id = instance.uid
+  }
+
+  effect.scheduler = () => queueJob(job)
+  effect.run()
+
+  // TODO recurse handling
+}
diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts
new file mode 100644 (file)
index 0000000..e277024
--- /dev/null
@@ -0,0 +1,304 @@
+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'
+
+// 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)
+
+    // @ts-expect-error
+    const instance = (vnode.component = createComponent(
+      vnode.type as any as VaporComponent,
+      {
+        $: [() => propsRef.value],
+      } 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) {
+    if (key === '_vapor') {
+      return target
+    } else {
+      return target[key]
+    }
+  },
+}
+
+/**
+ * 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']
+}
index f04080b9c3127d04024b7a18cdc499118ea55af9..221d3895e2288f80699d918f333d3631498825f2 100644 (file)
@@ -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}`
index b5f0166327fb3133e765fd47ff40a475ab3824de..80fb80f9d9cbad18d5505840ce74c58eb5e6f5fa 100644 (file)
@@ -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
+}
index 9c6a23132404d8c6bd54dacc8b073a2b18e837a7..bf11ba7a2937f3c27995fa8491ebc9ae7c2a2d5b 100644 (file)
@@ -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('-')
+  )
+}
index 08d4b07af8e56e50a0670f51072d0c3854e2bde4..0c38d640ba0982f7a8439279624ca9f71018042a 100644 (file)
@@ -12,4 +12,5 @@ export * from './escapeHtml'
 export * from './looseEqual'
 export * from './toDisplayString'
 export * from './typeUtils'
+export * from './subSequence'
 export * from './cssVars'
index ef598a03ced79d4862991ebadec82fd1bb2c61c7..eec620480de0bd47f0d1df66c4165a162b685a21 100644 (file)
@@ -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 (file)
index 0000000..8edde79
--- /dev/null
@@ -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
+}
index b23cf766a5c802e88c2324578b411e9e05074c0c..d2799667927c5b0cbf8643d0289370b3e354bd25 100644 (file)
@@ -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 (file)
index 0000000..2be48df
--- /dev/null
@@ -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,
+}
index 9428d6f6c7b0615b3759761008eb42b83018733e..97fe9a1cf19b8406db7a36cd1d0f9bcd446306f1 100644 (file)
@@ -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).
index 757ea82f5d7f81a51f8be75f58549c19899287c0..30e08cfd2ab5a900467cba5d7766f314f2c8bdca 100644 (file)
@@ -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)
 `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
 
index 455975217918cdfbd81616a71f445f3c7fc22d9f..3df50caf9e36bd43b451132b73e4d7379f0db5b7 100644 (file)
@@ -1,3 +1,5 @@
+// @vitest-environment jsdom
+
 import type { ElementHandle } from 'puppeteer'
 import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
 import path from 'node:path'
index 62d89db4e310c23d1c5d41a21ac2cc10298c2875..92efc228e80b503529746736f796e8ff3052fb39 100644 (file)
@@ -1,3 +1,5 @@
+// @vitest-environment jsdom
+
 import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
 import path from 'node:path'
 import { createApp, ref } from 'vue'
index 1ed5322d1f043a4c9efe5cca256432eefe6fc176..2ffebeb59508ff2d62c02b75ae99d33e8c976243 100644 (file)
@@ -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())
       }
     })
   })
index 5d5704cb3f5238065bd668fb39f7c9799681543f..4749cc64da9af434df110be6868d4f098500533c 100644 (file)
       "global",
       "global-runtime",
       "esm-browser",
-      "esm-browser-runtime"
+      "esm-browser-runtime",
+      "esm-browser-vapor"
     ]
   },
   "repository": {
     "type": "git",
-    "url": "git+https://github.com/vuejs/core.git"
+    "url": "git+https://github.com/vuejs/vue.git"
   },
   "keywords": [
     "vue"
   "author": "Evan You",
   "license": "MIT",
   "bugs": {
-    "url": "https://github.com/vuejs/core/issues"
+    "url": "https://github.com/vuejs/vue/issues"
   },
-  "homepage": "https://github.com/vuejs/core/tree/main/packages/vue#readme",
+  "homepage": "https://github.com/vuejs/vue/tree/main/packages/vue#readme",
   "dependencies": {
     "@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 (file)
index 0000000..21f4c80
--- /dev/null
@@ -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 (file)
index 0000000..4f03329
--- /dev/null
@@ -0,0 +1,2 @@
+export * from './runtime'
+export * from '@vue/runtime-vapor'
index 76f5fc74e25cb31f7b1a95e2cf65d0c8912f1073..af1ffe7a12a09ade54d6a8675176ac8c20b287f6 100644 (file)
@@ -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
 }
index 898a9fe61784e8f33f67db68cd98c8af7eb9a6f8..dc106d58bcd2205069de3f28f4a8d7373b1a161d 100644 (file)
@@ -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,18 @@ importers:
         specifier: workspace:*
         version: link:../shared
 
+  packages/compiler-vapor:
+    dependencies:
+      '@vue/compiler-dom':
+        specifier: workspace:*
+        version: link:../compiler-dom
+      '@vue/shared':
+        specifier: workspace:*
+        version: link:../shared
+      source-map-js:
+        specifier: 'catalog:'
+        version: 1.2.1
+
   packages/reactivity:
     dependencies:
       '@vue/shared':
@@ -388,6 +474,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 +511,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 +528,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 +545,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==}
+
+  '@asamuzakjp/css-color@3.2.0':
+    resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
 
-  '@babel/code-frame@7.26.2':
-    resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==}
+  '@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 +563,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 +588,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 +913,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 +923,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 +971,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 +995,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 +1026,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-arm64-glibc@2.4.1':
-    resolution: {integrity: sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==}
+  '@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.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 +1174,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 +1364,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 +1400,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 +1567,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 +1588,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.1.4':
-    resolution: {integrity: sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==}
+  '@vitest/snapshot@3.2.4':
+    resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==}
 
-  '@vitest/spy@3.1.4':
-    resolution: {integrity: sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==}
+  '@vitest/spy@3.2.4':
+    resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
 
-  '@vitest/utils@3.1.4':
-    resolution: {integrity: sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==}
+  '@vitest/ui@3.2.4':
+    resolution: {integrity: sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==}
+    peerDependencies:
+      vitest: 3.2.4
+
+  '@vitest/utils@3.2.4':
+    resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
+
+  '@vue/compiler-core@3.5.17':
+    resolution: {integrity: sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA==}
+
+  '@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 +1684,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 +1717,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 +1744,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 +1755,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 +1768,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 +1806,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 +1819,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 +1831,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 +1874,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 +1913,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 +1938,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 +1998,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 +2011,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 +2037,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 +2060,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 +2087,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 +2118,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 +2137,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 +2152,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 +2167,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 +2200,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 +2216,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 +2225,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 +2260,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 +2329,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 +2351,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 +2367,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 +2393,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 +2413,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 +2442,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 +2451,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 +2470,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 +2522,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 +2543,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 +2560,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 +2569,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 +2597,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 +2626,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 +2638,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 +2671,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 +2681,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 +2745,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 +2775,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 +2785,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 +2811,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 +2845,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 +2865,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 +2889,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 +2908,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 +2947,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 +2973,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 +2989,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 +3011,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 +3047,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 +3085,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 +3108,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 +3123,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 +3185,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 +3204,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 +3227,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 +3253,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 +3301,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 +3339,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 +3350,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 +3368,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 +3392,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 +3413,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 +3478,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 +3492,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 +3510,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 +3519,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 +3553,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 +3582,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 +3601,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 +3617,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 +3637,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 +3648,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 +3705,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 +3740,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 +3777,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 +3861,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 +3904,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 +3934,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 +4133,52 @@ snapshots:
   '@esbuild/win32-x64@0.25.5':
     optional: true
 
-  '@eslint-community/eslint-utils@4.6.1(eslint@9.27.0)':
+  '@eslint-community/eslint-utils@4.7.0(eslint@9.30.1)':
     dependencies:
-      eslint: 9.27.0
-      eslint-visitor-keys: 3.4.3
-
-  '@eslint-community/eslint-utils@4.7.0(eslint@9.27.0)':
-    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 +4192,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 +4213,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 +4248,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.5.1':
     optional: true
 
-  '@parcel/watcher-darwin-arm64@2.4.1':
+  '@parcel/watcher-darwin-x64@2.5.1':
     optional: true
 
-  '@parcel/watcher-darwin-x64@2.4.1':
+  '@parcel/watcher-freebsd-x64@2.5.1':
     optional: true
 
-  '@parcel/watcher-freebsd-x64@2.4.1':
+  '@parcel/watcher-linux-arm-glibc@2.5.1':
     optional: true
 
-  '@parcel/watcher-linux-arm-glibc@2.4.1':
+  '@parcel/watcher-linux-arm-musl@2.5.1':
     optional: true
 
-  '@parcel/watcher-linux-arm64-glibc@2.4.1':
+  '@parcel/watcher-linux-arm64-glibc@2.5.1':
     optional: true
 
-  '@parcel/watcher-linux-arm64-musl@2.4.1':
+  '@parcel/watcher-linux-arm64-musl@2.5.1':
     optional: true
 
-  '@parcel/watcher-linux-x64-glibc@2.4.1':
+  '@parcel/watcher-linux-x64-glibc@2.5.1':
     optional: true
 
-  '@parcel/watcher-linux-x64-musl@2.4.1':
+  '@parcel/watcher-linux-x64-musl@2.5.1':
     optional: true
 
-  '@parcel/watcher-win32-arm64@2.4.1':
+  '@parcel/watcher-win32-arm64@2.5.1':
     optional: true
 
-  '@parcel/watcher-win32-ia32@2.4.1':
+  '@parcel/watcher-win32-ia32@2.5.1':
     optional: true
 
-  '@parcel/watcher-win32-x64@2.4.1':
+  '@parcel/watcher-win32-x64@2.5.1':
     optional: true
 
-  '@parcel/watcher@2.4.1':
+  '@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 +4321,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 +4333,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 +4345,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 +4353,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 +4501,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 +4533,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/type-utils@8.32.1(eslint@9.27.0)(typescript@5.6.3)':
+  '@typescript-eslint/tsconfig-utils@8.35.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: 5.6.3
+
+  '@typescript-eslint/type-utils@8.35.1(eslint@9.30.1)(typescript@5.6.3)':
+    dependencies:
+      '@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 +4616,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 +4716,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: 3.0.2
+      tinyspy: 4.0.3
 
-  '@vitest/utils@3.1.4':
+  '@vitest/ui@3.2.4(vitest@3.2.4)':
     dependencies:
-      '@vitest/pretty-format': 3.1.4
-      loupe: 3.1.3
+      '@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.2.4':
+    dependencies:
+      '@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 +4867,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 +4905,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 +4923,7 @@ snapshots:
 
   asap@2.0.6: {}
 
-  assert-never@1.3.0: {}
+  assert-never@1.4.0: {}
 
   assertion-error@2.0.1: {}
 
@@ -4481,35 +4931,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 +4976,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 +4998,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 +5025,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 +5043,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 +5088,7 @@ snapshots:
 
   colorette@2.0.20: {}
 
-  commander@13.1.0: {}
+  commander@14.0.0: {}
 
   comment-parser@1.4.1: {}
 
@@ -4637,7 +5101,7 @@ snapshots:
 
   compressible@2.0.18:
     dependencies:
-      mime-db: 1.53.0
+      mime-db: 1.54.0
 
   compression@1.7.4:
     dependencies:
@@ -4653,10 +5117,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 +5158,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 +5183,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 +5208,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 +5217,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 +5231,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 +5249,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 +5263,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 +5289,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 +5318,8 @@ snapshots:
 
   entities@4.5.0: {}
 
+  entities@6.0.1: {}
+
   env-paths@2.2.1: {}
 
   environment@1.1.0: {}
@@ -4849,19 +5328,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 +5400,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 +5414,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
+      unrs-resolver: 1.10.1
 
-  eslint-import-resolver-node@0.3.9:
+  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:
-      debug: 3.2.7
-      is-core-module: 2.15.0
-      resolve: 1.22.8
-    transitivePeerDependencies:
-      - supports-color
-
-  eslint-plugin-import-x@4.13.1(eslint@9.27.0)(typescript@5.6.3):
-    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 +5487,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 +5563,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 +5579,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 +5600,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 +5627,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 +5691,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 +5722,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 +5751,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 +5774,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 +5789,7 @@ snapshots:
 
   imurmurhash@0.1.4: {}
 
-  index-to-position@0.1.2: {}
+  index-to-position@1.1.0: {}
 
   inherits@2.0.4: {}
 
@@ -5317,12 +5802,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 +5823,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 +5847,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 +5862,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 +5882,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 +5899,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 +5917,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 +5935,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 +5989,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 +6033,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 +6061,8 @@ snapshots:
 
   marked@13.0.3: {}
 
+  math-intrinsics@1.1.0: {}
+
   memorystream@0.3.1: {}
 
   meow@13.2.0: {}
@@ -5587,7 +6084,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 +6098,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 +6118,8 @@ snapshots:
 
   monaco-editor@0.52.2: {}
 
+  mrmime@2.0.1: {}
+
   ms@2.0.0: {}
 
   ms@2.1.3: {}
@@ -5633,7 +6128,7 @@ snapshots:
 
   nanoid@3.3.11: {}
 
-  napi-postinstall@0.2.3: {}
+  napi-postinstall@0.3.0: {}
 
   natural-compare@1.4.0: {}
 
@@ -5662,17 +6157,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 +6186,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 +6210,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 +6228,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 +6238,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 +6270,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 +6297,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 +6321,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 +6341,7 @@ snapshots:
 
   prelude-ls@1.2.1: {}
 
-  prettier@3.5.3: {}
+  prettier@3.6.2: {}
 
   pretty-bytes@6.1.1: {}
 
@@ -5853,7 +6360,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 +6393,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 +6435,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 +6449,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 +6465,7 @@ snapshots:
       puppeteer-core: 24.9.0
       typed-query-selector: 2.12.0
     transitivePeerDependencies:
+      - bare-buffer
       - bufferutil
       - supports-color
       - typescript
@@ -5964,8 +6473,6 @@ snapshots:
 
   queue-microtask@1.2.3: {}
 
-  queue-tick@1.0.1: {}
-
   range-parser@1.2.0: {}
 
   rc@1.2.8:
@@ -5982,16 +6489,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 +6511,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 +6530,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 +6547,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 +6556,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 +6602,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 +6616,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 +6654,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 +6662,7 @@ snapshots:
 
   shebang-regex@3.0.0: {}
 
-  shell-quote@1.8.1: {}
+  shell-quote@1.8.3: {}
 
   siginfo@2.0.0: {}
 
@@ -6172,6 +6672,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 +6700,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 +6716,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 +6762,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 +6776,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 +6784,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 +6796,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 +6824,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 +6857,8 @@ snapshots:
 
   token-stream@1.0.0: {}
 
+  totalist@3.0.1: {}
+
   tough-cookie@5.1.2:
     dependencies:
       tldts: 6.1.86
@@ -6358,23 +6879,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 +6904,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 +6946,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 +6955,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 +6990,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 +7037,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 +7091,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 +7146,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 +7175,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 +7190,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 +7204,4 @@ snapshots:
 
   yocto-queue@0.1.0: {}
 
-  zod@3.24.1: {}
+  zod@3.25.72: {}
index 5113b10daed95a448182283d676bd3c5aa95376e..51693b02d656243260843ded1e7ae1306fd2e904 100644 (file)
@@ -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'
index da7de554b64d4e780c086359b3ffcb1081ed3c46..7f2ecb8c8642b701152ec386a4a10204cd83e12b 100644 (file)
@@ -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),
@@ -319,47 +374,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 +387,8 @@ function createMinifiedConfig(/** @type {PackageFormat} */ format) {
   return createConfig(
     format,
     {
+      ...outputConfigs[format],
       file: outputConfigs[format].file.replace(/\.js$/, '.prod.js'),
-      format: outputConfigs[format].format,
     },
     [
       {
index d9af98f130663974e3a4baa30fa8dcbd4683ceb5..5ec8f3211378cacf135417d4d1c33960b35f84e7 100644 (file)
@@ -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',
index 02212a70e0ca4e8b44bb9fcaf09503e6b7f70feb..5b7e9e96fc756baf690289140cd2bc23dddf3fce 100644 (file)
@@ -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()
index fb4d3873e8b90f5fd0401ded9cb4702bd3f09f92..f8690749221e94aa34e225694d3d900a61f3c30d 100644 (file)
@@ -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`,
index b1baaa6c5c33afb2816f3c4652f2eb5459f2a1a6..4e582e25185a0ce613579ebfe4fbc4c22d40739c 100644 (file)
@@ -198,7 +198,9 @@ export function scanEnums() {
   }
 
   // 3. save cache
-  if (!existsSync('temp')) mkdirSync('temp')
+  try {
+    mkdirSync('temp')
+  } catch {}
 
   /** @type {EnumData} */
   const enumData = {
similarity index 95%
rename from scripts/pre-dev-sfc.js
rename to scripts/prepare-cjs.js
index b28705f3464a4a0bf0985ff1aedf7e6d9a8840c7..3e427e2b77f1b06d68f8ab6fd1071cb7734a0ffa 100644 (file)
@@ -6,6 +6,7 @@ const packagesToCheck = [
   'compiler-core',
   'compiler-dom',
   'compiler-ssr',
+  'compiler-vapor',
   'shared',
 ]
 
index cd287dfc12b15bb8e97a9f06c68ae2e8fafc7849..9872a1f3f37007b798b873ddfa59d2dbdea8e1f8 100644 (file)
@@ -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 (file)
index 0000000..313a1a9
--- /dev/null
@@ -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)
+          }
+        },
+      },
+    ]
+  }
+}
index c68e3703b91b8e1b04d3d96a26cd0851a837a473..a1a9da69771716c92631938d119d03eefc688be3 100644 (file)
@@ -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: [
index 3c92bf7bafb186809b1d4b4227c5a0cd94eba41d..cad185a4bc261058a131b453fac8c18da3a30ad6 100644 (file)
@@ -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
+}
index 9896083cf14878775d3937aff1260c55efe849ae..8973145f6f3731dd7fc7b45478d757b8a2d7c966 100644 (file)
@@ -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
index ed0bd0303e315ee36287ec9477320e15ec2500fe..8daa34f7ed36ba0ec301d69022b4eee7c54afa9c 100644 (file)
@@ -1,4 +1,4 @@
-import { defineConfig } from 'vitest/config'
+import { configDefaults, defineConfig } from 'vitest/config'
 import { entries } from './scripts/aliases.js'
 
 export default defineConfig({
@@ -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,
@@ -30,9 +31,6 @@ export default defineConfig({
       },
     },
     setupFiles: 'scripts/setup-vitest.ts',
-    environmentMatchGlobs: [
-      ['packages/{vue,vue-compat,runtime-dom}/**', 'jsdom'],
-    ],
     sequence: {
       hooks: 'list',
     },
@@ -54,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 (file)
index 622bda0..0000000
+++ /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 (file)
index 0082997..0000000
+++ /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 (file)
index a20586e..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-import { defineWorkspace } from 'vitest/config'
-
-export default defineWorkspace([
-  './vitest.unit.config.ts',
-  './vitest.e2e.config.ts',
-])