Merge branch 'minor' into edison/feat/svgAndMathML

This commit is contained in:
edison 2025-08-28 21:32:55 +08:00 committed by GitHub
commit d6e69c552b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
88 changed files with 3037 additions and 1307 deletions

View File

@ -11,7 +11,7 @@ jobs:
env: env:
PUPPETEER_SKIP_DOWNLOAD: 'true' PUPPETEER_SKIP_DOWNLOAD: 'true'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4.1.0 uses: pnpm/action-setup@v4.1.0

View File

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: Release environment: Release
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
ref: minor ref: minor

View File

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: Release environment: Release
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4.1.0 uses: pnpm/action-setup@v4.1.0

View File

@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4

View File

@ -21,7 +21,7 @@ jobs:
environment: Release environment: Release
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
@ -36,12 +36,13 @@ jobs:
- name: Install deps - name: Install deps
run: pnpm install run: pnpm install
- name: Update npm
run: npm i -g npm@latest
- name: Build and publish - name: Build and publish
id: publish id: publish
run: | run: |
pnpm release --publishOnly pnpm release --publishOnly
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Create GitHub release - name: Create GitHub release
id: release_tag id: release_tag

View File

@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4.1.0 uses: pnpm/action-setup@v4.1.0

View File

@ -22,7 +22,7 @@ jobs:
github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success' github.event.workflow_run.conclusion == 'success'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4.1.0 uses: pnpm/action-setup@v4.1.0
@ -37,7 +37,7 @@ jobs:
run: pnpm install run: pnpm install
- name: Download Size Data - name: Download Size Data
uses: dawidd6/action-download-artifact@v9 uses: dawidd6/action-download-artifact@v11
with: with:
name: size-data name: size-data
run_id: ${{ github.event.workflow_run.id }} run_id: ${{ github.event.workflow_run.id }}
@ -56,7 +56,7 @@ jobs:
path: temp/size/base.txt path: temp/size/base.txt
- name: Download Previous Size Data - name: Download Previous Size Data
uses: dawidd6/action-download-artifact@v9 uses: dawidd6/action-download-artifact@v11
with: with:
branch: ${{ steps.pr-base.outputs.content }} branch: ${{ steps.pr-base.outputs.content }}
workflow: size-data.yml workflow: size-data.yml

View File

@ -11,7 +11,7 @@ jobs:
env: env:
PUPPETEER_SKIP_DOWNLOAD: 'true' PUPPETEER_SKIP_DOWNLOAD: 'true'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4.1.0 uses: pnpm/action-setup@v4.1.0
@ -32,7 +32,7 @@ jobs:
env: env:
PUPPETEER_SKIP_DOWNLOAD: 'true' PUPPETEER_SKIP_DOWNLOAD: 'true'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4.1.0 uses: pnpm/action-setup@v4.1.0
@ -54,7 +54,7 @@ jobs:
e2e-test: e2e-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Setup cache for Chromium binary - name: Setup cache for Chromium binary
uses: actions/cache@v4 uses: actions/cache@v4
@ -111,7 +111,7 @@ jobs:
env: env:
PUPPETEER_SKIP_DOWNLOAD: 'true' PUPPETEER_SKIP_DOWNLOAD: 'true'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4.1.0 uses: pnpm/action-setup@v4.1.0

View File

@ -1,3 +1,44 @@
## [3.5.19](https://github.com/vuejs/core/compare/v3.5.18...v3.5.19) (2025-08-21)
### Bug Fixes
* **compiler-core:** adjacent v-else should cause a compiler error ([#13699](https://github.com/vuejs/core/issues/13699)) ([911e670](https://github.com/vuejs/core/commit/911e67045e2a63e0ecbd198ed4f567530f6d1c17)), closes [#13698](https://github.com/vuejs/core/issues/13698)
* **compiler-core:** prevent cached array children from retaining detached dom nodes ([#13691](https://github.com/vuejs/core/issues/13691)) ([7f60ef8](https://github.com/vuejs/core/commit/7f60ef83e735dbd29d323347acecf69f22b06d53)), closes [element-plus/element-plus#21408](https://github.com/element-plus/element-plus/issues/21408) [#13211](https://github.com/vuejs/core/issues/13211)
* **compiler-sfc:** improve type inference for generic type aliases types ([#12876](https://github.com/vuejs/core/issues/12876)) ([d9dd628](https://github.com/vuejs/core/commit/d9dd628800ae32e673bdfabfe79f1988037991d0)), closes [#12872](https://github.com/vuejs/core/issues/12872)
* **compiler-sfc:** throw mismatched script langs error before invoking babel ([#13194](https://github.com/vuejs/core/issues/13194)) ([0562548](https://github.com/vuejs/core/commit/0562548ab3a040073386021222225e0e9d43c632)), closes [#13193](https://github.com/vuejs/core/issues/13193)
* **compiler-ssr:** disable v-memo transform in ssr vdom fallback branch ([#13725](https://github.com/vuejs/core/issues/13725)) ([0a202d8](https://github.com/vuejs/core/commit/0a202d890ff2a564b1fab51e4ac621708640818e)), closes [#13724](https://github.com/vuejs/core/issues/13724)
* **devtools:** clear performance measures ([#13701](https://github.com/vuejs/core/issues/13701)) ([c875019](https://github.com/vuejs/core/commit/c875019d49b4c36a88d929ccadc31ad414747c7b)), closes [#13700](https://github.com/vuejs/core/issues/13700)
* **hmr:** prevent updating unmounting component during HMR rerender ([#13775](https://github.com/vuejs/core/issues/13775)) ([6e5143d](https://github.com/vuejs/core/commit/6e5143d9635dac3f20fb394a827109df30e232ae)), closes [#13771](https://github.com/vuejs/core/issues/13771) [#13772](https://github.com/vuejs/core/issues/13772)
* **hydration:** also set vShow name if `__FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__` flag is enabled ([#13777](https://github.com/vuejs/core/issues/13777)) ([439e1a5](https://github.com/vuejs/core/commit/439e1a543e62de4dbf7658d78d05c358c9677c86)), closes [#13744](https://github.com/vuejs/core/issues/13744)
* **reactivity:** warn on nested readonly ref update during unwrapping ([#12141](https://github.com/vuejs/core/issues/12141)) ([1498821](https://github.com/vuejs/core/commit/1498821ed9eeb22a0767e53ddc1f6a2840598a29))
* **runtime-core:** avoid setting direct ref of useTemplateRef in dev ([#13449](https://github.com/vuejs/core/issues/13449)) ([4a2953f](https://github.com/vuejs/core/commit/4a2953f57b90dfc24e34ff1a87cc1ebb0b97636d))
* **runtime-core:** improve consistency of `PublicInstanceProxyHandlers.has` ([#13507](https://github.com/vuejs/core/issues/13507)) ([d7283f3](https://github.com/vuejs/core/commit/d7283f3b7f0631c8b8a4a31a05983dac9f078c4f))
* **suspense:** don't immediately resolve suspense on last dep unmount ([#13456](https://github.com/vuejs/core/issues/13456)) ([a871315](https://github.com/vuejs/core/commit/a8713159ee24602c7c2b70c5fd52d2e5cd37dca5)), closes [#13453](https://github.com/vuejs/core/issues/13453)
* **transition:** handle KeepAlive + transition leaving edge case ([#13152](https://github.com/vuejs/core/issues/13152)) ([3190b17](https://github.com/vuejs/core/commit/3190b179b0545a3dc4549737793eec630cf9f0d1)), closes [#13153](https://github.com/vuejs/core/issues/13153)
## [3.5.18](https://github.com/vuejs/core/compare/v3.5.17...v3.5.18) (2025-07-23)
### Bug Fixes
* **compiler-core:** avoid cached text vnodes retaining detached DOM nodes ([#13662](https://github.com/vuejs/core/issues/13662)) ([00695a5](https://github.com/vuejs/core/commit/00695a5b41b2d032deaeada83831ff83aa6bfd4e)), closes [#13661](https://github.com/vuejs/core/issues/13661)
* **compiler-core:** avoid self updates of `v-pre` ([#12556](https://github.com/vuejs/core/issues/12556)) ([21b685a](https://github.com/vuejs/core/commit/21b685ad9d9d0e6060fc7d07b719bf35f2d9ae1f))
* **compiler-core:** identifiers in function parameters should not be inferred as references ([#13548](https://github.com/vuejs/core/issues/13548)) ([9b02923](https://github.com/vuejs/core/commit/9b029239edf88558465b941e1e4c085f92b1ebff))
* **compiler-core:** recognize empty string as non-identifier ([#12553](https://github.com/vuejs/core/issues/12553)) ([ce93339](https://github.com/vuejs/core/commit/ce933390ad1c72bed258f7ad959a78f0e8acdf57))
* **compiler-core:** transform empty `v-bind` dynamic argument content correctly ([#12554](https://github.com/vuejs/core/issues/12554)) ([d3af67e](https://github.com/vuejs/core/commit/d3af67e878790892f9d34cfea15d13625aabe733))
* **compiler-sfc:** transform empty srcset w/ includeAbsolute: true ([#13639](https://github.com/vuejs/core/issues/13639)) ([d8e40ef](https://github.com/vuejs/core/commit/d8e40ef7e1c20ee86b294e7cf78e2de60d12830e)), closes [vitejs/vite-plugin-vue#631](https://github.com/vitejs/vite-plugin-vue/issues/631)
* **css-vars:** nullish v-bind in style should not lead to unexpected inheritance ([#12461](https://github.com/vuejs/core/issues/12461)) ([c85f1b5](https://github.com/vuejs/core/commit/c85f1b5a132eb8ec25f71b250e25e65a5c20964f)), closes [#12434](https://github.com/vuejs/core/issues/12434) [#12439](https://github.com/vuejs/core/issues/12439) [#7474](https://github.com/vuejs/core/issues/7474) [#7475](https://github.com/vuejs/core/issues/7475)
* **custom-element:** ensure exposed methods are accessible from custom elements by making them enumerable ([#13634](https://github.com/vuejs/core/issues/13634)) ([90573b0](https://github.com/vuejs/core/commit/90573b06bf6fb6c14c6bbff6c4e34e0ab108953a)), closes [#13632](https://github.com/vuejs/core/issues/13632)
* **hydration:** prevent lazy hydration for updated components ([#13511](https://github.com/vuejs/core/issues/13511)) ([a9269c6](https://github.com/vuejs/core/commit/a9269c642bf944560bc29adb5dae471c11cd9ee8)), closes [#13510](https://github.com/vuejs/core/issues/13510)
* **runtime-core:** ensure correct anchor el for unresolved async components ([#13560](https://github.com/vuejs/core/issues/13560)) ([7f29943](https://github.com/vuejs/core/commit/7f2994393dcdb82cacbf62e02b5ba5565f32588b)), closes [#13559](https://github.com/vuejs/core/issues/13559)
* **slots:** refine internal key checking to support slot names starting with an underscore ([#13612](https://github.com/vuejs/core/issues/13612)) ([c5f7db1](https://github.com/vuejs/core/commit/c5f7db11542bb2246363aef78c88a8e6cef0ee93)), closes [#13611](https://github.com/vuejs/core/issues/13611)
* **ssr:** ensure empty slots render as a comment node in Transition ([#13396](https://github.com/vuejs/core/issues/13396)) ([8cfc10a](https://github.com/vuejs/core/commit/8cfc10a80b9cbf5d801ab149e49b8506d192e7e1)), closes [#13394](https://github.com/vuejs/core/issues/13394)
## [3.5.17](https://github.com/vuejs/core/compare/v3.5.16...v3.5.17) (2025-06-18) ## [3.5.17](https://github.com/vuejs/core/compare/v3.5.16...v3.5.17) (2025-06-18)

View File

@ -69,18 +69,18 @@
"@rollup/plugin-json": "^6.1.0", "@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-replace": "5.0.4", "@rollup/plugin-replace": "5.0.4",
"@swc/core": "^1.12.9", "@swc/core": "^1.13.3",
"@types/hash-sum": "^1.0.2", "@types/hash-sum": "^1.0.2",
"@types/node": "^22.16.0", "@types/node": "^22.17.2",
"@types/semver": "^7.7.0", "@types/semver": "^7.7.0",
"@types/serve-handler": "^6.1.4", "@types/serve-handler": "^6.1.4",
"@vitest/ui": "^3.0.2", "@vitest/ui": "^3.0.2",
"@vitest/coverage-v8": "^3.1.4", "@vitest/coverage-v8": "^3.2.4",
"@vitest/eslint-plugin": "^1.2.1", "@vitest/eslint-plugin": "^1.3.4",
"@vue/consolidate": "1.0.0", "@vue/consolidate": "1.0.0",
"conventional-changelog-cli": "^5.0.0", "conventional-changelog-cli": "^5.0.0",
"enquirer": "^2.4.1", "enquirer": "^2.4.1",
"esbuild": "^0.25.5", "esbuild": "^0.25.9",
"esbuild-plugin-polyfill-node": "^0.3.0", "esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^9.27.0", "eslint": "^9.27.0",
"eslint-plugin-import-x": "^4.13.1", "eslint-plugin-import-x": "^4.13.1",
@ -96,10 +96,10 @@
"prettier": "^3.5.3", "prettier": "^3.5.3",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"pug": "^3.0.3", "pug": "^3.0.3",
"puppeteer": "~24.9.0", "puppeteer": "~24.16.2",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"rollup": "^4.44.1", "rollup": "^4.46.4",
"rollup-plugin-dts": "^6.2.1", "rollup-plugin-dts": "^6.2.3",
"rollup-plugin-esbuild": "^6.2.1", "rollup-plugin-esbuild": "^6.2.1",
"rollup-plugin-polyfill-node": "^0.13.0", "rollup-plugin-polyfill-node": "^0.13.0",
"semver": "^7.7.2", "semver": "^7.7.2",
@ -111,6 +111,6 @@
"typescript": "~5.6.2", "typescript": "~5.6.2",
"typescript-eslint": "^8.32.1", "typescript-eslint": "^8.32.1",
"vite": "catalog:", "vite": "catalog:",
"vitest": "^3.1.4" "vitest": "^3.2.4"
} }
} }

View File

@ -13,7 +13,7 @@
"vite": "catalog:" "vite": "catalog:"
}, },
"dependencies": { "dependencies": {
"@vue/repl": "^4.6.1", "@vue/repl": "^4.6.3",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"vue": "workspace:*" "vue": "workspace:*"

View File

@ -8,7 +8,7 @@ import {
StoreState, StoreState,
} from '@vue/repl' } from '@vue/repl'
import Monaco from '@vue/repl/monaco-editor' import Monaco from '@vue/repl/monaco-editor'
import { ref, watchEffect, onMounted, computed } from 'vue' import { ref, watchEffect, onMounted, computed, watch } from 'vue'
const replRef = ref<InstanceType<typeof Repl>>() const replRef = ref<InstanceType<typeof Repl>>()
@ -130,6 +130,34 @@ onMounted(() => {
// @ts-expect-error process shim for old versions of @vue/compiler-sfc dependency // @ts-expect-error process shim for old versions of @vue/compiler-sfc dependency
window.process = { env: {} } window.process = { env: {} }
}) })
const isVaporSupported = ref(false)
watch(
() => store.vueVersion,
(version, oldVersion) => {
const [major, minor] = (version || store.compiler.version)
.split('.')
.map((v: string) => parseInt(v, 10))
isVaporSupported.value = major > 3 || (major === 3 && minor >= 6)
if (oldVersion) reloadPage()
},
{ immediate: true, flush: 'pre' },
)
const previewOptions = computed(() => ({
customCode: {
importCode: `import { initCustomFormatter${isVaporSupported.value ? ', vaporInteropPlugin' : ''} } from 'vue'`,
useCode: `
${isVaporSupported.value ? 'app.use(vaporInteropPlugin)' : ''}
if (window.devtoolsFormatters) {
const index = window.devtoolsFormatters.findIndex((v) => v.__vue_custom_formatter)
window.devtoolsFormatters.splice(index, 1)
initCustomFormatter()
} else {
initCustomFormatter()
}`,
},
}))
</script> </script>
<template> <template>
@ -160,20 +188,7 @@ onMounted(() => {
:showOpenSourceMap="true" :showOpenSourceMap="true"
:autoResize="true" :autoResize="true"
:clearConsole="false" :clearConsole="false"
:preview-options="{ :preview-options="previewOptions"
customCode: {
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()
} else {
initCustomFormatter()
}`,
},
}"
/> />
</template> </template>

View File

@ -11,7 +11,7 @@
"vue": "latest" "vue": "latest"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.2.4", "@vitejs/plugin-vue": "^6.0.1",
"vite": "^6.3.5" "vite": "^7.1.3"
} }
} }

View File

@ -7,9 +7,9 @@ return function render(_ctx, _cache) {
with (_ctx) { with (_ctx) {
const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("div", { key: "foo" }, null, -1 /* CACHED */) _createElementVNode("div", { key: "foo" }, null, -1 /* CACHED */)
]))) ]))]))
} }
}" }"
`; `;
@ -21,7 +21,7 @@ return function render(_ctx, _cache) {
with (_ctx) { with (_ctx) {
const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("p", null, [ _createElementVNode("p", null, [
_createElementVNode("span"), _createElementVNode("span"),
_createElementVNode("span") _createElementVNode("span")
@ -30,7 +30,7 @@ return function render(_ctx, _cache) {
_createElementVNode("span"), _createElementVNode("span"),
_createElementVNode("span") _createElementVNode("span")
], -1 /* CACHED */) ], -1 /* CACHED */)
]))) ]))]))
} }
}" }"
`; `;
@ -42,11 +42,11 @@ return function render(_ctx, _cache) {
with (_ctx) { with (_ctx) {
const { createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue const { createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("div", null, [ _createElementVNode("div", null, [
_createCommentVNode("comment") _createCommentVNode("comment")
], -1 /* CACHED */) ], -1 /* CACHED */)
]))) ]))]))
} }
}" }"
`; `;
@ -58,11 +58,11 @@ return function render(_ctx, _cache) {
with (_ctx) { with (_ctx) {
const { createElementVNode: _createElementVNode, createTextVNode: _createTextVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue const { createElementVNode: _createElementVNode, createTextVNode: _createTextVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("span", null, null, -1 /* CACHED */), _createElementVNode("span", null, null, -1 /* CACHED */),
_createTextVNode("foo"), _createTextVNode("foo", -1 /* CACHED */),
_createElementVNode("div", null, null, -1 /* CACHED */) _createElementVNode("div", null, null, -1 /* CACHED */)
]))) ]))]))
} }
}" }"
`; `;
@ -74,9 +74,9 @@ return function render(_ctx, _cache) {
with (_ctx) { with (_ctx) {
const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("span", { class: "inline" }, "hello", -1 /* CACHED */) _createElementVNode("span", { class: "inline" }, "hello", -1 /* CACHED */)
]))) ]))]))
} }
}" }"
`; `;
@ -147,9 +147,9 @@ return function render(_ctx, _cache) {
with (_ctx) { with (_ctx) {
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("span", null, "foo " + _toDisplayString(1) + " " + _toDisplayString(true), -1 /* CACHED */) _createElementVNode("span", null, "foo " + _toDisplayString(1) + " " + _toDisplayString(true), -1 /* CACHED */)
]))) ]))]))
} }
}" }"
`; `;
@ -161,9 +161,9 @@ return function render(_ctx, _cache) {
with (_ctx) { with (_ctx) {
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("span", { foo: 0 }, _toDisplayString(1), -1 /* CACHED */) _createElementVNode("span", { foo: 0 }, _toDisplayString(1), -1 /* CACHED */)
]))) ]))]))
} }
}" }"
`; `;
@ -215,9 +215,9 @@ return function render(_ctx, _cache) {
const _directive_foo = _resolveDirective("foo") const _directive_foo = _resolveDirective("foo")
return (_openBlock(), _createElementBlock("div", null, [ return (_openBlock(), _createElementBlock("div", null, [
_withDirectives((_openBlock(), _createElementBlock("svg", null, _cache[0] || (_cache[0] = [ _withDirectives((_openBlock(), _createElementBlock("svg", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("path", { d: "M2,3H5.5L12" }, null, -1 /* CACHED */) _createElementVNode("path", { d: "M2,3H5.5L12" }, null, -1 /* CACHED */)
]))), [ ]))])), [
[_directive_foo] [_directive_foo]
]) ])
])) ]))
@ -401,9 +401,9 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [ return (_openBlock(), _createElementBlock("div", null, [
ok ok
? (_openBlock(), _createElementBlock("div", _hoisted_1, _cache[0] || (_cache[0] = [ ? (_openBlock(), _createElementBlock("div", _hoisted_1, [...(_cache[0] || (_cache[0] = [
_createElementVNode("span", null, null, -1 /* CACHED */) _createElementVNode("span", null, null, -1 /* CACHED */)
]))) ]))]))
: _createCommentVNode("v-if", true) : _createCommentVNode("v-if", true)
])) ]))
} }
@ -422,7 +422,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(_Fragment, null, [ return (_openBlock(), _createElementBlock(_Fragment, null, [
_createCommentVNode("comment"), _createCommentVNode("comment"),
_createElementVNode("div", _hoisted_1, _cache[0] || (_cache[0] = [ _createElementVNode("div", _hoisted_1, [...(_cache[0] || (_cache[0] = [
_createElementVNode("div", { id: "b" }, [ _createElementVNode("div", { id: "b" }, [
_createElementVNode("div", { id: "c" }, [ _createElementVNode("div", { id: "c" }, [
_createElementVNode("div", { id: "d" }, [ _createElementVNode("div", { id: "d" }, [
@ -430,7 +430,7 @@ return function render(_ctx, _cache) {
]) ])
]) ])
], -1 /* CACHED */) ], -1 /* CACHED */)
])) ]))])
], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */)) ], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
} }
}" }"
@ -448,9 +448,9 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [ return (_openBlock(), _createElementBlock("div", null, [
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(list, (i) => { (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(list, (i) => {
return (_openBlock(), _createElementBlock("div", _hoisted_1, _cache[0] || (_cache[0] = [ return (_openBlock(), _createElementBlock("div", _hoisted_1, [...(_cache[0] || (_cache[0] = [
_createElementVNode("span", null, null, -1 /* CACHED */) _createElementVNode("span", null, null, -1 /* CACHED */)
]))) ]))]))
}), 256 /* UNKEYED_FRAGMENT */)) }), 256 /* UNKEYED_FRAGMENT */))
])) ]))
} }

View File

@ -27,7 +27,7 @@ import { PatchFlags } from '@vue/shared'
const cachedChildrenArrayMatcher = ( const cachedChildrenArrayMatcher = (
tags: string[], tags: string[],
needArraySpread = false, needArraySpread = true,
) => ({ ) => ({
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
needArraySpread, needArraySpread,
@ -170,11 +170,6 @@ describe('compiler: cacheStatic transform', () => {
{ {
/* _ slot flag */ /* _ slot flag */
}, },
{
type: NodeTypes.JS_PROPERTY,
key: { content: '__' },
value: { content: '[0]' },
},
], ],
}) })
}) })
@ -202,11 +197,6 @@ describe('compiler: cacheStatic transform', () => {
{ {
/* _ slot flag */ /* _ slot flag */
}, },
{
type: NodeTypes.JS_PROPERTY,
key: { content: '__' },
value: { content: '[0]' },
},
], ],
}) })
}) })

View File

@ -301,6 +301,25 @@ describe('compiler: v-if', () => {
]) ])
}) })
test('error on adjacent v-else', () => {
const onError = vi.fn()
const {
node: { branches },
} = parseWithIfTransform(
`<div v-if="false"/><div v-else/><div v-else/>`,
{ onError },
0,
)
expect(onError.mock.calls[0]).toMatchObject([
{
code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF,
loc: branches[branches.length - 1].loc,
},
])
})
test('error on user key', () => { test('error on user key', () => {
const onError = vi.fn() const onError = vi.fn()
// dynamic // dynamic

View File

@ -1,4 +1,9 @@
import type { ExpressionNode, TransformContext } from '../src' import { babelParse, walkIdentifiers } from '@vue/compiler-sfc'
import {
type ExpressionNode,
type TransformContext,
isReferencedIdentifier,
} from '../src'
import { type Position, createSimpleExpression } from '../src/ast' import { type Position, createSimpleExpression } from '../src/ast'
import { import {
advancePositionWithClone, advancePositionWithClone,
@ -115,3 +120,18 @@ test('toValidAssetId', () => {
'_component_test_2797935797_1', '_component_test_2797935797_1',
) )
}) })
describe('isReferencedIdentifier', () => {
test('identifiers in function parameters should not be inferred as references', () => {
expect.assertions(4)
const ast = babelParse(`(({ title }) => [])`)
walkIdentifiers(
ast.program.body[0],
(node, parent, parentStack, isReference) => {
expect(isReference).toBe(false)
expect(isReferencedIdentifier(node, parent, parentStack)).toBe(false)
},
true,
)
})
})

View File

@ -123,7 +123,7 @@ export function isReferencedIdentifier(
return false return false
} }
if (isReferenced(id, parent)) { if (isReferenced(id, parent, parentStack[parentStack.length - 2])) {
return true return true
} }
@ -133,7 +133,8 @@ export function isReferencedIdentifier(
case 'AssignmentExpression': case 'AssignmentExpression':
case 'AssignmentPattern': case 'AssignmentPattern':
return true return true
case 'ObjectPattern': case 'ObjectProperty':
return parent.key !== id && isInDestructureAssignment(parent, parentStack)
case 'ArrayPattern': case 'ArrayPattern':
return isInDestructureAssignment(parent, parentStack) return isInDestructureAssignment(parent, parentStack)
} }

View File

@ -43,6 +43,7 @@ import {
isCoreComponent, isCoreComponent,
isSimpleIdentifier, isSimpleIdentifier,
isStaticArgOf, isStaticArgOf,
isVPre,
} from './utils' } from './utils'
import { decodeHTML } from 'entities/lib/decode.js' import { decodeHTML } from 'entities/lib/decode.js'
import { import {
@ -246,7 +247,7 @@ const tokenizer = new Tokenizer(stack, {
ondirarg(start, end) { ondirarg(start, end) {
if (start === end) return if (start === end) return
const arg = getSlice(start, end) const arg = getSlice(start, end)
if (inVPre) { if (inVPre && !isVPre(currentProp!)) {
;(currentProp as AttributeNode).name += arg ;(currentProp as AttributeNode).name += arg
setLocEnd((currentProp as AttributeNode).nameLoc, end) setLocEnd((currentProp as AttributeNode).nameLoc, end)
} else { } else {
@ -262,7 +263,7 @@ const tokenizer = new Tokenizer(stack, {
ondirmodifier(start, end) { ondirmodifier(start, end) {
const mod = getSlice(start, end) const mod = getSlice(start, end)
if (inVPre) { if (inVPre && !isVPre(currentProp!)) {
;(currentProp as AttributeNode).name += '.' + mod ;(currentProp as AttributeNode).name += '.' + mod
setLocEnd((currentProp as AttributeNode).nameLoc, end) setLocEnd((currentProp as AttributeNode).nameLoc, end)
} else if ((currentProp as DirectiveNode).name === 'slot') { } else if ((currentProp as DirectiveNode).name === 'slot') {

View File

@ -12,19 +12,22 @@ import {
type RootNode, type RootNode,
type SimpleExpressionNode, type SimpleExpressionNode,
type SlotFunctionExpression, type SlotFunctionExpression,
type SlotsObjectProperty,
type TemplateChildNode, type TemplateChildNode,
type TemplateNode, type TemplateNode,
type TextCallNode, type TextCallNode,
type VNodeCall, type VNodeCall,
createArrayExpression, createArrayExpression,
createObjectProperty,
createSimpleExpression,
getVNodeBlockHelper, getVNodeBlockHelper,
getVNodeHelper, getVNodeHelper,
} from '../ast' } from '../ast'
import type { TransformContext } from '../transform' import type { TransformContext } from '../transform'
import { PatchFlags, isArray, isString, isSymbol } from '@vue/shared' import {
PatchFlagNames,
PatchFlags,
isArray,
isString,
isSymbol,
} from '@vue/shared'
import { findDir, isSlotOutlet } from '../utils' import { findDir, isSlotOutlet } from '../utils'
import { import {
GUARD_REACTIVE_PROPS, GUARD_REACTIVE_PROPS,
@ -109,6 +112,15 @@ function walk(
? ConstantTypes.NOT_CONSTANT ? ConstantTypes.NOT_CONSTANT
: getConstantType(child, context) : getConstantType(child, context)
if (constantType >= ConstantTypes.CAN_CACHE) { if (constantType >= ConstantTypes.CAN_CACHE) {
if (
child.codegenNode.type === NodeTypes.JS_CALL_EXPRESSION &&
child.codegenNode.arguments.length > 0
) {
child.codegenNode.arguments.push(
PatchFlags.CACHED +
(__DEV__ ? ` /* ${PatchFlagNames[PatchFlags.CACHED]} */` : ``),
)
}
toCache.push(child) toCache.push(child)
continue continue
} }
@ -142,7 +154,6 @@ function walk(
} }
let cachedAsArray = false let cachedAsArray = false
const slotCacheKeys = []
if (toCache.length === children.length && node.type === NodeTypes.ELEMENT) { if (toCache.length === children.length && node.type === NodeTypes.ELEMENT) {
if ( if (
node.tagType === ElementTypes.ELEMENT && node.tagType === ElementTypes.ELEMENT &&
@ -166,7 +177,6 @@ function walk(
// default slot // default slot
const slot = getSlotNode(node.codegenNode, 'default') const slot = getSlotNode(node.codegenNode, 'default')
if (slot) { if (slot) {
slotCacheKeys.push(context.cached.length)
slot.returns = getCacheExpression( slot.returns = getCacheExpression(
createArrayExpression(slot.returns as TemplateChildNode[]), createArrayExpression(slot.returns as TemplateChildNode[]),
) )
@ -190,7 +200,6 @@ function walk(
slotName.arg && slotName.arg &&
getSlotNode(parent.codegenNode, slotName.arg) getSlotNode(parent.codegenNode, slotName.arg)
if (slot) { if (slot) {
slotCacheKeys.push(context.cached.length)
slot.returns = getCacheExpression( slot.returns = getCacheExpression(
createArrayExpression(slot.returns as TemplateChildNode[]), createArrayExpression(slot.returns as TemplateChildNode[]),
) )
@ -201,39 +210,22 @@ function walk(
if (!cachedAsArray) { if (!cachedAsArray) {
for (const child of toCache) { for (const child of toCache) {
slotCacheKeys.push(context.cached.length)
child.codegenNode = context.cache(child.codegenNode!) child.codegenNode = context.cache(child.codegenNode!)
} }
} }
// put the slot cached keys on the slot object, so that the cache
// can be removed when component unmounting to prevent memory leaks
if (
slotCacheKeys.length &&
node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.COMPONENT &&
node.codegenNode &&
node.codegenNode.type === NodeTypes.VNODE_CALL &&
node.codegenNode.children &&
!isArray(node.codegenNode.children) &&
node.codegenNode.children.type === NodeTypes.JS_OBJECT_EXPRESSION
) {
node.codegenNode.children.properties.push(
createObjectProperty(
`__`,
createSimpleExpression(JSON.stringify(slotCacheKeys), false),
) as SlotsObjectProperty,
)
}
function getCacheExpression(value: JSChildNode): CacheExpression { function getCacheExpression(value: JSChildNode): CacheExpression {
const exp = context.cache(value) const exp = context.cache(value)
// #6978, #7138, #7114 // #6978, #7138, #7114
// a cached children array inside v-for can caused HMR errors since // a cached children array inside v-for can caused HMR errors since
// it might be mutated when mounting the first item // it might be mutated when mounting the first item
if (inFor && context.hmr) { // #13221
// fix memory leak in cached array:
// cached vnodes get replaced by cloned ones during mountChildren,
// which bind DOM elements. These DOM references persist after unmount,
// preventing garbage collection. Array spread avoids mutating cached
// array, preventing memory leaks.
exp.needArraySpread = true exp.needArraySpread = true
}
return exp return exp
} }

View File

@ -65,7 +65,7 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => {
arg.children.unshift(`(`) arg.children.unshift(`(`)
arg.children.push(`) || ""`) arg.children.push(`) || ""`)
} else if (!arg.isStatic) { } else if (!arg.isStatic) {
arg.content = `${arg.content} || ""` arg.content = arg.content ? `${arg.content} || ""` : `""`
} }
// .sync is replaced by v-model:arg // .sync is replaced by v-model:arg

View File

@ -141,9 +141,9 @@ export function processIf(
} }
if (sibling && sibling.type === NodeTypes.IF) { if (sibling && sibling.type === NodeTypes.IF) {
// Check if v-else was followed by v-else-if // Check if v-else was followed by v-else-if or there are two adjacent v-else
if ( if (
dir.name === 'else-if' && (dir.name === 'else-if' || dir.name === 'else') &&
sibling.branches[sibling.branches.length - 1].condition === undefined sibling.branches[sibling.branches.length - 1].condition === undefined
) { ) {
context.onError( context.onError(

View File

@ -16,7 +16,7 @@ const seen = new WeakSet()
export const transformMemo: NodeTransform = (node, context) => { export const transformMemo: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT) { if (node.type === NodeTypes.ELEMENT) {
const dir = findDir(node, 'memo') const dir = findDir(node, 'memo')
if (!dir || seen.has(node)) { if (!dir || seen.has(node) || context.inSSR) {
return return
} }
seen.add(node) seen.add(node)

View File

@ -63,7 +63,7 @@ export function isCoreComponent(tag: string): symbol | void {
} }
} }
const nonIdentifierRE = /^\d|[^\$\w\xA0-\uFFFF]/ const nonIdentifierRE = /^$|^\d|[^\$\w\xA0-\uFFFF]/
export const isSimpleIdentifier = (name: string): boolean => export const isSimpleIdentifier = (name: string): boolean =>
!nonIdentifierRE.test(name) !nonIdentifierRE.test(name)
@ -344,6 +344,10 @@ export function isText(
return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT
} }
export function isVPre(p: ElementNode['props'][0]): p is DirectiveNode {
return p.type === NodeTypes.DIRECTIVE && p.name === 'pre'
}
export function isVSlot(p: ElementNode['props'][0]): p is DirectiveNode { export function isVSlot(p: ElementNode['props'][0]): p is DirectiveNode {
return p.type === NodeTypes.DIRECTIVE && p.name === 'slot' return p.type === NodeTypes.DIRECTIVE && p.name === 'slot'
} }

View File

@ -4,11 +4,11 @@ exports[`stringify static html > eligible content (elements > 20) + non-eligible
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue "const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createStaticVNode("<span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>", 20), _createStaticVNode("<span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>", 20),
_createElementVNode("div", { key: "1" }, "1", -1 /* CACHED */), _createElementVNode("div", { key: "1" }, "1", -1 /* CACHED */),
_createStaticVNode("<span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>", 20) _createStaticVNode("<span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>", 20)
]))) ]))]))
}" }"
`; `;
@ -16,9 +16,9 @@ exports[`stringify static html > escape 1`] = `
"const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue "const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createStaticVNode("<div><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span></div>", 1) _createStaticVNode("<div><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span></div>", 1)
]))) ]))]))
}" }"
`; `;
@ -26,9 +26,9 @@ exports[`stringify static html > serializing constant bindings 1`] = `
"const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue "const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createStaticVNode("<div style=\\"color:red;\\"><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span></div>", 1) _createStaticVNode("<div style=\\"color:red;\\"><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span></div>", 1)
]))) ]))]))
}" }"
`; `;
@ -36,9 +36,9 @@ exports[`stringify static html > serializing template string style 1`] = `
"const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue "const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createStaticVNode("<div style=\\"color:red;\\"><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span></div>", 1) _createStaticVNode("<div style=\\"color:red;\\"><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span></div>", 1)
]))) ]))]))
}" }"
`; `;
@ -46,7 +46,7 @@ exports[`stringify static html > should bail for <option> elements with null val
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue "const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("select", null, [ _createElementVNode("select", null, [
_createElementVNode("option", { value: null }), _createElementVNode("option", { value: null }),
_createElementVNode("option", { value: "1" }), _createElementVNode("option", { value: "1" }),
@ -55,7 +55,7 @@ return function render(_ctx, _cache) {
_createElementVNode("option", { value: "1" }), _createElementVNode("option", { value: "1" }),
_createElementVNode("option", { value: "1" }) _createElementVNode("option", { value: "1" })
], -1 /* CACHED */) ], -1 /* CACHED */)
]))) ]))]))
}" }"
`; `;
@ -63,7 +63,7 @@ exports[`stringify static html > should bail for <option> elements with number v
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue "const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("select", null, [ _createElementVNode("select", null, [
_createElementVNode("option", { value: 1 }), _createElementVNode("option", { value: 1 }),
_createElementVNode("option", { value: 1 }), _createElementVNode("option", { value: 1 }),
@ -71,7 +71,7 @@ return function render(_ctx, _cache) {
_createElementVNode("option", { value: 1 }), _createElementVNode("option", { value: 1 }),
_createElementVNode("option", { value: 1 }) _createElementVNode("option", { value: 1 })
], -1 /* CACHED */) ], -1 /* CACHED */)
]))) ]))]))
}" }"
`; `;
@ -95,7 +95,7 @@ exports[`stringify static html > should bail on bindings that are cached but not
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue "const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("div", null, [ _createElementVNode("div", null, [
_createElementVNode("span", { class: "foo" }, "foo"), _createElementVNode("span", { class: "foo" }, "foo"),
_createElementVNode("span", { class: "foo" }, "foo"), _createElementVNode("span", { class: "foo" }, "foo"),
@ -104,7 +104,7 @@ return function render(_ctx, _cache) {
_createElementVNode("span", { class: "foo" }, "foo"), _createElementVNode("span", { class: "foo" }, "foo"),
_createElementVNode("img", { src: _imports_0_ }) _createElementVNode("img", { src: _imports_0_ })
], -1 /* CACHED */) ], -1 /* CACHED */)
]))) ]))]))
}" }"
`; `;
@ -112,9 +112,9 @@ exports[`stringify static html > should work for <option> elements with string v
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue "const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createStaticVNode("<select><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option></select>", 1) _createStaticVNode("<select><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option></select>", 1)
]))) ]))]))
}" }"
`; `;
@ -122,9 +122,9 @@ exports[`stringify static html > should work for multiple adjacent nodes 1`] = `
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue "const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createStaticVNode("<span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span>", 5) _createStaticVNode("<span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span>", 5)
]))) ]))]))
}" }"
`; `;
@ -132,9 +132,9 @@ exports[`stringify static html > should work on eligible content (elements > 20)
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue "const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createStaticVNode("<div><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>", 1) _createStaticVNode("<div><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>", 1)
]))) ]))]))
}" }"
`; `;
@ -142,9 +142,9 @@ exports[`stringify static html > should work on eligible content (elements with
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue "const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createStaticVNode("<div><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span></div>", 1) _createStaticVNode("<div><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span></div>", 1)
]))) ]))]))
}" }"
`; `;
@ -152,9 +152,9 @@ exports[`stringify static html > should work with bindings that are non-static b
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue "const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createStaticVNode("<div><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><img src=\\"" + _imports_0_ + "\\"></div>", 1) _createStaticVNode("<div><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><img src=\\"" + _imports_0_ + "\\"></div>", 1)
]))) ]))]))
}" }"
`; `;

View File

@ -42,7 +42,7 @@ if (__TEST__) {
if (DOMErrorCodes.X_V_HTML_NO_EXPRESSION < ErrorCodes.__EXTEND_POINT__) { if (DOMErrorCodes.X_V_HTML_NO_EXPRESSION < ErrorCodes.__EXTEND_POINT__) {
throw new Error( throw new Error(
`DOMErrorCodes need to be updated to ${ `DOMErrorCodes need to be updated to ${
ErrorCodes.__EXTEND_POINT__ + 1 ErrorCodes.__EXTEND_POINT__
} to match extension point from core ErrorCodes.`, } to match extension point from core ErrorCodes.`,
) )
} }

View File

@ -823,6 +823,228 @@ return (_ctx, _cache) => {
}" }"
`; `;
exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > should alias __emit to $emit when defineEmits is used 1`] = `
"import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export default {
emits: ['click'],
setup(__props, { emit: __emit }) {
const $emit = __emit
const emit = __emit
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock("div", {
onClick: _cache[0] || (_cache[0] = $event => ($emit('click')))
}))
}
}
}"
`;
exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > should alias __props to $props when $props is used 1`] = `
"import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export default {
setup(__props) {
const $props = __props
/* ... */
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock("div", null, _toDisplayString($props), 1 /* TEXT */))
}
}
}"
`;
exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > should extract all built-in properties when they are used 1`] = `
"import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export default {
setup(__props, { emit: $emit, attrs: $attrs, slots: $slots }) {
const $props = __props
/* ... */
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock("div", null, _toDisplayString($props) + _toDisplayString($slots) + _toDisplayString($emit) + _toDisplayString($attrs), 1 /* TEXT */))
}
}
}"
`;
exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > should extract attrs when $attrs is used 1`] = `
"import { normalizeProps as _normalizeProps, guardReactiveProps as _guardReactiveProps, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export default {
setup(__props, { attrs: $attrs }) {
/* ... */
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock("div", _normalizeProps(_guardReactiveProps($attrs)), null, 16 /* FULL_PROPS */))
}
}
}"
`;
exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > should extract emit when $emit is used 1`] = `
"import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export default {
setup(__props, { emit: $emit }) {
/* ... */
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock("div", {
onClick: _cache[0] || (_cache[0] = $event => ($emit('click')))
}))
}
}
}"
`;
exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > should extract slots when $slots is used 1`] = `
"import { resolveComponent as _resolveComponent, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export default {
setup(__props, { slots: $slots }) {
/* ... */
return (_ctx, _cache) => {
const _component_Comp = _resolveComponent("Comp")
return (_openBlock(), _createBlock(_component_Comp, {
foo: $slots.foo
}, null, 8 /* PROPS */, ["foo"]))
}
}
}"
`;
exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > should not extract built-in properties when neither is used 1`] = `
"import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export default {
setup(__props) {
/* ... */
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock("div", null, _toDisplayString(_ctx.msg), 1 /* TEXT */))
}
}
}"
`;
exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > user-defined properties override > should handle mixed defineEmits and user-defined $emit 1`] = `
"import { unref as _unref, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export default {
emits: ['click'],
setup(__props, { emit: __emit }) {
const emit = __emit
let $emit
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock("div", {
onClick: _cache[0] || (_cache[0] = $event => (_unref($emit)('click')))
}, "click"))
}
}
}"
`;
exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > user-defined properties override > should not extract $attrs when user defines it 1`] = `
"import { unref as _unref, normalizeProps as _normalizeProps, guardReactiveProps as _guardReactiveProps, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export default {
setup(__props) {
let $attrs
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock("div", _normalizeProps(_guardReactiveProps(_unref($attrs))), null, 16 /* FULL_PROPS */))
}
}
}"
`;
exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > user-defined properties override > should not extract $emit when user defines it 1`] = `
"import { unref as _unref, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export default {
setup(__props) {
let $emit
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock("div", {
onClick: _cache[0] || (_cache[0] = $event => (_unref($emit)('click')))
}, "click"))
}
}
}"
`;
exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > user-defined properties override > should not extract $slots when user defines it 1`] = `
"import { unref as _unref, resolveComponent as _resolveComponent, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export default {
setup(__props) {
let $slots
return (_ctx, _cache) => {
const _component_Comp = _resolveComponent("Comp")
return (_openBlock(), _createBlock(_component_Comp, {
foo: _unref($slots).foo
}, null, 8 /* PROPS */, ["foo"]))
}
}
}"
`;
exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > user-defined properties override > should not generate $props alias when user defines it 1`] = `
"import { unref as _unref, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export default {
setup(__props) {
let $props
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock("div", null, _toDisplayString(_unref($props).msg), 1 /* TEXT */))
}
}
}"
`;
exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > user-defined properties override > should only extract non-user-defined properties 1`] = `
"import { unref as _unref, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export default {
setup(__props, { emit: $emit, slots: $slots }) {
const $props = __props
let $attrs
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock("div", null, _toDisplayString(_unref($attrs)) + _toDisplayString($slots) + _toDisplayString($emit) + _toDisplayString($props), 1 /* TEXT */))
}
}
}"
`;
exports[`SFC compile <script setup> > inlineTemplate mode > referencing scope components and directives 1`] = ` exports[`SFC compile <script setup> > inlineTemplate mode > referencing scope components and directives 1`] = `
"import { unref as _unref, createElementVNode as _createElementVNode, withDirectives as _withDirectives, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" "import { unref as _unref, createElementVNode as _createElementVNode, withDirectives as _withDirectives, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

View File

@ -81,9 +81,9 @@ import _imports_1 from '/bar.png'
export function render(_ctx, _cache) { export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createStaticVNode("<img src=\\"" + _imports_0 + "\\"><img src=\\"" + _imports_1 + "\\"><img src=\\"https://foo.bar/baz.png\\"><img src=\\"//foo.bar/baz.png\\"><img src=\\"" + _imports_0 + "\\">", 5) _createStaticVNode("<img src=\\"" + _imports_0 + "\\"><img src=\\"" + _imports_1 + "\\"><img src=\\"https://foo.bar/baz.png\\"><img src=\\"//foo.bar/baz.png\\"><img src=\\"" + _imports_0 + "\\">", 5)
]))) ]))]))
}" }"
`; `;

View File

@ -16,6 +16,16 @@ export function render(_ctx, _cache) {
}" }"
`; `;
exports[`compiler sfc: transform srcset > transform empty srcset w/ includeAbsolute: true 1`] = `
"import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = { srcset: " " }
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("img", _hoisted_1))
}"
`;
exports[`compiler sfc: transform srcset > transform srcset 1`] = ` exports[`compiler sfc: transform srcset > transform srcset 1`] = `
"import { createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" "import { createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
import _imports_0 from './logo.png' import _imports_0 from './logo.png'
@ -228,8 +238,8 @@ const _hoisted_8 = _imports_1 + ', ' + _imports_1 + ' 2x'
const _hoisted_9 = _imports_1 + ', ' + _imports_0 + ' 2x' const _hoisted_9 = _imports_1 + ', ' + _imports_0 + ' 2x'
export function render(_ctx, _cache) { export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createStaticVNode("<img src=\\"./logo.png\\" srcset=\\"\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_1 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_2 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_3 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_4 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_5 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_6 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_7 + "\\"><img src=\\"/logo.png\\" srcset=\\"" + _hoisted_8 + "\\"><img src=\\"https://example.com/logo.png\\" srcset=\\"https://example.com/logo.png, https://example.com/logo.png 2x\\"><img src=\\"/logo.png\\" srcset=\\"" + _hoisted_9 + "\\"><img src=\\"data:image/png;base64,i\\" srcset=\\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\">", 12) _createStaticVNode("<img src=\\"./logo.png\\" srcset=\\"\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_1 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_2 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_3 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_4 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_5 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_6 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_7 + "\\"><img src=\\"/logo.png\\" srcset=\\"" + _hoisted_8 + "\\"><img src=\\"https://example.com/logo.png\\" srcset=\\"https://example.com/logo.png, https://example.com/logo.png 2x\\"><img src=\\"/logo.png\\" srcset=\\"" + _hoisted_9 + "\\"><img src=\\"data:image/png;base64,i\\" srcset=\\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\">", 12)
]))) ]))]))
}" }"
`; `;

View File

@ -717,6 +717,151 @@ describe('SFC compile <script setup>', () => {
consumer.originalPositionFor(getPositionInCode(content, 'Error')), consumer.originalPositionFor(getPositionInCode(content, 'Error')),
).toMatchObject(getPositionInCode(source, `Error`)) ).toMatchObject(getPositionInCode(source, `Error`))
}) })
describe('destructure setup context for built-in properties', () => {
const theCompile = (template: string, setup = '/* ... */') =>
compile(
`<script setup>${setup}</script>\n<template>${template}</template>`,
{ inlineTemplate: true },
)
test('should extract attrs when $attrs is used', () => {
let { content } = theCompile('<div v-bind="$attrs"></div>')
expect(content).toMatch('setup(__props, { attrs: $attrs })')
expect(content).not.toMatch('slots: $slots')
expect(content).not.toMatch('emit: $emit')
expect(content).not.toMatch('const $props = __props')
assertCode(content)
})
test('should extract slots when $slots is used', () => {
let { content } = theCompile('<Comp :foo="$slots.foo"></Comp>')
expect(content).toMatch('setup(__props, { slots: $slots })')
assertCode(content)
})
test('should alias __props to $props when $props is used', () => {
let { content } = theCompile('<div>{{ $props }}</div>')
expect(content).toMatch('setup(__props)')
expect(content).toMatch('const $props = __props')
assertCode(content)
})
test('should extract emit when $emit is used', () => {
let { content } = theCompile(`<div @click="$emit('click')"></div>`)
expect(content).toMatch('setup(__props, { emit: $emit })')
expect(content).not.toMatch('const $emit = __emit')
assertCode(content)
})
test('should alias __emit to $emit when defineEmits is used', () => {
let { content } = compile(
`
<script setup>
const emit = defineEmits(['click'])
</script>
<template>
<div @click="$emit('click')"></div>
</template>
`,
{ inlineTemplate: true },
)
expect(content).toMatch('setup(__props, { emit: __emit })')
expect(content).toMatch('const $emit = __emit')
expect(content).toMatch('const emit = __emit')
assertCode(content)
})
test('should extract all built-in properties when they are used', () => {
let { content } = theCompile(
'<div>{{ $props }}{{ $slots }}{{ $emit }}{{ $attrs }}</div>',
)
expect(content).toMatch(
'setup(__props, { emit: $emit, attrs: $attrs, slots: $slots })',
)
expect(content).toMatch('const $props = __props')
assertCode(content)
})
test('should not extract built-in properties when neither is used', () => {
let { content } = theCompile('<div>{{ msg }}</div>')
expect(content).toMatch('setup(__props)')
expect(content).not.toMatch('attrs: $attrs')
expect(content).not.toMatch('slots: $slots')
expect(content).not.toMatch('emit: $emit')
expect(content).not.toMatch('props: $props')
assertCode(content)
})
describe('user-defined properties override', () => {
test('should not extract $attrs when user defines it', () => {
let { content } = theCompile(
'<div v-bind="$attrs"></div>',
'let $attrs',
)
expect(content).toMatch('setup(__props)')
expect(content).not.toMatch('attrs: $attrs')
assertCode(content)
})
test('should not extract $slots when user defines it', () => {
let { content } = theCompile(
'<Comp :foo="$slots.foo"></Comp>',
'let $slots',
)
expect(content).toMatch('setup(__props)')
expect(content).not.toMatch('slots: $slots')
assertCode(content)
})
test('should not extract $emit when user defines it', () => {
let { content } = theCompile(
`<div @click="$emit('click')">click</div>`,
'let $emit',
)
expect(content).toMatch('setup(__props)')
expect(content).not.toMatch('emit: $emit')
assertCode(content)
})
test('should not generate $props alias when user defines it', () => {
let { content } = theCompile(
'<div>{{ $props.msg }}</div>',
'let $props',
)
expect(content).toMatch('setup(__props)')
expect(content).not.toMatch('const $props = __props')
assertCode(content)
})
test('should only extract non-user-defined properties', () => {
let { content } = theCompile(
'<div>{{ $attrs }}{{ $slots }}{{ $emit }}{{ $props }}</div>',
'let $attrs',
)
expect(content).toMatch(
'setup(__props, { emit: $emit, slots: $slots })',
)
expect(content).not.toMatch('attrs: $attrs')
expect(content).toMatch('const $props = __props')
assertCode(content)
})
test('should handle mixed defineEmits and user-defined $emit', () => {
let { content } = theCompile(
`<div @click="$emit('click')">click</div>`,
`
const emit = defineEmits(['click'])
let $emit
`,
)
expect(content).toMatch('setup(__props, { emit: __emit })')
expect(content).toMatch('const emit = __emit')
expect(content).not.toMatch('const $emit = __emit')
assertCode(content)
})
})
})
}) })
describe('with TypeScript', () => { describe('with TypeScript', () => {
@ -913,6 +1058,13 @@ describe('SFC compile <script setup>', () => {
expect(() => expect(() =>
compile(`<script>foo()</script><script setup lang="ts">bar()</script>`), compile(`<script>foo()</script><script setup lang="ts">bar()</script>`),
).toThrow(`<script> and <script setup> must have the same language type`) ).toThrow(`<script> and <script setup> must have the same language type`)
// #13193 must check lang before parsing with babel
expect(() =>
compile(
`<script lang="ts">const a = 1</script><script setup lang="tsx">const Comp = () => <p>test</p></script>`,
),
).toThrow(`<script> and <script setup> must have the same language type`)
}) })
const moduleErrorMsg = `cannot contain ES module exports` const moduleErrorMsg = `cannot contain ES module exports`

View File

@ -538,7 +538,7 @@ describe('resolveType', () => {
expect(props).toStrictEqual({ expect(props).toStrictEqual({
foo: ['Symbol', 'String', 'Number'], foo: ['Symbol', 'String', 'Number'],
bar: [UNKNOWN_TYPE], bar: ['String', 'Number'],
}) })
}) })
@ -749,7 +749,7 @@ describe('resolveType', () => {
}) })
}) })
test('fallback to Unknown', () => { test('with intersection type', () => {
expect( expect(
resolve(` resolve(`
type Brand<T> = T & {}; type Brand<T> = T & {};
@ -758,7 +758,18 @@ describe('resolveType', () => {
}>() }>()
`).props, `).props,
).toStrictEqual({ ).toStrictEqual({
foo: [UNKNOWN_TYPE], foo: ['String', 'Object'],
})
})
test('with union type', () => {
expect(
resolve(`
type Wrapped<T> = T | symbol | number
defineProps<{foo?: Wrapped<boolean>}>()
`).props,
).toStrictEqual({
foo: ['Boolean', 'Symbol', 'Number'],
}) })
}) })
}) })

View File

@ -72,6 +72,14 @@ describe('compiler sfc: transform srcset', () => {
).toMatchSnapshot() ).toMatchSnapshot()
}) })
test('transform empty srcset w/ includeAbsolute: true', () => {
expect(
compileWithSrcset(`<img srcset=" " />`, {
includeAbsolute: true,
}).code,
).toMatchSnapshot()
})
test('transform srcset w/ stringify', () => { test('transform srcset w/ stringify', () => {
const code = compileWithSrcset( const code = compileWithSrcset(
`<div>${src}</div>`, `<div>${src}</div>`,

View File

@ -63,6 +63,6 @@
"postcss-modules": "^6.0.1", "postcss-modules": "^6.0.1",
"postcss-selector-parser": "^7.1.0", "postcss-selector-parser": "^7.1.0",
"pug": "^3.0.3", "pug": "^3.0.3",
"sass": "^1.89.2" "sass": "^1.90.0"
} }
} }

View File

@ -59,7 +59,7 @@ import { DEFINE_SLOTS, processDefineSlots } from './script/defineSlots'
import { DEFINE_MODEL, processDefineModel } from './script/defineModel' import { DEFINE_MODEL, processDefineModel } from './script/defineModel'
import { getImportedName, isCallOf, isLiteralNode } from './script/utils' import { getImportedName, isCallOf, isLiteralNode } from './script/utils'
import { analyzeScriptBindings } from './script/analyzeScriptBindings' import { analyzeScriptBindings } from './script/analyzeScriptBindings'
import { isImportUsed } from './script/importUsageCheck' import { isUsedInTemplate } from './script/importUsageCheck'
import { processAwait } from './script/topLevelAwait' import { processAwait } from './script/topLevelAwait'
export interface SFCScriptCompileOptions { export interface SFCScriptCompileOptions {
@ -173,7 +173,6 @@ export function compileScript(
) )
} }
const ctx = new ScriptCompileContext(sfc, options)
const { script, scriptSetup, source, filename } = sfc const { script, scriptSetup, source, filename } = sfc
const hoistStatic = options.hoistStatic !== false && !script const hoistStatic = options.hoistStatic !== false && !script
const scopeId = options.id ? options.id.replace(/^data-v-/, '') : '' const scopeId = options.id ? options.id.replace(/^data-v-/, '') : ''
@ -181,6 +180,16 @@ export function compileScript(
const scriptSetupLang = scriptSetup && scriptSetup.lang const scriptSetupLang = scriptSetup && scriptSetup.lang
const vapor = sfc.vapor || options.vapor const vapor = sfc.vapor || options.vapor
const ssr = options.templateOptions?.ssr const ssr = options.templateOptions?.ssr
const setupPreambleLines = [] as string[]
if (script && scriptSetup && scriptLang !== scriptSetupLang) {
throw new Error(
`[@vue/compiler-sfc] <script> and <script setup> must have the same ` +
`language type.`,
)
}
const ctx = new ScriptCompileContext(sfc, options)
if (!scriptSetup) { if (!scriptSetup) {
if (!script) { if (!script) {
@ -190,13 +199,6 @@ export function compileScript(
return processNormalScript(ctx, scopeId) return processNormalScript(ctx, scopeId)
} }
if (script && scriptLang !== scriptSetupLang) {
throw new Error(
`[@vue/compiler-sfc] <script> and <script setup> must have the same ` +
`language type.`,
)
}
if (scriptSetupLang && !ctx.isJS && !ctx.isTS) { if (scriptSetupLang && !ctx.isJS && !ctx.isTS) {
// do not process non js/ts script blocks // do not process non js/ts script blocks
return scriptSetup return scriptSetup
@ -246,7 +248,7 @@ export function compileScript(
) { ) {
// template usage check is only needed in non-inline mode, so we can skip // template usage check is only needed in non-inline mode, so we can skip
// the work if inlineTemplate is true. // the work if inlineTemplate is true.
let isUsedInTemplate = needTemplateUsageCheck let isImportUsed = needTemplateUsageCheck
if ( if (
needTemplateUsageCheck && needTemplateUsageCheck &&
ctx.isTS && ctx.isTS &&
@ -254,7 +256,7 @@ export function compileScript(
!sfc.template.src && !sfc.template.src &&
!sfc.template.lang !sfc.template.lang
) { ) {
isUsedInTemplate = isImportUsed(local, sfc) isImportUsed = isUsedInTemplate(local, sfc)
} }
ctx.userImports[local] = { ctx.userImports[local] = {
@ -263,7 +265,7 @@ export function compileScript(
local, local,
source, source,
isFromSetup, isFromSetup,
isUsedInTemplate, isUsedInTemplate: isImportUsed,
} }
} }
@ -284,8 +286,42 @@ export function compileScript(
}) })
} }
function buildDestructureElements() {
if (!sfc.template || !sfc.template.ast) return
const builtins = {
$props: {
bindingType: BindingTypes.SETUP_REACTIVE_CONST,
setup: () => setupPreambleLines.push(`const $props = __props`),
},
$emit: {
bindingType: BindingTypes.SETUP_CONST,
setup: () =>
ctx.emitDecl
? setupPreambleLines.push(`const $emit = __emit`)
: destructureElements.push('emit: $emit'),
},
$attrs: {
bindingType: BindingTypes.SETUP_REACTIVE_CONST,
setup: () => destructureElements.push('attrs: $attrs'),
},
$slots: {
bindingType: BindingTypes.SETUP_REACTIVE_CONST,
setup: () => destructureElements.push('slots: $slots'),
},
}
for (const [name, config] of Object.entries(builtins)) {
if (isUsedInTemplate(name, sfc) && !ctx.bindingMetadata[name]) {
config.setup()
ctx.bindingMetadata[name] = config.bindingType
}
}
}
const scriptAst = ctx.scriptAst const scriptAst = ctx.scriptAst
const scriptSetupAst = ctx.scriptSetupAst! const scriptSetupAst = ctx.scriptSetupAst!
const inlineMode = options.inlineTemplate
// 1.1 walk import declarations of <script> // 1.1 walk import declarations of <script>
if (scriptAst) { if (scriptAst) {
@ -302,7 +338,7 @@ export function compileScript(
(specifier.type === 'ImportSpecifier' && (specifier.type === 'ImportSpecifier' &&
specifier.importKind === 'type'), specifier.importKind === 'type'),
false, false,
!options.inlineTemplate, !inlineMode,
) )
} }
} }
@ -370,7 +406,7 @@ export function compileScript(
(specifier.type === 'ImportSpecifier' && (specifier.type === 'ImportSpecifier' &&
specifier.importKind === 'type'), specifier.importKind === 'type'),
true, true,
!options.inlineTemplate, !inlineMode,
) )
} }
} }
@ -811,12 +847,16 @@ export function compileScript(
} }
const destructureElements = const destructureElements =
ctx.hasDefineExposeCall || !options.inlineTemplate ctx.hasDefineExposeCall || !inlineMode ? [`expose: __expose`] : []
? [`expose: __expose`]
: []
if (ctx.emitDecl) { if (ctx.emitDecl) {
destructureElements.push(`emit: __emit`) destructureElements.push(`emit: __emit`)
} }
// destructure built-in properties (e.g. $emit, $attrs, $slots)
if (inlineMode) {
buildDestructureElements()
}
if (destructureElements.length) { if (destructureElements.length) {
args += `, { ${destructureElements.join(', ')} }` args += `, { ${destructureElements.join(', ')} }`
} }
@ -824,10 +864,7 @@ export function compileScript(
let templateMap let templateMap
// 9. generate return statement // 9. generate return statement
let returned let returned
if ( if (!inlineMode || (!sfc.template && ctx.hasDefaultExportRender)) {
!options.inlineTemplate ||
(!sfc.template && ctx.hasDefaultExportRender)
) {
// non-inline mode, or has manual render in normal <script> // non-inline mode, or has manual render in normal <script>
// return bindings from script and script setup // return bindings from script and script setup
const allBindings: Record<string, any> = { const allBindings: Record<string, any> = {
@ -927,7 +964,7 @@ export function compileScript(
} }
} }
if (!options.inlineTemplate && !__TEST__) { if (!inlineMode && !__TEST__) {
// in non-inline mode, the `__isScriptSetup: true` flag is used by // in non-inline mode, the `__isScriptSetup: true` flag is used by
// componentPublicInstance proxy to allow properties that start with $ or _ // componentPublicInstance proxy to allow properties that start with $ or _
ctx.s.appendRight( ctx.s.appendRight(
@ -976,8 +1013,12 @@ export function compileScript(
// <script setup> components are closed by default. If the user did not // <script setup> components are closed by default. If the user did not
// explicitly call `defineExpose`, call expose() with no args. // explicitly call `defineExpose`, call expose() with no args.
const exposeCall = if (!ctx.hasDefineExposeCall && !inlineMode)
ctx.hasDefineExposeCall || options.inlineTemplate ? `` : ` __expose();\n` setupPreambleLines.push(`__expose();`)
const setupPreamble = setupPreambleLines.length
? ` ${setupPreambleLines.join('\n ')}\n`
: ''
// wrap setup code with function. // wrap setup code with function.
if (ctx.isTS) { if (ctx.isTS) {
// for TS, make sure the exported type is still valid type with // for TS, make sure the exported type is still valid type with
@ -994,7 +1035,7 @@ export function compileScript(
vapor && !ssr ? `defineVaporComponent` : `defineComponent`, vapor && !ssr ? `defineVaporComponent` : `defineComponent`,
)}({${def}${runtimeOptions}\n ${ )}({${def}${runtimeOptions}\n ${
hasAwait ? `async ` : `` hasAwait ? `async ` : ``
}setup(${args}) {\n${exposeCall}`, }setup(${args}) {\n${setupPreamble}`,
) )
ctx.s.appendRight(endOffset, `})`) ctx.s.appendRight(endOffset, `})`)
} else { } else {
@ -1010,14 +1051,14 @@ export function compileScript(
`\n${genDefaultAs} /*@__PURE__*/Object.assign(${ `\n${genDefaultAs} /*@__PURE__*/Object.assign(${
defaultExport ? `${normalScriptDefaultVar}, ` : '' defaultExport ? `${normalScriptDefaultVar}, ` : ''
}${definedOptions ? `${definedOptions}, ` : ''}{${runtimeOptions}\n ` + }${definedOptions ? `${definedOptions}, ` : ''}{${runtimeOptions}\n ` +
`${hasAwait ? `async ` : ``}setup(${args}) {\n${exposeCall}`, `${hasAwait ? `async ` : ``}setup(${args}) {\n${setupPreamble}`,
) )
ctx.s.appendRight(endOffset, `})`) ctx.s.appendRight(endOffset, `})`)
} else { } else {
ctx.s.prependLeft( ctx.s.prependLeft(
startOffset, startOffset,
`\n${genDefaultAs} {${runtimeOptions}\n ` + `\n${genDefaultAs} {${runtimeOptions}\n ` +
`${hasAwait ? `async ` : ``}setup(${args}) {\n${exposeCall}`, `${hasAwait ? `async ` : ``}setup(${args}) {\n${setupPreamble}`,
) )
ctx.s.appendRight(endOffset, `}`) ctx.s.appendRight(endOffset, `}`)
} }

View File

@ -16,7 +16,7 @@ import type { TemplateCompiler } from './compileTemplate'
import { parseCssVars } from './style/cssVars' import { parseCssVars } from './style/cssVars'
import { createCache } from './cache' import { createCache } from './cache'
import type { ImportBinding } from './compileScript' import type { ImportBinding } from './compileScript'
import { isImportUsed } from './script/importUsageCheck' import { isUsedInTemplate } from './script/importUsageCheck'
import type { LRUCache } from 'lru-cache' import type { LRUCache } from 'lru-cache'
import { genCacheKey } from '@vue/shared' import { genCacheKey } from '@vue/shared'
@ -449,7 +449,7 @@ export function hmrShouldReload(
for (const key in prevImports) { for (const key in prevImports) {
// if an import was previous unused, but now is used, we need to force // if an import was previous unused, but now is used, we need to force
// reload so that the script now includes that import. // reload so that the script now includes that import.
if (!prevImports[key].isUsedInTemplate && isImportUsed(key, next)) { if (!prevImports[key].isUsedInTemplate && isUsedInTemplate(key, next)) {
return true return true
} }
} }

View File

@ -11,12 +11,16 @@ import { createCache } from '../cache'
import { camelize, capitalize, isBuiltInDirective } from '@vue/shared' import { camelize, capitalize, isBuiltInDirective } from '@vue/shared'
/** /**
* Check if an import is used in the SFC's template. This is used to determine * Check if an identifier is used in the SFC's template.
* the properties that should be included in the object returned from setup() * - 1.used to determine the properties that should be included in the object returned from setup()
* when not using inline mode. * when not using inline mode.
* - 2.check whether the built-in properties such as $attrs, $slots, $emit are used in the template
*/ */
export function isImportUsed(local: string, sfc: SFCDescriptor): boolean { export function isUsedInTemplate(
return resolveTemplateUsedIdentifiers(sfc).has(local) identifier: string,
sfc: SFCDescriptor,
): boolean {
return resolveTemplateUsedIdentifiers(sfc).has(identifier)
} }
const templateUsageCheckCache = createCache<Set<string>>() const templateUsageCheckCache = createCache<Set<string>>()

View File

@ -1500,6 +1500,7 @@ export function inferRuntimeType(
node: Node & MaybeWithScope, node: Node & MaybeWithScope,
scope: TypeScope = node._ownerScope || ctxToScope(ctx), scope: TypeScope = node._ownerScope || ctxToScope(ctx),
isKeyOf = false, isKeyOf = false,
typeParameters?: Record<string, Node>,
): string[] { ): string[] {
try { try {
switch (node.type) { switch (node.type) {
@ -1588,19 +1589,43 @@ export function inferRuntimeType(
case 'TSTypeReference': { case 'TSTypeReference': {
const resolved = resolveTypeReference(ctx, node, scope) const resolved = resolveTypeReference(ctx, node, scope)
if (resolved) { if (resolved) {
if (resolved.type === 'TSTypeAliasDeclaration') {
// #13240 // #13240
// Special case for function type aliases to ensure correct runtime behavior // Special case for function type aliases to ensure correct runtime behavior
// other type aliases still fallback to unknown as before // other type aliases still fallback to unknown as before
if ( if (resolved.typeAnnotation.type === 'TSFunctionType') {
resolved.type === 'TSTypeAliasDeclaration' &&
resolved.typeAnnotation.type === 'TSFunctionType'
) {
return ['Function'] return ['Function']
} }
return inferRuntimeType(ctx, resolved, resolved._ownerScope, isKeyOf)
if (node.typeParameters) {
const typeParams: Record<string, Node> = Object.create(null)
if (resolved.typeParameters) {
resolved.typeParameters.params.forEach((p, i) => {
typeParams![p.name] = node.typeParameters!.params[i]
})
}
return inferRuntimeType(
ctx,
resolved.typeAnnotation,
resolved._ownerScope,
isKeyOf,
typeParams,
)
}
} }
return inferRuntimeType(ctx, resolved, resolved._ownerScope, isKeyOf)
}
if (node.typeName.type === 'Identifier') { if (node.typeName.type === 'Identifier') {
if (typeParameters && typeParameters[node.typeName.name]) {
return inferRuntimeType(
ctx,
typeParameters[node.typeName.name],
scope,
isKeyOf,
typeParameters,
)
}
if (isKeyOf) { if (isKeyOf) {
switch (node.typeName.name) { switch (node.typeName.name) {
case 'String': case 'String':
@ -1733,11 +1758,15 @@ export function inferRuntimeType(
return inferRuntimeType(ctx, node.typeAnnotation, scope) return inferRuntimeType(ctx, node.typeAnnotation, scope)
case 'TSUnionType': case 'TSUnionType':
return flattenTypes(ctx, node.types, scope, isKeyOf) return flattenTypes(ctx, node.types, scope, isKeyOf, typeParameters)
case 'TSIntersectionType': { case 'TSIntersectionType': {
return flattenTypes(ctx, node.types, scope, isKeyOf).filter( return flattenTypes(
t => t !== UNKNOWN_TYPE, ctx,
) node.types,
scope,
isKeyOf,
typeParameters,
).filter(t => t !== UNKNOWN_TYPE)
} }
case 'TSEnumDeclaration': case 'TSEnumDeclaration':
@ -1808,14 +1837,17 @@ function flattenTypes(
types: TSType[], types: TSType[],
scope: TypeScope, scope: TypeScope,
isKeyOf: boolean = false, isKeyOf: boolean = false,
typeParameters: Record<string, Node> | undefined = undefined,
): string[] { ): string[] {
if (types.length === 1) { if (types.length === 1) {
return inferRuntimeType(ctx, types[0], scope, isKeyOf) return inferRuntimeType(ctx, types[0], scope, isKeyOf, typeParameters)
} }
return [ return [
...new Set( ...new Set(
([] as string[]).concat( ([] as string[]).concat(
...types.map(t => inferRuntimeType(ctx, t, scope, isKeyOf)), ...types.map(t =>
inferRuntimeType(ctx, t, scope, isKeyOf, typeParameters),
),
), ),
), ),
] ]

View File

@ -10,6 +10,7 @@ import { warn } from '../warn'
const animationNameRE = /^(-\w+-)?animation-name$/ const animationNameRE = /^(-\w+-)?animation-name$/
const animationRE = /^(-\w+-)?animation$/ const animationRE = /^(-\w+-)?animation$/
const keyframesRE = /^(?:-\w+-)?keyframes$/
const scopedPlugin: PluginCreator<string> = (id = '') => { const scopedPlugin: PluginCreator<string> = (id = '') => {
const keyframes = Object.create(null) const keyframes = Object.create(null)
@ -21,10 +22,7 @@ const scopedPlugin: PluginCreator<string> = (id = '') => {
processRule(id, rule) processRule(id, rule)
}, },
AtRule(node) { AtRule(node) {
if ( if (keyframesRE.test(node.name) && !node.params.endsWith(`-${shortId}`)) {
/-?keyframes$/.test(node.name) &&
!node.params.endsWith(`-${shortId}`)
) {
// register keyframes // register keyframes
keyframes[node.params] = node.params = node.params + '-' + shortId keyframes[node.params] = node.params = node.params + '-' + shortId
} }
@ -72,7 +70,7 @@ function processRule(id: string, rule: Rule) {
processedRules.has(rule) || processedRules.has(rule) ||
(rule.parent && (rule.parent &&
rule.parent.type === 'atrule' && rule.parent.type === 'atrule' &&
/-?keyframes$/.test((rule.parent as AtRule).name)) keyframesRE.test((rule.parent as AtRule).name))
) { ) {
return return
} }

View File

@ -71,6 +71,7 @@ export const transformSrcset: NodeTransform = (
const shouldProcessUrl = (url: string) => { const shouldProcessUrl = (url: string) => {
return ( return (
url &&
!isExternalUrl(url) && !isExternalUrl(url) &&
!isDataUrl(url) && !isDataUrl(url) &&
(options.includeAbsolute || isRelativeUrl(url)) (options.includeAbsolute || isRelativeUrl(url))

View File

@ -317,6 +317,35 @@ describe('ssr: components', () => {
`) `)
}) })
// #13724
test('slot content with v-memo', () => {
const { code } = compile(`<foo><bar v-memo="[]" /></foo>`)
expect(code).not.toMatch(`_cache`)
expect(compile(`<foo><bar v-memo="[]" /></foo>`).code)
.toMatchInlineSnapshot(`
"const { resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode } = require("vue")
const { ssrRenderComponent: _ssrRenderComponent } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
const _component_foo = _resolveComponent("foo")
const _component_bar = _resolveComponent("bar")
_push(_ssrRenderComponent(_component_foo, _attrs, {
default: _withCtx((_, _push, _parent, _scopeId) => {
if (_push) {
_push(_ssrRenderComponent(_component_bar, null, null, _parent, _scopeId))
} else {
return [
_createVNode(_component_bar)
]
}
}),
_: 1 /* STABLE */
}, _parent))
}"
`)
})
describe('built-in fallthroughs', () => { describe('built-in fallthroughs', () => {
test('transition', () => { test('transition', () => {
expect(compile(`<transition><div/></transition>`).code) expect(compile(`<transition><div/></transition>`).code)

View File

@ -29,7 +29,7 @@ if (__TEST__) {
if (SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME < DOMErrorCodes.__EXTEND_POINT__) { if (SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME < DOMErrorCodes.__EXTEND_POINT__) {
throw new Error( throw new Error(
`SSRErrorCodes need to be updated to ${ `SSRErrorCodes need to be updated to ${
DOMErrorCodes.__EXTEND_POINT__ + 1 DOMErrorCodes.__EXTEND_POINT__
} to match extension point from core DOMErrorCodes.`, } to match extension point from core DOMErrorCodes.`,
) )
} }

View File

@ -212,6 +212,22 @@ export function render(_ctx) {
}" }"
`; `;
exports[`compile > execution order > with insertionState 1`] = `
"import { resolveComponent as _resolveComponent, child as _child, setInsertionState as _setInsertionState, createSlot as _createSlot, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
const t0 = _template("<div><div></div></div>", true)
export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp")
const n3 = t0()
const n1 = _child(n3)
_setInsertionState(n1)
const n0 = _createSlot("default", null)
_setInsertionState(n3)
const n2 = _createComponentWithFallback(_component_Comp)
return n3
}"
`;
exports[`compile > execution order > with v-once 1`] = ` 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'; "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) const t0 = _template("<div><span> </span> <br> </div>", true)

View File

@ -247,6 +247,7 @@ describe('compile', () => {
_setText(x0, _toDisplayString(_ctx.bar))`, _setText(x0, _toDisplayString(_ctx.bar))`,
) )
}) })
test('with v-once', () => { test('with v-once', () => {
const code = compile( const code = compile(
`<div> `<div>
@ -261,5 +262,10 @@ describe('compile', () => {
_setText(n2, " " + _toDisplayString(_ctx.baz))`, _setText(n2, " " + _toDisplayString(_ctx.baz))`,
) )
}) })
test('with insertionState', () => {
const code = compile(`<div><div><slot /></div><Comp/></div>`)
expect(code).matchSnapshot()
})
}) })
}) })

View File

@ -57,10 +57,10 @@ const t0 = _template("<div><div>x</div><div><span> </span></div><div><span> </sp
export function render(_ctx) { export function render(_ctx) {
const n3 = t0() const n3 = t0()
const p0 = _next(_child(n3)) const p0 = _next(_child(n3))
const p1 = _next(p0)
const p2 = _next(p1)
const n0 = _child(p0) const n0 = _child(p0)
const p1 = _next(p0)
const n1 = _child(p1) const n1 = _child(p1)
const p2 = _next(p1)
const n2 = _child(p2) const n2 = _child(p2)
const x0 = _child(n0) const x0 = _child(n0)
const x1 = _child(n1) const x1 = _child(n1)

View File

@ -353,8 +353,8 @@ const t2 = _template("<form></form>")
export function render(_ctx) { export function render(_ctx) {
const n1 = t1() const n1 = t1()
const n3 = t2()
const n0 = t0() const n0 = t0()
const n3 = t2()
const n2 = t2() const n2 = t2()
_insert(n0, n1) _insert(n0, n1)
_insert(n2, n3) _insert(n2, n3)

View File

@ -65,8 +65,10 @@ export function genBlockContent(
push(...genSelf(child, context)) push(...genSelf(child, context))
} }
for (const child of dynamic.children) { for (const child of dynamic.children) {
if (!child.hasDynamicChild) {
push(...genChildren(child, context, push, `n${child.id!}`)) push(...genChildren(child, context, push, `n${child.id!}`))
} }
}
push(...genOperations(operation, context)) push(...genOperations(operation, context))
push(...genEffects(effect, context, genEffectsExtraFrag)) push(...genEffects(effect, context, genEffectsExtraFrag))

View File

@ -27,7 +27,7 @@ export function genSelf(
context: CodegenContext, context: CodegenContext,
): CodeFragment[] { ): CodeFragment[] {
const [frag, push] = buildCodeFragment() const [frag, push] = buildCodeFragment()
const { id, template, operation } = dynamic const { id, template, operation, hasDynamicChild } = dynamic
if (id !== undefined && template !== undefined) { if (id !== undefined && template !== undefined) {
push(NEWLINE, `const n${id} = t${template}()`) push(NEWLINE, `const n${id} = t${template}()`)
@ -38,6 +38,10 @@ export function genSelf(
push(...genOperationWithInsertionState(operation, context)) push(...genOperationWithInsertionState(operation, context))
} }
if (hasDynamicChild) {
push(...genChildren(dynamic, context, push, `n${id}`))
}
return frag return frag
} }
@ -53,7 +57,6 @@ export function genChildren(
let offset = 0 let offset = 0
let prev: [variable: string, elementIndex: number] | undefined let prev: [variable: string, elementIndex: number] | undefined
const childrenToGen: [IRDynamicInfo, string][] = []
for (const [index, child] of children.entries()) { for (const [index, child] of children.entries()) {
if (child.flags & DynamicFlag.NON_TEMPLATE) { if (child.flags & DynamicFlag.NON_TEMPLATE) {
@ -99,7 +102,7 @@ export function genChildren(
} }
} }
if (id === child.anchor) { if (id === child.anchor && !child.hasDynamicChild) {
push(...genSelf(child, context)) push(...genSelf(child, context))
} }
@ -108,13 +111,7 @@ export function genChildren(
} }
prev = [variable, elementIndex] prev = [variable, elementIndex]
childrenToGen.push([child, variable]) push(...genChildren(child, context, pushBlock, variable))
}
if (childrenToGen.length) {
for (const [child, from] of childrenToGen) {
push(...genChildren(child, context, pushBlock, from))
}
} }
return frag return frag

View File

@ -498,9 +498,10 @@ describe('reactivity/readonly', () => {
const r = ref(false) const r = ref(false)
const ror = readonly(r) const ror = readonly(r)
const obj = reactive({ ror }) const obj = reactive({ ror })
expect(() => {
obj.ror = true obj.ror = true
}).toThrow() expect(
`Set operation on key "ror" failed: target is readonly.`,
).toHaveBeenWarned()
expect(obj.ror).toBe(false) expect(obj.ror).toBe(false)
}) })

View File

@ -158,7 +158,13 @@ class MutableReactiveHandler extends BaseReactiveHandler {
} }
if (!isArray(target) && isRef(oldValue) && !isRef(value)) { if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
if (isOldValueReadonly) { if (isOldValueReadonly) {
return false if (__DEV__) {
warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target[key],
)
}
return true
} else { } else {
oldValue.value = value oldValue.value = value
return true return true

View File

@ -167,13 +167,26 @@ describe('component: proxy', () => {
data() { data() {
return { return {
foo: 0, foo: 0,
$foo: 0,
} }
}, },
computed: {
cmp: () => {
throw new Error('value of cmp should not be accessed')
},
$cmp: () => {
throw new Error('value of $cmp should not be read')
},
},
setup() { setup() {
return { return {
bar: 1, bar: 1,
} }
}, },
__cssModules: {
$style: {},
cssStyles: {},
},
mounted() { mounted() {
instanceProxy = this instanceProxy = this
}, },
@ -181,6 +194,7 @@ describe('component: proxy', () => {
const app = createApp(Comp, { msg: 'hello' }) const app = createApp(Comp, { msg: 'hello' })
app.config.globalProperties.global = 1 app.config.globalProperties.global = 1
app.config.globalProperties.$global = 1
app.mount(nodeOps.createElement('div')) app.mount(nodeOps.createElement('div'))
@ -188,12 +202,20 @@ describe('component: proxy', () => {
expect('msg' in instanceProxy).toBe(true) expect('msg' in instanceProxy).toBe(true)
// data // data
expect('foo' in instanceProxy).toBe(true) expect('foo' in instanceProxy).toBe(true)
// ctx expect('$foo' in instanceProxy).toBe(false)
// setupState
expect('bar' in instanceProxy).toBe(true) expect('bar' in instanceProxy).toBe(true)
// ctx
expect('cmp' in instanceProxy).toBe(true)
expect('$cmp' in instanceProxy).toBe(true)
// public properties // public properties
expect('$el' in instanceProxy).toBe(true) expect('$el' in instanceProxy).toBe(true)
// CSS modules
expect('$style' in instanceProxy).toBe(true)
expect('cssStyles' in instanceProxy).toBe(true)
// global properties // global properties
expect('global' in instanceProxy).toBe(true) expect('global' in instanceProxy).toBe(true)
expect('$global' in instanceProxy).toBe(true)
// non-existent // non-existent
expect('$foobar' in instanceProxy).toBe(false) expect('$foobar' in instanceProxy).toBe(false)
@ -202,11 +224,15 @@ describe('component: proxy', () => {
// #4962 triggering getter should not cause non-existent property to // #4962 triggering getter should not cause non-existent property to
// pass the has check // pass the has check
instanceProxy.baz instanceProxy.baz
instanceProxy.$baz
expect('baz' in instanceProxy).toBe(false) expect('baz' in instanceProxy).toBe(false)
expect('$baz' in instanceProxy).toBe(false)
// set non-existent (goes into proxyTarget sink) // set non-existent (goes into proxyTarget sink)
instanceProxy.baz = 1 instanceProxy.baz = 1
expect('baz' in instanceProxy).toBe(true) expect('baz' in instanceProxy).toBe(true)
instanceProxy.$baz = 1
expect('$baz' in instanceProxy).toBe(true)
// dev mode ownKeys check for console inspection // dev mode ownKeys check for console inspection
// should only expose own keys // should only expose own keys
@ -214,7 +240,10 @@ describe('component: proxy', () => {
'msg', 'msg',
'bar', 'bar',
'foo', 'foo',
'cmp',
'$cmp',
'baz', 'baz',
'$baz',
]) ])
}) })

View File

@ -6,6 +6,7 @@ import {
nodeOps, nodeOps,
ref, ref,
render, render,
serializeInner,
useSlots, useSlots,
} from '@vue/runtime-test' } from '@vue/runtime-test'
import { createBlock, normalizeVNode } from '../src/vnode' import { createBlock, normalizeVNode } from '../src/vnode'
@ -55,14 +56,10 @@ describe('component: slots', () => {
expect(Object.getOwnPropertyDescriptor(slots, '_')!.enumerable).toBe( expect(Object.getOwnPropertyDescriptor(slots, '_')!.enumerable).toBe(
false, false,
) )
expect(slots).toHaveProperty('__')
expect(Object.getOwnPropertyDescriptor(slots, '__')!.enumerable).toBe(
false,
)
return h('div') return h('div')
}, },
} }
const slots = { foo: () => {}, _: 1, __: [1] } const slots = { foo: () => {}, _: 1 }
render(createBlock(Comp, null, slots), nodeOps.createElement('div')) render(createBlock(Comp, null, slots), nodeOps.createElement('div'))
}) })
@ -74,6 +71,10 @@ describe('component: slots', () => {
footer: ['f1', 'f2'], footer: ['f1', 'f2'],
}) })
expect(
'[Vue warn]: Non-function value encountered for slot "_inner". Prefer function slots for better performance.',
).toHaveBeenWarned()
expect( expect(
'[Vue warn]: Non-function value encountered for slot "header". Prefer function slots for better performance.', '[Vue warn]: Non-function value encountered for slot "header". Prefer function slots for better performance.',
).toHaveBeenWarned() ).toHaveBeenWarned()
@ -82,8 +83,8 @@ describe('component: slots', () => {
'[Vue warn]: Non-function value encountered for slot "footer". Prefer function slots for better performance.', '[Vue warn]: Non-function value encountered for slot "footer". Prefer function slots for better performance.',
).toHaveBeenWarned() ).toHaveBeenWarned()
expect(slots).not.toHaveProperty('_inner')
expect(slots).not.toHaveProperty('foo') expect(slots).not.toHaveProperty('foo')
expect(slots._inner()).toMatchObject([normalizeVNode('_inner')])
expect(slots.header()).toMatchObject([normalizeVNode('header')]) expect(slots.header()).toMatchObject([normalizeVNode('header')])
expect(slots.footer()).toMatchObject([ expect(slots.footer()).toMatchObject([
normalizeVNode('f1'), normalizeVNode('f1'),
@ -442,4 +443,22 @@ describe('component: slots', () => {
'Slot "default" invoked outside of the render function', 'Slot "default" invoked outside of the render function',
).toHaveBeenWarned() ).toHaveBeenWarned()
}) })
test('slot name starts with underscore', () => {
const Comp = {
setup(_: any, { slots }: any) {
return () => slots._foo()
},
}
const App = {
setup() {
return () => h(Comp, null, { _foo: () => 'foo' })
},
}
const root = nodeOps.createElement('div')
createApp(App).mount(root)
expect(serializeInner(root)).toBe('foo')
})
}) })

View File

@ -17,6 +17,8 @@ import {
onUnmounted, onUnmounted,
ref, ref,
render, render,
renderList,
renderSlot,
resolveDynamicComponent, resolveDynamicComponent,
serializeInner, serializeInner,
shallowRef, shallowRef,
@ -2161,6 +2163,80 @@ describe('Suspense', () => {
await Promise.all(deps) await Promise.all(deps)
}) })
// #13453
test('add new async deps during patching', async () => {
const getComponent = (type: string) => {
if (type === 'A') {
return defineAsyncComponent({
setup() {
return () => h('div', 'A')
},
})
}
return defineAsyncComponent({
setup() {
return () => h('div', 'B')
},
})
}
const types = ref(['A'])
const add = async () => {
types.value.push('B')
}
const update = async () => {
// mount Suspense B
// [Suspense A] -> [Suspense A(pending), Suspense B(pending)]
await add()
// patch Suspense B (still pending)
// [Suspense A(pending), Suspense B(pending)] -> [Suspense B(pending)]
types.value.shift()
}
const Comp = {
render(this: any) {
return h(Fragment, null, [
renderList(types.value, type => {
return h(
Suspense,
{ key: type },
{
default: () => [
renderSlot(this.$slots, 'default', { type: type }),
],
},
)
}),
])
},
}
const App = {
setup() {
return () =>
h(Comp, null, {
default: (params: any) => [h(getComponent(params.type))],
})
},
}
const root = nodeOps.createElement('div')
render(h(App), root)
expect(serializeInner(root)).toBe(`<!---->`)
await Promise.all(deps)
expect(serializeInner(root)).toBe(`<div>A</div>`)
update()
await nextTick()
// wait for both A and B to resolve
await Promise.all(deps)
// wait for new B to resolve
await Promise.all(deps)
expect(serializeInner(root)).toBe(`<div>B</div>`)
})
describe('warnings', () => { describe('warnings', () => {
// base function to check if a combination of slots warns or not // base function to check if a combination of slots warns or not
function baseCheckWarn( function baseCheckWarn(
@ -2230,5 +2306,57 @@ describe('Suspense', () => {
fallback: [h('div'), h('div')], fallback: [h('div'), h('div')],
}) })
}) })
// #13559
test('renders multiple async components in Suspense with v-for and updates on items change', async () => {
const CompAsyncSetup = defineAsyncComponent({
props: ['item'],
render(ctx: any) {
return h('div', ctx.item.name)
},
})
const items = ref([
{ id: 1, name: '111' },
{ id: 2, name: '222' },
{ id: 3, name: '333' },
])
const Comp = {
setup() {
return () =>
h(Suspense, null, {
default: () =>
h(
Fragment,
null,
items.value.map(item =>
h(CompAsyncSetup, { item, key: item.id }),
),
),
})
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
await nextTick()
await Promise.all(deps)
expect(serializeInner(root)).toBe(
`<div>111</div><div>222</div><div>333</div>`,
)
items.value = [
{ id: 4, name: '444' },
{ id: 5, name: '555' },
{ id: 6, name: '666' },
]
await nextTick()
await Promise.all(deps)
expect(serializeInner(root)).toBe(
`<div>444</div><div>555</div><div>666</div>`,
)
})
}) })
}) })

View File

@ -106,6 +106,134 @@ describe('useTemplateRef', () => {
expect(tRef!.value).toBe(null) expect(tRef!.value).toBe(null)
}) })
test('should work when used with direct ref value with ref_key', () => {
let tRef: ShallowRef
const key = 'refKey'
const Comp = {
setup() {
tRef = useTemplateRef(key)
return () => h('div', { ref: tRef, ref_key: key })
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect('target is readonly').not.toHaveBeenWarned()
expect(tRef!.value).toBe(root.children[0])
})
test('should work when used with direct ref value with ref_key and ref_for', () => {
let tRef: ShallowRef
const key = 'refKey'
const Comp = {
setup() {
tRef = useTemplateRef(key)
},
render() {
return h(
'div',
[1, 2, 3].map(x =>
h('span', { ref: tRef, ref_key: key, ref_for: true }, x.toString()),
),
)
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect('target is readonly').not.toHaveBeenWarned()
expect(tRef!.value).toHaveLength(3)
})
test('should work when used with direct ref value with ref_key and dynamic value', async () => {
const refMode = ref('h1-ref')
let tRef: ShallowRef
const key = 'refKey'
const Comp = {
setup() {
tRef = useTemplateRef(key)
},
render() {
switch (refMode.value) {
case 'h1-ref':
return h('h1', { ref: tRef, ref_key: key })
case 'h2-ref':
return h('h2', { ref: tRef, ref_key: key })
case 'no-ref':
return h('span')
case 'nothing':
return null
}
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(tRef!.value.tag).toBe('h1')
refMode.value = 'h2-ref'
await nextTick()
expect(tRef!.value.tag).toBe('h2')
refMode.value = 'no-ref'
await nextTick()
expect(tRef!.value).toBeNull()
refMode.value = 'nothing'
await nextTick()
expect(tRef!.value).toBeNull()
expect('target is readonly').not.toHaveBeenWarned()
})
test('should work when used with dynamic direct refs and ref_keys', async () => {
const refKey = ref('foo')
let tRefs: Record<string, ShallowRef>
const Comp = {
setup() {
tRefs = {
foo: useTemplateRef('foo'),
bar: useTemplateRef('bar'),
}
},
render() {
return h('div', { ref: tRefs[refKey.value], ref_key: refKey.value })
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(tRefs!['foo'].value).toBe(root.children[0])
expect(tRefs!['bar'].value).toBeNull()
refKey.value = 'bar'
await nextTick()
expect(tRefs!['foo'].value).toBeNull()
expect(tRefs!['bar'].value).toBe(root.children[0])
expect('target is readonly').not.toHaveBeenWarned()
})
test('should not work when used with direct ref value without ref_key (in dev mode)', () => {
let tRef: ShallowRef
const Comp = {
setup() {
tRef = useTemplateRef('refKey')
return () => h('div', { ref: tRef })
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(tRef!.value).toBeNull()
})
test('should work when used as direct ref value (compiled in prod mode)', () => { test('should work when used as direct ref value (compiled in prod mode)', () => {
__DEV__ = false __DEV__ = false
try { try {
@ -125,4 +253,65 @@ describe('useTemplateRef', () => {
__DEV__ = true __DEV__ = true
} }
}) })
test('should work when used as direct ref value with ref_key and ref_for (compiled in prod mode)', () => {
__DEV__ = false
try {
let tRef: ShallowRef
const key = 'refKey'
const Comp = {
setup() {
tRef = useTemplateRef(key)
},
render() {
return h(
'div',
[1, 2, 3].map(x =>
h(
'span',
{ ref: tRef, ref_key: key, ref_for: true },
x.toString(),
),
),
)
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect('target is readonly').not.toHaveBeenWarned()
expect(tRef!.value).toHaveLength(3)
} finally {
__DEV__ = true
}
})
test('should work when used as direct ref value with ref_for but without ref_key (compiled in prod mode)', () => {
__DEV__ = false
try {
let tRef: ShallowRef
const Comp = {
setup() {
tRef = useTemplateRef('refKey')
},
render() {
return h(
'div',
[1, 2, 3].map(x =>
h('span', { ref: tRef, ref_for: true }, x.toString()),
),
)
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect('target is readonly').not.toHaveBeenWarned()
expect(tRef!.value).toHaveLength(3)
} finally {
__DEV__ = true
}
})
}) })

View File

@ -894,4 +894,47 @@ describe('hot module replacement', () => {
await timeout() await timeout()
expect(serializeInner(root)).toBe('<div>bar</div>') expect(serializeInner(root)).toBe('<div>bar</div>')
}) })
test('rerender for nested component', () => {
const id = 'child-nested-rerender'
const Foo: ComponentOptions = {
__hmrId: id,
render() {
return this.$slots.default()
},
}
createRecord(id, Foo)
const parentId = 'parent-nested-rerender'
const Parent: ComponentOptions = {
__hmrId: parentId,
render() {
return h(Foo, null, {
default: () => this.$slots.default(),
_: 3 /* FORWARDED */,
})
},
}
const appId = 'app-nested-rerender'
const App: ComponentOptions = {
__hmrId: appId,
render: () =>
h(Parent, null, {
default: () => [
h(Foo, null, {
default: () => ['foo'],
}),
],
}),
}
createRecord(parentId, App)
const root = nodeOps.createElement('div')
render(h(App), root)
expect(serializeInner(root)).toBe('foo')
rerender(id, () => 'bar')
expect(serializeInner(root)).toBe('bar')
})
}) })

View File

@ -1160,6 +1160,69 @@ describe('SSR hydration', () => {
) )
}) })
// #13510
test('update async component after parent mount before async component resolve', async () => {
const Comp = {
props: ['toggle'],
render(this: any) {
return h('h1', [
this.toggle ? 'Async component' : 'Updated async component',
])
},
}
let serverResolve: any
let AsyncComp = defineAsyncComponent(
() =>
new Promise(r => {
serverResolve = r
}),
)
const toggle = ref(true)
const App = {
setup() {
onMounted(() => {
// change state, after mount and before async component resolve
nextTick(() => (toggle.value = false))
})
return () => {
return h(AsyncComp, { toggle: toggle.value })
}
},
}
// server render
const htmlPromise = renderToString(h(App))
serverResolve(Comp)
const html = await htmlPromise
expect(html).toMatchInlineSnapshot(`"<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))
// prevent lazy hydration since the component has been patched
expect('Skipping lazy hydration for component').toHaveBeenWarned()
expect(`Hydration node mismatch`).not.toHaveBeenWarned()
expect(container.innerHTML).toMatchInlineSnapshot(
`"<h1>Updated async component</h1>"`,
)
})
test('hydrate safely when property used by async setup changed before render', async () => { test('hydrate safely when property used by async setup changed before render', async () => {
const toggle = ref(true) const toggle = ref(true)
@ -1677,6 +1740,35 @@ describe('SSR hydration', () => {
expect(`mismatch`).not.toHaveBeenWarned() expect(`mismatch`).not.toHaveBeenWarned()
}) })
// #13394
test('transition appear work with empty content', async () => {
const show = ref(true)
const { vnode, container } = mountWithHydration(
`<template><!----></template>`,
function (this: any) {
return h(
Transition,
{ appear: true },
{
default: () =>
show.value
? renderSlot(this.$slots, 'default')
: createTextVNode('foo'),
},
)
},
)
// empty slot render as a comment node
expect(container.firstChild!.nodeType).toBe(Node.COMMENT_NODE)
expect(vnode.el).toBe(container.firstChild)
expect(`mismatch`).not.toHaveBeenWarned()
show.value = false
await nextTick()
expect(container.innerHTML).toBe('foo')
})
test('transition appear with v-if', () => { test('transition appear with v-if', () => {
const show = false const show = false
const { vnode, container } = mountWithHydration( const { vnode, container } = mountWithHydration(

View File

@ -553,18 +553,6 @@ describe('vnode', () => {
expect(vnode.dynamicChildren).toStrictEqual([vnode1]) expect(vnode.dynamicChildren).toStrictEqual([vnode1])
}) })
test('with suspense', () => {
const hoist = createVNode('div')
let vnode1
const vnode =
(openBlock(),
createBlock('div', null, [
hoist,
(vnode1 = createVNode(() => {}, null, 'text')),
]))
expect(vnode.dynamicChildren).toStrictEqual([vnode1])
})
// #1039 // #1039
// <component :is="foo">{{ bar }}</component> // <component :is="foo">{{ bar }}</component>
// - content is compiled as slot // - content is compiled as slot

View File

@ -124,28 +124,30 @@ export function defineAsyncComponent<
__asyncHydrate(el, instance, hydrate) { __asyncHydrate(el, instance, hydrate) {
let patched = false let patched = false
const doHydrate = hydrateStrategy ;(instance.bu || (instance.bu = [])).push(() => (patched = true))
? () => {
const performHydrate = () => { const performHydrate = () => {
// skip hydration if the component has been patched // skip hydration if the component has been patched
if (__DEV__ && patched) { if (patched) {
if (__DEV__) {
warn( warn(
`Skipping lazy hydration for component '${getComponentName(resolvedComp!)}': ` + `Skipping lazy hydration for component '${getComponentName(resolvedComp!) || resolvedComp!.__file}': ` +
`it was updated before lazy hydration performed.`, `it was updated before lazy hydration performed.`,
) )
}
return return
} }
hydrate() hydrate()
} }
const doHydrate = hydrateStrategy
? () => {
const teardown = hydrateStrategy(performHydrate, cb => const teardown = hydrateStrategy(performHydrate, cb =>
forEachElement(el, cb), forEachElement(el, cb),
) )
if (teardown) { if (teardown) {
;(instance.bum || (instance.bum = [])).push(teardown) ;(instance.bum || (instance.bum = [])).push(teardown)
} }
;(instance.u || (instance.u = [])).push(() => (patched = true))
} }
: hydrate : performHydrate
if (resolvedComp) { if (resolvedComp) {
doHydrate() doHydrate()
} else { } else {

View File

@ -383,17 +383,17 @@ export function withDefaults<
// TODO return type for Vapor components // TODO return type for Vapor components
export function useSlots(): SetupContext['slots'] { export function useSlots(): SetupContext['slots'] {
return getContext().slots return getContext('useSlots').slots
} }
export function useAttrs(): SetupContext['attrs'] { export function useAttrs(): SetupContext['attrs'] {
return getContext().attrs return getContext('useAttrs').attrs
} }
function getContext(): SetupContext { function getContext(calledFunctionName: string): SetupContext {
const i = getCurrentGenericInstance()! const i = getCurrentGenericInstance()!
if (__DEV__ && !i) { if (__DEV__ && !i) {
warn(`useContext() called without active instance.`) warn(`${calledFunctionName}() called without active instance.`)
} }
if (i.vapor) { if (i.vapor) {
return i as any // vapor instance act as its own setup context return i as any // vapor instance act as its own setup context

View File

@ -756,6 +756,7 @@ export function applyOptions(instance: ComponentInternalInstance): void {
Object.defineProperty(exposed, key, { Object.defineProperty(exposed, key, {
get: () => publicThis[key], get: () => publicThis[key],
set: val => (publicThis[key] = val), set: val => (publicThis[key] = val),
enumerable: true,
}) })
}) })
} else if (!instance.exposed) { } else if (!instance.exposed) {

View File

@ -577,19 +577,20 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
has( has(
{ {
_: { data, setupState, accessCache, ctx, appContext, propsOptions }, _: { data, setupState, accessCache, ctx, appContext, propsOptions, type },
}: ComponentRenderContext, }: ComponentRenderContext,
key: string, key: string,
) { ) {
let normalizedProps let normalizedProps, cssModules
return ( return !!(
!!accessCache![key] || accessCache![key] ||
(data !== EMPTY_OBJ && hasOwn(data, key)) || (data !== EMPTY_OBJ && key[0] !== '$' && hasOwn(data, key)) ||
hasSetupBinding(setupState, key) || hasSetupBinding(setupState, key) ||
((normalizedProps = propsOptions[0]) && hasOwn(normalizedProps, key)) || ((normalizedProps = propsOptions[0]) && hasOwn(normalizedProps, key)) ||
hasOwn(ctx, key) || hasOwn(ctx, key) ||
hasOwn(publicPropertiesMap, key) || hasOwn(publicPropertiesMap, key) ||
hasOwn(appContext.config.globalProperties, key) hasOwn(appContext.config.globalProperties, key) ||
((cssModules = type.__cssModules) && cssModules[key])
) )
}, },

View File

@ -79,14 +79,10 @@ export type RawSlots = {
* @internal * @internal
*/ */
_?: SlotFlags _?: SlotFlags
/**
* cache indexes for slot content
* @internal
*/
__?: number[]
} }
const isInternalKey = (key: string) => key[0] === '_' || key === '$stable' const isInternalKey = (key: string) =>
key === '_' || key === '_ctx' || key === '$stable'
const normalizeSlotValue = (value: unknown): VNode[] => const normalizeSlotValue = (value: unknown): VNode[] =>
isArray(value) isArray(value)
@ -194,10 +190,6 @@ export const initSlots = (
): void => { ): void => {
const slots = (instance.slots = createInternalObject()) const slots = (instance.slots = createInternalObject())
if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) { if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
const cacheIndexes = (children as RawSlots).__
// make cache indexes marker non-enumerable
if (cacheIndexes) def(slots, '__', cacheIndexes, true)
const type = (children as RawSlots)._ const type = (children as RawSlots)._
if (type) { if (type) {
assignSlots(slots, children as Slots, optimized) assignSlots(slots, children as Slots, optimized)

View File

@ -24,7 +24,7 @@ import { SchedulerJobFlags } from '../scheduler'
type Hook<T = () => void> = T | T[] type Hook<T = () => void> = T | T[]
const leaveCbKey: unique symbol = Symbol('_leaveCb') export const leaveCbKey: unique symbol = Symbol('_leaveCb')
const enterCbKey: unique symbol = Symbol('_enterCb') const enterCbKey: unique symbol = Symbol('_enterCb')
export interface BaseTransitionProps<HostElement = RendererElement> { export interface BaseTransitionProps<HostElement = RendererElement> {

View File

@ -1,4 +1,5 @@
/* eslint-disable no-restricted-globals */ /* eslint-disable no-restricted-globals */
import { EffectFlags } from '@vue/reactivity'
import { import {
type ClassComponent, type ClassComponent,
type ComponentInternalInstance, type ComponentInternalInstance,
@ -99,9 +100,12 @@ function rerender(id: string, newRender?: Function): void {
instance.hmrRerender!() instance.hmrRerender!()
} else { } else {
const i = instance as ComponentInternalInstance const i = instance as ComponentInternalInstance
// #13771 don't update if the job is already disposed
if (!(i.effect.flags! & EffectFlags.STOP)) {
i.renderCache = [] i.renderCache = []
i.effect.run() i.effect.run()
} }
}
nextTick(() => { nextTick(() => {
isHmrUpdating = false isHmrUpdating = false
}) })

View File

@ -562,3 +562,7 @@ export { initFeatureFlags } from './featureFlags'
* @internal * @internal
*/ */
export { createInternalObject } from './internalObject' export { createInternalObject } from './internalObject'
/**
* @internal
*/
export { createCanSetSetupRefChecker } from './rendererTemplateRef'

View File

@ -40,12 +40,10 @@ export function endMeasure(
if (instance.appContext.config.performance && isSupported()) { if (instance.appContext.config.performance && isSupported()) {
const startTag = `vue-${type}-${instance.uid}` const startTag = `vue-${type}-${instance.uid}`
const endTag = startTag + `:end` const endTag = startTag + `:end`
const measureName = `<${formatComponentName(instance, instance.type)}> ${type}`
perf.mark(endTag) perf.mark(endTag)
perf.measure( perf.measure(measureName, startTag, endTag)
`<${formatComponentName(instance, instance.type)}> ${type}`, perf.clearMeasures(measureName)
startTag,
endTag,
)
perf.clearMarks(startTag) perf.clearMarks(startTag)
perf.clearMarks(endTag) perf.clearMarks(endTag)
} }

View File

@ -96,8 +96,8 @@ import { initFeatureFlags } from './featureFlags'
import { isAsyncWrapper } from './apiAsyncComponent' import { isAsyncWrapper } from './apiAsyncComponent'
import { isCompatEnabled } from './compat/compatConfig' import { isCompatEnabled } from './compat/compatConfig'
import { DeprecationTypes } from './compat/compatConfig' import { DeprecationTypes } from './compat/compatConfig'
import type { TransitionHooks } from './components/BaseTransition'
import type { VaporInteropInterface } from './apiCreateApp' import type { VaporInteropInterface } from './apiCreateApp'
import { type TransitionHooks, leaveCbKey } from './components/BaseTransition'
import type { VueElement } from '@vue/runtime-dom' import type { VueElement } from '@vue/runtime-dom'
export interface Renderer<HostElement = RendererElement> { export interface Renderer<HostElement = RendererElement> {
@ -1278,6 +1278,7 @@ function baseCreateRenderer(
if (!initialVNode.el) { if (!initialVNode.el) {
const placeholder = (instance.subTree = createVNode(Comment)) const placeholder = (instance.subTree = createVNode(Comment))
processCommentNode(null, placeholder, container!, anchor) processCommentNode(null, placeholder, container!, anchor)
initialVNode.placeholder = placeholder.el
} }
} else { } else {
setupRenderEffect( setupRenderEffect(
@ -2081,8 +2082,12 @@ function baseCreateRenderer(
for (i = toBePatched - 1; i >= 0; i--) { for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i const nextIndex = s2 + i
const nextChild = c2[nextIndex] as VNode const nextChild = c2[nextIndex] as VNode
const anchorVNode = c2[nextIndex + 1] as VNode
const anchor = const anchor =
nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor nextIndex + 1 < l2
? // #13559, fallback to el placeholder for unresolved async component
anchorVNode.el || anchorVNode.placeholder
: parentAnchor
if (newIndexToOldIndexMap[i] === 0) { if (newIndexToOldIndexMap[i] === 0) {
// mount new // mount new
patch( patch(
@ -2200,6 +2205,12 @@ function baseCreateRenderer(
} }
} }
const performLeave = () => { const performLeave = () => {
// #13153 move kept-alive node before v-show transition leave finishes
// it needs to call the leaving callback to ensure element's `display`
// is `none`
if (el!._isLeaving) {
el![leaveCbKey](true /* cancelled */)
}
leave(el!, () => { leave(el!, () => {
remove() remove()
afterLeave && afterLeave() afterLeave && afterLeave()
@ -2421,17 +2432,7 @@ function baseCreateRenderer(
unregisterHMR(instance) unregisterHMR(instance)
} }
const { const { bum, scope, effect, subTree, um, m, a } = instance
bum,
scope,
effect,
subTree,
um,
m,
a,
parent,
slots: { __: slotCacheKeys },
} = instance
invalidateMount(m) invalidateMount(m)
invalidateMount(a) invalidateMount(a)
@ -2440,13 +2441,6 @@ function baseCreateRenderer(
invokeArrayFns(bum) invokeArrayFns(bum)
} }
// remove slots content from parent renderCache
if (parent && isArray(slotCacheKeys)) {
slotCacheKeys.forEach(v => {
;(parent as ComponentInternalInstance).renderCache[v] = undefined
})
}
if ( if (
__COMPAT__ && __COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance) isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
@ -2484,24 +2478,6 @@ function baseCreateRenderer(
parentSuspense, parentSuspense,
) )
// A component with async dep inside a pending suspense is unmounted before
// its async dep resolves. This should remove the dep from the suspense, and
// cause the suspense to resolve immediately if that was the last dep.
if (
__FEATURE_SUSPENSE__ &&
parentSuspense &&
parentSuspense.pendingBranch &&
!parentSuspense.isUnmounted &&
instance.asyncDep &&
!instance.asyncResolved &&
instance.suspenseId === parentSuspense.pendingId
) {
parentSuspense.deps--
if (parentSuspense.deps === 0) {
parentSuspense.resolve()
}
}
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsComponentRemoved(instance) devtoolsComponentRemoved(instance)
} }
@ -2708,7 +2684,11 @@ export function traverseStaticChildren(
traverseStaticChildren(c1, c2) traverseStaticChildren(c1, c2)
} }
// #6852 also inherit for text nodes // #6852 also inherit for text nodes
if (c2.type === Text) { if (
c2.type === Text &&
// avoid cached text nodes retaining detached dom nodes
c2.patchFlag !== PatchFlags.CACHED
) {
c2.el = c1.el c2.el = c1.el
} }
// #2324 also inherit for comment nodes, but not placeholders (e.g. v-if which // #2324 also inherit for comment nodes, but not placeholders (e.g. v-if which

View File

@ -1,7 +1,13 @@
import type { SuspenseBoundary } from './components/Suspense' import type { SuspenseBoundary } from './components/Suspense'
import type { VNode, VNodeNormalizedRef, VNodeNormalizedRefAtom } from './vnode' import type {
VNode,
VNodeNormalizedRef,
VNodeNormalizedRefAtom,
VNodeRef,
} from './vnode'
import { import {
EMPTY_OBJ, EMPTY_OBJ,
NO,
ShapeFlags, ShapeFlags,
hasOwn, hasOwn,
isArray, isArray,
@ -14,7 +20,11 @@ import { warn } from './warning'
import { isRef, toRaw } from '@vue/reactivity' import { isRef, toRaw } from '@vue/reactivity'
import { ErrorCodes, callWithErrorHandling } from './errorHandling' import { ErrorCodes, callWithErrorHandling } from './errorHandling'
import { queuePostRenderEffect } from './renderer' import { queuePostRenderEffect } from './renderer'
import { type ComponentOptions, getComponentPublicInstance } from './component' import {
type ComponentOptions,
type Data,
getComponentPublicInstance,
} from './component'
import { knownTemplateRefs } from './helpers/useTemplateRef' import { knownTemplateRefs } from './helpers/useTemplateRef'
/** /**
@ -73,24 +83,10 @@ export function setRef(
const oldRef = oldRawRef && (oldRawRef as VNodeNormalizedRefAtom).r const oldRef = oldRawRef && (oldRawRef as VNodeNormalizedRefAtom).r
const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs
const setupState = owner.setupState const setupState = owner.setupState
const rawSetupState = toRaw(setupState) const canSetSetupRef = createCanSetSetupRefChecker(setupState)
const canSetSetupRef =
setupState === EMPTY_OBJ
? () => false
: (key: string) => {
if (__DEV__) {
if (hasOwn(rawSetupState, key) && !isRef(rawSetupState[key])) {
warn(
`Template ref "${key}" used on a non-ref value. ` +
`It will not work in the production build.`,
)
}
if (knownTemplateRefs.has(rawSetupState[key] as any)) { const canSetRef = (ref: VNodeRef) => {
return false return !__DEV__ || !knownTemplateRefs.has(ref as any)
}
}
return hasOwn(rawSetupState, key)
} }
// dynamic ref changed. unset old ref // dynamic ref changed. unset old ref
@ -101,8 +97,14 @@ export function setRef(
setupState[oldRef] = null setupState[oldRef] = null
} }
} else if (isRef(oldRef)) { } else if (isRef(oldRef)) {
if (canSetRef(oldRef)) {
oldRef.value = null oldRef.value = null
} }
// this type assertion is valid since `oldRef` has already been asserted to be non-null
const oldRawRefAtom = oldRawRef as VNodeNormalizedRefAtom
if (oldRawRefAtom.k) refs[oldRawRefAtom.k] = null
}
} }
if (isFunction(ref)) { if (isFunction(ref)) {
@ -118,7 +120,9 @@ export function setRef(
? canSetSetupRef(ref) ? canSetSetupRef(ref)
? setupState[ref] ? setupState[ref]
: refs[ref] : refs[ref]
: ref.value : canSetRef(ref) || !rawRef.k
? ref.value
: refs[rawRef.k]
if (isUnmount) { if (isUnmount) {
isArray(existing) && remove(existing, refValue) isArray(existing) && remove(existing, refValue)
} else { } else {
@ -129,8 +133,11 @@ export function setRef(
setupState[ref] = refs[ref] setupState[ref] = refs[ref]
} }
} else { } else {
ref.value = [refValue] const newVal = [refValue]
if (rawRef.k) refs[rawRef.k] = ref.value if (canSetRef(ref)) {
ref.value = newVal
}
if (rawRef.k) refs[rawRef.k] = newVal
} }
} else if (!existing.includes(refValue)) { } else if (!existing.includes(refValue)) {
existing.push(refValue) existing.push(refValue)
@ -142,7 +149,9 @@ export function setRef(
setupState[ref] = value setupState[ref] = value
} }
} else if (_isRef) { } else if (_isRef) {
if (canSetRef(ref)) {
ref.value = value ref.value = value
}
if (rawRef.k) refs[rawRef.k] = value if (rawRef.k) refs[rawRef.k] = value
} else if (__DEV__) { } else if (__DEV__) {
warn('Invalid template ref type:', ref, `(${typeof ref})`) warn('Invalid template ref type:', ref, `(${typeof ref})`)
@ -161,3 +170,26 @@ export function setRef(
} }
} }
} }
export function createCanSetSetupRefChecker(
setupState: Data,
): (key: string) => boolean {
const rawSetupState = toRaw(setupState)
return setupState === EMPTY_OBJ
? NO
: (key: string) => {
if (__DEV__) {
if (hasOwn(rawSetupState, key) && !isRef(rawSetupState[key])) {
warn(
`Template ref "${key}" used on a non-ref value. ` +
`It will not work in the production build.`,
)
}
if (knownTemplateRefs.has(rawSetupState[key] as any)) {
return false
}
}
return hasOwn(rawSetupState, key)
}
}

View File

@ -200,6 +200,7 @@ export interface VNode<
// DOM // DOM
el: HostNode | null el: HostNode | null
placeholder: HostNode | null // async component el placeholder
anchor: HostNode | null // fragment anchor anchor: HostNode | null // fragment anchor
target: HostElement | null // teleport target target: HostElement | null // teleport target
targetStart: HostNode | null // teleport target start anchor targetStart: HostNode | null // teleport target start anchor
@ -731,6 +732,8 @@ export function cloneVNode<T, U>(
suspense: vnode.suspense, suspense: vnode.suspense,
ssContent: vnode.ssContent && cloneVNode(vnode.ssContent), ssContent: vnode.ssContent && cloneVNode(vnode.ssContent),
ssFallback: vnode.ssFallback && cloneVNode(vnode.ssFallback), ssFallback: vnode.ssFallback && cloneVNode(vnode.ssFallback),
placeholder: vnode.placeholder,
el: vnode.el, el: vnode.el,
anchor: vnode.anchor, anchor: vnode.anchor,
ctx: vnode.ctx, ctx: vnode.ctx,

View File

@ -1402,6 +1402,34 @@ describe('defineCustomElement', () => {
}) })
describe('expose', () => { describe('expose', () => {
test('expose w/ options api', async () => {
const E = defineCustomElement({
data() {
return {
value: 0,
}
},
methods: {
foo() {
;(this as any).value++
},
},
expose: ['foo'],
render(_ctx: any) {
return h('div', null, _ctx.value)
},
})
customElements.define('my-el-expose-options-api', E)
container.innerHTML = `<my-el-expose-options-api></my-el-expose-options-api>`
const e = container.childNodes[0] as VueElement & {
foo: () => void
}
expect(e.shadowRoot!.innerHTML).toBe(`<div>0</div>`)
e.foo()
await nextTick()
expect(e.shadowRoot!.innerHTML).toBe(`<div>1</div>`)
})
test('expose attributes and callback', async () => { test('expose attributes and callback', async () => {
type SetValue = (value: string) => void type SetValue = (value: string) => void
let fn: MockedFunction<SetValue> let fn: MockedFunction<SetValue>

View File

@ -46,7 +46,7 @@ export const vShow: ObjectDirective<VShowElement> & { name?: 'show' } = {
}, },
} }
if (__DEV__) { if (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) {
vShow.name = 'show' vShow.name = 'show'
} }

View File

@ -319,28 +319,6 @@ describe('api: createVaporApp', () => {
}) })
}) })
describe('config.performance', () => {
afterEach(() => {
window.performance.clearMeasures()
})
test('with performance enabled', () => {
const { app, mount } = define({ setup: () => [] }).create()
app.config.performance = true
mount()
expect(window.performance.getEntries()).lengthOf(2)
})
test('with performance disabled', () => {
const { app, mount } = define({ setup: () => [] }).create()
app.config.performance = false
mount()
expect(window.performance.getEntries()).lengthOf(0)
})
})
test.todo('config.globalProperty', () => { test.todo('config.globalProperty', () => {
const { app } = define({ const { app } = define({
setup() { setup() {

View File

@ -10,6 +10,7 @@ import {
insert, insert,
prepend, prepend,
renderEffect, renderEffect,
setInsertionState,
template, template,
} from '../src' } from '../src'
import { currentInstance, nextTick, ref } from '@vue/runtime-dom' import { currentInstance, nextTick, ref } from '@vue/runtime-dom'
@ -502,5 +503,35 @@ describe('component: slots', () => {
await nextTick() await nextTick()
expect(host.innerHTML).toBe('<div><h1></h1><!--slot--></div>') expect(host.innerHTML).toBe('<div><h1></h1><!--slot--></div>')
}) })
test('consecutive slots with insertion state', async () => {
const { component: Child } = define({
setup() {
const n2 = template('<div><div>baz</div></div>', true)() as any
setInsertionState(n2, 0)
createSlot('default', null)
setInsertionState(n2, 0)
createSlot('foo', null)
return n2
},
})
const { html } = define({
setup() {
return createComponent(Child, null, {
default: () => template('default')(),
foo: () => template('foo')(),
})
},
}).render()
expect(html()).toBe(
`<div>` +
`default<!--slot-->` +
`foo<!--slot-->` +
`<div>baz</div>` +
`</div>`,
)
})
}) })
}) })

View File

@ -19,6 +19,7 @@ import {
nextTick, nextTick,
reactive, reactive,
ref, ref,
shallowRef,
useTemplateRef, useTemplateRef,
watchEffect, watchEffect,
} from '@vue/runtime-dom' } from '@vue/runtime-dom'
@ -208,8 +209,8 @@ describe('api: template ref', () => {
const { render } = define({ const { render } = define({
setup() { setup() {
return { return {
foo: fooEl, foo: shallowRef(fooEl),
bar: barEl, bar: shallowRef(barEl),
} }
}, },
render() { render() {
@ -251,6 +252,7 @@ describe('api: template ref', () => {
}) })
const { host } = render() const { host } = render()
expect(state.refKey).toBe(host.children[0]) expect(state.refKey).toBe(host.children[0])
expect('Template ref "refKey" used on a non-ref value').toHaveBeenWarned()
}) })
test('multiple root refs', () => { test('multiple root refs', () => {
@ -713,6 +715,45 @@ describe('api: template ref', () => {
expect(html()).toBe('<div>changed</div><!--dynamic-component-->') expect(html()).toBe('<div>changed</div><!--dynamic-component-->')
}) })
test('should not attempt to set when variable name is same as key', () => {
let tRef: ShallowRef
const key = 'refKey'
define({
setup() {
tRef = useTemplateRef('_')
return {
[key]: tRef,
}
},
render() {
const n0 = template('<div></div>')() as Element
createTemplateRefSetter()(n0, key)
return n0
},
}).render()
expect('target is readonly').not.toHaveBeenWarned()
expect(tRef!.value).toBe(null)
})
test('should work when used as direct ref value (compiled in prod mode)', () => {
__DEV__ = false
try {
let foo: ShallowRef
const { host } = define({
setup() {
foo = useTemplateRef('foo')
const n0 = template('<div></div>')() as Element
createTemplateRefSetter()(n0, foo)
return n0
},
}).render()
expect('target is readonly').not.toHaveBeenWarned()
expect(foo!.value).toBe(host.children[0])
} finally {
__DEV__ = true
}
})
// TODO: can not reproduce in Vapor // TODO: can not reproduce in Vapor
// // #2078 // // #2078
// test('handling multiple merged refs', async () => { // test('handling multiple merged refs', async () => {

View File

@ -778,7 +778,7 @@ describe('createFor', () => {
) )
}) })
test.todo('prepend', async () => { test('prepend', async () => {
const arr = ref<number[]>([4, 5]) const arr = ref<number[]>([4, 5])
const { host, html } = render(arr) const { host, html } = render(arr)
expect(host.children.length).toBe(2) expect(host.children.length).toBe(2)
@ -940,7 +940,7 @@ describe('createFor', () => {
) )
}) })
test.todo('remove from beginning and insert at end', async () => { test('remove from beginning and insert at end', async () => {
const arr = ref<number[]>([1, 2, 3]) const arr = ref<number[]>([1, 2, 3])
const { host, html } = render(arr) const { host, html } = render(arr)
expect(host.children.length).toBe(3) expect(host.children.length).toBe(3)
@ -1028,7 +1028,7 @@ describe('createFor', () => {
) )
}) })
test.todo('move to left & replace', async () => { test('move to left & replace', async () => {
const arr = ref<number[]>([1, 2, 3, 4, 5]) const arr = ref<number[]>([1, 2, 3, 4, 5])
const { host, html } = render(arr) const { host, html } = render(arr)
expect(host.children.length).toBe(5) expect(host.children.length).toBe(5)
@ -1044,7 +1044,7 @@ describe('createFor', () => {
) )
}) })
test.todo('move to left and leaves hold', async () => { test('move to left and leaves hold', async () => {
const arr = ref<number[]>([1, 4, 5]) const arr = ref<number[]>([1, 4, 5])
const { host, html } = render(arr) const { host, html } = render(arr)
expect(host.children.length).toBe(3) expect(host.children.length).toBe(3)
@ -1058,9 +1058,7 @@ describe('createFor', () => {
expect(html()).toBe(`<span>4</span><span>6</span><!--for-->`) expect(html()).toBe(`<span>4</span><span>6</span><!--for-->`)
}) })
test.todo( test('moved and set to undefined element ending at the end', async () => {
'moved and set to undefined element ending at the end',
async () => {
const arr = ref<number[]>([2, 4, 5]) const arr = ref<number[]>([2, 4, 5])
const { host, html } = render(arr) const { host, html } = render(arr)
expect(host.children.length).toBe(3) expect(host.children.length).toBe(3)
@ -1074,8 +1072,7 @@ describe('createFor', () => {
expect(html()).toBe( expect(html()).toBe(
`<span>4</span><span>5</span><span>3</span><!--for-->`, `<span>4</span><span>5</span><span>3</span><!--for-->`,
) )
}, })
)
test('reverse element', async () => { test('reverse element', async () => {
const arr = ref<number[]>([1, 2, 3, 4, 5, 6, 7, 8]) const arr = ref<number[]>([1, 2, 3, 4, 5, 6, 7, 8])
@ -1323,7 +1320,7 @@ describe('createFor', () => {
}).render() }).render()
} }
test.todo('move a key in non-keyed nodes with a size up', async () => { test('move a key in non-keyed nodes with a size up', async () => {
const arr = ref<any[]>([1, 'a', 'b', 'c']) const arr = ref<any[]>([1, 'a', 'b', 'c'])
const { host, html } = define({ const { host, html } = define({
setup() { setup() {

View File

@ -11,6 +11,7 @@ import {
import { makeInteropRender } from './_utils' import { makeInteropRender } from './_utils'
import { import {
applyTextModel, applyTextModel,
applyVShow,
child, child,
createComponent, createComponent,
defineVaporComponent, defineVaporComponent,
@ -113,6 +114,37 @@ describe('vdomInterop', () => {
}) })
}) })
describe('v-show', () => {
test('apply v-show to vdom child', async () => {
const VDomChild = {
setup() {
return () => h('div')
},
}
const show = ref(false)
const VaporChild = defineVaporComponent({
setup() {
const n1 = createComponent(VDomChild as any)
applyVShow(n1, () => show.value)
return n1
},
})
const { html } = define({
setup() {
return () => h(VaporChild as any)
},
}).render()
expect(html()).toBe('<div style="display: none;"></div>')
show.value = true
await nextTick()
expect(html()).toBe('<div style=""></div>')
})
})
describe('slots', () => { describe('slots', () => {
test('basic', () => { test('basic', () => {
const VDomChild = defineComponent({ const VDomChild = defineComponent({

View File

@ -196,12 +196,15 @@ export const createFor = (
endOffset++ endOffset++
continue continue
} }
if (endOffset !== 0) {
anchorFallback = normalizeAnchor(newBlocks[currentIndex + 1].nodes)
}
break break
} }
if (endOffset !== 0) {
anchorFallback = normalizeAnchor(
newBlocks[newLength - endOffset].nodes,
)
}
while (startOffset < sharedBlockCount - endOffset) { while (startOffset < sharedBlockCount - endOffset) {
const currentItem = getItem(source, startOffset) const currentItem = getItem(source, startOffset)
const currentKey = getKey(...currentItem) const currentKey = getKey(...currentItem)
@ -251,13 +254,9 @@ export const createFor = (
previousKeyIndexPairs.length = previousKeyIndexInsertIndex previousKeyIndexPairs.length = previousKeyIndexInsertIndex
const previousKeyIndexMap = new Map(previousKeyIndexPairs) const previousKeyIndexMap = new Map(previousKeyIndexPairs)
const blocksToMount: [ const operations: (() => void)[] = []
blockIndex: number,
blockItem: ReturnType<typeof getItem>,
blockKey: any,
anchorOffset: number,
][] = []
let mountCounter = 0
const relocateOrMountBlock = ( const relocateOrMountBlock = (
blockIndex: number, blockIndex: number,
blockItem: ReturnType<typeof getItem>, blockItem: ReturnType<typeof getItem>,
@ -269,21 +268,31 @@ export const createFor = (
const reusedBlock = (newBlocks[blockIndex] = const reusedBlock = (newBlocks[blockIndex] =
oldBlocks[previousIndex]) oldBlocks[previousIndex])
update(reusedBlock, ...blockItem) update(reusedBlock, ...blockItem)
previousKeyIndexMap.delete(blockKey)
if (previousIndex !== blockIndex) {
operations.push(() =>
insert( insert(
reusedBlock, reusedBlock,
parent!, parent!,
anchorOffset === -1 anchorOffset === -1
? anchorFallback ? anchorFallback
: normalizeAnchor(newBlocks[anchorOffset].nodes), : normalizeAnchor(newBlocks[anchorOffset].nodes),
),
) )
previousKeyIndexMap.delete(blockKey) }
} else { } else {
blocksToMount.push([ mountCounter++
operations.push(() =>
mount(
source,
blockIndex, blockIndex,
anchorOffset === -1
? anchorFallback
: normalizeAnchor(newBlocks[anchorOffset].nodes),
blockItem, blockItem,
blockKey, blockKey,
anchorOffset, ),
]) )
} }
} }
@ -303,7 +312,7 @@ export const createFor = (
relocateOrMountBlock(i, blockItem, blockKey, -1) relocateOrMountBlock(i, blockItem, blockKey, -1)
} }
const useFastRemove = blocksToMount.length === newLength const useFastRemove = mountCounter === newLength
for (const leftoverIndex of previousKeyIndexMap.values()) { for (const leftoverIndex of previousKeyIndexMap.values()) {
unmount( unmount(
@ -322,21 +331,9 @@ export const createFor = (
} }
} }
for (const [ // perform mount and move operations
blockIndex, for (const action of operations) {
blockItem, action()
blockKey,
anchorOffset,
] of blocksToMount) {
mount(
source,
blockIndex,
anchorOffset === -1
? anchorFallback
: normalizeAnchor(newBlocks[anchorOffset].nodes),
blockItem,
blockKey,
)
} }
} }
} }

View File

@ -9,6 +9,7 @@ import {
ErrorCodes, ErrorCodes,
type SchedulerJob, type SchedulerJob,
callWithErrorHandling, callWithErrorHandling,
createCanSetSetupRefChecker,
queuePostFlushCb, queuePostFlushCb,
warn, warn,
} from '@vue/runtime-dom' } from '@vue/runtime-dom'
@ -55,6 +56,7 @@ export function setRef(
const refs = const refs =
instance.refs === EMPTY_OBJ ? (instance.refs = {}) : instance.refs instance.refs === EMPTY_OBJ ? (instance.refs = {}) : instance.refs
const canSetSetupRef = createCanSetSetupRefChecker(setupState)
// dynamic ref changed. unset old ref // dynamic ref changed. unset old ref
if (oldRef != null && oldRef !== ref) { if (oldRef != null && oldRef !== ref) {
if (isString(oldRef)) { if (isString(oldRef)) {
@ -87,7 +89,7 @@ export function setRef(
const doSet: SchedulerJob = () => { const doSet: SchedulerJob = () => {
if (refFor) { if (refFor) {
existing = _isString existing = _isString
? __DEV__ && hasOwn(setupState, ref) ? __DEV__ && canSetSetupRef(ref)
? setupState[ref] ? setupState[ref]
: refs[ref] : refs[ref]
: ref.value : ref.value
@ -96,7 +98,7 @@ export function setRef(
existing = [refValue] existing = [refValue]
if (_isString) { if (_isString) {
refs[ref] = existing refs[ref] = existing
if (__DEV__ && hasOwn(setupState, ref)) { if (__DEV__ && canSetSetupRef(ref)) {
setupState[ref] = refs[ref] setupState[ref] = refs[ref]
// if setupState[ref] is a reactivity ref, // if setupState[ref] is a reactivity ref,
// the existing will also become reactivity too // the existing will also become reactivity too
@ -111,7 +113,7 @@ export function setRef(
} }
} else if (_isString) { } else if (_isString) {
refs[ref] = refValue refs[ref] = refValue
if (__DEV__ && hasOwn(setupState, ref)) { if (__DEV__ && canSetSetupRef(ref)) {
setupState[ref] = refValue setupState[ref] = refValue
} }
} else if (_isRef) { } else if (_isRef) {
@ -129,7 +131,7 @@ export function setRef(
remove(existing, refValue) remove(existing, refValue)
} else if (_isString) { } else if (_isString) {
refs[ref] = null refs[ref] = null
if (__DEV__ && hasOwn(setupState, ref)) { if (__DEV__ && canSetSetupRef(ref)) {
setupState[ref] = null setupState[ref] = null
} }
} else if (_isRef) { } else if (_isRef) {

View File

@ -105,10 +105,10 @@ export function isValidBlock(block: Block): boolean {
export function insert( export function insert(
block: Block, block: Block,
parent: ParentNode, parent: ParentNode & { $anchor?: Node | null },
anchor: Node | null | 0 = null, // 0 means prepend anchor: Node | null | 0 = null, // 0 means prepend
): void { ): void {
anchor = anchor === 0 ? parent.firstChild : anchor anchor = anchor === 0 ? parent.$anchor || parent.firstChild : anchor
if (block instanceof Node) { if (block instanceof Node) {
if (!isHydrating) { if (!isHydrating) {
parent.insertBefore(block, anchor) parent.insertBefore(block, anchor)

View File

@ -6,7 +6,7 @@ import {
} from '@vue/runtime-dom' } from '@vue/runtime-dom'
import { renderEffect } from '../renderEffect' import { renderEffect } from '../renderEffect'
import { isVaporComponent } from '../component' import { isVaporComponent } from '../component'
import { type Block, DynamicFragment } from '../block' import { type Block, DynamicFragment, VaporFragment } from '../block'
import { isArray } from '@vue/shared' import { isArray } from '@vue/shared'
export function applyVShow(target: Block, source: () => any): void { export function applyVShow(target: Block, source: () => any): void {
@ -24,6 +24,12 @@ export function applyVShow(target: Block, source: () => any): void {
update.call(target, render, key) update.call(target, render, key)
setDisplay(target, source()) setDisplay(target, source())
} }
} else if (target instanceof VaporFragment && target.insert) {
const insert = target.insert
target.insert = (parent, anchor) => {
insert.call(target, parent, anchor)
setDisplay(target, source())
}
} }
renderEffect(() => setDisplay(target, source())) renderEffect(() => setDisplay(target, source()))
@ -33,12 +39,16 @@ function setDisplay(target: Block, value: unknown): void {
if (isVaporComponent(target)) { if (isVaporComponent(target)) {
return setDisplay(target, value) return setDisplay(target, value)
} }
if (isArray(target) && target.length === 1) { if (isArray(target)) {
return setDisplay(target[0], value) if (target.length === 0) return
if (target.length === 1) return setDisplay(target[0], value)
} }
if (target instanceof DynamicFragment) { if (target instanceof DynamicFragment) {
return setDisplay(target.nodes, value) return setDisplay(target.nodes, value)
} }
if (target instanceof VaporFragment && target.insert) {
return setDisplay(target.nodes, value)
}
if (target instanceof Element) { if (target instanceof Element) {
const el = target as VShowElement const el = target as VShowElement
if (!(vShowOriginalDisplay in el)) { if (!(vShowOriginalDisplay in el)) {

View File

@ -296,7 +296,7 @@ export function optimizePropertyLookup(): void {
if (isOptimized) return if (isOptimized) return
isOptimized = true isOptimized = true
const proto = Element.prototype as any const proto = Element.prototype as any
proto.$evtclick = undefined proto.$anchor = proto.$evtclick = undefined
proto.$root = false proto.$root = false
proto.$html = proto.$html =
proto.$txt = proto.$txt =

View File

@ -6,7 +6,18 @@ export let insertionAnchor: Node | 0 | undefined
* (component, slot outlet, if, for) is created. The state is used for actual * (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. * insertion on client-side render, and used for node adoption during hydration.
*/ */
export function setInsertionState(parent: ParentNode, anchor?: Node | 0): void { export function setInsertionState(
parent: ParentNode & { $anchor?: Node | null },
anchor?: Node | 0,
): void {
// When setInsertionState(n3, 0) is called consecutively, the first prepend operation
// uses parent.firstChild as the anchor. However, after insertion, parent.firstChild
// changes and cannot serve as the anchor for subsequent prepends. Therefore, we cache
// the original parent.firstChild on the first call for subsequent prepend operations.
if (anchor === 0 && !parent.$anchor) {
parent.$anchor = parent.firstChild
}
insertionParent = parent insertionParent = parent
insertionAnchor = anchor insertionAnchor = anchor
} }

View File

@ -216,6 +216,8 @@ function createVDOMComponent(
parentInstance as any, parentInstance as any,
) )
} }
frag.nodes = vnode.el as Block
} }
frag.remove = unmount frag.remove = unmount

View File

@ -111,26 +111,106 @@ describe('ssr: slot', () => {
}) })
test('transition slot', async () => { test('transition slot', async () => {
const ReusableTransition = {
template: `<transition><slot/></transition>`,
}
const ReusableTransitionWithAppear = {
template: `<transition appear><slot/></transition>`,
}
expect( expect(
await renderToString( await renderToString(
createApp({ createApp({
components: { components: {
one: { one: ReusableTransition,
template: `<transition><slot/></transition>`,
},
}, },
template: `<one><div v-if="false">foo</div></one>`, template: `<one><div v-if="false">foo</div></one>`,
}), }),
), ),
).toBe(`<!---->`) ).toBe(`<!---->`)
expect(await renderToString(createApp(ReusableTransition))).toBe(`<!---->`)
expect(await renderToString(createApp(ReusableTransitionWithAppear))).toBe(
`<template><!----></template>`,
)
expect(
await renderToString(
createApp({
components: {
one: ReusableTransition,
},
template: `<one><slot/></one>`,
}),
),
).toBe(`<!---->`)
expect( expect(
await renderToString( await renderToString(
createApp({ createApp({
components: { components: {
one: { one: ReusableTransitionWithAppear,
template: `<transition><slot/></transition>`,
}, },
template: `<one><slot/></one>`,
}),
),
).toBe(`<template><!----></template>`)
expect(
await renderToString(
createApp({
render() {
return h(ReusableTransition, null, {
default: () => null,
})
},
}),
),
).toBe(`<!---->`)
expect(
await renderToString(
createApp({
render() {
return h(ReusableTransitionWithAppear, null, {
default: () => null,
})
},
}),
),
).toBe(`<template><!----></template>`)
expect(
await renderToString(
createApp({
render() {
return h(ReusableTransitionWithAppear, null, {
default: () => [],
})
},
}),
),
).toBe(`<template><!----></template>`)
expect(
await renderToString(
createApp({
render() {
return h(ReusableTransition, null, {
default: () => [],
})
},
}),
),
).toBe(`<!---->`)
expect(
await renderToString(
createApp({
components: {
one: ReusableTransition,
}, },
template: `<one><div v-if="true">foo</div></one>`, template: `<one><div v-if="true">foo</div></one>`,
}), }),

View File

@ -74,6 +74,8 @@ export function ssrRenderSlotInner(
) )
} else if (fallbackRenderFn) { } else if (fallbackRenderFn) {
fallbackRenderFn() fallbackRenderFn()
} else if (transition) {
push(`<!---->`)
} }
} else { } else {
// ssr slot. // ssr slot.
@ -110,13 +112,19 @@ export function ssrRenderSlotInner(
end-- end--
} }
if (start < end) {
for (let i = start; i < end; i++) { for (let i = start; i < end; i++) {
push(slotBuffer[i]) push(slotBuffer[i])
} }
} else if (transition) {
push(`<!---->`)
}
} }
} }
} else if (fallbackRenderFn) { } else if (fallbackRenderFn) {
fallbackRenderFn() fallbackRenderFn()
} else if (transition) {
push(`<!---->`)
} }
} }

View File

@ -1724,6 +1724,107 @@ describe('e2e: Transition', () => {
}, },
E2E_TIMEOUT, E2E_TIMEOUT,
) )
// #13153
test(
'move kept-alive node before v-show transition leave finishes',
async () => {
await page().evaluate(() => {
const { createApp, ref } = (window as any).Vue
const show = ref(true)
createApp({
template: `
<div id="container">
<KeepAlive :include="['Comp1', 'Comp2']">
<component :is="state === 1 ? 'Comp1' : 'Comp2'"/>
</KeepAlive>
</div>
<button id="toggleBtn" @click="click">button</button>
`,
setup: () => {
const state = ref(1)
const click = () => (state.value = state.value === 1 ? 2 : 1)
return { state, click }
},
components: {
Comp1: {
components: {
Item: {
name: 'Item',
setup() {
return { show }
},
template: `
<Transition name="test">
<div v-show="show" >
<h2>{{ show ? "I should show" : "I shouldn't show " }}</h2>
</div>
</Transition>
`,
},
},
name: 'Comp1',
setup() {
const toggle = () => (show.value = !show.value)
return { show, toggle }
},
template: `
<Item />
<h2>This is page1</h2>
<button id="changeShowBtn" @click="toggle">{{ show }}</button>
`,
},
Comp2: {
name: 'Comp2',
template: `<h2>This is page2</h2>`,
},
},
}).mount('#app')
})
expect(await html('#container')).toBe(
`<div><h2>I should show</h2></div>` +
`<h2>This is page1</h2>` +
`<button id="changeShowBtn">true</button>`,
)
// trigger v-show transition leave
await click('#changeShowBtn')
await nextTick()
expect(await html('#container')).toBe(
`<div class="test-leave-from test-leave-active"><h2>I shouldn't show </h2></div>` +
`<h2>This is page1</h2>` +
`<button id="changeShowBtn">false</button>`,
)
// switch to page2, before leave finishes
// expect v-show element's display to be none
await click('#toggleBtn')
await nextTick()
expect(await html('#container')).toBe(
`<div class="test-leave-from test-leave-active" style="display: none;"><h2>I shouldn't show </h2></div>` +
`<h2>This is page2</h2>`,
)
// switch back to page1
// expect v-show element's display to be none
await click('#toggleBtn')
await nextTick()
expect(await html('#container')).toBe(
`<div class="test-enter-from test-enter-active" style="display: none;"><h2>I shouldn't show </h2></div>` +
`<h2>This is page1</h2>` +
`<button id="changeShowBtn">false</button>`,
)
await transitionFinish()
expect(await html('#container')).toBe(
`<div class="" style="display: none;"><h2>I shouldn't show </h2></div>` +
`<h2>This is page1</h2>` +
`<button id="changeShowBtn">false</button>`,
)
},
E2E_TIMEOUT,
)
}) })
describe('transition with Suspense', () => { describe('transition with Suspense', () => {

View File

@ -0,0 +1,227 @@
import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
import path from 'node:path'
const { page, html, click } = setupPuppeteer()
beforeEach(async () => {
await page().setContent(`<div id="app"></div>`)
await page().addScriptTag({
path: path.resolve(__dirname, '../../dist/vue.global.js'),
})
})
describe('not leaking', async () => {
// #13661
test(
'cached text vnodes should not retaining detached DOM nodes',
async () => {
const client = await page().createCDPSession()
await page().evaluate(async () => {
const { createApp, ref } = (window as any).Vue
createApp({
components: {
Comp1: {
template: `
<h1><slot></slot></h1>
<div>{{ test.length }}</div>
`,
setup() {
const test = ref([...Array(3000)].map((_, i) => ({ i })))
// @ts-expect-error
window.__REF__ = new WeakRef(test)
return { test }
},
},
Comp2: {
template: `<h2>comp2</h2>`,
},
},
template: `
<button id="toggleBtn" @click="click">button</button>
<Comp1 v-if="toggle">
<div>
<Comp2/>
text node
</div>
</Comp1>
`,
setup() {
const toggle = ref(true)
const click = () => (toggle.value = !toggle.value)
return { toggle, click }
},
}).mount('#app')
})
expect(await html('#app')).toBe(
`<button id="toggleBtn">button</button>` +
`<h1>` +
`<div>` +
`<h2>comp2</h2>` +
` text node ` +
`</div>` +
`</h1>` +
`<div>3000</div>`,
)
await click('#toggleBtn')
expect(await html('#app')).toBe(
`<button id="toggleBtn">button</button><!--v-if-->`,
)
const isCollected = async () =>
// @ts-expect-error
await page().evaluate(() => window.__REF__.deref() === undefined)
while ((await isCollected()) === false) {
await client.send('HeapProfiler.collectGarbage')
}
expect(await isCollected()).toBe(true)
},
E2E_TIMEOUT,
)
// #13211
test(
'cached array vnodes should not retaining detached DOM nodes',
async () => {
const client = await page().createCDPSession()
await page().evaluate(async () => {
const { createApp, ref } = (window as any).Vue
createApp({
components: {
Comp1: {
template: `
<h1><slot></slot></h1>
<div>{{ test.length }}</div>
`,
setup() {
const test = ref([...Array(3000)].map((_, i) => ({ i })))
// @ts-expect-error
window.__REF__ = new WeakRef(test)
return { test }
},
},
},
template: `
<button id="toggleBtn" @click="click">button</button>
<Comp1 v-if="toggle">slot content</Comp1>
`,
setup() {
const toggle = ref(true)
const click = () => (toggle.value = !toggle.value)
return { toggle, click }
},
}).mount('#app')
})
expect(await html('#app')).toBe(
`<button id="toggleBtn">button</button>` +
`<h1>` +
`slot content` +
`</h1>` +
`<div>3000</div>`,
)
await click('#toggleBtn')
expect(await html('#app')).toBe(
`<button id="toggleBtn">button</button><!--v-if-->`,
)
const isCollected = async () =>
// @ts-expect-error
await page().evaluate(() => window.__REF__.deref() === undefined)
while ((await isCollected()) === false) {
await client.send('HeapProfiler.collectGarbage')
}
expect(await isCollected()).toBe(true)
},
E2E_TIMEOUT,
)
// https://github.com/element-plus/element-plus/issues/21408
test(
'cached text nodes in Fragment should not retaining detached DOM nodes',
async () => {
const client = await page().createCDPSession()
await page().evaluate(async () => {
const { createApp, ref } = (window as any).Vue
createApp({
components: {
Comp: {
template: `<div>{{ test.length }}</div>`,
setup() {
const test = ref([...Array(3000)].map((_, i) => ({ i })))
// @ts-expect-error
window.__REF__ = new WeakRef(test)
return { test }
},
},
},
template: `
<button id="addBtn" @click="add">add</button>
<button id="toggleBtn" @click="click">button</button>
<div v-if="toggle">
<template v-for="item in items" :key="item">
text
<div>{{ item }}</div>
</template>
<Comp/>
</div>
`,
setup() {
const toggle = ref(true)
const items = ref([1])
const click = () => (toggle.value = !toggle.value)
const add = () => items.value.push(2)
return { toggle, click, items, add }
},
}).mount('#app')
})
expect(await html('#app')).toBe(
`<button id="addBtn">add</button>` +
`<button id="toggleBtn">button</button>` +
`<div>` +
` text ` +
`<div>1</div>` +
`<div>3000</div></div>`,
)
await click('#addBtn')
expect(await html('#app')).toBe(
`<button id="addBtn">add</button>` +
`<button id="toggleBtn">button</button>` +
`<div>` +
` text ` +
`<div>1</div>` +
` text ` +
`<div>2</div>` +
`<div>3000</div></div>`,
)
await click('#toggleBtn')
expect(await html('#app')).toBe(
`<button id="addBtn">add</button>` +
`<button id="toggleBtn">button</button><!--v-if-->`,
)
const isCollected = async () =>
// @ts-expect-error
await page().evaluate(() => window.__REF__.deref() === undefined)
while ((await isCollected()) === false) {
await client.send('HeapProfiler.collectGarbage')
}
expect(await isCollected()).toBe(true)
},
E2E_TIMEOUT,
)
})

File diff suppressed because it is too large Load Diff

View File

@ -3,11 +3,11 @@ packages:
- 'packages-private/*' - 'packages-private/*'
catalog: catalog:
'@babel/parser': ^7.27.5 '@babel/parser': ^7.28.3
'@babel/types': ^7.27.6 '@babel/types': ^7.28.2
'estree-walker': ^2.0.2 'estree-walker': ^2.0.2
'vite': ^6.1.0 'vite': ^6.1.0
'@vitejs/plugin-vue': ^5.2.4 '@vitejs/plugin-vue': ^6.0.1
'magic-string': ^0.30.17 'magic-string': ^0.30.17
'source-map-js': ^1.2.1 'source-map-js': ^1.2.1