Merge branch 'minor' into edison/feat/svgAndMathML

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

View File

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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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"
}
}

View File

@ -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:*"

View File

@ -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>

View File

@ -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"
}
}

View File

@ -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 */))
]))
}

View File

@ -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]' },
},
],
})
})

View File

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

View File

@ -1,4 +1,9 @@
import type { ExpressionNode, TransformContext } from '../src'
import { babelParse, walkIdentifiers } from '@vue/compiler-sfc'
import {
type ExpressionNode,
type TransformContext,
isReferencedIdentifier,
} from '../src'
import { type Position, createSimpleExpression } from '../src/ast'
import {
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,
)
})
})

View File

@ -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)
}

View File

@ -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') {

View File

@ -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
}

View File

@ -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

View File

@ -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(

View File

@ -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)

View File

@ -63,7 +63,7 @@ export function isCoreComponent(tag: string): symbol | void {
}
}
const nonIdentifierRE = /^\d|[^\$\w\xA0-\uFFFF]/
const nonIdentifierRE = /^$|^\d|[^\$\w\xA0-\uFFFF]/
export const isSimpleIdentifier = (name: string): boolean =>
!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'
}

View File

@ -4,11 +4,11 @@ exports[`stringify static html > eligible content (elements > 20) + non-eligible
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
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&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span></div>", 1)
])))
]))]))
}"
`;
@ -26,9 +26,9 @@ exports[`stringify static html > serializing constant bindings 1`] = `
"const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
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)
])))
]))]))
}"
`;

View File

@ -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.`,
)
}

View File

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

View File

@ -81,9 +81,9 @@ import _imports_1 from '/bar.png'
export function render(_ctx, _cache) {
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)
])))
]))]))
}"
`;

View File

@ -16,6 +16,16 @@ export function render(_ctx, _cache) {
}"
`;
exports[`compiler sfc: transform srcset > transform empty srcset w/ includeAbsolute: true 1`] = `
"import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = { srcset: " " }
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("img", _hoisted_1))
}"
`;
exports[`compiler sfc: transform srcset > transform srcset 1`] = `
"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)
])))
]))]))
}"
`;

View File

@ -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`

View File

@ -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'],
})
})
})

View File

@ -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>`,

View File

@ -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"
}
}

View File

@ -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, `}`)
}

View File

@ -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
}
}

View File

@ -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>>()

View File

@ -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),
),
),
),
]

View File

@ -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
}

View File

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

View File

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

View File

@ -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.`,
)
}

View File

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

View File

@ -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()
})
})
})

View File

@ -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)

View File

@ -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)

View File

@ -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))

View File

@ -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

View File

@ -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)
})

View File

@ -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

View File

@ -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',
])
})

View File

@ -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')
})
})

View File

@ -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>`,
)
})
})
})

View File

@ -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
}
})
})

View File

@ -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')
})
})

View File

@ -1160,6 +1160,69 @@ describe('SSR hydration', () => {
)
})
// #13510
test('update async component after parent mount before async component resolve', async () => {
const Comp = {
props: ['toggle'],
render(this: any) {
return h('h1', [
this.toggle ? 'Async component' : 'Updated async component',
])
},
}
let serverResolve: any
let AsyncComp = defineAsyncComponent(
() =>
new Promise(r => {
serverResolve = r
}),
)
const toggle = ref(true)
const App = {
setup() {
onMounted(() => {
// change state, after mount and before async component resolve
nextTick(() => (toggle.value = false))
})
return () => {
return h(AsyncComp, { toggle: toggle.value })
}
},
}
// server render
const htmlPromise = renderToString(h(App))
serverResolve(Comp)
const html = await htmlPromise
expect(html).toMatchInlineSnapshot(`"<h1>Async component</h1>"`)
// hydration
let clientResolve: any
AsyncComp = defineAsyncComponent(
() =>
new Promise(r => {
clientResolve = r
}),
)
const container = document.createElement('div')
container.innerHTML = html
createSSRApp(App).mount(container)
// resolve
clientResolve(Comp)
await new Promise(r => setTimeout(r))
// prevent lazy hydration since the component has been patched
expect('Skipping lazy hydration for component').toHaveBeenWarned()
expect(`Hydration node mismatch`).not.toHaveBeenWarned()
expect(container.innerHTML).toMatchInlineSnapshot(
`"<h1>Updated async component</h1>"`,
)
})
test('hydrate safely when property used by async setup changed before render', async () => {
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(

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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) {

View File

@ -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])
)
},

View File

@ -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)

View File

@ -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> {

View File

@ -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

View File

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

View File

@ -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)
}

View File

@ -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![leaveCbKey](true /* cancelled */)
}
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

View File

@ -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)
}
}

View File

@ -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,

View File

@ -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>

View File

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

View File

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

View File

@ -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>`,
)
})
})
})

View File

@ -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 () => {

View File

@ -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() {

View File

@ -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({

View File

@ -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()
}
}
}

View File

@ -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) {

View File

@ -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)

View File

@ -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)) {

View File

@ -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 =

View File

@ -6,7 +6,18 @@ export let insertionAnchor: Node | 0 | undefined
* (component, slot outlet, if, for) is created. The state is used for actual
* 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
}

View File

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

View File

@ -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>`,
}),

View File

@ -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(`<!---->`)
}
}

View File

@ -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', () => {

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -3,11 +3,11 @@ packages:
- 'packages-private/*'
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