mirror of https://github.com/vuejs/core.git
Merge branch 'minor' into edison/feat/svgAndMathML
This commit is contained in:
commit
d6e69c552b
|
@ -11,7 +11,7 @@ jobs:
|
|||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
|
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
environment: Release
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: minor
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
environment: Release
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
|
|
|
@ -21,7 +21,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
|
|
@ -21,7 +21,7 @@ jobs:
|
|||
environment: Release
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
@ -36,12 +36,13 @@ jobs:
|
|||
- name: Install deps
|
||||
run: pnpm install
|
||||
|
||||
- name: Update npm
|
||||
run: npm i -g npm@latest
|
||||
|
||||
- name: Build and publish
|
||||
id: publish
|
||||
run: |
|
||||
pnpm release --publishOnly
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Create GitHub release
|
||||
id: release_tag
|
||||
|
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
|
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
|
@ -37,7 +37,7 @@ jobs:
|
|||
run: pnpm install
|
||||
|
||||
- name: Download Size Data
|
||||
uses: dawidd6/action-download-artifact@v9
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
name: size-data
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
|
@ -56,7 +56,7 @@ jobs:
|
|||
path: temp/size/base.txt
|
||||
|
||||
- name: Download Previous Size Data
|
||||
uses: dawidd6/action-download-artifact@v9
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
branch: ${{ steps.pr-base.outputs.content }}
|
||||
workflow: size-data.yml
|
||||
|
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
|
@ -32,7 +32,7 @@ jobs:
|
|||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
|
@ -54,7 +54,7 @@ jobs:
|
|||
e2e-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup cache for Chromium binary
|
||||
uses: actions/cache@v4
|
||||
|
@ -111,7 +111,7 @@ jobs:
|
|||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
18
package.json
18
package.json
|
@ -69,18 +69,18 @@
|
|||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-replace": "5.0.4",
|
||||
"@swc/core": "^1.12.9",
|
||||
"@swc/core": "^1.13.3",
|
||||
"@types/hash-sum": "^1.0.2",
|
||||
"@types/node": "^22.16.0",
|
||||
"@types/node": "^22.17.2",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/serve-handler": "^6.1.4",
|
||||
"@vitest/ui": "^3.0.2",
|
||||
"@vitest/coverage-v8": "^3.1.4",
|
||||
"@vitest/eslint-plugin": "^1.2.1",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/eslint-plugin": "^1.3.4",
|
||||
"@vue/consolidate": "1.0.0",
|
||||
"conventional-changelog-cli": "^5.0.0",
|
||||
"enquirer": "^2.4.1",
|
||||
"esbuild": "^0.25.5",
|
||||
"esbuild": "^0.25.9",
|
||||
"esbuild-plugin-polyfill-node": "^0.3.0",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-plugin-import-x": "^4.13.1",
|
||||
|
@ -96,10 +96,10 @@
|
|||
"prettier": "^3.5.3",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"pug": "^3.0.3",
|
||||
"puppeteer": "~24.9.0",
|
||||
"puppeteer": "~24.16.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.44.1",
|
||||
"rollup-plugin-dts": "^6.2.1",
|
||||
"rollup": "^4.46.4",
|
||||
"rollup-plugin-dts": "^6.2.3",
|
||||
"rollup-plugin-esbuild": "^6.2.1",
|
||||
"rollup-plugin-polyfill-node": "^0.13.0",
|
||||
"semver": "^7.7.2",
|
||||
|
@ -111,6 +111,6 @@
|
|||
"typescript": "~5.6.2",
|
||||
"typescript-eslint": "^8.32.1",
|
||||
"vite": "catalog:",
|
||||
"vitest": "^3.1.4"
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
"vite": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/repl": "^4.6.1",
|
||||
"@vue/repl": "^4.6.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"jszip": "^3.10.1",
|
||||
"vue": "workspace:*"
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
StoreState,
|
||||
} from '@vue/repl'
|
||||
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>>()
|
||||
|
||||
|
@ -130,6 +130,34 @@ onMounted(() => {
|
|||
// @ts-expect-error process shim for old versions of @vue/compiler-sfc dependency
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
@ -160,20 +188,7 @@ onMounted(() => {
|
|||
:showOpenSourceMap="true"
|
||||
:autoResize="true"
|
||||
:clearConsole="false"
|
||||
:preview-options="{
|
||||
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()
|
||||
}`,
|
||||
},
|
||||
}"
|
||||
:preview-options="previewOptions"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
"vue": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"vite": "^6.3.5"
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"vite": "^7.1.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,9 +7,9 @@ return function render(_ctx, _cache) {
|
|||
with (_ctx) {
|
||||
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 */)
|
||||
])))
|
||||
]))]))
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
@ -21,7 +21,7 @@ return function render(_ctx, _cache) {
|
|||
with (_ctx) {
|
||||
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("span"),
|
||||
_createElementVNode("span")
|
||||
|
@ -30,7 +30,7 @@ return function render(_ctx, _cache) {
|
|||
_createElementVNode("span"),
|
||||
_createElementVNode("span")
|
||||
], -1 /* CACHED */)
|
||||
])))
|
||||
]))]))
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
@ -42,11 +42,11 @@ return function render(_ctx, _cache) {
|
|||
with (_ctx) {
|
||||
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, [
|
||||
_createCommentVNode("comment")
|
||||
], -1 /* CACHED */)
|
||||
])))
|
||||
]))]))
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
@ -58,11 +58,11 @@ return function render(_ctx, _cache) {
|
|||
with (_ctx) {
|
||||
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 */),
|
||||
_createTextVNode("foo"),
|
||||
_createTextVNode("foo", -1 /* CACHED */),
|
||||
_createElementVNode("div", null, null, -1 /* CACHED */)
|
||||
])))
|
||||
]))]))
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
@ -74,9 +74,9 @@ return function render(_ctx, _cache) {
|
|||
with (_ctx) {
|
||||
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 */)
|
||||
])))
|
||||
]))]))
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
@ -147,9 +147,9 @@ return function render(_ctx, _cache) {
|
|||
with (_ctx) {
|
||||
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 */)
|
||||
])))
|
||||
]))]))
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
@ -161,9 +161,9 @@ return function render(_ctx, _cache) {
|
|||
with (_ctx) {
|
||||
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 */)
|
||||
])))
|
||||
]))]))
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
@ -215,9 +215,9 @@ return function render(_ctx, _cache) {
|
|||
const _directive_foo = _resolveDirective("foo")
|
||||
|
||||
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 */)
|
||||
]))), [
|
||||
]))])), [
|
||||
[_directive_foo]
|
||||
])
|
||||
]))
|
||||
|
@ -401,9 +401,9 @@ return function render(_ctx, _cache) {
|
|||
|
||||
return (_openBlock(), _createElementBlock("div", null, [
|
||||
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 */)
|
||||
])))
|
||||
]))]))
|
||||
: _createCommentVNode("v-if", true)
|
||||
]))
|
||||
}
|
||||
|
@ -422,7 +422,7 @@ return function render(_ctx, _cache) {
|
|||
|
||||
return (_openBlock(), _createElementBlock(_Fragment, null, [
|
||||
_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: "c" }, [
|
||||
_createElementVNode("div", { id: "d" }, [
|
||||
|
@ -430,7 +430,7 @@ return function render(_ctx, _cache) {
|
|||
])
|
||||
])
|
||||
], -1 /* CACHED */)
|
||||
]))
|
||||
]))])
|
||||
], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
|
||||
}
|
||||
}"
|
||||
|
@ -448,9 +448,9 @@ return function render(_ctx, _cache) {
|
|||
|
||||
return (_openBlock(), _createElementBlock("div", null, [
|
||||
(_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 */)
|
||||
])))
|
||||
]))]))
|
||||
}), 256 /* UNKEYED_FRAGMENT */))
|
||||
]))
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ import { PatchFlags } from '@vue/shared'
|
|||
|
||||
const cachedChildrenArrayMatcher = (
|
||||
tags: string[],
|
||||
needArraySpread = false,
|
||||
needArraySpread = true,
|
||||
) => ({
|
||||
type: NodeTypes.JS_CACHE_EXPRESSION,
|
||||
needArraySpread,
|
||||
|
@ -170,11 +170,6 @@ describe('compiler: cacheStatic transform', () => {
|
|||
{
|
||||
/* _ slot flag */
|
||||
},
|
||||
{
|
||||
type: NodeTypes.JS_PROPERTY,
|
||||
key: { content: '__' },
|
||||
value: { content: '[0]' },
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
@ -202,11 +197,6 @@ describe('compiler: cacheStatic transform', () => {
|
|||
{
|
||||
/* _ slot flag */
|
||||
},
|
||||
{
|
||||
type: NodeTypes.JS_PROPERTY,
|
||||
key: { content: '__' },
|
||||
value: { content: '[0]' },
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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', () => {
|
||||
const onError = vi.fn()
|
||||
// dynamic
|
||||
|
|
|
@ -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 {
|
||||
advancePositionWithClone,
|
||||
|
@ -115,3 +120,18 @@ test('toValidAssetId', () => {
|
|||
'_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,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -123,7 +123,7 @@ export function isReferencedIdentifier(
|
|||
return false
|
||||
}
|
||||
|
||||
if (isReferenced(id, parent)) {
|
||||
if (isReferenced(id, parent, parentStack[parentStack.length - 2])) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -133,7 +133,8 @@ export function isReferencedIdentifier(
|
|||
case 'AssignmentExpression':
|
||||
case 'AssignmentPattern':
|
||||
return true
|
||||
case 'ObjectPattern':
|
||||
case 'ObjectProperty':
|
||||
return parent.key !== id && isInDestructureAssignment(parent, parentStack)
|
||||
case 'ArrayPattern':
|
||||
return isInDestructureAssignment(parent, parentStack)
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ import {
|
|||
isCoreComponent,
|
||||
isSimpleIdentifier,
|
||||
isStaticArgOf,
|
||||
isVPre,
|
||||
} from './utils'
|
||||
import { decodeHTML } from 'entities/lib/decode.js'
|
||||
import {
|
||||
|
@ -246,7 +247,7 @@ const tokenizer = new Tokenizer(stack, {
|
|||
ondirarg(start, end) {
|
||||
if (start === end) return
|
||||
const arg = getSlice(start, end)
|
||||
if (inVPre) {
|
||||
if (inVPre && !isVPre(currentProp!)) {
|
||||
;(currentProp as AttributeNode).name += arg
|
||||
setLocEnd((currentProp as AttributeNode).nameLoc, end)
|
||||
} else {
|
||||
|
@ -262,7 +263,7 @@ const tokenizer = new Tokenizer(stack, {
|
|||
|
||||
ondirmodifier(start, end) {
|
||||
const mod = getSlice(start, end)
|
||||
if (inVPre) {
|
||||
if (inVPre && !isVPre(currentProp!)) {
|
||||
;(currentProp as AttributeNode).name += '.' + mod
|
||||
setLocEnd((currentProp as AttributeNode).nameLoc, end)
|
||||
} else if ((currentProp as DirectiveNode).name === 'slot') {
|
||||
|
|
|
@ -12,19 +12,22 @@ import {
|
|||
type RootNode,
|
||||
type SimpleExpressionNode,
|
||||
type SlotFunctionExpression,
|
||||
type SlotsObjectProperty,
|
||||
type TemplateChildNode,
|
||||
type TemplateNode,
|
||||
type TextCallNode,
|
||||
type VNodeCall,
|
||||
createArrayExpression,
|
||||
createObjectProperty,
|
||||
createSimpleExpression,
|
||||
getVNodeBlockHelper,
|
||||
getVNodeHelper,
|
||||
} from '../ast'
|
||||
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 {
|
||||
GUARD_REACTIVE_PROPS,
|
||||
|
@ -109,6 +112,15 @@ function walk(
|
|||
? ConstantTypes.NOT_CONSTANT
|
||||
: getConstantType(child, context)
|
||||
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)
|
||||
continue
|
||||
}
|
||||
|
@ -142,7 +154,6 @@ function walk(
|
|||
}
|
||||
|
||||
let cachedAsArray = false
|
||||
const slotCacheKeys = []
|
||||
if (toCache.length === children.length && node.type === NodeTypes.ELEMENT) {
|
||||
if (
|
||||
node.tagType === ElementTypes.ELEMENT &&
|
||||
|
@ -166,7 +177,6 @@ function walk(
|
|||
// default slot
|
||||
const slot = getSlotNode(node.codegenNode, 'default')
|
||||
if (slot) {
|
||||
slotCacheKeys.push(context.cached.length)
|
||||
slot.returns = getCacheExpression(
|
||||
createArrayExpression(slot.returns as TemplateChildNode[]),
|
||||
)
|
||||
|
@ -190,7 +200,6 @@ function walk(
|
|||
slotName.arg &&
|
||||
getSlotNode(parent.codegenNode, slotName.arg)
|
||||
if (slot) {
|
||||
slotCacheKeys.push(context.cached.length)
|
||||
slot.returns = getCacheExpression(
|
||||
createArrayExpression(slot.returns as TemplateChildNode[]),
|
||||
)
|
||||
|
@ -201,39 +210,22 @@ function walk(
|
|||
|
||||
if (!cachedAsArray) {
|
||||
for (const child of toCache) {
|
||||
slotCacheKeys.push(context.cached.length)
|
||||
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 {
|
||||
const exp = context.cache(value)
|
||||
// #6978, #7138, #7114
|
||||
// a cached children array inside v-for can caused HMR errors since
|
||||
// it might be mutated when mounting the first item
|
||||
if (inFor && context.hmr) {
|
||||
exp.needArraySpread = true
|
||||
}
|
||||
// #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
|
||||
return exp
|
||||
}
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => {
|
|||
arg.children.unshift(`(`)
|
||||
arg.children.push(`) || ""`)
|
||||
} else if (!arg.isStatic) {
|
||||
arg.content = `${arg.content} || ""`
|
||||
arg.content = arg.content ? `${arg.content} || ""` : `""`
|
||||
}
|
||||
|
||||
// .sync is replaced by v-model:arg
|
||||
|
|
|
@ -141,9 +141,9 @@ export function processIf(
|
|||
}
|
||||
|
||||
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 (
|
||||
dir.name === 'else-if' &&
|
||||
(dir.name === 'else-if' || dir.name === 'else') &&
|
||||
sibling.branches[sibling.branches.length - 1].condition === undefined
|
||||
) {
|
||||
context.onError(
|
||||
|
|
|
@ -16,7 +16,7 @@ const seen = new WeakSet()
|
|||
export const transformMemo: NodeTransform = (node, context) => {
|
||||
if (node.type === NodeTypes.ELEMENT) {
|
||||
const dir = findDir(node, 'memo')
|
||||
if (!dir || seen.has(node)) {
|
||||
if (!dir || seen.has(node) || context.inSSR) {
|
||||
return
|
||||
}
|
||||
seen.add(node)
|
||||
|
|
|
@ -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 =>
|
||||
!nonIdentifierRE.test(name)
|
||||
|
||||
|
@ -344,6 +344,10 @@ export function isText(
|
|||
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 {
|
||||
return p.type === NodeTypes.DIRECTIVE && p.name === 'slot'
|
||||
}
|
||||
|
|
|
@ -4,11 +4,11 @@ exports[`stringify static html > eligible content (elements > 20) + non-eligible
|
|||
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
|
||||
|
||||
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),
|
||||
_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)
|
||||
])))
|
||||
]))]))
|
||||
}"
|
||||
`;
|
||||
|
||||
|
@ -16,9 +16,9 @@ exports[`stringify static html > escape 1`] = `
|
|||
"const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
|
||||
|
||||
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>ar\\">1 + <</span><span>&</span><span class=\\"foo>ar\\">1 + <</span><span>&</span><span class=\\"foo>ar\\">1 + <</span><span>&</span><span class=\\"foo>ar\\">1 + <</span><span>&</span><span class=\\"foo>ar\\">1 + <</span><span>&</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
|
||||
|
||||
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)
|
||||
])))
|
||||
]))]))
|
||||
}"
|
||||
`;
|
||||
|
||||
|
@ -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
|
||||
|
||||
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)
|
||||
])))
|
||||
]))]))
|
||||
}"
|
||||
`;
|
||||
|
||||
|
@ -46,7 +46,7 @@ exports[`stringify static html > should bail for <option> elements with null val
|
|||
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
|
||||
|
||||
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("option", { value: null }),
|
||||
_createElementVNode("option", { value: "1" }),
|
||||
|
@ -55,7 +55,7 @@ return function render(_ctx, _cache) {
|
|||
_createElementVNode("option", { value: "1" }),
|
||||
_createElementVNode("option", { value: "1" })
|
||||
], -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
|
||||
|
||||
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("option", { value: 1 }),
|
||||
_createElementVNode("option", { value: 1 }),
|
||||
|
@ -71,7 +71,7 @@ return function render(_ctx, _cache) {
|
|||
_createElementVNode("option", { value: 1 }),
|
||||
_createElementVNode("option", { value: 1 })
|
||||
], -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
|
||||
|
||||
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("span", { class: "foo" }, "foo"),
|
||||
_createElementVNode("span", { class: "foo" }, "foo"),
|
||||
|
@ -104,7 +104,7 @@ return function render(_ctx, _cache) {
|
|||
_createElementVNode("span", { class: "foo" }, "foo"),
|
||||
_createElementVNode("img", { src: _imports_0_ })
|
||||
], -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
|
||||
|
||||
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)
|
||||
])))
|
||||
]))]))
|
||||
}"
|
||||
`;
|
||||
|
||||
|
@ -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
|
||||
|
||||
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)
|
||||
])))
|
||||
]))]))
|
||||
}"
|
||||
`;
|
||||
|
||||
|
@ -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
|
||||
|
||||
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)
|
||||
])))
|
||||
]))]))
|
||||
}"
|
||||
`;
|
||||
|
||||
|
@ -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
|
||||
|
||||
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)
|
||||
])))
|
||||
]))]))
|
||||
}"
|
||||
`;
|
||||
|
||||
|
@ -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
|
||||
|
||||
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)
|
||||
])))
|
||||
]))]))
|
||||
}"
|
||||
`;
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ if (__TEST__) {
|
|||
if (DOMErrorCodes.X_V_HTML_NO_EXPRESSION < ErrorCodes.__EXTEND_POINT__) {
|
||||
throw new Error(
|
||||
`DOMErrorCodes need to be updated to ${
|
||||
ErrorCodes.__EXTEND_POINT__ + 1
|
||||
ErrorCodes.__EXTEND_POINT__
|
||||
} to match extension point from core ErrorCodes.`,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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`] = `
|
||||
"import { unref as _unref, createElementVNode as _createElementVNode, withDirectives as _withDirectives, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
|
||||
|
||||
|
|
|
@ -81,9 +81,9 @@ import _imports_1 from '/bar.png'
|
|||
|
||||
|
||||
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)
|
||||
])))
|
||||
]))]))
|
||||
}"
|
||||
`;
|
||||
|
||||
|
|
|
@ -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`] = `
|
||||
"import { createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
|
||||
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'
|
||||
|
||||
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)
|
||||
])))
|
||||
]))]))
|
||||
}"
|
||||
`;
|
||||
|
|
|
@ -717,6 +717,151 @@ describe('SFC compile <script setup>', () => {
|
|||
consumer.originalPositionFor(getPositionInCode(content, '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', () => {
|
||||
|
@ -913,6 +1058,13 @@ describe('SFC compile <script setup>', () => {
|
|||
expect(() =>
|
||||
compile(`<script>foo()</script><script setup lang="ts">bar()</script>`),
|
||||
).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`
|
||||
|
|
|
@ -538,7 +538,7 @@ describe('resolveType', () => {
|
|||
|
||||
expect(props).toStrictEqual({
|
||||
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(
|
||||
resolve(`
|
||||
type Brand<T> = T & {};
|
||||
|
@ -758,7 +758,18 @@ describe('resolveType', () => {
|
|||
}>()
|
||||
`).props,
|
||||
).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'],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -72,6 +72,14 @@ describe('compiler sfc: transform srcset', () => {
|
|||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('transform empty srcset w/ includeAbsolute: true', () => {
|
||||
expect(
|
||||
compileWithSrcset(`<img srcset=" " />`, {
|
||||
includeAbsolute: true,
|
||||
}).code,
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('transform srcset w/ stringify', () => {
|
||||
const code = compileWithSrcset(
|
||||
`<div>${src}</div>`,
|
||||
|
|
|
@ -63,6 +63,6 @@
|
|||
"postcss-modules": "^6.0.1",
|
||||
"postcss-selector-parser": "^7.1.0",
|
||||
"pug": "^3.0.3",
|
||||
"sass": "^1.89.2"
|
||||
"sass": "^1.90.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ import { DEFINE_SLOTS, processDefineSlots } from './script/defineSlots'
|
|||
import { DEFINE_MODEL, processDefineModel } from './script/defineModel'
|
||||
import { getImportedName, isCallOf, isLiteralNode } from './script/utils'
|
||||
import { analyzeScriptBindings } from './script/analyzeScriptBindings'
|
||||
import { isImportUsed } from './script/importUsageCheck'
|
||||
import { isUsedInTemplate } from './script/importUsageCheck'
|
||||
import { processAwait } from './script/topLevelAwait'
|
||||
|
||||
export interface SFCScriptCompileOptions {
|
||||
|
@ -173,7 +173,6 @@ export function compileScript(
|
|||
)
|
||||
}
|
||||
|
||||
const ctx = new ScriptCompileContext(sfc, options)
|
||||
const { script, scriptSetup, source, filename } = sfc
|
||||
const hoistStatic = options.hoistStatic !== false && !script
|
||||
const scopeId = options.id ? options.id.replace(/^data-v-/, '') : ''
|
||||
|
@ -181,6 +180,16 @@ export function compileScript(
|
|||
const scriptSetupLang = scriptSetup && scriptSetup.lang
|
||||
const vapor = sfc.vapor || options.vapor
|
||||
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 (!script) {
|
||||
|
@ -190,13 +199,6 @@ export function compileScript(
|
|||
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) {
|
||||
// do not process non js/ts script blocks
|
||||
return scriptSetup
|
||||
|
@ -246,7 +248,7 @@ export function compileScript(
|
|||
) {
|
||||
// template usage check is only needed in non-inline mode, so we can skip
|
||||
// the work if inlineTemplate is true.
|
||||
let isUsedInTemplate = needTemplateUsageCheck
|
||||
let isImportUsed = needTemplateUsageCheck
|
||||
if (
|
||||
needTemplateUsageCheck &&
|
||||
ctx.isTS &&
|
||||
|
@ -254,7 +256,7 @@ export function compileScript(
|
|||
!sfc.template.src &&
|
||||
!sfc.template.lang
|
||||
) {
|
||||
isUsedInTemplate = isImportUsed(local, sfc)
|
||||
isImportUsed = isUsedInTemplate(local, sfc)
|
||||
}
|
||||
|
||||
ctx.userImports[local] = {
|
||||
|
@ -263,7 +265,7 @@ export function compileScript(
|
|||
local,
|
||||
source,
|
||||
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 scriptSetupAst = ctx.scriptSetupAst!
|
||||
const inlineMode = options.inlineTemplate
|
||||
|
||||
// 1.1 walk import declarations of <script>
|
||||
if (scriptAst) {
|
||||
|
@ -302,7 +338,7 @@ export function compileScript(
|
|||
(specifier.type === 'ImportSpecifier' &&
|
||||
specifier.importKind === 'type'),
|
||||
false,
|
||||
!options.inlineTemplate,
|
||||
!inlineMode,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -370,7 +406,7 @@ export function compileScript(
|
|||
(specifier.type === 'ImportSpecifier' &&
|
||||
specifier.importKind === 'type'),
|
||||
true,
|
||||
!options.inlineTemplate,
|
||||
!inlineMode,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -811,12 +847,16 @@ export function compileScript(
|
|||
}
|
||||
|
||||
const destructureElements =
|
||||
ctx.hasDefineExposeCall || !options.inlineTemplate
|
||||
? [`expose: __expose`]
|
||||
: []
|
||||
ctx.hasDefineExposeCall || !inlineMode ? [`expose: __expose`] : []
|
||||
if (ctx.emitDecl) {
|
||||
destructureElements.push(`emit: __emit`)
|
||||
}
|
||||
|
||||
// destructure built-in properties (e.g. $emit, $attrs, $slots)
|
||||
if (inlineMode) {
|
||||
buildDestructureElements()
|
||||
}
|
||||
|
||||
if (destructureElements.length) {
|
||||
args += `, { ${destructureElements.join(', ')} }`
|
||||
}
|
||||
|
@ -824,10 +864,7 @@ export function compileScript(
|
|||
let templateMap
|
||||
// 9. generate return statement
|
||||
let returned
|
||||
if (
|
||||
!options.inlineTemplate ||
|
||||
(!sfc.template && ctx.hasDefaultExportRender)
|
||||
) {
|
||||
if (!inlineMode || (!sfc.template && ctx.hasDefaultExportRender)) {
|
||||
// non-inline mode, or has manual render in normal <script>
|
||||
// return bindings from script and script setup
|
||||
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
|
||||
// componentPublicInstance proxy to allow properties that start with $ or _
|
||||
ctx.s.appendRight(
|
||||
|
@ -976,8 +1013,12 @@ export function compileScript(
|
|||
|
||||
// <script setup> components are closed by default. If the user did not
|
||||
// explicitly call `defineExpose`, call expose() with no args.
|
||||
const exposeCall =
|
||||
ctx.hasDefineExposeCall || options.inlineTemplate ? `` : ` __expose();\n`
|
||||
if (!ctx.hasDefineExposeCall && !inlineMode)
|
||||
setupPreambleLines.push(`__expose();`)
|
||||
|
||||
const setupPreamble = setupPreambleLines.length
|
||||
? ` ${setupPreambleLines.join('\n ')}\n`
|
||||
: ''
|
||||
// wrap setup code with function.
|
||||
if (ctx.isTS) {
|
||||
// for TS, make sure the exported type is still valid type with
|
||||
|
@ -994,7 +1035,7 @@ export function compileScript(
|
|||
vapor && !ssr ? `defineVaporComponent` : `defineComponent`,
|
||||
)}({${def}${runtimeOptions}\n ${
|
||||
hasAwait ? `async ` : ``
|
||||
}setup(${args}) {\n${exposeCall}`,
|
||||
}setup(${args}) {\n${setupPreamble}`,
|
||||
)
|
||||
ctx.s.appendRight(endOffset, `})`)
|
||||
} else {
|
||||
|
@ -1010,14 +1051,14 @@ export function compileScript(
|
|||
`\n${genDefaultAs} /*@__PURE__*/Object.assign(${
|
||||
defaultExport ? `${normalScriptDefaultVar}, ` : ''
|
||||
}${definedOptions ? `${definedOptions}, ` : ''}{${runtimeOptions}\n ` +
|
||||
`${hasAwait ? `async ` : ``}setup(${args}) {\n${exposeCall}`,
|
||||
`${hasAwait ? `async ` : ``}setup(${args}) {\n${setupPreamble}`,
|
||||
)
|
||||
ctx.s.appendRight(endOffset, `})`)
|
||||
} else {
|
||||
ctx.s.prependLeft(
|
||||
startOffset,
|
||||
`\n${genDefaultAs} {${runtimeOptions}\n ` +
|
||||
`${hasAwait ? `async ` : ``}setup(${args}) {\n${exposeCall}`,
|
||||
`${hasAwait ? `async ` : ``}setup(${args}) {\n${setupPreamble}`,
|
||||
)
|
||||
ctx.s.appendRight(endOffset, `}`)
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import type { TemplateCompiler } from './compileTemplate'
|
|||
import { parseCssVars } from './style/cssVars'
|
||||
import { createCache } from './cache'
|
||||
import type { ImportBinding } from './compileScript'
|
||||
import { isImportUsed } from './script/importUsageCheck'
|
||||
import { isUsedInTemplate } from './script/importUsageCheck'
|
||||
import type { LRUCache } from 'lru-cache'
|
||||
import { genCacheKey } from '@vue/shared'
|
||||
|
||||
|
@ -449,7 +449,7 @@ export function hmrShouldReload(
|
|||
for (const key in prevImports) {
|
||||
// if an import was previous unused, but now is used, we need to force
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,12 +11,16 @@ import { createCache } from '../cache'
|
|||
import { camelize, capitalize, isBuiltInDirective } from '@vue/shared'
|
||||
|
||||
/**
|
||||
* Check if an import is used in the SFC's template. This is used to determine
|
||||
* the properties that should be included in the object returned from setup()
|
||||
* when not using inline mode.
|
||||
* Check if an identifier is used in the SFC's template.
|
||||
* - 1.used to determine the properties that should be included in the object returned from setup()
|
||||
* 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 {
|
||||
return resolveTemplateUsedIdentifiers(sfc).has(local)
|
||||
export function isUsedInTemplate(
|
||||
identifier: string,
|
||||
sfc: SFCDescriptor,
|
||||
): boolean {
|
||||
return resolveTemplateUsedIdentifiers(sfc).has(identifier)
|
||||
}
|
||||
|
||||
const templateUsageCheckCache = createCache<Set<string>>()
|
||||
|
|
|
@ -1500,6 +1500,7 @@ export function inferRuntimeType(
|
|||
node: Node & MaybeWithScope,
|
||||
scope: TypeScope = node._ownerScope || ctxToScope(ctx),
|
||||
isKeyOf = false,
|
||||
typeParameters?: Record<string, Node>,
|
||||
): string[] {
|
||||
try {
|
||||
switch (node.type) {
|
||||
|
@ -1588,19 +1589,43 @@ export function inferRuntimeType(
|
|||
case 'TSTypeReference': {
|
||||
const resolved = resolveTypeReference(ctx, node, scope)
|
||||
if (resolved) {
|
||||
// #13240
|
||||
// Special case for function type aliases to ensure correct runtime behavior
|
||||
// other type aliases still fallback to unknown as before
|
||||
if (
|
||||
resolved.type === 'TSTypeAliasDeclaration' &&
|
||||
resolved.typeAnnotation.type === 'TSFunctionType'
|
||||
) {
|
||||
return ['Function']
|
||||
if (resolved.type === 'TSTypeAliasDeclaration') {
|
||||
// #13240
|
||||
// Special case for function type aliases to ensure correct runtime behavior
|
||||
// other type aliases still fallback to unknown as before
|
||||
if (resolved.typeAnnotation.type === 'TSFunctionType') {
|
||||
return ['Function']
|
||||
}
|
||||
|
||||
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 (typeParameters && typeParameters[node.typeName.name]) {
|
||||
return inferRuntimeType(
|
||||
ctx,
|
||||
typeParameters[node.typeName.name],
|
||||
scope,
|
||||
isKeyOf,
|
||||
typeParameters,
|
||||
)
|
||||
}
|
||||
if (isKeyOf) {
|
||||
switch (node.typeName.name) {
|
||||
case 'String':
|
||||
|
@ -1733,11 +1758,15 @@ export function inferRuntimeType(
|
|||
return inferRuntimeType(ctx, node.typeAnnotation, scope)
|
||||
|
||||
case 'TSUnionType':
|
||||
return flattenTypes(ctx, node.types, scope, isKeyOf)
|
||||
return flattenTypes(ctx, node.types, scope, isKeyOf, typeParameters)
|
||||
case 'TSIntersectionType': {
|
||||
return flattenTypes(ctx, node.types, scope, isKeyOf).filter(
|
||||
t => t !== UNKNOWN_TYPE,
|
||||
)
|
||||
return flattenTypes(
|
||||
ctx,
|
||||
node.types,
|
||||
scope,
|
||||
isKeyOf,
|
||||
typeParameters,
|
||||
).filter(t => t !== UNKNOWN_TYPE)
|
||||
}
|
||||
|
||||
case 'TSEnumDeclaration':
|
||||
|
@ -1808,14 +1837,17 @@ function flattenTypes(
|
|||
types: TSType[],
|
||||
scope: TypeScope,
|
||||
isKeyOf: boolean = false,
|
||||
typeParameters: Record<string, Node> | undefined = undefined,
|
||||
): string[] {
|
||||
if (types.length === 1) {
|
||||
return inferRuntimeType(ctx, types[0], scope, isKeyOf)
|
||||
return inferRuntimeType(ctx, types[0], scope, isKeyOf, typeParameters)
|
||||
}
|
||||
return [
|
||||
...new Set(
|
||||
([] as string[]).concat(
|
||||
...types.map(t => inferRuntimeType(ctx, t, scope, isKeyOf)),
|
||||
...types.map(t =>
|
||||
inferRuntimeType(ctx, t, scope, isKeyOf, typeParameters),
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -10,6 +10,7 @@ import { warn } from '../warn'
|
|||
|
||||
const animationNameRE = /^(-\w+-)?animation-name$/
|
||||
const animationRE = /^(-\w+-)?animation$/
|
||||
const keyframesRE = /^(?:-\w+-)?keyframes$/
|
||||
|
||||
const scopedPlugin: PluginCreator<string> = (id = '') => {
|
||||
const keyframes = Object.create(null)
|
||||
|
@ -21,10 +22,7 @@ const scopedPlugin: PluginCreator<string> = (id = '') => {
|
|||
processRule(id, rule)
|
||||
},
|
||||
AtRule(node) {
|
||||
if (
|
||||
/-?keyframes$/.test(node.name) &&
|
||||
!node.params.endsWith(`-${shortId}`)
|
||||
) {
|
||||
if (keyframesRE.test(node.name) && !node.params.endsWith(`-${shortId}`)) {
|
||||
// register keyframes
|
||||
keyframes[node.params] = node.params = node.params + '-' + shortId
|
||||
}
|
||||
|
@ -72,7 +70,7 @@ function processRule(id: string, rule: Rule) {
|
|||
processedRules.has(rule) ||
|
||||
(rule.parent &&
|
||||
rule.parent.type === 'atrule' &&
|
||||
/-?keyframes$/.test((rule.parent as AtRule).name))
|
||||
keyframesRE.test((rule.parent as AtRule).name))
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -71,6 +71,7 @@ export const transformSrcset: NodeTransform = (
|
|||
|
||||
const shouldProcessUrl = (url: string) => {
|
||||
return (
|
||||
url &&
|
||||
!isExternalUrl(url) &&
|
||||
!isDataUrl(url) &&
|
||||
(options.includeAbsolute || isRelativeUrl(url))
|
||||
|
|
|
@ -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', () => {
|
||||
test('transition', () => {
|
||||
expect(compile(`<transition><div/></transition>`).code)
|
||||
|
|
|
@ -29,7 +29,7 @@ if (__TEST__) {
|
|||
if (SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME < DOMErrorCodes.__EXTEND_POINT__) {
|
||||
throw new Error(
|
||||
`SSRErrorCodes need to be updated to ${
|
||||
DOMErrorCodes.__EXTEND_POINT__ + 1
|
||||
DOMErrorCodes.__EXTEND_POINT__
|
||||
} to match extension point from core DOMErrorCodes.`,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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`] = `
|
||||
"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)
|
||||
|
|
|
@ -247,6 +247,7 @@ describe('compile', () => {
|
|||
_setText(x0, _toDisplayString(_ctx.bar))`,
|
||||
)
|
||||
})
|
||||
|
||||
test('with v-once', () => {
|
||||
const code = compile(
|
||||
`<div>
|
||||
|
@ -261,5 +262,10 @@ describe('compile', () => {
|
|||
_setText(n2, " " + _toDisplayString(_ctx.baz))`,
|
||||
)
|
||||
})
|
||||
|
||||
test('with insertionState', () => {
|
||||
const code = compile(`<div><div><slot /></div><Comp/></div>`)
|
||||
expect(code).matchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -57,10 +57,10 @@ const t0 = _template("<div><div>x</div><div><span> </span></div><div><span> </sp
|
|||
export function render(_ctx) {
|
||||
const n3 = t0()
|
||||
const p0 = _next(_child(n3))
|
||||
const p1 = _next(p0)
|
||||
const p2 = _next(p1)
|
||||
const n0 = _child(p0)
|
||||
const p1 = _next(p0)
|
||||
const n1 = _child(p1)
|
||||
const p2 = _next(p1)
|
||||
const n2 = _child(p2)
|
||||
const x0 = _child(n0)
|
||||
const x1 = _child(n1)
|
||||
|
|
|
@ -353,8 +353,8 @@ const t2 = _template("<form></form>")
|
|||
|
||||
export function render(_ctx) {
|
||||
const n1 = t1()
|
||||
const n3 = t2()
|
||||
const n0 = t0()
|
||||
const n3 = t2()
|
||||
const n2 = t2()
|
||||
_insert(n0, n1)
|
||||
_insert(n2, n3)
|
||||
|
|
|
@ -65,7 +65,9 @@ export function genBlockContent(
|
|||
push(...genSelf(child, context))
|
||||
}
|
||||
for (const child of dynamic.children) {
|
||||
push(...genChildren(child, context, push, `n${child.id!}`))
|
||||
if (!child.hasDynamicChild) {
|
||||
push(...genChildren(child, context, push, `n${child.id!}`))
|
||||
}
|
||||
}
|
||||
|
||||
push(...genOperations(operation, context))
|
||||
|
|
|
@ -27,7 +27,7 @@ export function genSelf(
|
|||
context: CodegenContext,
|
||||
): CodeFragment[] {
|
||||
const [frag, push] = buildCodeFragment()
|
||||
const { id, template, operation } = dynamic
|
||||
const { id, template, operation, hasDynamicChild } = dynamic
|
||||
|
||||
if (id !== undefined && template !== undefined) {
|
||||
push(NEWLINE, `const n${id} = t${template}()`)
|
||||
|
@ -38,6 +38,10 @@ export function genSelf(
|
|||
push(...genOperationWithInsertionState(operation, context))
|
||||
}
|
||||
|
||||
if (hasDynamicChild) {
|
||||
push(...genChildren(dynamic, context, push, `n${id}`))
|
||||
}
|
||||
|
||||
return frag
|
||||
}
|
||||
|
||||
|
@ -53,7 +57,6 @@ export function genChildren(
|
|||
|
||||
let offset = 0
|
||||
let prev: [variable: string, elementIndex: number] | undefined
|
||||
const childrenToGen: [IRDynamicInfo, string][] = []
|
||||
|
||||
for (const [index, child] of children.entries()) {
|
||||
if (child.flags & DynamicFlag.NON_TEMPLATE) {
|
||||
|
@ -99,7 +102,7 @@ export function genChildren(
|
|||
}
|
||||
}
|
||||
|
||||
if (id === child.anchor) {
|
||||
if (id === child.anchor && !child.hasDynamicChild) {
|
||||
push(...genSelf(child, context))
|
||||
}
|
||||
|
||||
|
@ -108,13 +111,7 @@ export function genChildren(
|
|||
}
|
||||
|
||||
prev = [variable, elementIndex]
|
||||
childrenToGen.push([child, variable])
|
||||
}
|
||||
|
||||
if (childrenToGen.length) {
|
||||
for (const [child, from] of childrenToGen) {
|
||||
push(...genChildren(child, context, pushBlock, from))
|
||||
}
|
||||
push(...genChildren(child, context, pushBlock, variable))
|
||||
}
|
||||
|
||||
return frag
|
||||
|
|
|
@ -498,9 +498,10 @@ describe('reactivity/readonly', () => {
|
|||
const r = ref(false)
|
||||
const ror = readonly(r)
|
||||
const obj = reactive({ ror })
|
||||
expect(() => {
|
||||
obj.ror = true
|
||||
}).toThrow()
|
||||
obj.ror = true
|
||||
expect(
|
||||
`Set operation on key "ror" failed: target is readonly.`,
|
||||
).toHaveBeenWarned()
|
||||
expect(obj.ror).toBe(false)
|
||||
})
|
||||
|
||||
|
|
|
@ -158,7 +158,13 @@ class MutableReactiveHandler extends BaseReactiveHandler {
|
|||
}
|
||||
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
|
||||
if (isOldValueReadonly) {
|
||||
return false
|
||||
if (__DEV__) {
|
||||
warn(
|
||||
`Set operation on key "${String(key)}" failed: target is readonly.`,
|
||||
target[key],
|
||||
)
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
oldValue.value = value
|
||||
return true
|
||||
|
|
|
@ -167,13 +167,26 @@ describe('component: proxy', () => {
|
|||
data() {
|
||||
return {
|
||||
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() {
|
||||
return {
|
||||
bar: 1,
|
||||
}
|
||||
},
|
||||
__cssModules: {
|
||||
$style: {},
|
||||
cssStyles: {},
|
||||
},
|
||||
mounted() {
|
||||
instanceProxy = this
|
||||
},
|
||||
|
@ -181,6 +194,7 @@ describe('component: proxy', () => {
|
|||
|
||||
const app = createApp(Comp, { msg: 'hello' })
|
||||
app.config.globalProperties.global = 1
|
||||
app.config.globalProperties.$global = 1
|
||||
|
||||
app.mount(nodeOps.createElement('div'))
|
||||
|
||||
|
@ -188,12 +202,20 @@ describe('component: proxy', () => {
|
|||
expect('msg' in instanceProxy).toBe(true)
|
||||
// data
|
||||
expect('foo' in instanceProxy).toBe(true)
|
||||
// ctx
|
||||
expect('$foo' in instanceProxy).toBe(false)
|
||||
// setupState
|
||||
expect('bar' in instanceProxy).toBe(true)
|
||||
// ctx
|
||||
expect('cmp' in instanceProxy).toBe(true)
|
||||
expect('$cmp' in instanceProxy).toBe(true)
|
||||
// public properties
|
||||
expect('$el' in instanceProxy).toBe(true)
|
||||
// CSS modules
|
||||
expect('$style' in instanceProxy).toBe(true)
|
||||
expect('cssStyles' in instanceProxy).toBe(true)
|
||||
// global properties
|
||||
expect('global' in instanceProxy).toBe(true)
|
||||
expect('$global' in instanceProxy).toBe(true)
|
||||
|
||||
// non-existent
|
||||
expect('$foobar' in instanceProxy).toBe(false)
|
||||
|
@ -202,11 +224,15 @@ describe('component: proxy', () => {
|
|||
// #4962 triggering getter should not cause non-existent property to
|
||||
// pass the has check
|
||||
instanceProxy.baz
|
||||
instanceProxy.$baz
|
||||
expect('baz' in instanceProxy).toBe(false)
|
||||
expect('$baz' in instanceProxy).toBe(false)
|
||||
|
||||
// set non-existent (goes into proxyTarget sink)
|
||||
instanceProxy.baz = 1
|
||||
expect('baz' in instanceProxy).toBe(true)
|
||||
instanceProxy.$baz = 1
|
||||
expect('$baz' in instanceProxy).toBe(true)
|
||||
|
||||
// dev mode ownKeys check for console inspection
|
||||
// should only expose own keys
|
||||
|
@ -214,7 +240,10 @@ describe('component: proxy', () => {
|
|||
'msg',
|
||||
'bar',
|
||||
'foo',
|
||||
'cmp',
|
||||
'$cmp',
|
||||
'baz',
|
||||
'$baz',
|
||||
])
|
||||
})
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
nodeOps,
|
||||
ref,
|
||||
render,
|
||||
serializeInner,
|
||||
useSlots,
|
||||
} from '@vue/runtime-test'
|
||||
import { createBlock, normalizeVNode } from '../src/vnode'
|
||||
|
@ -55,14 +56,10 @@ describe('component: slots', () => {
|
|||
expect(Object.getOwnPropertyDescriptor(slots, '_')!.enumerable).toBe(
|
||||
false,
|
||||
)
|
||||
expect(slots).toHaveProperty('__')
|
||||
expect(Object.getOwnPropertyDescriptor(slots, '__')!.enumerable).toBe(
|
||||
false,
|
||||
)
|
||||
return h('div')
|
||||
},
|
||||
}
|
||||
const slots = { foo: () => {}, _: 1, __: [1] }
|
||||
const slots = { foo: () => {}, _: 1 }
|
||||
render(createBlock(Comp, null, slots), nodeOps.createElement('div'))
|
||||
})
|
||||
|
||||
|
@ -74,6 +71,10 @@ describe('component: slots', () => {
|
|||
footer: ['f1', 'f2'],
|
||||
})
|
||||
|
||||
expect(
|
||||
'[Vue warn]: Non-function value encountered for slot "_inner". Prefer function slots for better performance.',
|
||||
).toHaveBeenWarned()
|
||||
|
||||
expect(
|
||||
'[Vue warn]: Non-function value encountered for slot "header". Prefer function slots for better performance.',
|
||||
).toHaveBeenWarned()
|
||||
|
@ -82,8 +83,8 @@ describe('component: slots', () => {
|
|||
'[Vue warn]: Non-function value encountered for slot "footer". Prefer function slots for better performance.',
|
||||
).toHaveBeenWarned()
|
||||
|
||||
expect(slots).not.toHaveProperty('_inner')
|
||||
expect(slots).not.toHaveProperty('foo')
|
||||
expect(slots._inner()).toMatchObject([normalizeVNode('_inner')])
|
||||
expect(slots.header()).toMatchObject([normalizeVNode('header')])
|
||||
expect(slots.footer()).toMatchObject([
|
||||
normalizeVNode('f1'),
|
||||
|
@ -442,4 +443,22 @@ describe('component: slots', () => {
|
|||
'Slot "default" invoked outside of the render function',
|
||||
).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')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -17,6 +17,8 @@ import {
|
|||
onUnmounted,
|
||||
ref,
|
||||
render,
|
||||
renderList,
|
||||
renderSlot,
|
||||
resolveDynamicComponent,
|
||||
serializeInner,
|
||||
shallowRef,
|
||||
|
@ -2161,6 +2163,80 @@ describe('Suspense', () => {
|
|||
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', () => {
|
||||
// base function to check if a combination of slots warns or not
|
||||
function baseCheckWarn(
|
||||
|
@ -2230,5 +2306,57 @@ describe('Suspense', () => {
|
|||
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>`,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -106,6 +106,134 @@ describe('useTemplateRef', () => {
|
|||
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)', () => {
|
||||
__DEV__ = false
|
||||
try {
|
||||
|
@ -125,4 +253,65 @@ describe('useTemplateRef', () => {
|
|||
__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
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -894,4 +894,47 @@ describe('hot module replacement', () => {
|
|||
await timeout()
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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 () => {
|
||||
const toggle = ref(true)
|
||||
|
||||
|
@ -1677,6 +1740,35 @@ describe('SSR hydration', () => {
|
|||
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', () => {
|
||||
const show = false
|
||||
const { vnode, container } = mountWithHydration(
|
||||
|
|
|
@ -553,18 +553,6 @@ describe('vnode', () => {
|
|||
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
|
||||
// <component :is="foo">{{ bar }}</component>
|
||||
// - content is compiled as slot
|
||||
|
|
|
@ -124,28 +124,30 @@ export function defineAsyncComponent<
|
|||
|
||||
__asyncHydrate(el, instance, hydrate) {
|
||||
let patched = false
|
||||
;(instance.bu || (instance.bu = [])).push(() => (patched = true))
|
||||
const performHydrate = () => {
|
||||
// skip hydration if the component has been patched
|
||||
if (patched) {
|
||||
if (__DEV__) {
|
||||
warn(
|
||||
`Skipping lazy hydration for component '${getComponentName(resolvedComp!) || resolvedComp!.__file}': ` +
|
||||
`it was updated before lazy hydration performed.`,
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
hydrate()
|
||||
}
|
||||
const doHydrate = hydrateStrategy
|
||||
? () => {
|
||||
const performHydrate = () => {
|
||||
// skip hydration if the component has been patched
|
||||
if (__DEV__ && patched) {
|
||||
warn(
|
||||
`Skipping lazy hydration for component '${getComponentName(resolvedComp!)}': ` +
|
||||
`it was updated before lazy hydration performed.`,
|
||||
)
|
||||
return
|
||||
}
|
||||
hydrate()
|
||||
}
|
||||
const teardown = hydrateStrategy(performHydrate, cb =>
|
||||
forEachElement(el, cb),
|
||||
)
|
||||
if (teardown) {
|
||||
;(instance.bum || (instance.bum = [])).push(teardown)
|
||||
}
|
||||
;(instance.u || (instance.u = [])).push(() => (patched = true))
|
||||
}
|
||||
: hydrate
|
||||
: performHydrate
|
||||
if (resolvedComp) {
|
||||
doHydrate()
|
||||
} else {
|
||||
|
|
|
@ -383,17 +383,17 @@ export function withDefaults<
|
|||
|
||||
// TODO return type for Vapor components
|
||||
export function useSlots(): SetupContext['slots'] {
|
||||
return getContext().slots
|
||||
return getContext('useSlots').slots
|
||||
}
|
||||
|
||||
export function useAttrs(): SetupContext['attrs'] {
|
||||
return getContext().attrs
|
||||
return getContext('useAttrs').attrs
|
||||
}
|
||||
|
||||
function getContext(): SetupContext {
|
||||
function getContext(calledFunctionName: string): SetupContext {
|
||||
const i = getCurrentGenericInstance()!
|
||||
if (__DEV__ && !i) {
|
||||
warn(`useContext() called without active instance.`)
|
||||
warn(`${calledFunctionName}() called without active instance.`)
|
||||
}
|
||||
if (i.vapor) {
|
||||
return i as any // vapor instance act as its own setup context
|
||||
|
|
|
@ -756,6 +756,7 @@ export function applyOptions(instance: ComponentInternalInstance): void {
|
|||
Object.defineProperty(exposed, key, {
|
||||
get: () => publicThis[key],
|
||||
set: val => (publicThis[key] = val),
|
||||
enumerable: true,
|
||||
})
|
||||
})
|
||||
} else if (!instance.exposed) {
|
||||
|
|
|
@ -577,19 +577,20 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
|
|||
|
||||
has(
|
||||
{
|
||||
_: { data, setupState, accessCache, ctx, appContext, propsOptions },
|
||||
_: { data, setupState, accessCache, ctx, appContext, propsOptions, type },
|
||||
}: ComponentRenderContext,
|
||||
key: string,
|
||||
) {
|
||||
let normalizedProps
|
||||
return (
|
||||
!!accessCache![key] ||
|
||||
(data !== EMPTY_OBJ && hasOwn(data, key)) ||
|
||||
let normalizedProps, cssModules
|
||||
return !!(
|
||||
accessCache![key] ||
|
||||
(data !== EMPTY_OBJ && key[0] !== '$' && hasOwn(data, key)) ||
|
||||
hasSetupBinding(setupState, key) ||
|
||||
((normalizedProps = propsOptions[0]) && hasOwn(normalizedProps, key)) ||
|
||||
hasOwn(ctx, key) ||
|
||||
hasOwn(publicPropertiesMap, key) ||
|
||||
hasOwn(appContext.config.globalProperties, key)
|
||||
hasOwn(appContext.config.globalProperties, key) ||
|
||||
((cssModules = type.__cssModules) && cssModules[key])
|
||||
)
|
||||
},
|
||||
|
||||
|
|
|
@ -79,14 +79,10 @@ export type RawSlots = {
|
|||
* @internal
|
||||
*/
|
||||
_?: 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[] =>
|
||||
isArray(value)
|
||||
|
@ -194,10 +190,6 @@ export const initSlots = (
|
|||
): void => {
|
||||
const slots = (instance.slots = createInternalObject())
|
||||
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)._
|
||||
if (type) {
|
||||
assignSlots(slots, children as Slots, optimized)
|
||||
|
|
|
@ -24,7 +24,7 @@ import { SchedulerJobFlags } from '../scheduler'
|
|||
|
||||
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')
|
||||
|
||||
export interface BaseTransitionProps<HostElement = RendererElement> {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* eslint-disable no-restricted-globals */
|
||||
import { EffectFlags } from '@vue/reactivity'
|
||||
import {
|
||||
type ClassComponent,
|
||||
type ComponentInternalInstance,
|
||||
|
@ -99,8 +100,11 @@ function rerender(id: string, newRender?: Function): void {
|
|||
instance.hmrRerender!()
|
||||
} else {
|
||||
const i = instance as ComponentInternalInstance
|
||||
i.renderCache = []
|
||||
i.effect.run()
|
||||
// #13771 don't update if the job is already disposed
|
||||
if (!(i.effect.flags! & EffectFlags.STOP)) {
|
||||
i.renderCache = []
|
||||
i.effect.run()
|
||||
}
|
||||
}
|
||||
nextTick(() => {
|
||||
isHmrUpdating = false
|
||||
|
|
|
@ -562,3 +562,7 @@ export { initFeatureFlags } from './featureFlags'
|
|||
* @internal
|
||||
*/
|
||||
export { createInternalObject } from './internalObject'
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export { createCanSetSetupRefChecker } from './rendererTemplateRef'
|
||||
|
|
|
@ -40,12 +40,10 @@ export function endMeasure(
|
|||
if (instance.appContext.config.performance && isSupported()) {
|
||||
const startTag = `vue-${type}-${instance.uid}`
|
||||
const endTag = startTag + `:end`
|
||||
const measureName = `<${formatComponentName(instance, instance.type)}> ${type}`
|
||||
perf.mark(endTag)
|
||||
perf.measure(
|
||||
`<${formatComponentName(instance, instance.type)}> ${type}`,
|
||||
startTag,
|
||||
endTag,
|
||||
)
|
||||
perf.measure(measureName, startTag, endTag)
|
||||
perf.clearMeasures(measureName)
|
||||
perf.clearMarks(startTag)
|
||||
perf.clearMarks(endTag)
|
||||
}
|
||||
|
|
|
@ -96,8 +96,8 @@ import { initFeatureFlags } from './featureFlags'
|
|||
import { isAsyncWrapper } from './apiAsyncComponent'
|
||||
import { isCompatEnabled } from './compat/compatConfig'
|
||||
import { DeprecationTypes } from './compat/compatConfig'
|
||||
import type { TransitionHooks } from './components/BaseTransition'
|
||||
import type { VaporInteropInterface } from './apiCreateApp'
|
||||
import { type TransitionHooks, leaveCbKey } from './components/BaseTransition'
|
||||
import type { VueElement } from '@vue/runtime-dom'
|
||||
|
||||
export interface Renderer<HostElement = RendererElement> {
|
||||
|
@ -1278,6 +1278,7 @@ function baseCreateRenderer(
|
|||
if (!initialVNode.el) {
|
||||
const placeholder = (instance.subTree = createVNode(Comment))
|
||||
processCommentNode(null, placeholder, container!, anchor)
|
||||
initialVNode.placeholder = placeholder.el
|
||||
}
|
||||
} else {
|
||||
setupRenderEffect(
|
||||
|
@ -2081,8 +2082,12 @@ function baseCreateRenderer(
|
|||
for (i = toBePatched - 1; i >= 0; i--) {
|
||||
const nextIndex = s2 + i
|
||||
const nextChild = c2[nextIndex] as VNode
|
||||
const anchorVNode = c2[nextIndex + 1] as VNode
|
||||
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) {
|
||||
// mount new
|
||||
patch(
|
||||
|
@ -2200,6 +2205,12 @@ function baseCreateRenderer(
|
|||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
leave(el!, () => {
|
||||
remove()
|
||||
afterLeave && afterLeave()
|
||||
|
@ -2421,17 +2432,7 @@ function baseCreateRenderer(
|
|||
unregisterHMR(instance)
|
||||
}
|
||||
|
||||
const {
|
||||
bum,
|
||||
scope,
|
||||
effect,
|
||||
subTree,
|
||||
um,
|
||||
m,
|
||||
a,
|
||||
parent,
|
||||
slots: { __: slotCacheKeys },
|
||||
} = instance
|
||||
const { bum, scope, effect, subTree, um, m, a } = instance
|
||||
invalidateMount(m)
|
||||
invalidateMount(a)
|
||||
|
||||
|
@ -2440,13 +2441,6 @@ function baseCreateRenderer(
|
|||
invokeArrayFns(bum)
|
||||
}
|
||||
|
||||
// remove slots content from parent renderCache
|
||||
if (parent && isArray(slotCacheKeys)) {
|
||||
slotCacheKeys.forEach(v => {
|
||||
;(parent as ComponentInternalInstance).renderCache[v] = undefined
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
__COMPAT__ &&
|
||||
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
|
||||
|
@ -2484,24 +2478,6 @@ function baseCreateRenderer(
|
|||
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__) {
|
||||
devtoolsComponentRemoved(instance)
|
||||
}
|
||||
|
@ -2708,7 +2684,11 @@ export function traverseStaticChildren(
|
|||
traverseStaticChildren(c1, c2)
|
||||
}
|
||||
// #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
|
||||
}
|
||||
// #2324 also inherit for comment nodes, but not placeholders (e.g. v-if which
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
import type { SuspenseBoundary } from './components/Suspense'
|
||||
import type { VNode, VNodeNormalizedRef, VNodeNormalizedRefAtom } from './vnode'
|
||||
import type {
|
||||
VNode,
|
||||
VNodeNormalizedRef,
|
||||
VNodeNormalizedRefAtom,
|
||||
VNodeRef,
|
||||
} from './vnode'
|
||||
import {
|
||||
EMPTY_OBJ,
|
||||
NO,
|
||||
ShapeFlags,
|
||||
hasOwn,
|
||||
isArray,
|
||||
|
@ -14,7 +20,11 @@ import { warn } from './warning'
|
|||
import { isRef, toRaw } from '@vue/reactivity'
|
||||
import { ErrorCodes, callWithErrorHandling } from './errorHandling'
|
||||
import { queuePostRenderEffect } from './renderer'
|
||||
import { type ComponentOptions, getComponentPublicInstance } from './component'
|
||||
import {
|
||||
type ComponentOptions,
|
||||
type Data,
|
||||
getComponentPublicInstance,
|
||||
} from './component'
|
||||
import { knownTemplateRefs } from './helpers/useTemplateRef'
|
||||
|
||||
/**
|
||||
|
@ -73,25 +83,11 @@ export function setRef(
|
|||
const oldRef = oldRawRef && (oldRawRef as VNodeNormalizedRefAtom).r
|
||||
const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs
|
||||
const setupState = owner.setupState
|
||||
const rawSetupState = toRaw(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.`,
|
||||
)
|
||||
}
|
||||
const canSetSetupRef = createCanSetSetupRefChecker(setupState)
|
||||
|
||||
if (knownTemplateRefs.has(rawSetupState[key] as any)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return hasOwn(rawSetupState, key)
|
||||
}
|
||||
const canSetRef = (ref: VNodeRef) => {
|
||||
return !__DEV__ || !knownTemplateRefs.has(ref as any)
|
||||
}
|
||||
|
||||
// dynamic ref changed. unset old ref
|
||||
if (oldRef != null && oldRef !== ref) {
|
||||
|
@ -101,7 +97,13 @@ export function setRef(
|
|||
setupState[oldRef] = null
|
||||
}
|
||||
} else if (isRef(oldRef)) {
|
||||
oldRef.value = null
|
||||
if (canSetRef(oldRef)) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -118,7 +120,9 @@ export function setRef(
|
|||
? canSetSetupRef(ref)
|
||||
? setupState[ref]
|
||||
: refs[ref]
|
||||
: ref.value
|
||||
: canSetRef(ref) || !rawRef.k
|
||||
? ref.value
|
||||
: refs[rawRef.k]
|
||||
if (isUnmount) {
|
||||
isArray(existing) && remove(existing, refValue)
|
||||
} else {
|
||||
|
@ -129,8 +133,11 @@ export function setRef(
|
|||
setupState[ref] = refs[ref]
|
||||
}
|
||||
} else {
|
||||
ref.value = [refValue]
|
||||
if (rawRef.k) refs[rawRef.k] = ref.value
|
||||
const newVal = [refValue]
|
||||
if (canSetRef(ref)) {
|
||||
ref.value = newVal
|
||||
}
|
||||
if (rawRef.k) refs[rawRef.k] = newVal
|
||||
}
|
||||
} else if (!existing.includes(refValue)) {
|
||||
existing.push(refValue)
|
||||
|
@ -142,7 +149,9 @@ export function setRef(
|
|||
setupState[ref] = value
|
||||
}
|
||||
} else if (_isRef) {
|
||||
ref.value = value
|
||||
if (canSetRef(ref)) {
|
||||
ref.value = value
|
||||
}
|
||||
if (rawRef.k) refs[rawRef.k] = value
|
||||
} else if (__DEV__) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -200,6 +200,7 @@ export interface VNode<
|
|||
|
||||
// DOM
|
||||
el: HostNode | null
|
||||
placeholder: HostNode | null // async component el placeholder
|
||||
anchor: HostNode | null // fragment anchor
|
||||
target: HostElement | null // teleport target
|
||||
targetStart: HostNode | null // teleport target start anchor
|
||||
|
@ -731,6 +732,8 @@ export function cloneVNode<T, U>(
|
|||
suspense: vnode.suspense,
|
||||
ssContent: vnode.ssContent && cloneVNode(vnode.ssContent),
|
||||
ssFallback: vnode.ssFallback && cloneVNode(vnode.ssFallback),
|
||||
placeholder: vnode.placeholder,
|
||||
|
||||
el: vnode.el,
|
||||
anchor: vnode.anchor,
|
||||
ctx: vnode.ctx,
|
||||
|
|
|
@ -1402,6 +1402,34 @@ describe('defineCustomElement', () => {
|
|||
})
|
||||
|
||||
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 () => {
|
||||
type SetValue = (value: string) => void
|
||||
let fn: MockedFunction<SetValue>
|
||||
|
|
|
@ -46,7 +46,7 @@ export const vShow: ObjectDirective<VShowElement> & { name?: 'show' } = {
|
|||
},
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
if (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) {
|
||||
vShow.name = 'show'
|
||||
}
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
const { app } = define({
|
||||
setup() {
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
insert,
|
||||
prepend,
|
||||
renderEffect,
|
||||
setInsertionState,
|
||||
template,
|
||||
} from '../src'
|
||||
import { currentInstance, nextTick, ref } from '@vue/runtime-dom'
|
||||
|
@ -502,5 +503,35 @@ describe('component: slots', () => {
|
|||
await nextTick()
|
||||
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>`,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
nextTick,
|
||||
reactive,
|
||||
ref,
|
||||
shallowRef,
|
||||
useTemplateRef,
|
||||
watchEffect,
|
||||
} from '@vue/runtime-dom'
|
||||
|
@ -208,8 +209,8 @@ describe('api: template ref', () => {
|
|||
const { render } = define({
|
||||
setup() {
|
||||
return {
|
||||
foo: fooEl,
|
||||
bar: barEl,
|
||||
foo: shallowRef(fooEl),
|
||||
bar: shallowRef(barEl),
|
||||
}
|
||||
},
|
||||
render() {
|
||||
|
@ -251,6 +252,7 @@ describe('api: template ref', () => {
|
|||
})
|
||||
const { host } = render()
|
||||
expect(state.refKey).toBe(host.children[0])
|
||||
expect('Template ref "refKey" used on a non-ref value').toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('multiple root refs', () => {
|
||||
|
@ -713,6 +715,45 @@ describe('api: template ref', () => {
|
|||
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
|
||||
// // #2078
|
||||
// test('handling multiple merged refs', async () => {
|
||||
|
|
|
@ -778,7 +778,7 @@ describe('createFor', () => {
|
|||
)
|
||||
})
|
||||
|
||||
test.todo('prepend', async () => {
|
||||
test('prepend', async () => {
|
||||
const arr = ref<number[]>([4, 5])
|
||||
const { host, html } = render(arr)
|
||||
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 { host, html } = render(arr)
|
||||
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 { host, html } = render(arr)
|
||||
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 { host, html } = render(arr)
|
||||
expect(host.children.length).toBe(3)
|
||||
|
@ -1058,24 +1058,21 @@ describe('createFor', () => {
|
|||
expect(html()).toBe(`<span>4</span><span>6</span><!--for-->`)
|
||||
})
|
||||
|
||||
test.todo(
|
||||
'moved and set to undefined element ending at the end',
|
||||
async () => {
|
||||
const arr = ref<number[]>([2, 4, 5])
|
||||
const { host, html } = render(arr)
|
||||
expect(host.children.length).toBe(3)
|
||||
expect(html()).toBe(
|
||||
`<span>2</span><span>4</span><span>5</span><!--for-->`,
|
||||
)
|
||||
test('moved and set to undefined element ending at the end', async () => {
|
||||
const arr = ref<number[]>([2, 4, 5])
|
||||
const { host, html } = render(arr)
|
||||
expect(host.children.length).toBe(3)
|
||||
expect(html()).toBe(
|
||||
`<span>2</span><span>4</span><span>5</span><!--for-->`,
|
||||
)
|
||||
|
||||
arr.value = [4, 5, 3]
|
||||
await nextTick()
|
||||
expect(host.children.length).toBe(3)
|
||||
expect(html()).toBe(
|
||||
`<span>4</span><span>5</span><span>3</span><!--for-->`,
|
||||
)
|
||||
},
|
||||
)
|
||||
arr.value = [4, 5, 3]
|
||||
await nextTick()
|
||||
expect(host.children.length).toBe(3)
|
||||
expect(html()).toBe(
|
||||
`<span>4</span><span>5</span><span>3</span><!--for-->`,
|
||||
)
|
||||
})
|
||||
|
||||
test('reverse element', async () => {
|
||||
const arr = ref<number[]>([1, 2, 3, 4, 5, 6, 7, 8])
|
||||
|
@ -1323,7 +1320,7 @@ describe('createFor', () => {
|
|||
}).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 { host, html } = define({
|
||||
setup() {
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
import { makeInteropRender } from './_utils'
|
||||
import {
|
||||
applyTextModel,
|
||||
applyVShow,
|
||||
child,
|
||||
createComponent,
|
||||
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', () => {
|
||||
test('basic', () => {
|
||||
const VDomChild = defineComponent({
|
||||
|
|
|
@ -196,12 +196,15 @@ export const createFor = (
|
|||
endOffset++
|
||||
continue
|
||||
}
|
||||
if (endOffset !== 0) {
|
||||
anchorFallback = normalizeAnchor(newBlocks[currentIndex + 1].nodes)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if (endOffset !== 0) {
|
||||
anchorFallback = normalizeAnchor(
|
||||
newBlocks[newLength - endOffset].nodes,
|
||||
)
|
||||
}
|
||||
|
||||
while (startOffset < sharedBlockCount - endOffset) {
|
||||
const currentItem = getItem(source, startOffset)
|
||||
const currentKey = getKey(...currentItem)
|
||||
|
@ -251,13 +254,9 @@ export const createFor = (
|
|||
previousKeyIndexPairs.length = previousKeyIndexInsertIndex
|
||||
|
||||
const previousKeyIndexMap = new Map(previousKeyIndexPairs)
|
||||
const blocksToMount: [
|
||||
blockIndex: number,
|
||||
blockItem: ReturnType<typeof getItem>,
|
||||
blockKey: any,
|
||||
anchorOffset: number,
|
||||
][] = []
|
||||
const operations: (() => void)[] = []
|
||||
|
||||
let mountCounter = 0
|
||||
const relocateOrMountBlock = (
|
||||
blockIndex: number,
|
||||
blockItem: ReturnType<typeof getItem>,
|
||||
|
@ -269,21 +268,31 @@ export const createFor = (
|
|||
const reusedBlock = (newBlocks[blockIndex] =
|
||||
oldBlocks[previousIndex])
|
||||
update(reusedBlock, ...blockItem)
|
||||
insert(
|
||||
reusedBlock,
|
||||
parent!,
|
||||
anchorOffset === -1
|
||||
? anchorFallback
|
||||
: normalizeAnchor(newBlocks[anchorOffset].nodes),
|
||||
)
|
||||
previousKeyIndexMap.delete(blockKey)
|
||||
if (previousIndex !== blockIndex) {
|
||||
operations.push(() =>
|
||||
insert(
|
||||
reusedBlock,
|
||||
parent!,
|
||||
anchorOffset === -1
|
||||
? anchorFallback
|
||||
: normalizeAnchor(newBlocks[anchorOffset].nodes),
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
blocksToMount.push([
|
||||
blockIndex,
|
||||
blockItem,
|
||||
blockKey,
|
||||
anchorOffset,
|
||||
])
|
||||
mountCounter++
|
||||
operations.push(() =>
|
||||
mount(
|
||||
source,
|
||||
blockIndex,
|
||||
anchorOffset === -1
|
||||
? anchorFallback
|
||||
: normalizeAnchor(newBlocks[anchorOffset].nodes),
|
||||
blockItem,
|
||||
blockKey,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -303,7 +312,7 @@ export const createFor = (
|
|||
relocateOrMountBlock(i, blockItem, blockKey, -1)
|
||||
}
|
||||
|
||||
const useFastRemove = blocksToMount.length === newLength
|
||||
const useFastRemove = mountCounter === newLength
|
||||
|
||||
for (const leftoverIndex of previousKeyIndexMap.values()) {
|
||||
unmount(
|
||||
|
@ -322,21 +331,9 @@ export const createFor = (
|
|||
}
|
||||
}
|
||||
|
||||
for (const [
|
||||
blockIndex,
|
||||
blockItem,
|
||||
blockKey,
|
||||
anchorOffset,
|
||||
] of blocksToMount) {
|
||||
mount(
|
||||
source,
|
||||
blockIndex,
|
||||
anchorOffset === -1
|
||||
? anchorFallback
|
||||
: normalizeAnchor(newBlocks[anchorOffset].nodes),
|
||||
blockItem,
|
||||
blockKey,
|
||||
)
|
||||
// perform mount and move operations
|
||||
for (const action of operations) {
|
||||
action()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
ErrorCodes,
|
||||
type SchedulerJob,
|
||||
callWithErrorHandling,
|
||||
createCanSetSetupRefChecker,
|
||||
queuePostFlushCb,
|
||||
warn,
|
||||
} from '@vue/runtime-dom'
|
||||
|
@ -55,6 +56,7 @@ export function setRef(
|
|||
const refs =
|
||||
instance.refs === EMPTY_OBJ ? (instance.refs = {}) : instance.refs
|
||||
|
||||
const canSetSetupRef = createCanSetSetupRefChecker(setupState)
|
||||
// dynamic ref changed. unset old ref
|
||||
if (oldRef != null && oldRef !== ref) {
|
||||
if (isString(oldRef)) {
|
||||
|
@ -87,7 +89,7 @@ export function setRef(
|
|||
const doSet: SchedulerJob = () => {
|
||||
if (refFor) {
|
||||
existing = _isString
|
||||
? __DEV__ && hasOwn(setupState, ref)
|
||||
? __DEV__ && canSetSetupRef(ref)
|
||||
? setupState[ref]
|
||||
: refs[ref]
|
||||
: ref.value
|
||||
|
@ -96,7 +98,7 @@ export function setRef(
|
|||
existing = [refValue]
|
||||
if (_isString) {
|
||||
refs[ref] = existing
|
||||
if (__DEV__ && hasOwn(setupState, ref)) {
|
||||
if (__DEV__ && canSetSetupRef(ref)) {
|
||||
setupState[ref] = refs[ref]
|
||||
// if setupState[ref] is a reactivity ref,
|
||||
// the existing will also become reactivity too
|
||||
|
@ -111,7 +113,7 @@ export function setRef(
|
|||
}
|
||||
} else if (_isString) {
|
||||
refs[ref] = refValue
|
||||
if (__DEV__ && hasOwn(setupState, ref)) {
|
||||
if (__DEV__ && canSetSetupRef(ref)) {
|
||||
setupState[ref] = refValue
|
||||
}
|
||||
} else if (_isRef) {
|
||||
|
@ -129,7 +131,7 @@ export function setRef(
|
|||
remove(existing, refValue)
|
||||
} else if (_isString) {
|
||||
refs[ref] = null
|
||||
if (__DEV__ && hasOwn(setupState, ref)) {
|
||||
if (__DEV__ && canSetSetupRef(ref)) {
|
||||
setupState[ref] = null
|
||||
}
|
||||
} else if (_isRef) {
|
||||
|
|
|
@ -105,10 +105,10 @@ export function isValidBlock(block: Block): boolean {
|
|||
|
||||
export function insert(
|
||||
block: Block,
|
||||
parent: ParentNode,
|
||||
parent: ParentNode & { $anchor?: Node | null },
|
||||
anchor: Node | null | 0 = null, // 0 means prepend
|
||||
): void {
|
||||
anchor = anchor === 0 ? parent.firstChild : anchor
|
||||
anchor = anchor === 0 ? parent.$anchor || parent.firstChild : anchor
|
||||
if (block instanceof Node) {
|
||||
if (!isHydrating) {
|
||||
parent.insertBefore(block, anchor)
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
} from '@vue/runtime-dom'
|
||||
import { renderEffect } from '../renderEffect'
|
||||
import { isVaporComponent } from '../component'
|
||||
import { type Block, DynamicFragment } from '../block'
|
||||
import { type Block, DynamicFragment, VaporFragment } from '../block'
|
||||
import { isArray } from '@vue/shared'
|
||||
|
||||
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)
|
||||
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()))
|
||||
|
@ -33,12 +39,16 @@ function setDisplay(target: Block, value: unknown): void {
|
|||
if (isVaporComponent(target)) {
|
||||
return setDisplay(target, value)
|
||||
}
|
||||
if (isArray(target) && target.length === 1) {
|
||||
return setDisplay(target[0], value)
|
||||
if (isArray(target)) {
|
||||
if (target.length === 0) return
|
||||
if (target.length === 1) return setDisplay(target[0], value)
|
||||
}
|
||||
if (target instanceof DynamicFragment) {
|
||||
return setDisplay(target.nodes, value)
|
||||
}
|
||||
if (target instanceof VaporFragment && target.insert) {
|
||||
return setDisplay(target.nodes, value)
|
||||
}
|
||||
if (target instanceof Element) {
|
||||
const el = target as VShowElement
|
||||
if (!(vShowOriginalDisplay in el)) {
|
||||
|
|
|
@ -296,7 +296,7 @@ export function optimizePropertyLookup(): void {
|
|||
if (isOptimized) return
|
||||
isOptimized = true
|
||||
const proto = Element.prototype as any
|
||||
proto.$evtclick = undefined
|
||||
proto.$anchor = proto.$evtclick = undefined
|
||||
proto.$root = false
|
||||
proto.$html =
|
||||
proto.$txt =
|
||||
|
|
|
@ -6,7 +6,18 @@ export let insertionAnchor: Node | 0 | undefined
|
|||
* (component, slot outlet, if, for) is created. The state is used for actual
|
||||
* insertion on client-side render, and used for node adoption during hydration.
|
||||
*/
|
||||
export function setInsertionState(parent: ParentNode, anchor?: Node | 0): void {
|
||||
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
|
||||
insertionAnchor = anchor
|
||||
}
|
||||
|
|
|
@ -216,6 +216,8 @@ function createVDOMComponent(
|
|||
parentInstance as any,
|
||||
)
|
||||
}
|
||||
|
||||
frag.nodes = vnode.el as Block
|
||||
}
|
||||
|
||||
frag.remove = unmount
|
||||
|
|
|
@ -111,26 +111,106 @@ describe('ssr: slot', () => {
|
|||
})
|
||||
|
||||
test('transition slot', async () => {
|
||||
const ReusableTransition = {
|
||||
template: `<transition><slot/></transition>`,
|
||||
}
|
||||
|
||||
const ReusableTransitionWithAppear = {
|
||||
template: `<transition appear><slot/></transition>`,
|
||||
}
|
||||
|
||||
expect(
|
||||
await renderToString(
|
||||
createApp({
|
||||
components: {
|
||||
one: {
|
||||
template: `<transition><slot/></transition>`,
|
||||
},
|
||||
one: ReusableTransition,
|
||||
},
|
||||
template: `<one><div v-if="false">foo</div></one>`,
|
||||
}),
|
||||
),
|
||||
).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(
|
||||
await renderToString(
|
||||
createApp({
|
||||
components: {
|
||||
one: {
|
||||
template: `<transition><slot/></transition>`,
|
||||
},
|
||||
one: ReusableTransitionWithAppear,
|
||||
},
|
||||
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>`,
|
||||
}),
|
||||
|
|
|
@ -74,6 +74,8 @@ export function ssrRenderSlotInner(
|
|||
)
|
||||
} else if (fallbackRenderFn) {
|
||||
fallbackRenderFn()
|
||||
} else if (transition) {
|
||||
push(`<!---->`)
|
||||
}
|
||||
} else {
|
||||
// ssr slot.
|
||||
|
@ -110,13 +112,19 @@ export function ssrRenderSlotInner(
|
|||
end--
|
||||
}
|
||||
|
||||
for (let i = start; i < end; i++) {
|
||||
push(slotBuffer[i])
|
||||
if (start < end) {
|
||||
for (let i = start; i < end; i++) {
|
||||
push(slotBuffer[i])
|
||||
}
|
||||
} else if (transition) {
|
||||
push(`<!---->`)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (fallbackRenderFn) {
|
||||
fallbackRenderFn()
|
||||
} else if (transition) {
|
||||
push(`<!---->`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1724,6 +1724,107 @@ describe('e2e: Transition', () => {
|
|||
},
|
||||
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', () => {
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
})
|
1766
pnpm-lock.yaml
1766
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -3,11 +3,11 @@ packages:
|
|||
- 'packages-private/*'
|
||||
|
||||
catalog:
|
||||
'@babel/parser': ^7.27.5
|
||||
'@babel/types': ^7.27.6
|
||||
'@babel/parser': ^7.28.3
|
||||
'@babel/types': ^7.28.2
|
||||
'estree-walker': ^2.0.2
|
||||
'vite': ^6.1.0
|
||||
'@vitejs/plugin-vue': ^5.2.4
|
||||
'@vitejs/plugin-vue': ^6.0.1
|
||||
'magic-string': ^0.30.17
|
||||
'source-map-js': ^1.2.1
|
||||
|
||||
|
|
Loading…
Reference in New Issue