Merge remote-tracking branch 'upstream/minor'

This commit is contained in:
三咲智子 Kevin Deng 2024-06-16 17:01:49 +08:00
commit cf8be999df
No known key found for this signature in database
118 changed files with 3326 additions and 1932 deletions

View File

@ -20,7 +20,7 @@ jobs:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v3.0.0
uses: pnpm/action-setup@v4.0.0
- name: Install Node.js
uses: actions/setup-node@v4
@ -42,7 +42,7 @@ jobs:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v3.0.0
uses: pnpm/action-setup@v4.0.0
- name: Install Node.js
uses: actions/setup-node@v4

View File

@ -23,7 +23,7 @@ jobs:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v3.0.0
uses: pnpm/action-setup@v4.0.0
- name: Install Node.js
uses: actions/setup-node@v4

View File

@ -24,7 +24,7 @@ jobs:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v3.0.0
uses: pnpm/action-setup@v4.0.0
- name: Install Node.js
uses: actions/setup-node@v4
@ -36,14 +36,14 @@ jobs:
run: pnpm install
- name: Download Size Data
uses: dawidd6/action-download-artifact@v3
uses: dawidd6/action-download-artifact@v4
with:
name: size-data
run_id: ${{ github.event.workflow_run.id }}
path: temp/size
- name: Download Previous Size Data
uses: dawidd6/action-download-artifact@v3
uses: dawidd6/action-download-artifact@v4
with:
branch: main
workflow: size-data.yml

View File

@ -1,3 +1,68 @@
## [3.4.29](https://github.com/vuejs/core/compare/v3.4.28...v3.4.29) (2024-06-14)
### Bug Fixes
* **build:** fix accidental inclusion of runtime-core in server-renderer cjs build ([11cc12b](https://github.com/vuejs/core/commit/11cc12b915edfe0e4d3175e57464f73bc2c1cb04)), closes [#11137](https://github.com/vuejs/core/issues/11137)
* **compiler-sfc:** fix missing scope for extends error message ([4ec387b](https://github.com/vuejs/core/commit/4ec387b100985b008cdcc4cd883a5b6328c05766))
* **compiler-sfc:** fix parsing of mts, d.mts, and mtsx files ([a476692](https://github.com/vuejs/core/commit/a476692ed2d7308f2742d8ff3554cf97a392b0b7))
* **compiler-sfc:** support [@vue-ignore](https://github.com/vue-ignore) comment on more type sources ([a23e99b](https://github.com/vuejs/core/commit/a23e99bedf1d65841d162951f10ce35b907a5680))
* **custom-element:** support same direct setup function signature in defineCustomElement ([7c8b126](https://github.com/vuejs/core/commit/7c8b12620aad4969b8dc4944d4fc486d16c3033c)), closes [#11116](https://github.com/vuejs/core/issues/11116)
* **reactivity:** avoid infinite loop when render access a side effect computed ([#11135](https://github.com/vuejs/core/issues/11135)) ([8296e19](https://github.com/vuejs/core/commit/8296e19855e369a7826f5ea26540a6da01dc7093)), closes [#11121](https://github.com/vuejs/core/issues/11121)
## [3.4.28](https://github.com/vuejs/core/compare/v3.4.27...v3.4.28) (2024-06-14)
### Bug Fixes
* **compat:** correctly transform non-identifier expressions in legacy filter syntax ([#10896](https://github.com/vuejs/core/issues/10896)) ([07b3c4b](https://github.com/vuejs/core/commit/07b3c4b7860009e19446f3d78571556c5737d82a)), closes [#10852](https://github.com/vuejs/core/issues/10852)
* **compat:** ensure proper handling of render fuction from SFC using Vue.extend ([#7781](https://github.com/vuejs/core/issues/7781)) ([c73847f](https://github.com/vuejs/core/commit/c73847f2becc20f03cb9c68748eea92455e688ee)), closes [#7766](https://github.com/vuejs/core/issues/7766)
* **compat:** only warn ATTR_FALSE_VALUE when enabled ([04729ba](https://github.com/vuejs/core/commit/04729ba2163d840f0ca7866bc964696eb5557804)), closes [#11126](https://github.com/vuejs/core/issues/11126)
* **compile-sfc:** register props destructure rest id as setup bindings ([#10888](https://github.com/vuejs/core/issues/10888)) ([b2b5f57](https://github.com/vuejs/core/commit/b2b5f57c2c945edd0eebc1b545ec1b7568e51484)), closes [#10885](https://github.com/vuejs/core/issues/10885)
* **compile-sfc:** Support project reference with folder, ([#10908](https://github.com/vuejs/core/issues/10908)) ([bdeac37](https://github.com/vuejs/core/commit/bdeac377c7b85888193b49ac187e927636cc40bc)), closes [#10907](https://github.com/vuejs/core/issues/10907)
* **compiler-core:** allow unicode to appear in simple identifiers ([#6765](https://github.com/vuejs/core/issues/6765)) ([3ea9644](https://github.com/vuejs/core/commit/3ea964473d3ac0ba3e7b0b2c22d71f23d0f69123)), closes [#6367](https://github.com/vuejs/core/issues/6367)
* **compiler-core:** change v-for key type to match Object.keys ([#10963](https://github.com/vuejs/core/issues/10963)) ([9fead52](https://github.com/vuejs/core/commit/9fead5234320848f8be82275c6b5dd0a290f2cca)), closes [#8819](https://github.com/vuejs/core/issues/8819)
* **compiler-core:** emit TS-compatible function declaration when requested ([#9363](https://github.com/vuejs/core/issues/9363)) ([5d25850](https://github.com/vuejs/core/commit/5d258502a0faffc8a451b8701f13a31b2566d068))
* **compiler-core:** fix :key shorthand on v-for ([#10942](https://github.com/vuejs/core/issues/10942)) ([29425df](https://github.com/vuejs/core/commit/29425df1acb9e520c6ae894d06bcff73fde90edd)), closes [#10882](https://github.com/vuejs/core/issues/10882) [#10939](https://github.com/vuejs/core/issues/10939)
* **compiler-core:** make `ForIteratorExpression`'s `returns` property optional ([#11011](https://github.com/vuejs/core/issues/11011)) ([5b8c1af](https://github.com/vuejs/core/commit/5b8c1afb74e39045fcb53a011420d26e3f67eab4))
* **compiler-core:** should set `<math>` tag as block to retain MathML namespace after patching ([#10891](https://github.com/vuejs/core/issues/10891)) ([87c5443](https://github.com/vuejs/core/commit/87c54430448005294c41803f07f517fef848f917))
* **compiler-core:** v-for expression missing source with spaces should emit error ([#5821](https://github.com/vuejs/core/issues/5821)) ([b9ca202](https://github.com/vuejs/core/commit/b9ca202f477be595477e182972ee9bae3f2b9f74)), closes [#5819](https://github.com/vuejs/core/issues/5819)
* **compiler-sfc:** improve type resolving for the keyof operator ([#10921](https://github.com/vuejs/core/issues/10921)) ([293cf4e](https://github.com/vuejs/core/commit/293cf4e131b6d4606e1de2cd7ea87814e2544952)), closes [#10920](https://github.com/vuejs/core/issues/10920) [#11002](https://github.com/vuejs/core/issues/11002)
* **compiler-sfc:** support as keyword with template literal types ([#11100](https://github.com/vuejs/core/issues/11100)) ([2594b1d](https://github.com/vuejs/core/commit/2594b1df57f672ac6621ac2880645e975fea581c)), closes [#10962](https://github.com/vuejs/core/issues/10962)
* **compiler-sfc:** support type resolve for keyof for intersection & union types ([#11132](https://github.com/vuejs/core/issues/11132)) ([495263a](https://github.com/vuejs/core/commit/495263a9cb356861e58a4364f2570608265486b5)), closes [#11129](https://github.com/vuejs/core/issues/11129)
* **compiler-sfc:** throw error when import macro as alias ([#11041](https://github.com/vuejs/core/issues/11041)) ([34a97ed](https://github.com/vuejs/core/commit/34a97edd2c8273c213599c44770accdb0846da8e))
* correct the type of `<details>`'s `onToggle` event handler ([#10938](https://github.com/vuejs/core/issues/10938)) ([fd18ce7](https://github.com/vuejs/core/commit/fd18ce70b1a260a2485c9cd7faa30193da4b79f5)), closes [#10928](https://github.com/vuejs/core/issues/10928)
* **custom-element:** disconnect MutationObserver in nextTick in case that custom elements are moved ([#10613](https://github.com/vuejs/core/issues/10613)) ([bbb5be2](https://github.com/vuejs/core/commit/bbb5be299b500a00e60c757118c846c3b5ddd8e0)), closes [#10610](https://github.com/vuejs/core/issues/10610)
* **custom-elements:** compatibility of createElement in older versions of Chrome ([#9615](https://github.com/vuejs/core/issues/9615)) ([a88295d](https://github.com/vuejs/core/commit/a88295dc076ee867939d8b0ee2225e63c5ffb0ca)), closes [#9614](https://github.com/vuejs/core/issues/9614)
* **hmr:** avoid infinite recursion when reloading hmr components ([#6936](https://github.com/vuejs/core/issues/6936)) ([36bd9b0](https://github.com/vuejs/core/commit/36bd9b0a1fb83e61731fb80d66e265dccbedcfa8)), closes [#6930](https://github.com/vuejs/core/issues/6930)
* **hydration:** log hydration error even when using async components ([#9403](https://github.com/vuejs/core/issues/9403)) ([5afc76c](https://github.com/vuejs/core/commit/5afc76c229f9ad30eef07f34c7b65e8fe427e637)), closes [#9369](https://github.com/vuejs/core/issues/9369)
* **KeepAlive:** properly cache nested Suspense subtree ([#10912](https://github.com/vuejs/core/issues/10912)) ([07764fe](https://github.com/vuejs/core/commit/07764fe330692fadf0fc9fb9e92cb5b111df33be))
* **npm:** explicitly add `@vue/reactivity` as dependency of `@vue/runtime-dom` ([#10468](https://github.com/vuejs/core/issues/10468)) ([ec424f6](https://github.com/vuejs/core/commit/ec424f6cd96b7e6ba74fc244c484c00fa5590aac))
* **reactivity:** pass oldValue in debug info when triggering refs ([#8210](https://github.com/vuejs/core/issues/8210)) ([3b0a56a](https://github.com/vuejs/core/commit/3b0a56a9c4d162ec3bd725a4f2dfd776b045e727)), closes [vuejs/pinia#2061](https://github.com/vuejs/pinia/issues/2061)
* **runtime-core:** avoid traversing static children for vnodes w/ PatchFlags.BAIL ([#11115](https://github.com/vuejs/core/issues/11115)) ([b557d3f](https://github.com/vuejs/core/commit/b557d3fb8ae1e4e926c4ad0fbb2fa7abe50fd661)), closes [#10547](https://github.com/vuejs/core/issues/10547)
* **runtime-core:** do not fire mount/activated hooks if unmounted before mounted ([#9370](https://github.com/vuejs/core/issues/9370)) ([aa156ed](https://github.com/vuejs/core/commit/aa156ed5c4dc0d33ff37e201a7e89d5e0e29160e)), closes [#8898](https://github.com/vuejs/core/issues/8898) [#9264](https://github.com/vuejs/core/issues/9264) [#9617](https://github.com/vuejs/core/issues/9617)
* **runtime-core:** ensure suspense creates dep component's render effect with correct optimized flag ([#7689](https://github.com/vuejs/core/issues/7689)) ([c521f95](https://github.com/vuejs/core/commit/c521f956e1697cda36a7f1b913599e5e2004f7ba)), closes [#7688](https://github.com/vuejs/core/issues/7688)
* **runtime-core:** fix missed updates when passing text vnode to `<component :is>` ([#8304](https://github.com/vuejs/core/issues/8304)) ([b310ec3](https://github.com/vuejs/core/commit/b310ec389d9738247e5b0f01711186216eb49955)), closes [#8298](https://github.com/vuejs/core/issues/8298)
* **runtime-core:** fix stale v-memo after v-if toggle ([#6606](https://github.com/vuejs/core/issues/6606)) ([edf2638](https://github.com/vuejs/core/commit/edf263847eddc910f4d2de68287d84b8c66c3860)), closes [#6593](https://github.com/vuejs/core/issues/6593)
* **runtime-core:** fix Transition for components with root-level v-if ([#7678](https://github.com/vuejs/core/issues/7678)) ([ef2e737](https://github.com/vuejs/core/commit/ef2e737577de42ea38771403f8a4dee8c892daa5)), closes [#7649](https://github.com/vuejs/core/issues/7649)
* **runtime-dom:** also set attribute for form element state ([537a571](https://github.com/vuejs/core/commit/537a571f8cf09dfe0a020e9e8891ecdd351fc3e4)), closes [#6007](https://github.com/vuejs/core/issues/6007) [#6012](https://github.com/vuejs/core/issues/6012)
* **runtime-dom:** support Symbol for input value bindings ([#10608](https://github.com/vuejs/core/issues/10608)) ([188f3ae](https://github.com/vuejs/core/commit/188f3ae533fd340603068a516a8fecc5d57426c5)), closes [#10597](https://github.com/vuejs/core/issues/10597)
* **shared:** ensure invokeArrayFns handles undefined arguments ([#10869](https://github.com/vuejs/core/issues/10869)) ([9b40d0f](https://github.com/vuejs/core/commit/9b40d0f25da868a83b0d6bf99dbbdb3ca68bb700)), closes [#10863](https://github.com/vuejs/core/issues/10863)
* **ssr:** directive binding.instance should respect exposed during ssr ([df686ab](https://github.com/vuejs/core/commit/df686abb4f0ac9d898e4fd93751e860f8cbbdbea)), closes [#7499](https://github.com/vuejs/core/issues/7499) [#7502](https://github.com/vuejs/core/issues/7502)
* **ssr:** fix hydration for node with empty text node ([#7216](https://github.com/vuejs/core/issues/7216)) ([d1011c0](https://github.com/vuejs/core/commit/d1011c07a957d858cb37725b13bc8e4d7a395490))
* **ssr:** fix the bug that multi slot scope id does not work on component ([#6100](https://github.com/vuejs/core/issues/6100)) ([4c74302](https://github.com/vuejs/core/commit/4c74302aae64c118752db7fc2a2c229a11ebaead)), closes [#6093](https://github.com/vuejs/core/issues/6093)
* **teleport:** do not throw target warning when teleport is disabled ([#9818](https://github.com/vuejs/core/issues/9818)) ([15ee43f](https://github.com/vuejs/core/commit/15ee43f66ad2485ac212b02b444345d867b3c060))
* **transition:** ensure Transition enterHooks are updated after clone ([#11066](https://github.com/vuejs/core/issues/11066)) ([671cf29](https://github.com/vuejs/core/commit/671cf297a550d15b19fa3fecce1b30e26cad8154)), closes [#11061](https://github.com/vuejs/core/issues/11061)
* **types/apiWatch:** correct type inference for reactive array ([#11036](https://github.com/vuejs/core/issues/11036)) ([aae2d78](https://github.com/vuejs/core/commit/aae2d78875daa476280a45e71c2f38292964efae)), closes [#9416](https://github.com/vuejs/core/issues/9416)
* **types:** improve `app.provide` type checking ([#10603](https://github.com/vuejs/core/issues/10603)) ([612bbf0](https://github.com/vuejs/core/commit/612bbf0507cbe39d701acc5dff11824802078063)), closes [#10602](https://github.com/vuejs/core/issues/10602)
* **types:** support generic argument in setup context expose method ([#8507](https://github.com/vuejs/core/issues/8507)) ([635a59b](https://github.com/vuejs/core/commit/635a59b96fe6be445525c6595ca27da7ef7c1feb))
* **v-model:** fix the lazy modifier is not reset by other modifications ([#8547](https://github.com/vuejs/core/issues/8547)) ([a52a02f](https://github.com/vuejs/core/commit/a52a02f43fdf73d8aaad99c9cafed07f12ee422a)), closes [#8546](https://github.com/vuejs/core/issues/8546) [#6564](https://github.com/vuejs/core/issues/6564) [#6773](https://github.com/vuejs/core/issues/6773)
* **watch:** support traversing symbol properties in deep watcher ([#10969](https://github.com/vuejs/core/issues/10969)) ([a3e8aaf](https://github.com/vuejs/core/commit/a3e8aafbcc82003a66caded61143eb64c4ef02cd)), closes [#402](https://github.com/vuejs/core/issues/402)
## [3.4.27](https://github.com/vuejs/core/compare/v3.4.26...v3.4.27) (2024-05-06)

View File

@ -12,3 +12,4 @@ We would like to thank the following security researchers for responsibly disclo
- Jeet Pal - [@jeetpal2007](https://github.com/jeetpal2007) | [Email](jeetpal2007@gmail.com) | [LinkedIn](https://in.linkedin.com/in/jeet-pal-22601a290 )
- Mix - [@mnixry](https://github.com/mnixry)
- Aviv Keller - [@RedYetiDev](https://github.com/redyetidev) | [LinkedIn](https://www.linkedin.com/in/redyetidev) <redyetidev@gmail.com>

View File

@ -1,7 +1,7 @@
{
"private": true,
"version": "3.0.0-vapor",
"packageManager": "pnpm@9.1.2",
"packageManager": "pnpm@9.2.0",
"type": "module",
"scripts": {
"dev": "node scripts/dev.js vue vue-vapor",
@ -59,8 +59,8 @@
"node": ">=18.12.0"
},
"devDependencies": {
"@babel/parser": "^7.24.6",
"@babel/types": "^7.24.6",
"@babel/parser": "^7.24.7",
"@babel/types": "^7.24.7",
"@codspeed/vitest-plugin": "^3.1.0",
"@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-commonjs": "^25.0.8",
@ -70,20 +70,20 @@
"@rollup/plugin-terser": "^0.4.4",
"@types/hash-sum": "^1.0.2",
"@types/minimist": "^1.2.5",
"@types/node": "^20.12.12",
"@types/node": "^20.14.2",
"@types/semver": "^7.5.8",
"@vitest/coverage-istanbul": "^1.5.2",
"@vitest/ui": "^1.5.2",
"@vue/consolidate": "1.0.0",
"conventional-changelog-cli": "^4.1.0",
"enquirer": "^2.4.1",
"esbuild": "^0.21.4",
"esbuild": "^0.21.5",
"esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^9.3.0",
"eslint": "^9.4.0",
"eslint-plugin-import-x": "^0.5.1",
"eslint-plugin-vitest": "^0.5.4",
"estree-walker": "^2.0.2",
"execa": "^8.0.1",
"execa": "^9.2.0",
"jsdom": "^24.0.0",
"lint-staged": "^15.2.5",
"lodash": "^4.17.21",
@ -93,7 +93,7 @@
"minimist": "^1.2.8",
"npm-run-all2": "^6.2.0",
"picocolors": "^1.0.1",
"prettier": "^3.2.5",
"prettier": "^3.3.1",
"pretty-bytes": "^6.1.1",
"pug": "^3.0.3",
"puppeteer": "~22.7.1",
@ -105,13 +105,13 @@
"semver": "^7.6.2",
"serve": "^14.2.3",
"simple-git-hooks": "^2.11.1",
"terser": "^5.31.0",
"terser": "^5.31.1",
"todomvc-app-css": "^2.4.3",
"tslib": "^2.6.2",
"tsx": "^4.11.0",
"tslib": "^2.6.3",
"tsx": "^4.15.1",
"typescript": "~5.4.5",
"typescript-eslint": "^7.10.0",
"vite": "^5.2.11",
"typescript-eslint": "^7.12.0",
"vite": "^5.2.13",
"vitest": "^1.5.2"
},
"pnpm": {

View File

@ -2,7 +2,7 @@
exports[`compiler: parse > Edge Cases > invalid html 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -86,7 +86,7 @@ exports[`compiler: parse > Edge Cases > invalid html 1`] = `
exports[`compiler: parse > Edge Cases > self closing multiple tag 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [],
@ -280,7 +280,7 @@ exports[`compiler: parse > Edge Cases > self closing multiple tag 1`] = `
exports[`compiler: parse > Edge Cases > valid html 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -498,7 +498,7 @@ exports[`compiler: parse > Edge Cases > valid html 1`] = `
exports[`compiler: parse > Errors > CDATA_IN_HTML_CONTENT > <template><![CDATA[cdata]]></template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [],
@ -550,7 +550,7 @@ exports[`compiler: parse > Errors > CDATA_IN_HTML_CONTENT > <template><![CDATA[c
exports[`compiler: parse > Errors > CDATA_IN_HTML_CONTENT > <template><svg><![CDATA[cdata]]></svg></template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -643,7 +643,7 @@ exports[`compiler: parse > Errors > CDATA_IN_HTML_CONTENT > <template><svg><![CD
exports[`compiler: parse > Errors > DUPLICATE_ATTRIBUTE > <template><div id="" id=""></div></template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -813,7 +813,7 @@ exports[`compiler: parse > Errors > DUPLICATE_ATTRIBUTE > <template><div id="" i
exports[`compiler: parse > Errors > EOF_BEFORE_TAG_NAME > <template>< 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -883,7 +883,7 @@ exports[`compiler: parse > Errors > EOF_BEFORE_TAG_NAME > <template>< 1`] = `
exports[`compiler: parse > Errors > EOF_BEFORE_TAG_NAME > <template></ 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -953,7 +953,7 @@ exports[`compiler: parse > Errors > EOF_BEFORE_TAG_NAME > <template></ 1`] = `
exports[`compiler: parse > Errors > EOF_IN_CDATA > <template><svg><![CDATA[ 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -1028,7 +1028,7 @@ exports[`compiler: parse > Errors > EOF_IN_CDATA > <template><svg><![CDATA[ 1`]
exports[`compiler: parse > Errors > EOF_IN_CDATA > <template><svg><![CDATA[cdata 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -1121,7 +1121,7 @@ exports[`compiler: parse > Errors > EOF_IN_CDATA > <template><svg><![CDATA[cdata
exports[`compiler: parse > Errors > EOF_IN_COMMENT > <template><!-- 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [],
@ -1173,7 +1173,7 @@ exports[`compiler: parse > Errors > EOF_IN_COMMENT > <template><!-- 1`] = `
exports[`compiler: parse > Errors > EOF_IN_COMMENT > <template><!--comment 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -1243,7 +1243,7 @@ exports[`compiler: parse > Errors > EOF_IN_COMMENT > <template><!--comment 1`] =
exports[`compiler: parse > Errors > EOF_IN_TAG > <div></div 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [],
@ -1295,7 +1295,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <div></div 1`] = `
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [],
@ -1347,7 +1347,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div 1`] = `
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [],
@ -1399,7 +1399,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div 1`] = `
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [],
@ -1451,7 +1451,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id 1`] = `
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id = 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [],
@ -1503,7 +1503,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id = 1`] = `
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [],
@ -1555,7 +1555,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id 1`] = `
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id="abc 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [],
@ -1607,7 +1607,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id="abc 1`] = `
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id="abc" 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [],
@ -1659,7 +1659,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id="abc" 1`] = `
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id="abc"/ 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -1729,7 +1729,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id="abc"/ 1`] =
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id='abc 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [],
@ -1781,7 +1781,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id='abc 1`] = `
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id='abc' 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [],
@ -1833,7 +1833,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id='abc' 1`] = `
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id='abc'/ 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -1903,7 +1903,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id='abc'/ 1`] =
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id=abc / 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -1973,7 +1973,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id=abc / 1`] = `
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id=abc 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [],
@ -2025,7 +2025,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id=abc 1`] = `
exports[`compiler: parse > Errors > MISSING_ATTRIBUTE_VALUE > <template><div id= /></div></template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -2148,7 +2148,7 @@ exports[`compiler: parse > Errors > MISSING_ATTRIBUTE_VALUE > <template><div id=
exports[`compiler: parse > Errors > MISSING_ATTRIBUTE_VALUE > <template><div id= ></div></template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -2271,7 +2271,7 @@ exports[`compiler: parse > Errors > MISSING_ATTRIBUTE_VALUE > <template><div id=
exports[`compiler: parse > Errors > MISSING_ATTRIBUTE_VALUE > <template><div id=></div></template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -2394,7 +2394,7 @@ exports[`compiler: parse > Errors > MISSING_ATTRIBUTE_VALUE > <template><div id=
exports[`compiler: parse > Errors > MISSING_END_TAG_NAME > <template></></template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [],
@ -2446,7 +2446,7 @@ exports[`compiler: parse > Errors > MISSING_END_TAG_NAME > <template></></templa
exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME > <template><div a"bc=''></div></template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -2569,7 +2569,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME > <te
exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME > <template><div a'bc=''></div></template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -2692,7 +2692,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME > <te
exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME > <template><div a<bc=''></div></template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -2815,7 +2815,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME > <te
exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE > <template><div foo=bar"></div></template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -2938,7 +2938,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_V
exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE > <template><div foo=bar'></div></template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -3061,7 +3061,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_V
exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE > <template><div foo=bar<div></div></template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -3184,7 +3184,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_V
exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE > <template><div foo=bar=baz></div></template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -3307,7 +3307,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_V
exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE > <template><div foo=bar\`></div></template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -3430,7 +3430,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_V
exports[`compiler: parse > Errors > UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME > <template><div =></div></template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -3537,7 +3537,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME
exports[`compiler: parse > Errors > UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME > <template><div =foo=bar></div></template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -3660,7 +3660,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME
exports[`compiler: parse > Errors > UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME > <template><?xml?></template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [],
@ -3712,7 +3712,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME
exports[`compiler: parse > Errors > UNEXPECTED_SOLIDUS_IN_TAG > <template><div a/b></div></template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -3850,7 +3850,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_SOLIDUS_IN_TAG > <template><div a
exports[`compiler: parse > Errors > X_INVALID_END_TAG > <svg><![CDATA[</div>]]></svg> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -3920,7 +3920,7 @@ exports[`compiler: parse > Errors > X_INVALID_END_TAG > <svg><![CDATA[</div>]]><
exports[`compiler: parse > Errors > X_INVALID_END_TAG > <svg><!--</div>--></svg> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -3990,7 +3990,7 @@ exports[`compiler: parse > Errors > X_INVALID_END_TAG > <svg><!--</div>--></svg>
exports[`compiler: parse > Errors > X_INVALID_END_TAG > <template></div></div></template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [],
@ -4042,7 +4042,7 @@ exports[`compiler: parse > Errors > X_INVALID_END_TAG > <template></div></div></
exports[`compiler: parse > Errors > X_INVALID_END_TAG > <template></div></template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [],
@ -4094,7 +4094,7 @@ exports[`compiler: parse > Errors > X_INVALID_END_TAG > <template></div></templa
exports[`compiler: parse > Errors > X_INVALID_END_TAG > <template>{{'</div>'}}</template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -4182,7 +4182,7 @@ exports[`compiler: parse > Errors > X_INVALID_END_TAG > <template>{{'</div>'}}</
exports[`compiler: parse > Errors > X_INVALID_END_TAG > <template>a </ b</template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -4252,7 +4252,7 @@ exports[`compiler: parse > Errors > X_INVALID_END_TAG > <template>a </ b</templa
exports[`compiler: parse > Errors > X_INVALID_END_TAG > <textarea></div></textarea> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -4322,7 +4322,7 @@ exports[`compiler: parse > Errors > X_INVALID_END_TAG > <textarea></div></textar
exports[`compiler: parse > Errors > X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END > <div v-foo:[sef fsef] /> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [],
@ -4446,7 +4446,7 @@ exports[`compiler: parse > Errors > X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END > <
exports[`compiler: parse > Errors > X_MISSING_END_TAG > <template><div> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -4521,7 +4521,7 @@ exports[`compiler: parse > Errors > X_MISSING_END_TAG > <template><div> 1`] = `
exports[`compiler: parse > Errors > X_MISSING_END_TAG > <template><div></template> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -4596,7 +4596,7 @@ exports[`compiler: parse > Errors > X_MISSING_END_TAG > <template><div></templat
exports[`compiler: parse > Errors > X_MISSING_INTERPOLATION_END > <div>{{ foo</div> 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"children": [
@ -4666,7 +4666,7 @@ exports[`compiler: parse > Errors > X_MISSING_INTERPOLATION_END > <div>{{ foo</d
exports[`compiler: parse > Errors > X_MISSING_INTERPOLATION_END > {{ 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"content": "{{",
@ -4713,7 +4713,7 @@ exports[`compiler: parse > Errors > X_MISSING_INTERPOLATION_END > {{ 1`] = `
exports[`compiler: parse > Errors > X_MISSING_INTERPOLATION_END > {{ foo 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"content": "{{ foo",
@ -4760,7 +4760,7 @@ exports[`compiler: parse > Errors > X_MISSING_INTERPOLATION_END > {{ foo 1`] = `
exports[`compiler: parse > Errors > X_MISSING_INTERPOLATION_END > {{}} 1`] = `
{
"cached": 0,
"cached": [],
"children": [
{
"content": {

View File

@ -1,21 +1,5 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`scopeId compiler support > should push scopeId for hoisted nodes 1`] = `
"import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, openBlock as _openBlock, createElementBlock as _createElementBlock, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from "vue"
const _withScopeId = n => (_pushScopeId("test"),n=n(),_popScopeId(),n)
const _hoisted_1 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode("div", null, "hello", -1 /* HOISTED */))
const _hoisted_2 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode("div", null, "world", -1 /* HOISTED */))
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1,
_createTextVNode(_toDisplayString(_ctx.foo), 1 /* TEXT */),
_hoisted_2
]))
}"
`;
exports[`scopeId compiler support > should wrap default slot 1`] = `
"import { createElementVNode as _createElementVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, openBlock as _openBlock, createBlock as _createBlock } from "vue"

View File

@ -47,7 +47,7 @@ function createRoot(options: Partial<RootNode> = {}): RootNode {
directives: [],
imports: [],
hoists: [],
cached: 0,
cached: [],
temps: 0,
codegenNode: createSimpleExpression(`null`, false),
loc: locStub,
@ -422,7 +422,7 @@ describe('compiler: codegen', () => {
test('CacheExpression', () => {
const { code } = generate(
createRoot({
cached: 1,
cached: [],
codegenNode: createCacheExpression(
1,
createSimpleExpression(`foo`, false),
@ -440,7 +440,7 @@ describe('compiler: codegen', () => {
test('CacheExpression w/ isVNode: true', () => {
const { code } = generate(
createRoot({
cached: 1,
cached: [],
codegenNode: createCacheExpression(
1,
createSimpleExpression(`foo`, false),

View File

@ -1,7 +1,4 @@
import { baseCompile } from '../src/compile'
import { POP_SCOPE_ID, PUSH_SCOPE_ID } from '../src/runtimeHelpers'
import { PatchFlags } from '@vue/shared'
import { genFlagText } from './testUtils'
/**
* Ensure all slot functions are wrapped with _withCtx
@ -57,28 +54,4 @@ describe('scopeId compiler support', () => {
expect(code).toMatch(/name: i,\s+fn: _withCtx\(/)
expect(code).toMatchSnapshot()
})
test('should push scopeId for hoisted nodes', () => {
const { ast, code } = baseCompile(
`<div><div>hello</div>{{ foo }}<div>world</div></div>`,
{
mode: 'module',
scopeId: 'test',
hoistStatic: true,
},
)
expect(ast.helpers).toContain(PUSH_SCOPE_ID)
expect(ast.helpers).toContain(POP_SCOPE_ID)
expect(ast.hoists.length).toBe(2)
;[
`const _withScopeId = n => (_pushScopeId("test"),n=n(),_popScopeId(),n)`,
`const _hoisted_1 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode("div", null, "hello", ${genFlagText(
PatchFlags.HOISTED,
)}))`,
`const _hoisted_2 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode("div", null, "world", ${genFlagText(
PatchFlags.HOISTED,
)}))`,
].forEach(c => expect(code).toMatch(c))
expect(code).toMatchSnapshot()
})
})

View File

@ -1,103 +1,87 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`compiler: hoistStatic transform > hoist element with static key 1`] = `
exports[`compiler: cacheStatic transform > cache element with static key 1`] = `
"const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("div", { key: "foo" }, null, -1 /* HOISTED */)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) {
with (_ctx) {
const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _hoisted_2))
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("div", { key: "foo" }, null, -1 /* CACHED */)
])))
}
}"
`;
exports[`compiler: hoistStatic transform > hoist nested static tree 1`] = `
exports[`compiler: cacheStatic transform > cache nested children array 1`] = `
"const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("p", null, [
/*#__PURE__*/_createElementVNode("span"),
/*#__PURE__*/_createElementVNode("span")
], -1 /* HOISTED */)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) {
with (_ctx) {
const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _hoisted_2))
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("p", null, [
_createElementVNode("span"),
_createElementVNode("span")
], -1 /* CACHED */),
_createElementVNode("p", null, [
_createElementVNode("span"),
_createElementVNode("span")
], -1 /* CACHED */)
])))
}
}"
`;
exports[`compiler: hoistStatic transform > hoist nested static tree with comments 1`] = `
exports[`compiler: cacheStatic transform > cache nested static tree with comments 1`] = `
"const _Vue = Vue
const { createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode } = _Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("div", null, [
/*#__PURE__*/_createCommentVNode("comment")
], -1 /* HOISTED */)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) {
with (_ctx) {
const { createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _hoisted_2))
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("div", null, [
_createCommentVNode("comment")
], -1 /* CACHED */)
])))
}
}"
`;
exports[`compiler: hoistStatic transform > hoist siblings with common non-hoistable parent 1`] = `
exports[`compiler: cacheStatic transform > cache siblings including text with common non-hoistable parent 1`] = `
"const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", null, null, -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createElementVNode("div", null, null, -1 /* HOISTED */)
const _hoisted_3 = [
_hoisted_1,
_hoisted_2
]
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] = [
_createElementVNode("span", null, null, -1 /* CACHED */),
_createTextVNode("foo"),
_createElementVNode("div", null, null, -1 /* CACHED */)
])))
}
}"
`;
exports[`compiler: cacheStatic transform > cache single children array 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _hoisted_3))
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("span", { class: "inline" }, "hello", -1 /* CACHED */)
])))
}
}"
`;
exports[`compiler: hoistStatic transform > hoist simple element 1`] = `
"const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", { class: "inline" }, "hello", -1 /* HOISTED */)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) {
with (_ctx) {
const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _hoisted_2))
}
}"
`;
exports[`compiler: hoistStatic transform > hoist static props for elements with directives 1`] = `
exports[`compiler: cacheStatic transform > hoist static props for elements with directives 1`] = `
"const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
@ -118,7 +102,7 @@ return function render(_ctx, _cache) {
}"
`;
exports[`compiler: hoistStatic transform > hoist static props for elements with dynamic text children 1`] = `
exports[`compiler: cacheStatic transform > hoist static props for elements with dynamic text children 1`] = `
"const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
@ -135,7 +119,7 @@ return function render(_ctx, _cache) {
}"
`;
exports[`compiler: hoistStatic transform > hoist static props for elements with unhoistable children 1`] = `
exports[`compiler: cacheStatic transform > hoist static props for elements with unhoistable children 1`] = `
"const _Vue = Vue
const { createVNode: _createVNode, createElementVNode: _createElementVNode } = _Vue
@ -156,7 +140,53 @@ return function render(_ctx, _cache) {
}"
`;
exports[`compiler: hoistStatic transform > prefixIdentifiers > hoist class with static object value 1`] = `
exports[`compiler: cacheStatic transform > prefixIdentifiers > cache nested static tree with static interpolation 1`] = `
"const _Vue = Vue
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] = [
_createElementVNode("span", null, "foo " + _toDisplayString(1) + " " + _toDisplayString(true), -1 /* CACHED */)
])))
}
}"
`;
exports[`compiler: cacheStatic transform > prefixIdentifiers > cache nested static tree with static prop value 1`] = `
"const _Vue = Vue
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] = [
_createElementVNode("span", { foo: 0 }, _toDisplayString(1), -1 /* CACHED */)
])))
}
}"
`;
exports[`compiler: cacheStatic transform > prefixIdentifiers > clone hoisted array children in v-for + HMR mode 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createElementVNode: _createElementVNode } = _Vue
return (_openBlock(), _createElementBlock("div", null, [
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(1, (i) => {
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("span", { class: "hi" }, null, -1 /* CACHED */)
]))]))
}), 256 /* UNKEYED_FRAGMENT */))
]))
}
}"
`;
exports[`compiler: cacheStatic transform > prefixIdentifiers > hoist class with static object value 1`] = `
"const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
@ -175,50 +205,8 @@ return function render(_ctx, _cache) {
}"
`;
exports[`compiler: hoistStatic transform > prefixIdentifiers > hoist nested static tree with static interpolation 1`] = `
exports[`compiler: cacheStatic transform > prefixIdentifiers > should NOT cache SVG with directives 1`] = `
"const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", null, "foo " + /*#__PURE__*/_toDisplayString(1) + " " + /*#__PURE__*/_toDisplayString(true), -1 /* HOISTED */)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _hoisted_2))
}
}"
`;
exports[`compiler: hoistStatic transform > prefixIdentifiers > hoist nested static tree with static prop value 1`] = `
"const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", { foo: 0 }, /*#__PURE__*/_toDisplayString(1), -1 /* HOISTED */)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _hoisted_2))
}
}"
`;
exports[`compiler: hoistStatic transform > prefixIdentifiers > should NOT hoist SVG with directives 1`] = `
"const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("path", { d: "M2,3H5.5L12" }, null, -1 /* HOISTED */)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) {
with (_ctx) {
@ -227,7 +215,9 @@ return function render(_ctx, _cache) {
const _directive_foo = _resolveDirective("foo")
return (_openBlock(), _createElementBlock("div", null, [
_withDirectives((_openBlock(), _createElementBlock("svg", null, _hoisted_2)), [
_withDirectives((_openBlock(), _createElementBlock("svg", null, _cache[0] || (_cache[0] = [
_createElementVNode("path", { d: "M2,3H5.5L12" }, null, -1 /* CACHED */)
]))), [
[_directive_foo]
])
]))
@ -235,7 +225,7 @@ return function render(_ctx, _cache) {
}"
`;
exports[`compiler: hoistStatic transform > prefixIdentifiers > should NOT hoist elements with cached handlers + other bindings 1`] = `
exports[`compiler: cacheStatic transform > prefixIdentifiers > should NOT cache elements with cached handlers + other bindings 1`] = `
"import { normalizeClass as _normalizeClass, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache) {
@ -250,7 +240,7 @@ export function render(_ctx, _cache) {
}"
`;
exports[`compiler: hoistStatic transform > prefixIdentifiers > should NOT hoist elements with cached handlers 1`] = `
exports[`compiler: cacheStatic transform > prefixIdentifiers > should NOT cache elements with cached handlers 1`] = `
"import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache) {
@ -264,7 +254,7 @@ export function render(_ctx, _cache) {
}"
`;
exports[`compiler: hoistStatic transform > prefixIdentifiers > should NOT hoist expressions that refer scope variables (2) 1`] = `
exports[`compiler: cacheStatic transform > prefixIdentifiers > should NOT cache expressions that refer scope variables (2) 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
@ -282,7 +272,7 @@ return function render(_ctx, _cache) {
}"
`;
exports[`compiler: hoistStatic transform > prefixIdentifiers > should NOT hoist expressions that refer scope variables (v-slot) 1`] = `
exports[`compiler: cacheStatic transform > prefixIdentifiers > should NOT cache expressions that refer scope variables (v-slot) 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
@ -301,7 +291,7 @@ return function render(_ctx, _cache) {
}"
`;
exports[`compiler: hoistStatic transform > prefixIdentifiers > should NOT hoist expressions that refer scope variables 1`] = `
exports[`compiler: cacheStatic transform > prefixIdentifiers > should NOT cache expressions that refer scope variables 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
@ -319,7 +309,7 @@ return function render(_ctx, _cache) {
}"
`;
exports[`compiler: hoistStatic transform > prefixIdentifiers > should NOT hoist keyed template v-for with plain element child 1`] = `
exports[`compiler: cacheStatic transform > prefixIdentifiers > should NOT cache keyed template v-for with plain element child 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
@ -335,7 +325,7 @@ return function render(_ctx, _cache) {
}"
`;
exports[`compiler: hoistStatic transform > should NOT hoist components 1`] = `
exports[`compiler: cacheStatic transform > should NOT cache components 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
@ -351,7 +341,7 @@ return function render(_ctx, _cache) {
}"
`;
exports[`compiler: hoistStatic transform > should NOT hoist element with dynamic key 1`] = `
exports[`compiler: cacheStatic transform > should NOT cache element with dynamic key 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
@ -365,7 +355,7 @@ return function render(_ctx, _cache) {
}"
`;
exports[`compiler: hoistStatic transform > should NOT hoist element with dynamic props (but hoist the props list) 1`] = `
exports[`compiler: cacheStatic transform > should NOT cache element with dynamic props (but hoist the props list) 1`] = `
"const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
@ -382,7 +372,7 @@ return function render(_ctx, _cache) {
}"
`;
exports[`compiler: hoistStatic transform > should NOT hoist element with dynamic ref 1`] = `
exports[`compiler: cacheStatic transform > should NOT cache element with dynamic ref 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
@ -396,42 +386,7 @@ return function render(_ctx, _cache) {
}"
`;
exports[`compiler: hoistStatic transform > should NOT hoist root node 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div"))
}
}"
`;
exports[`compiler: hoistStatic transform > should hoist v-for children if static 1`] = `
"const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = { id: "foo" }
const _hoisted_2 = /*#__PURE__*/_createElementVNode("span", null, null, -1 /* HOISTED */)
const _hoisted_3 = [
_hoisted_2
]
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createElementVNode: _createElementVNode } = _Vue
return (_openBlock(), _createElementBlock("div", null, [
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(list, (i) => {
return (_openBlock(), _createElementBlock("div", _hoisted_1, _hoisted_3))
}), 256 /* UNKEYED_FRAGMENT */))
]))
}
}"
`;
exports[`compiler: hoistStatic transform > should hoist v-if props/children if static 1`] = `
exports[`compiler: cacheStatic transform > should cache v-if props/children if static 1`] = `
"const _Vue = Vue
const { createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode } = _Vue
@ -439,10 +394,6 @@ const _hoisted_1 = {
key: 0,
id: "foo"
}
const _hoisted_2 = /*#__PURE__*/_createElementVNode("span", null, null, -1 /* HOISTED */)
const _hoisted_3 = [
_hoisted_2
]
return function render(_ctx, _cache) {
with (_ctx) {
@ -450,9 +401,32 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
ok
? (_openBlock(), _createElementBlock("div", _hoisted_1, _hoisted_3))
? (_openBlock(), _createElementBlock("div", _hoisted_1, _cache[0] || (_cache[0] = [
_createElementVNode("span", null, null, -1 /* CACHED */)
])))
: _createCommentVNode("v-if", true)
]))
}
}"
`;
exports[`compiler: cacheStatic transform > should hoist v-for children if static 1`] = `
"const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = { id: "foo" }
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createElementVNode: _createElementVNode } = _Vue
return (_openBlock(), _createElementBlock("div", null, [
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(list, (i) => {
return (_openBlock(), _createElementBlock("div", _hoisted_1, _cache[0] || (_cache[0] = [
_createElementVNode("span", null, null, -1 /* CACHED */)
])))
}), 256 /* UNKEYED_FRAGMENT */))
]))
}
}"
`;

View File

@ -25,18 +25,33 @@ import { createObjectMatcher, genFlagText } from '../testUtils'
import { transformText } from '../../src/transforms/transformText'
import { PatchFlags } from '@vue/shared'
const hoistedChildrenArrayMatcher = (startIndex = 1, length = 1) => ({
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: new Array(length).fill(0).map((_, i) => ({
type: NodeTypes.ELEMENT,
codegenNode: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_hoisted_${startIndex + i}`,
},
})),
const cachedChildrenArrayMatcher = (
tags: string[],
needArraySpread = false,
) => ({
type: NodeTypes.JS_CACHE_EXPRESSION,
needArraySpread,
value: {
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: tags.map(tag => {
if (tag === '') {
return {
type: NodeTypes.TEXT_CALL,
}
} else {
return {
type: NodeTypes.ELEMENT,
codegenNode: {
type: NodeTypes.VNODE_CALL,
tag: JSON.stringify(tag),
},
}
}
}),
},
})
function transformWithHoist(template: string, options: CompilerOptions = {}) {
function transformWithCache(template: string, options: CompilerOptions = {}) {
const ast = parse(template)
transform(ast, {
hoistStatic: true,
@ -60,101 +75,253 @@ function transformWithHoist(template: string, options: CompilerOptions = {}) {
return ast
}
describe('compiler: hoistStatic transform', () => {
test('should NOT hoist root node', () => {
describe('compiler: cacheStatic transform', () => {
test('should NOT cache root node', () => {
// if the whole tree is static, the root still needs to be a block
// so that it's patched in optimized mode to skip children
const root = transformWithHoist(`<div/>`)
expect(root.hoists.length).toBe(0)
const root = transformWithCache(`<div/>`)
expect(root.codegenNode).toMatchObject({
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
})
expect(generate(root).code).toMatchSnapshot()
expect(root.cached.length).toBe(0)
})
test('hoist simple element', () => {
const root = transformWithHoist(
test('cache root node children', () => {
// we don't have access to the root codegenNode during the transform
// so we only cache each child individually
const root = transformWithCache(
`<span class="inline">hello</span><span class="inline">hello</span>`,
)
expect(root.codegenNode).toMatchObject({
type: NodeTypes.VNODE_CALL,
children: [
{ codegenNode: { type: NodeTypes.JS_CACHE_EXPRESSION } },
{ codegenNode: { type: NodeTypes.JS_CACHE_EXPRESSION } },
],
})
expect(root.cached.length).toBe(2)
})
test('cache single children array', () => {
const root = transformWithCache(
`<div><span class="inline">hello</span></div>`,
)
expect(root.hoists).toMatchObject([
{
type: NodeTypes.VNODE_CALL,
tag: `"span"`,
props: createObjectMatcher({ class: 'inline' }),
children: {
type: NodeTypes.TEXT,
content: `hello`,
},
},
hoistedChildrenArrayMatcher(),
])
expect(root.codegenNode).toMatchObject({
tag: `"div"`,
props: undefined,
children: { content: `_hoisted_2` },
children: cachedChildrenArrayMatcher(['span']),
})
expect(root.cached.length).toBe(1)
expect(generate(root).code).toMatchSnapshot()
})
test('hoist nested static tree', () => {
const root = transformWithHoist(`<div><p><span/><span/></p></div>`)
expect(root.hoists).toMatchObject([
{
type: NodeTypes.VNODE_CALL,
tag: `"p"`,
props: undefined,
children: [
{ type: NodeTypes.ELEMENT, tag: `span` },
{ type: NodeTypes.ELEMENT, tag: `span` },
],
},
hoistedChildrenArrayMatcher(),
])
test('cache nested children array', () => {
const root = transformWithCache(
`<div><p><span/><span/></p><p><span/><span/></p></div>`,
)
expect((root.codegenNode as VNodeCall).children).toMatchObject(
cachedChildrenArrayMatcher(['p', 'p']),
)
expect(root.cached.length).toBe(1)
expect(generate(root).code).toMatchSnapshot()
})
test('cache nested static tree with comments', () => {
const root = transformWithCache(`<div><div><!--comment--></div></div>`)
expect((root.codegenNode as VNodeCall).children).toMatchObject(
cachedChildrenArrayMatcher(['div']),
)
expect(root.cached.length).toBe(1)
expect(generate(root).code).toMatchSnapshot()
})
test('cache siblings including text with common non-hoistable parent', () => {
const root = transformWithCache(`<div><span/>foo<div/></div>`)
expect((root.codegenNode as VNodeCall).children).toMatchObject(
cachedChildrenArrayMatcher(['span', '', 'div']),
)
expect(root.cached.length).toBe(1)
expect(generate(root).code).toMatchSnapshot()
})
test('cache inside default slot', () => {
const root = transformWithCache(`<Foo>{{x}}<span/></Foo>`)
expect((root.codegenNode as VNodeCall).children).toMatchObject({
content: '_hoisted_2',
properties: [
{
key: { content: 'default' },
value: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
returns: [
{
type: NodeTypes.TEXT_CALL,
},
// first slot child cached
{
type: NodeTypes.ELEMENT,
codegenNode: {
type: NodeTypes.JS_CACHE_EXPRESSION,
},
},
],
},
},
{
/* _ slot flag */
},
],
})
expect(generate(root).code).toMatchSnapshot()
})
test('hoist nested static tree with comments', () => {
const root = transformWithHoist(`<div><div><!--comment--></div></div>`)
expect(root.hoists).toMatchObject([
{
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
props: undefined,
children: [{ type: NodeTypes.COMMENT, content: `comment` }],
},
hoistedChildrenArrayMatcher(),
])
test('cache default slot as a whole', () => {
const root = transformWithCache(`<Foo><span/><span/></Foo>`)
expect((root.codegenNode as VNodeCall).children).toMatchObject({
content: `_hoisted_2`,
properties: [
{
key: { content: 'default' },
value: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
returns: {
type: NodeTypes.JS_CACHE_EXPRESSION,
value: {
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: [
{ type: NodeTypes.ELEMENT },
{ type: NodeTypes.ELEMENT },
],
},
},
},
},
{
/* _ slot flag */
},
],
})
expect(generate(root).code).toMatchSnapshot()
})
test('hoist siblings with common non-hoistable parent', () => {
const root = transformWithHoist(`<div><span/><div/></div>`)
expect(root.hoists).toMatchObject([
{
type: NodeTypes.VNODE_CALL,
tag: `"span"`,
},
{
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
},
hoistedChildrenArrayMatcher(1, 2),
])
test('cache inside named slot', () => {
const root = transformWithCache(
`<Foo><template #foo>{{x}}<span/></template></Foo>`,
)
expect((root.codegenNode as VNodeCall).children).toMatchObject({
content: '_hoisted_3',
properties: [
{
key: { content: 'foo' },
value: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
returns: [
{
type: NodeTypes.TEXT_CALL,
},
// first slot child cached
{
type: NodeTypes.ELEMENT,
codegenNode: {
type: NodeTypes.JS_CACHE_EXPRESSION,
},
},
],
},
},
{
/* _ slot flag */
},
],
})
expect(generate(root).code).toMatchSnapshot()
})
test('should NOT hoist components', () => {
const root = transformWithHoist(`<div><Comp/></div>`)
expect(root.hoists.length).toBe(0)
test('cache named slot as a whole', () => {
const root = transformWithCache(
`<Foo><template #foo><span/><span/></template></Foo>`,
)
expect((root.codegenNode as VNodeCall).children).toMatchObject({
properties: [
{
key: { content: 'foo' },
value: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
returns: {
type: NodeTypes.JS_CACHE_EXPRESSION,
value: {
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: [
{ type: NodeTypes.ELEMENT },
{ type: NodeTypes.ELEMENT },
],
},
},
},
},
{
/* _ slot flag */
},
],
})
})
test('cache dynamically named slot as a whole', () => {
const root = transformWithCache(
`<Foo><template #[foo]><span/><span/></template></Foo>`,
)
expect((root.codegenNode as VNodeCall).children).toMatchObject({
properties: [
{
key: { content: 'foo', isStatic: false },
value: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
returns: {
type: NodeTypes.JS_CACHE_EXPRESSION,
value: {
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: [
{ type: NodeTypes.ELEMENT },
{ type: NodeTypes.ELEMENT },
],
},
},
},
},
{
/* _ slot flag */
},
],
})
})
test('cache dynamically named (expression) slot as a whole', () => {
const root = transformWithCache(
`<Foo><template #[foo+1]><span/><span/></template></Foo>`,
{ prefixIdentifiers: true },
)
expect((root.codegenNode as VNodeCall).children).toMatchObject({
properties: [
{
key: { type: NodeTypes.COMPOUND_EXPRESSION },
value: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
returns: {
type: NodeTypes.JS_CACHE_EXPRESSION,
value: {
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: [
{ type: NodeTypes.ELEMENT },
{ type: NodeTypes.ELEMENT },
],
},
},
},
},
{
/* _ slot flag */
},
],
})
})
test('should NOT cache components', () => {
const root = transformWithCache(`<div><Comp/></div>`)
expect((root.codegenNode as VNodeCall).children).toMatchObject([
{
type: NodeTypes.ELEMENT,
@ -164,11 +331,12 @@ describe('compiler: hoistStatic transform', () => {
},
},
])
expect(root.cached.length).toBe(0)
expect(generate(root).code).toMatchSnapshot()
})
test('should NOT hoist element with dynamic props (but hoist the props list)', () => {
const root = transformWithHoist(`<div><div :id="foo"/></div>`)
test('should NOT cache element with dynamic props (but hoist the props list)', () => {
const root = transformWithCache(`<div><div :id="foo"/></div>`)
expect(root.hoists.length).toBe(1)
expect((root.codegenNode as VNodeCall).children).toMatchObject([
{
@ -189,31 +357,23 @@ describe('compiler: hoistStatic transform', () => {
},
},
])
expect(root.cached.length).toBe(0)
expect(generate(root).code).toMatchSnapshot()
})
test('hoist element with static key', () => {
const root = transformWithHoist(`<div><div key="foo"/></div>`)
expect(root.hoists.length).toBe(2)
expect(root.hoists).toMatchObject([
{
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
props: createObjectMatcher({ key: 'foo' }),
},
hoistedChildrenArrayMatcher(),
])
test('cache element with static key', () => {
const root = transformWithCache(`<div><div key="foo"/></div>`)
expect(root.codegenNode).toMatchObject({
tag: `"div"`,
props: undefined,
children: { content: `_hoisted_2` },
children: cachedChildrenArrayMatcher(['div']),
})
expect(root.cached.length).toBe(1)
expect(generate(root).code).toMatchSnapshot()
})
test('should NOT hoist element with dynamic key', () => {
const root = transformWithHoist(`<div><div :key="foo"/></div>`)
expect(root.hoists.length).toBe(0)
test('should NOT cache element with dynamic key', () => {
const root = transformWithCache(`<div><div :key="foo"/></div>`)
expect((root.codegenNode as VNodeCall).children).toMatchObject([
{
type: NodeTypes.ELEMENT,
@ -226,12 +386,12 @@ describe('compiler: hoistStatic transform', () => {
},
},
])
expect(root.cached.length).toBe(0)
expect(generate(root).code).toMatchSnapshot()
})
test('should NOT hoist element with dynamic ref', () => {
const root = transformWithHoist(`<div><div :ref="foo"/></div>`)
expect(root.hoists.length).toBe(0)
test('should NOT cache element with dynamic ref', () => {
const root = transformWithCache(`<div><div :ref="foo"/></div>`)
expect((root.codegenNode as VNodeCall).children).toMatchObject([
{
type: NodeTypes.ELEMENT,
@ -246,11 +406,12 @@ describe('compiler: hoistStatic transform', () => {
},
},
])
expect(root.cached.length).toBe(0)
expect(generate(root).code).toMatchSnapshot()
})
test('hoist static props for elements with directives', () => {
const root = transformWithHoist(`<div><div id="foo" v-foo/></div>`)
const root = transformWithCache(`<div><div id="foo" v-foo/></div>`)
expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })])
expect((root.codegenNode as VNodeCall).children).toMatchObject([
{
@ -270,11 +431,12 @@ describe('compiler: hoistStatic transform', () => {
},
},
])
expect(root.cached.length).toBe(0)
expect(generate(root).code).toMatchSnapshot()
})
test('hoist static props for elements with dynamic text children', () => {
const root = transformWithHoist(
const root = transformWithCache(
`<div><div id="foo">{{ hello }}</div></div>`,
)
expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })])
@ -290,11 +452,12 @@ describe('compiler: hoistStatic transform', () => {
},
},
])
expect(root.cached.length).toBe(0)
expect(generate(root).code).toMatchSnapshot()
})
test('hoist static props for elements with unhoistable children', () => {
const root = transformWithHoist(`<div><div id="foo"><Comp/></div></div>`)
const root = transformWithCache(`<div><div id="foo"><Comp/></div></div>`)
expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })])
expect((root.codegenNode as VNodeCall).children).toMatchObject([
{
@ -307,11 +470,12 @@ describe('compiler: hoistStatic transform', () => {
},
},
])
expect(root.cached.length).toBe(0)
expect(generate(root).code).toMatchSnapshot()
})
test('should hoist v-if props/children if static', () => {
const root = transformWithHoist(
test('should cache v-if props/children if static', () => {
const root = transformWithCache(
`<div><div v-if="ok" id="foo"><span/></div></div>`,
)
expect(root.hoists).toMatchObject([
@ -319,40 +483,31 @@ describe('compiler: hoistStatic transform', () => {
key: `[0]`, // key injected by v-if branch
id: 'foo',
}),
{
type: NodeTypes.VNODE_CALL,
tag: `"span"`,
},
hoistedChildrenArrayMatcher(2),
])
expect(
((root.children[0] as ElementNode).children[0] as IfNode).codegenNode,
).toMatchObject({
type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
consequent: {
// blocks should NOT be hoisted
// blocks should NOT be cached
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
props: { content: `_hoisted_1` },
children: { content: `_hoisted_3` },
children: cachedChildrenArrayMatcher(['span']),
},
})
expect(root.cached.length).toBe(1)
expect(generate(root).code).toMatchSnapshot()
})
test('should hoist v-for children if static', () => {
const root = transformWithHoist(
const root = transformWithCache(
`<div><div v-for="i in list" id="foo"><span/></div></div>`,
)
expect(root.hoists).toMatchObject([
createObjectMatcher({
id: 'foo',
}),
{
type: NodeTypes.VNODE_CALL,
tag: `"span"`,
},
hoistedChildrenArrayMatcher(2),
])
const forBlockCodegen = (
(root.children[0] as ElementNode).children[0] as ForNode
@ -372,78 +527,47 @@ describe('compiler: hoistStatic transform', () => {
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
props: { content: `_hoisted_1` },
children: { content: `_hoisted_3` },
children: cachedChildrenArrayMatcher(['span']),
})
expect(root.cached.length).toBe(1)
expect(generate(root).code).toMatchSnapshot()
})
describe('prefixIdentifiers', () => {
test('hoist nested static tree with static interpolation', () => {
const root = transformWithHoist(
test('cache nested static tree with static interpolation', () => {
const root = transformWithCache(
`<div><span>foo {{ 1 }} {{ true }}</span></div>`,
{
prefixIdentifiers: true,
},
)
expect(root.hoists).toMatchObject([
{
type: NodeTypes.VNODE_CALL,
tag: `"span"`,
props: undefined,
children: {
type: NodeTypes.COMPOUND_EXPRESSION,
},
},
hoistedChildrenArrayMatcher(),
])
expect(root.codegenNode).toMatchObject({
tag: `"div"`,
props: undefined,
children: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_hoisted_2`,
},
children: cachedChildrenArrayMatcher(['span']),
})
expect(root.cached.length).toBe(1)
expect(generate(root).code).toMatchSnapshot()
})
test('hoist nested static tree with static prop value', () => {
const root = transformWithHoist(
test('cache nested static tree with static prop value', () => {
const root = transformWithCache(
`<div><span :foo="0">{{ 1 }}</span></div>`,
{
prefixIdentifiers: true,
},
)
expect(root.hoists).toMatchObject([
{
type: NodeTypes.VNODE_CALL,
tag: `"span"`,
props: createObjectMatcher({ foo: `[0]` }),
children: {
type: NodeTypes.INTERPOLATION,
content: {
content: `1`,
isStatic: false,
constType: ConstantTypes.CAN_STRINGIFY,
},
},
},
hoistedChildrenArrayMatcher(),
])
expect(root.codegenNode).toMatchObject({
tag: `"div"`,
props: undefined,
children: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_hoisted_2`,
},
children: cachedChildrenArrayMatcher(['span']),
})
expect(root.cached.length).toBe(1)
expect(generate(root).code).toMatchSnapshot()
})
test('hoist class with static object value', () => {
const root = transformWithHoist(
const root = transformWithCache(
`<div><span :class="{ foo: true }">{{ bar }}</span></div>`,
{
prefixIdentifiers: true,
@ -504,44 +628,44 @@ describe('compiler: hoistStatic transform', () => {
expect(generate(root).code).toMatchSnapshot()
})
test('should NOT hoist expressions that refer scope variables', () => {
const root = transformWithHoist(
test('should NOT cache expressions that refer scope variables', () => {
const root = transformWithCache(
`<div><p v-for="o in list"><span>{{ o }}</span></p></div>`,
{
prefixIdentifiers: true,
},
)
expect(root.hoists.length).toBe(0)
expect(root.cached.length).toBe(0)
expect(generate(root).code).toMatchSnapshot()
})
test('should NOT hoist expressions that refer scope variables (2)', () => {
const root = transformWithHoist(
test('should NOT cache expressions that refer scope variables (2)', () => {
const root = transformWithCache(
`<div><p v-for="o in list"><span>{{ o + 'foo' }}</span></p></div>`,
{
prefixIdentifiers: true,
},
)
expect(root.hoists.length).toBe(0)
expect(root.cached.length).toBe(0)
expect(generate(root).code).toMatchSnapshot()
})
test('should NOT hoist expressions that refer scope variables (v-slot)', () => {
const root = transformWithHoist(
test('should NOT cache expressions that refer scope variables (v-slot)', () => {
const root = transformWithCache(
`<Comp v-slot="{ foo }">{{ foo }}</Comp>`,
{
prefixIdentifiers: true,
},
)
expect(root.hoists.length).toBe(0)
expect(root.cached.length).toBe(0)
expect(generate(root).code).toMatchSnapshot()
})
test('should NOT hoist elements with cached handlers', () => {
const root = transformWithHoist(
test('should NOT cache elements with cached handlers', () => {
const root = transformWithCache(
`<div><div><div @click="foo"/></div></div>`,
{
prefixIdentifiers: true,
@ -549,7 +673,7 @@ describe('compiler: hoistStatic transform', () => {
},
)
expect(root.cached).toBe(1)
expect(root.cached.length).toBe(1)
expect(root.hoists.length).toBe(0)
expect(
generate(root, {
@ -559,8 +683,8 @@ describe('compiler: hoistStatic transform', () => {
).toMatchSnapshot()
})
test('should NOT hoist elements with cached handlers + other bindings', () => {
const root = transformWithHoist(
test('should NOT cache elements with cached handlers + other bindings', () => {
const root = transformWithCache(
`<div><div><div :class="{}" @click="foo"/></div></div>`,
{
prefixIdentifiers: true,
@ -568,7 +692,7 @@ describe('compiler: hoistStatic transform', () => {
},
)
expect(root.cached).toBe(1)
expect(root.cached.length).toBe(1)
expect(root.hoists.length).toBe(0)
expect(
generate(root, {
@ -578,32 +702,66 @@ describe('compiler: hoistStatic transform', () => {
).toMatchSnapshot()
})
test('should NOT hoist keyed template v-for with plain element child', () => {
const root = transformWithHoist(
test('should NOT cache keyed template v-for with plain element child', () => {
const root = transformWithCache(
`<div><template v-for="item in items" :key="item"><span/></template></div>`,
)
expect(root.hoists.length).toBe(0)
expect(generate(root).code).toMatchSnapshot()
})
test('should NOT hoist SVG with directives', () => {
const root = transformWithHoist(
test('should NOT cache SVG with directives', () => {
const root = transformWithCache(
`<div><svg v-foo><path d="M2,3H5.5L12"/></svg></div>`,
)
expect(root.hoists.length).toBe(2)
expect(root.cached.length).toBe(1)
expect(root.codegenNode).toMatchObject({
children: [
{
tag: 'svg',
// only cache the children, not the svg tag itself
codegenNode: {
children: {
type: NodeTypes.JS_CACHE_EXPRESSION,
},
},
},
],
})
expect(generate(root).code).toMatchSnapshot()
})
test('clone hoisted array children in HMR mode', () => {
const root = transformWithHoist(`<div><span class="hi"></span></div>`, {
hmr: true,
})
expect(root.hoists.length).toBe(2)
expect(root.codegenNode).toMatchObject({
children: {
content: '[..._hoisted_2]',
test('clone hoisted array children in v-for + HMR mode', () => {
const root = transformWithCache(
`<div><div v-for="i in 1"><span class="hi"></span></div></div>`,
{
hmr: true,
},
)
expect(root.cached.length).toBe(1)
const forBlockCodegen = (
(root.children[0] as ElementNode).children[0] as ForNode
).codegenNode
expect(forBlockCodegen).toMatchObject({
type: NodeTypes.VNODE_CALL,
tag: FRAGMENT,
props: undefined,
children: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_LIST,
},
patchFlag: genFlagText(PatchFlags.UNKEYED_FRAGMENT),
})
const innerBlockCodegen = forBlockCodegen!.children.arguments[1]
expect(innerBlockCodegen.returns).toMatchObject({
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
children: cachedChildrenArrayMatcher(
['span'],
true /* needArraySpread */,
),
})
expect(generate(root).code).toMatchSnapshot()
})
})
})

View File

@ -18,7 +18,7 @@ import {
import { ErrorCodes } from '../../src/errors'
import { type CompilerOptions, generate } from '../../src'
import { FRAGMENT, RENDER_LIST, RENDER_SLOT } from '../../src/runtimeHelpers'
import { PatchFlags } from '@vue/shared'
import { PatchFlagNames, PatchFlags } from '@vue/shared'
import { createObjectMatcher, genFlagText } from '../testUtils'
export function parseWithForTransform(
@ -202,6 +202,18 @@ describe('compiler: v-for', () => {
expect(forNode.valueAlias).toBeUndefined()
expect((forNode.source as SimpleExpressionNode).content).toBe('items')
})
test('source containing string expression with spaces', () => {
const { node: forNode } = parseWithForTransform(
`<span v-for="item in state ['my items']" />`,
)
expect(forNode.keyAlias).toBeUndefined()
expect(forNode.objectIndexAlias).toBeUndefined()
expect((forNode.valueAlias as SimpleExpressionNode).content).toBe('item')
expect((forNode.source as SimpleExpressionNode).content).toBe(
"state ['my items']",
)
})
})
describe('errors', () => {
@ -253,6 +265,18 @@ describe('compiler: v-for', () => {
)
})
test('missing source and have multiple spaces with', () => {
const onError = vi.fn()
parseWithForTransform('<span v-for="item in " />', { onError })
expect(onError).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({
code: ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION,
}),
)
})
test('missing value', () => {
const onError = vi.fn()
parseWithForTransform('<span v-for="in items" />', { onError })
@ -1019,5 +1043,33 @@ describe('compiler: v-for', () => {
})
expect(generate(root).code).toMatchSnapshot()
})
test('template v-for key w/ :key shorthand on div', () => {
const {
node: { codegenNode },
} = parseWithForTransform('<div v-for="key in keys" :key>test</div>')
expect(codegenNode.patchFlag).toBe(
`${PatchFlags.KEYED_FRAGMENT} /* ${PatchFlagNames[PatchFlags.KEYED_FRAGMENT]} */`,
)
})
test('template v-for key w/ :key shorthand on template injected to the child', () => {
const {
node: { codegenNode },
} = parseWithForTransform(
'<template v-for="key in keys" :key><div>test</div></template>',
)
expect(assertSharedCodegen(codegenNode, true)).toMatchObject({
source: { content: `keys` },
params: [{ content: `key` }],
innerVNodeCall: {
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
props: createObjectMatcher({
key: '[key]',
}),
},
})
})
})
})

View File

@ -399,7 +399,7 @@ describe('compiler: transform v-model', () => {
prefixIdentifiers: true,
cacheHandlers: true,
})
expect(root.cached).toBe(1)
expect(root.cached.length).toBe(1)
const codegen = (root.children[0] as PlainElementNode)
.codegenNode as VNodeCall
// should not list cached prop in dynamicProps
@ -417,7 +417,7 @@ describe('compiler: transform v-model', () => {
cacheHandlers: true,
},
)
expect(root.cached).toBe(0)
expect(root.cached.length).toBe(0)
const codegen = (
(root.children[0] as ForNode).children[0] as PlainElementNode
).codegenNode as VNodeCall
@ -433,7 +433,7 @@ describe('compiler: transform v-model', () => {
cacheHandlers: true,
})
expect(root.cached).not.toBe(2)
expect(root.cached).toBe(1)
expect(root.cached.length).toBe(1)
})
test('should mark update handler dynamic if it refers slot scope variables', () => {

View File

@ -10,6 +10,7 @@ import {
baseParse as parse,
transform,
} from '../../src'
import { transformFor } from '../../src/transforms/vFor'
import { transformOn } from '../../src/transforms/vOn'
import { transformElement } from '../../src/transforms/transformElement'
import { transformExpression } from '../../src/transforms/transformExpression'
@ -17,7 +18,7 @@ import { transformExpression } from '../../src/transforms/transformExpression'
function parseWithVOn(template: string, options: CompilerOptions = {}) {
const ast = parse(template, options)
transform(ast, {
nodeTransforms: [transformExpression, transformElement],
nodeTransforms: [transformExpression, transformElement, transformFor],
directiveTransforms: {
on: transformOn,
},
@ -504,7 +505,7 @@ describe('compiler: transform v-on', () => {
prefixIdentifiers: true,
cacheHandlers: true,
})
expect(root.cached).toBe(1)
expect(root.cached.length).toBe(1)
const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags
expect(vnodeCall.patchFlag).toBeUndefined()
@ -525,7 +526,7 @@ describe('compiler: transform v-on', () => {
prefixIdentifiers: true,
cacheHandlers: true,
})
expect(root.cached).toBe(1)
expect(root.cached.length).toBe(1)
const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags
expect(vnodeCall.patchFlag).toBeUndefined()
@ -550,7 +551,7 @@ describe('compiler: transform v-on', () => {
prefixIdentifiers: true,
cacheHandlers: true,
})
expect(root.cached).toBe(1)
expect(root.cached.length).toBe(1)
const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags
expect(vnodeCall.patchFlag).toBeUndefined()
@ -587,7 +588,7 @@ describe('compiler: transform v-on', () => {
cacheHandlers: true,
isNativeTag: tag => tag === 'div',
})
expect(root.cached).toBe(0)
expect(root.cached.length).toBe(0)
})
test('should not be cached inside v-once', () => {
@ -598,8 +599,19 @@ describe('compiler: transform v-on', () => {
cacheHandlers: true,
},
)
expect(root.cached).not.toBe(2)
expect(root.cached).toBe(1)
expect(root.cached.length).not.toBe(2)
expect(root.cached.length).toBe(1)
})
test('unicode identifier should not be cached (v-for)', () => {
const { root } = parseWithVOn(
`<div v-for="项 in items" :key="value"><div v-on:click="foo(项)"/></div>`,
{
prefixIdentifiers: true,
cacheHandlers: true,
},
)
expect(root.cached.length).toBe(0)
})
test('inline function expression handler', () => {
@ -607,7 +619,7 @@ describe('compiler: transform v-on', () => {
prefixIdentifiers: true,
cacheHandlers: true,
})
expect(root.cached).toBe(1)
expect(root.cached.length).toBe(1)
const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags
expect(vnodeCall.patchFlag).toBeUndefined()
@ -631,7 +643,7 @@ describe('compiler: transform v-on', () => {
cacheHandlers: true,
},
)
expect(root.cached).toBe(1)
expect(root.cached.length).toBe(1)
const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags
expect(vnodeCall.patchFlag).toBeUndefined()
@ -656,7 +668,7 @@ describe('compiler: transform v-on', () => {
},
)
expect(root.cached).toBe(1)
expect(root.cached.length).toBe(1)
const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags
expect(vnodeCall.patchFlag).toBeUndefined()
@ -688,7 +700,7 @@ describe('compiler: transform v-on', () => {
cacheHandlers: true,
},
)
expect(root.cached).toBe(1)
expect(root.cached.length).toBe(1)
const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags
expect(vnodeCall.patchFlag).toBeUndefined()
@ -713,8 +725,8 @@ describe('compiler: transform v-on', () => {
prefixIdentifiers: true,
cacheHandlers: true,
})
expect(root.cached).toBe(1)
expect(root.cached).toBe(1)
expect(root.cached.length).toBe(1)
expect(root.cached.length).toBe(1)
const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags
expect(vnodeCall.patchFlag).toBeUndefined()

View File

@ -22,7 +22,7 @@ function transformWithOnce(template: string, options: CompilerOptions = {}) {
describe('compiler: v-once transform', () => {
test('as root node', () => {
const root = transformWithOnce(`<div :id="foo" v-once />`)
expect(root.cached).toBe(1)
expect(root.cached.length).toBe(1)
expect(root.helpers).toContain(SET_BLOCK_TRACKING)
expect(root.codegenNode).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
@ -37,7 +37,7 @@ describe('compiler: v-once transform', () => {
test('on nested plain element', () => {
const root = transformWithOnce(`<div><div :id="foo" v-once /></div>`)
expect(root.cached).toBe(1)
expect(root.cached.length).toBe(1)
expect(root.helpers).toContain(SET_BLOCK_TRACKING)
expect((root.children[0] as any).children[0].codegenNode).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
@ -52,7 +52,7 @@ describe('compiler: v-once transform', () => {
test('on component', () => {
const root = transformWithOnce(`<div><Comp :id="foo" v-once /></div>`)
expect(root.cached).toBe(1)
expect(root.cached.length).toBe(1)
expect(root.helpers).toContain(SET_BLOCK_TRACKING)
expect((root.children[0] as any).children[0].codegenNode).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
@ -67,7 +67,7 @@ describe('compiler: v-once transform', () => {
test('on slot outlet', () => {
const root = transformWithOnce(`<div><slot v-once /></div>`)
expect(root.cached).toBe(1)
expect(root.cached.length).toBe(1)
expect(root.helpers).toContain(SET_BLOCK_TRACKING)
expect((root.children[0] as any).children[0].codegenNode).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
@ -84,7 +84,7 @@ describe('compiler: v-once transform', () => {
test('inside v-once', () => {
const root = transformWithOnce(`<div v-once><div v-once/></div>`)
expect(root.cached).not.toBe(2)
expect(root.cached).toBe(1)
expect(root.cached.length).toBe(1)
})
// cached nodes should be ignored by hoistStatic transform
@ -92,7 +92,7 @@ describe('compiler: v-once transform', () => {
const root = transformWithOnce(`<div><div v-once /></div>`, {
hoistStatic: true,
})
expect(root.cached).toBe(1)
expect(root.cached.length).toBe(1)
expect(root.helpers).toContain(SET_BLOCK_TRACKING)
expect(root.hoists.length).toBe(0)
expect((root.children[0] as any).children[0].codegenNode).toMatchObject({
@ -108,7 +108,7 @@ describe('compiler: v-once transform', () => {
test('with v-if/else', () => {
const root = transformWithOnce(`<div v-if="BOOLEAN" v-once /><p v-else/>`)
expect(root.cached).toBe(1)
expect(root.cached.length).toBe(1)
expect(root.helpers).toContain(SET_BLOCK_TRACKING)
expect(root.children[0]).toMatchObject({
type: NodeTypes.IF,
@ -132,7 +132,7 @@ describe('compiler: v-once transform', () => {
test('with v-for', () => {
const root = transformWithOnce(`<div v-for="i in list" v-once />`)
expect(root.cached).toBe(1)
expect(root.cached.length).toBe(1)
expect(root.helpers).toContain(SET_BLOCK_TRACKING)
expect(root.children[0]).toMatchObject({
type: NodeTypes.FOR,

View File

@ -46,13 +46,13 @@
},
"homepage": "https://github.com/vuejs/core-vapor/tree/main/packages/compiler-core#readme",
"dependencies": {
"@babel/parser": "^7.24.6",
"@babel/parser": "^7.24.7",
"@vue/shared": "workspace:*",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.0"
},
"devDependencies": {
"@babel/types": "^7.24.6"
"@babel/types": "^7.24.7"
}
}

View File

@ -117,7 +117,7 @@ export interface RootNode extends Node {
directives: string[]
hoists: (JSChildNode | null)[]
imports: ImportItem[]
cached: number
cached: (CacheExpression | null)[]
temps: number
ssrHelpers?: symbol[]
codegenNode?: TemplateChildNode | JSChildNode | BlockStatement
@ -225,7 +225,7 @@ export interface DirectiveNode extends Node {
export enum ConstantTypes {
NOT_CONSTANT = 0,
CAN_SKIP_PATCH,
CAN_HOIST,
CAN_CACHE,
CAN_STRINGIFY,
}
@ -337,6 +337,7 @@ export interface VNodeCall extends Node {
| SlotsExpression // component slots
| ForRenderListExpression // v-for fragment call
| SimpleExpressionNode // hoisted
| CacheExpression // cached
| undefined
patchFlag: string | undefined
dynamicProps: string | SimpleExpressionNode | undefined
@ -423,7 +424,8 @@ export interface CacheExpression extends Node {
type: NodeTypes.JS_CACHE_EXPRESSION
index: number
value: JSChildNode
isVNode: boolean
needPauseTracking: boolean
needArraySpread: boolean
}
export interface MemoExpression extends CallExpression {
@ -518,7 +520,7 @@ export interface SlotsObjectProperty extends Property {
}
export interface SlotFunctionExpression extends FunctionExpression {
returns: TemplateChildNode[]
returns: TemplateChildNode[] | CacheExpression
}
// createSlots({ ... }, [
@ -605,7 +607,7 @@ export function createRoot(
directives: [],
hoists: [],
imports: [],
cached: 0,
cached: [],
temps: 0,
codegenNode: undefined,
loc: locStub,
@ -778,13 +780,14 @@ export function createConditionalExpression(
export function createCacheExpression(
index: number,
value: JSChildNode,
isVNode: boolean = false,
needPauseTracking: boolean = false,
): CacheExpression {
return {
type: NodeTypes.JS_CACHE_EXPRESSION,
index,
value,
isVNode,
needPauseTracking: needPauseTracking,
needArraySpread: false,
loc: locStub,
}
}

View File

@ -43,8 +43,6 @@ import {
CREATE_TEXT,
CREATE_VNODE,
OPEN_BLOCK,
POP_SCOPE_ID,
PUSH_SCOPE_ID,
RESOLVE_COMPONENT,
RESOLVE_DIRECTIVE,
RESOLVE_FILTER,
@ -490,11 +488,6 @@ function genModulePreamble(
ssrRuntimeModuleName,
} = context
if (genScopeId && ast.hoists.length) {
ast.helpers.add(PUSH_SCOPE_ID)
ast.helpers.add(POP_SCOPE_ID)
}
// generate import statements for helpers
if (ast.helpers.size) {
const helpers = Array.from(ast.helpers)
@ -583,33 +576,14 @@ function genHoists(hoists: (JSChildNode | null)[], context: CodegenContext) {
return
}
context.pure = true
const { push, newline, helper, scopeId, mode } = context
const genScopeId = !__BROWSER__ && scopeId != null && mode !== 'function'
const { push, newline } = context
newline()
// generate inlined withScopeId helper
if (genScopeId) {
push(
`const _withScopeId = n => (${helper(
PUSH_SCOPE_ID,
)}("${scopeId}"),n=n(),${helper(POP_SCOPE_ID)}(),n)`,
)
newline()
}
for (let i = 0; i < hoists.length; i++) {
const exp = hoists[i]
if (exp) {
const needScopeIdWrapper = genScopeId && exp.type === NodeTypes.VNODE_CALL
push(
`const _hoisted_${i + 1} = ${
needScopeIdWrapper ? `${PURE_ANNOTATION} _withScopeId(() => ` : ``
}`,
)
push(`const _hoisted_${i + 1} = `)
genNode(exp, context)
if (needScopeIdWrapper) {
push(`)`)
}
newline()
}
}
@ -1024,15 +998,19 @@ function genConditionalExpression(
function genCacheExpression(node: CacheExpression, context: CodegenContext) {
const { push, helper, indent, deindent, newline } = context
const { needPauseTracking, needArraySpread } = node
if (needArraySpread) {
push(`[...(`)
}
push(`_cache[${node.index}] || (`)
if (node.isVNode) {
if (needPauseTracking) {
indent()
push(`${helper(SET_BLOCK_TRACKING)}(-1),`)
newline()
}
push(`_cache[${node.index}] = `)
genNode(node.value, context)
if (node.isVNode) {
if (needPauseTracking) {
push(`,`)
newline()
push(`${helper(SET_BLOCK_TRACKING)}(1),`)
@ -1041,6 +1019,9 @@ function genCacheExpression(node: CacheExpression, context: CodegenContext) {
deindent()
}
push(`)`)
if (needArraySpread) {
push(`)]`)
}
}
function genTemplateLiteral(node: TemplateLiteral, context: CodegenContext) {

View File

@ -72,7 +72,7 @@ export {
type PropsExpression,
} from './transforms/transformElement'
export { processSlotOutlet } from './transforms/transformSlotOutlet'
export { getConstantType } from './transforms/hoistStatic'
export { getConstantType } from './transforms/cacheStatic'
export { generateCodeFrame } from '@vue/shared'
// v2 compat only

View File

@ -247,7 +247,16 @@ export interface TransformOptions
*/
isCustomElement?: (tag: string) => boolean | void
/**
* Hoist static VNodes and props objects to `_hoisted_x` constants
* Transform expressions like {{ foo }} to `_ctx.foo`.
* If this option is false, the generated code will be wrapped in a
* `with (this) { ... }` block.
* - This is force-enabled in module mode, since modules are by default strict
* and cannot use `with`
* @default mode === 'module'
*/
prefixIdentifiers?: boolean
/**
* Cache static VNodes and props objects to `_hoisted_x` constants
* @default false
*/
hoistStatic?: boolean

View File

@ -32,7 +32,14 @@ export const CAMELIZE = Symbol(__DEV__ ? `camelize` : ``)
export const CAPITALIZE = Symbol(__DEV__ ? `capitalize` : ``)
export const TO_HANDLER_KEY = Symbol(__DEV__ ? `toHandlerKey` : ``)
export const SET_BLOCK_TRACKING = Symbol(__DEV__ ? `setBlockTracking` : ``)
/**
* @deprecated no longer needed in 3.5+ because we no longer hoist element nodes
* but kept for backwards compat
*/
export const PUSH_SCOPE_ID = Symbol(__DEV__ ? `pushScopeId` : ``)
/**
* @deprecated kept for backwards compat
*/
export const POP_SCOPE_ID = Symbol(__DEV__ ? `popScopeId` : ``)
export const WITH_CTX = Symbol(__DEV__ ? `withCtx` : ``)
export const UNREF = Symbol(__DEV__ ? `unref` : ``)

View File

@ -38,7 +38,7 @@ import {
helperNameMap,
} from './runtimeHelpers'
import { isVSlot } from './utils'
import { hoistStatic, isSingleElementRoot } from './transforms/hoistStatic'
import { cacheStatic, isSingleElementRoot } from './transforms/cacheStatic'
import type { CompilerCompatOptions } from './compat/compatConfig'
// There are two types of transforms:
@ -93,7 +93,7 @@ export interface TransformContext
hoists: (JSChildNode | null)[]
imports: ImportItem[]
temps: number
cached: number
cached: (CacheExpression | null)[]
identifiers: { [name: string]: number | undefined }
scopes: {
vFor: number
@ -117,7 +117,7 @@ export interface TransformContext
addIdentifiers(exp: ExpressionNode | string): void
removeIdentifiers(exp: ExpressionNode | string): void
hoist(exp: string | JSChildNode | ArrayExpression): SimpleExpressionNode
cache<T extends JSChildNode>(exp: T, isVNode?: boolean): CacheExpression | T
cache(exp: JSChildNode, isVNode?: boolean): CacheExpression
constantCache: WeakMap<TemplateChildNode, ConstantTypes>
// 2.x Compat only
@ -185,9 +185,9 @@ export function createTransformContext(
directives: new Set(),
hoists: [],
imports: [],
cached: [],
constantCache: new WeakMap(),
temps: 0,
cached: 0,
identifiers: Object.create(null),
scopes: {
vFor: 0,
@ -291,13 +291,19 @@ export function createTransformContext(
`_hoisted_${context.hoists.length}`,
false,
exp.loc,
ConstantTypes.CAN_HOIST,
ConstantTypes.CAN_CACHE,
)
identifier.hoisted = exp
return identifier
},
cache(exp, isVNode = false) {
return createCacheExpression(context.cached++, exp, isVNode)
const cacheExp = createCacheExpression(
context.cached.length,
exp,
isVNode,
)
context.cached.push(cacheExp)
return cacheExp
},
}
@ -324,7 +330,7 @@ export function transform(root: RootNode, options: TransformOptions) {
const context = createTransformContext(root, options)
traverseNode(root, context)
if (options.hoistStatic) {
hoistStatic(root, context)
cacheStatic(root, context)
}
if (!options.ssr) {
createRootCodegen(root, context)

View File

@ -1,16 +1,20 @@
import {
type CacheExpression,
type CallExpression,
type ComponentNode,
ConstantTypes,
ElementTypes,
type ExpressionNode,
type JSChildNode,
NodeTypes,
type ParentNode,
type PlainElementNode,
type RootNode,
type SimpleExpressionNode,
type SlotFunctionExpression,
type TemplateChildNode,
type TemplateNode,
type TextCallNode,
type VNodeCall,
createArrayExpression,
getVNodeBlockHelper,
@ -18,7 +22,7 @@ import {
} from '../ast'
import type { TransformContext } from '../transform'
import { PatchFlags, isArray, isString, isSymbol } from '@vue/shared'
import { isSlotOutlet } from '../utils'
import { findDir, isSlotOutlet } from '../utils'
import {
GUARD_REACTIVE_PROPS,
NORMALIZE_CLASS,
@ -27,9 +31,10 @@ import {
OPEN_BLOCK,
} from '../runtimeHelpers'
export function hoistStatic(root: RootNode, context: TransformContext) {
export function cacheStatic(root: RootNode, context: TransformContext) {
walk(
root,
undefined,
context,
// Root node is unfortunately non-hoistable due to potential parent
// fallthrough attributes.
@ -51,16 +56,16 @@ export function isSingleElementRoot(
function walk(
node: ParentNode,
parent: ParentNode | undefined,
context: TransformContext,
doNotHoistNode: boolean = false,
inFor = false,
) {
const { children } = node
const originalCount = children.length
let hoistedCount = 0
const toCache: (PlainElementNode | TextCallNode)[] = []
for (let i = 0; i < children.length; i++) {
const child = children[i]
// only plain elements & text calls are eligible for hoisting.
// only plain elements & text calls are eligible for caching.
if (
child.type === NodeTypes.ELEMENT &&
child.tagType === ElementTypes.ELEMENT
@ -69,11 +74,10 @@ function walk(
? ConstantTypes.NOT_CONSTANT
: getConstantType(child, context)
if (constantType > ConstantTypes.NOT_CONSTANT) {
if (constantType >= ConstantTypes.CAN_HOIST) {
if (constantType >= ConstantTypes.CAN_CACHE) {
;(child.codegenNode as VNodeCall).patchFlag =
PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
child.codegenNode = context.hoist(child.codegenNode!)
hoistedCount++
PatchFlags.CACHED + (__DEV__ ? ` /* CACHED */` : ``)
toCache.push(child)
continue
}
} else {
@ -87,7 +91,7 @@ function walk(
flag === PatchFlags.NEED_PATCH ||
flag === PatchFlags.TEXT) &&
getGeneratedPropsConstantType(child, context) >=
ConstantTypes.CAN_HOIST
ConstantTypes.CAN_CACHE
) {
const props = getNodeProps(child)
if (props) {
@ -99,6 +103,14 @@ function walk(
}
}
}
} else if (child.type === NodeTypes.TEXT_CALL) {
const constantType = doNotHoistNode
? ConstantTypes.NOT_CONSTANT
: getConstantType(child, context)
if (constantType >= ConstantTypes.CAN_CACHE) {
toCache.push(child)
continue
}
}
// walk further
@ -107,54 +119,122 @@ function walk(
if (isComponent) {
context.scopes.vSlot++
}
walk(child, context)
walk(child, node, context, false, inFor)
if (isComponent) {
context.scopes.vSlot--
}
} else if (child.type === NodeTypes.FOR) {
// Do not hoist v-for single child because it has to be a block
walk(child, context, child.children.length === 1)
walk(child, node, context, child.children.length === 1, true)
} else if (child.type === NodeTypes.IF) {
for (let i = 0; i < child.branches.length; i++) {
// Do not hoist v-if single child because it has to be a block
walk(
child.branches[i],
node,
context,
child.branches[i].children.length === 1,
inFor,
)
}
}
}
if (hoistedCount && context.transformHoist) {
context.transformHoist(children, context, node)
let cachedAsArray = false
if (toCache.length === children.length && node.type === NodeTypes.ELEMENT) {
if (
node.tagType === ElementTypes.ELEMENT &&
node.codegenNode &&
node.codegenNode.type === NodeTypes.VNODE_CALL &&
isArray(node.codegenNode.children)
) {
// all children were hoisted - the entire children array is cacheable.
node.codegenNode.children = getCacheExpression(
createArrayExpression(node.codegenNode.children),
)
cachedAsArray = true
} else if (
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
) {
// default slot
const slot = getSlotNode(node.codegenNode, 'default')
if (slot) {
slot.returns = getCacheExpression(
createArrayExpression(slot.returns as TemplateChildNode[]),
)
cachedAsArray = true
}
} else if (
node.tagType === ElementTypes.TEMPLATE &&
parent &&
parent.type === NodeTypes.ELEMENT &&
parent.tagType === ElementTypes.COMPONENT &&
parent.codegenNode &&
parent.codegenNode.type === NodeTypes.VNODE_CALL &&
parent.codegenNode.children &&
!isArray(parent.codegenNode.children) &&
parent.codegenNode.children.type === NodeTypes.JS_OBJECT_EXPRESSION
) {
// named <template> slot
const slotName = findDir(node, 'slot', true)
const slot =
slotName &&
slotName.arg &&
getSlotNode(parent.codegenNode, slotName.arg)
if (slot) {
slot.returns = getCacheExpression(
createArrayExpression(slot.returns as TemplateChildNode[]),
)
cachedAsArray = true
}
}
}
// all children were hoisted - the entire children array is hoistable.
if (
hoistedCount &&
hoistedCount === originalCount &&
node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.ELEMENT &&
node.codegenNode &&
node.codegenNode.type === NodeTypes.VNODE_CALL &&
isArray(node.codegenNode.children)
) {
const hoisted = context.hoist(
createArrayExpression(node.codegenNode.children),
)
// #6978, #7138, #7114
// a hoisted children array inside v-for can caused HMR errors since
// it might be mutated when mounting the v-for list
if (context.hmr) {
hoisted.content = `[...${hoisted.content}]`
if (!cachedAsArray) {
for (const child of toCache) {
child.codegenNode = context.cache(child.codegenNode!)
}
node.codegenNode.children = hoisted
}
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
}
return exp
}
function getSlotNode(
node: VNodeCall,
name: string | ExpressionNode,
): SlotFunctionExpression | undefined {
if (
node.children &&
!isArray(node.children) &&
node.children.type === NodeTypes.JS_OBJECT_EXPRESSION
) {
const slot = node.children.properties.find(
p => p.key === name || (p.key as SimpleExpressionNode).content === name,
)
return slot && slot.value
}
}
if (toCache.length && context.transformHoist) {
context.transformHoist(children, context, node)
}
}
export function getConstantType(
node: TemplateChildNode | SimpleExpressionNode,
node: TemplateChildNode | SimpleExpressionNode | CacheExpression,
context: TransformContext,
): ConstantTypes {
const { constantCache } = context
@ -284,6 +364,8 @@ export function getConstantType(
}
}
return returnType
case NodeTypes.JS_CACHE_EXPRESSION:
return ConstantTypes.CAN_CACHE
default:
if (__DEV__) {
const exhaustiveCheck: never = node

View File

@ -57,7 +57,7 @@ import {
toValidAssetId,
} from '../utils'
import { buildSlots } from './vSlot'
import { getConstantType } from './hoistStatic'
import { getConstantType } from './cacheStatic'
import { BindingTypes } from '../options'
import {
CompilerDeprecationTypes,

View File

@ -252,7 +252,7 @@ export function processExpression(
if (isLiteral) {
node.constType = ConstantTypes.CAN_STRINGIFY
} else {
node.constType = ConstantTypes.CAN_HOIST
node.constType = ConstantTypes.CAN_CACHE
}
}
return node

View File

@ -11,7 +11,7 @@ import {
import { isText } from '../utils'
import { CREATE_TEXT } from '../runtimeHelpers'
import { PatchFlagNames, PatchFlags } from '@vue/shared'
import { getConstantType } from './hoistStatic'
import { getConstantType } from './cacheStatic'
// Merge adjacent text nodes and expressions into a single expression
// e.g. <div>abc {{ d }} {{ e }}</div> should have a single expression node as child.

View File

@ -1,5 +1,6 @@
import type { DirectiveTransform } from '../transform'
import type { DirectiveTransform, TransformContext } from '../transform'
import {
type DirectiveNode,
type ExpressionNode,
NodeTypes,
type SimpleExpressionNode,
@ -56,11 +57,8 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => {
}
}
const propName = camelize((arg as SimpleExpressionNode).content)
exp = dir.exp = createSimpleExpression(propName, false, arg.loc)
if (!__BROWSER__) {
exp = dir.exp = processExpression(exp, context)
}
transformBindShorthand(dir, context)
exp = dir.exp!
}
if (arg.type !== NodeTypes.SIMPLE_EXPRESSION) {
@ -98,6 +96,19 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => {
}
}
export const transformBindShorthand = (
dir: DirectiveNode,
context: TransformContext,
) => {
const arg = dir.arg!
const propName = camelize((arg as SimpleExpressionNode).content)
dir.exp = createSimpleExpression(propName, false, arg.loc)
if (!__BROWSER__) {
dir.exp = processExpression(dir.exp, context)
}
}
const injectPrefix = (arg: ExpressionNode, prefix: string) => {
if (arg.type === NodeTypes.SIMPLE_EXPRESSION) {
if (arg.isStatic) {

View File

@ -47,6 +47,7 @@ import {
import { processExpression } from './transformExpression'
import { validateBrowserExpression } from '../validateExpression'
import { PatchFlagNames, PatchFlags } from '@vue/shared'
import { transformBindShorthand } from './vBind'
export const transformFor = createStructuralDirectiveTransform(
'for',
@ -60,13 +61,20 @@ export const transformFor = createStructuralDirectiveTransform(
]) as ForRenderListExpression
const isTemplate = isTemplateNode(node)
const memo = findDir(node, 'memo')
const keyProp = findProp(node, `key`)
const keyProp = findProp(node, `key`, false, true)
if (keyProp && keyProp.type === NodeTypes.DIRECTIVE && !keyProp.exp) {
// resolve :key shorthand #10882
transformBindShorthand(keyProp, context)
}
const keyExp =
keyProp &&
(keyProp.type === NodeTypes.ATTRIBUTE
? createSimpleExpression(keyProp.value!.content, true)
: keyProp.exp!)
const keyProperty = keyProp ? createObjectProperty(`key`, keyExp!) : null
? keyProp.value
? createSimpleExpression(keyProp.value.content, true)
: undefined
: keyProp.exp)
const keyProperty =
keyProp && keyExp ? createObjectProperty(`key`, keyExp) : null
if (!__BROWSER__ && isTemplate) {
// #2085 / #5288 process :key and v-memo expressions need to be
@ -224,8 +232,10 @@ export const transformFor = createStructuralDirectiveTransform(
renderExp.arguments.push(
loop as ForIteratorExpression,
createSimpleExpression(`_cache`),
createSimpleExpression(String(context.cached++)),
createSimpleExpression(String(context.cached.length)),
)
// increment cache count
context.cached.push(null)
} else {
renderExp.arguments.push(
createFunctionExpression(

View File

@ -248,7 +248,7 @@ function createChildrenCodegenNode(
`${keyIndex}`,
false,
locStub,
ConstantTypes.CAN_HOIST,
ConstantTypes.CAN_CACHE,
),
)
const { children } = branch

View File

@ -33,8 +33,10 @@ export const transformMemo: NodeTransform = (node, context) => {
dir.exp!,
createFunctionExpression(undefined, codegenNode),
`_cache`,
String(context.cached++),
String(context.cached.length),
]) as MemoExpression
// increment cache count
context.cached.push(null)
}
}
}

View File

@ -148,7 +148,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
`{ ${modifiers} }`,
false,
dir.loc,
ConstantTypes.CAN_HOIST,
ConstantTypes.CAN_CACHE,
),
),
)

View File

@ -1,5 +1,6 @@
import {
type BlockCodegenNode,
type CacheExpression,
type CallExpression,
type DirectiveNode,
type ElementNode,
@ -62,7 +63,7 @@ export function isCoreComponent(tag: string): symbol | void {
}
}
const nonIdentifierRE = /^\d|[^\$\w]/
const nonIdentifierRE = /^\d|[^\$\w\xA0-\uFFFF]/
export const isSimpleIdentifier = (name: string): boolean =>
!nonIdentifierRE.test(name)
@ -445,7 +446,12 @@ export function toValidAssetId(
// Check if a node contains expressions that reference current context scope ids
export function hasScopeRef(
node: TemplateChildNode | IfBranchNode | ExpressionNode | undefined,
node:
| TemplateChildNode
| IfBranchNode
| ExpressionNode
| CacheExpression
| undefined,
ids: TransformContext['identifiers'],
): boolean {
if (!node || Object.keys(ids).length === 0) {
@ -488,6 +494,7 @@ export function hasScopeRef(
return hasScopeRef(node.content, ids)
case NodeTypes.TEXT:
case NodeTypes.COMMENT:
case NodeTypes.JS_CACHE_EXPRESSION:
return false
default:
if (__DEV__) {
@ -506,4 +513,4 @@ export function getMemoedVNodeCall(node: BlockCodegenNode | MemoExpression) {
}
}
export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+(\S[\s\S]*)/

View File

@ -1,96 +1,128 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
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] = [
_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)
])))
}"
`;
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] = [
_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)
])))
}"
`;
exports[`stringify static html > should bail for <option> elements with number values 1`] = `
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("select", null, [
/*#__PURE__*/_createElementVNode("option", { value: 1 }),
/*#__PURE__*/_createElementVNode("option", { value: 1 }),
/*#__PURE__*/_createElementVNode("option", { value: 1 }),
/*#__PURE__*/_createElementVNode("option", { value: 1 }),
/*#__PURE__*/_createElementVNode("option", { value: 1 })
], -1 /* HOISTED */)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _hoisted_2))
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("select", null, [
_createElementVNode("option", { value: 1 }),
_createElementVNode("option", { value: 1 }),
_createElementVNode("option", { value: 1 }),
_createElementVNode("option", { value: 1 }),
_createElementVNode("option", { value: 1 })
], -1 /* CACHED */)
])))
}"
`;
exports[`stringify static html > should bail on bindings that are hoisted but not stringifiable 1`] = `
exports[`stringify static html > should bail on bindings that are cached but not stringifiable 1`] = `
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("div", null, [
/*#__PURE__*/_createElementVNode("span", { class: "foo" }, "foo"),
/*#__PURE__*/_createElementVNode("span", { class: "foo" }, "foo"),
/*#__PURE__*/_createElementVNode("span", { class: "foo" }, "foo"),
/*#__PURE__*/_createElementVNode("span", { class: "foo" }, "foo"),
/*#__PURE__*/_createElementVNode("span", { class: "foo" }, "foo"),
/*#__PURE__*/_createElementVNode("img", { src: _imports_0_ })
], -1 /* HOISTED */)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _hoisted_2))
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("div", null, [
_createElementVNode("span", { class: "foo" }, "foo"),
_createElementVNode("span", { class: "foo" }, "foo"),
_createElementVNode("span", { class: "foo" }, "foo"),
_createElementVNode("span", { class: "foo" }, "foo"),
_createElementVNode("span", { class: "foo" }, "foo"),
_createElementVNode("img", { src: _imports_0_ })
], -1 /* CACHED */)
])))
}"
`;
exports[`stringify static html > should work for <option> elements with string values 1`] = `
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<select><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option></select>", 1)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) {
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)
])))
}"
`;
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, _hoisted_2))
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)
])))
}"
`;
exports[`stringify static html > should work on eligible content (elements > 20) 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] = [
_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)
])))
}"
`;
exports[`stringify static html > should work on eligible content (elements with binding > 5) 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] = [
_createStaticVNode("<div><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span></div>", 1)
])))
}"
`;
exports[`stringify static html > should work with bindings that are non-static but stringifiable 1`] = `
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
const _hoisted_1 = /*#__PURE__*/_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)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _hoisted_2))
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)
])))
}"
`;
exports[`stringify static html > stringify v-html 1`] = `
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode } = Vue
const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<pre data-type=\\"js\\"><code><span>show-it </span></code></pre><div class><span class>1</span><span class>2</span></div>", 2)
return function render(_ctx, _cache) {
return _hoisted_1
return _cache[0] || (_cache[0] = _createStaticVNode("<pre data-type=\\"js\\"><code><span>show-it </span></code></pre><div class><span class>1</span><span class>2</span></div>", 2))
}"
`;
exports[`stringify static html > stringify v-text 1`] = `
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode } = Vue
const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<pre data-type=\\"js\\"><code>&lt;span&gt;show-it &lt;/span&gt;</code></pre><div class><span class>1</span><span class>2</span></div>", 2)
return function render(_ctx, _cache) {
return _hoisted_1
return _cache[0] || (_cache[0] = _createStaticVNode("<pre data-type=\\"js\\"><code>&lt;span&gt;show-it &lt;/span&gt;</code></pre><div class><span class>1</span><span class>2</span></div>", 2))
}"
`;
exports[`stringify static html > stringify v-text with escape 1`] = `
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode } = Vue
const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<pre data-type=\\"js\\"><code>text1</code></pre><div class><span class>1</span><span class>2</span></div>", 2)
return function render(_ctx, _cache) {
return _hoisted_1
return _cache[0] || (_cache[0] = _createStaticVNode("<pre data-type=\\"js\\"><code>text1</code></pre><div class><span class>1</span><span class>2</span></div>", 2))
}"
`;

View File

@ -23,134 +23,147 @@ describe('stringify static html', () => {
return code.repeat(n)
}
/**
* Assert cached node NOT stringified
*/
function cachedArrayBailedMatcher(n = 1) {
return {
type: NodeTypes.JS_CACHE_EXPRESSION,
value: {
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: new Array(n).fill(0).map(() => ({
// should remain VNODE_CALL instead of JS_CALL_EXPRESSION
codegenNode: { type: NodeTypes.VNODE_CALL },
})),
},
}
}
/**
* Assert cached node is stringified (no content check)
*/
function cachedArraySuccessMatcher(n = 1) {
return {
type: NodeTypes.JS_CACHE_EXPRESSION,
value: {
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: new Array(n).fill(0).map(() => ({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
})),
},
}
}
/**
* Assert cached node stringified with desired content and node count
*/
function cachedArrayStaticNodeMatcher(content: string, count: number) {
return {
type: NodeTypes.JS_CACHE_EXPRESSION,
value: {
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: [
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [JSON.stringify(content), String(count)],
},
],
},
}
}
test('should bail on non-eligible static trees', () => {
const { ast } = compileWithStringify(
`<div><div><div>hello</div><div>hello</div></div></div>`,
)
// should be a normal vnode call
expect(ast.hoists[0]!.type).toBe(NodeTypes.VNODE_CALL)
// should be cached children array
expect(ast.cached[0]!.value.type).toBe(NodeTypes.JS_ARRAY_EXPRESSION)
})
test('should work on eligible content (elements with binding > 5)', () => {
const { ast } = compileWithStringify(
const { code, ast } = compileWithStringify(
`<div><div>${repeat(
`<span class="foo"/>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div></div>`,
)
// should be optimized now
expect(ast.hoists).toMatchObject([
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
`<div>${repeat(
`<span class="foo"></span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div>`,
),
'1',
],
}, // the children array is hoisted as well
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
expect(ast.cached).toMatchObject([
cachedArrayStaticNodeMatcher(
`<div>${repeat(
`<span class="foo"></span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div>`,
1,
),
])
expect(code).toMatchSnapshot()
})
test('should work on eligible content (elements > 20)', () => {
const { ast } = compileWithStringify(
const { code, ast } = compileWithStringify(
`<div><div>${repeat(
`<span/>`,
StringifyThresholds.NODE_COUNT,
)}</div></div>`,
)
// should be optimized now
expect(ast.hoists).toMatchObject([
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
`<div>${repeat(
`<span></span>`,
StringifyThresholds.NODE_COUNT,
)}</div>`,
),
'1',
],
},
// the children array is hoisted as well
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
expect(ast.cached).toMatchObject([
cachedArrayStaticNodeMatcher(
`<div>${repeat(`<span></span>`, StringifyThresholds.NODE_COUNT)}</div>`,
1,
),
])
expect(code).toMatchSnapshot()
})
test('should work for multiple adjacent nodes', () => {
const { ast } = compileWithStringify(
const { ast, code } = compileWithStringify(
`<div>${repeat(
`<span class="foo"/>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div>`,
)
// should have 6 hoisted nodes (including the entire array),
// but 2~5 should be null because they are merged into 1
expect(ast.hoists).toMatchObject([
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
repeat(
`<span class="foo"></span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
),
),
'5',
],
},
null,
null,
null,
null,
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
expect(ast.cached).toMatchObject([
cachedArrayStaticNodeMatcher(
repeat(
`<span class="foo"></span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
),
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
),
])
expect(code).toMatchSnapshot()
})
test('serializing constant bindings', () => {
const { ast } = compileWithStringify(
const { ast, code } = compileWithStringify(
`<div><div :style="{ color: 'red' }">${repeat(
`<span :class="[{ foo: true }, { bar: true }]">{{ 1 }} + {{ false }}</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div></div>`,
)
// should be optimized now
expect(ast.hoists).toMatchObject([
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
`<div style="color:red;">${repeat(
`<span class="foo bar">1 + false</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div>`,
),
'1',
],
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
expect(ast.cached).toMatchObject([
cachedArrayStaticNodeMatcher(
`<div style="color:red;">${repeat(
`<span class="foo bar">1 + false</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div>`,
1,
),
])
expect(code).toMatchSnapshot()
})
test('escape', () => {
const { ast } = compileWithStringify(
const { ast, code } = compileWithStringify(
`<div><div>${repeat(
`<span :class="'foo' + '&gt;ar'">{{ 1 }} + {{ '<' }}</span>` +
`<span>&amp;</span>`,
@ -158,27 +171,19 @@ describe('stringify static html', () => {
)}</div></div>`,
)
// should be optimized now
expect(ast.hoists).toMatchObject([
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
`<div>${repeat(
`<span class="foo&gt;ar">1 + &lt;</span>` + `<span>&amp;</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div>`,
),
'1',
],
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
expect(ast.cached).toMatchObject([
cachedArrayStaticNodeMatcher(
`<div>${repeat(
`<span class="foo&gt;ar">1 + &lt;</span>` + `<span>&amp;</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div>`,
1,
),
])
expect(code).toMatchSnapshot()
})
test('should bail on bindings that are hoisted but not stringifiable', () => {
test('should bail on bindings that are cached but not stringifiable', () => {
const { ast, code } = compile(
`<div><div>${repeat(
`<span class="foo">foo</span>`,
@ -195,7 +200,7 @@ describe('stringify static html', () => {
'_imports_0_',
false,
node.loc,
ConstantTypes.CAN_HOIST,
ConstantTypes.CAN_CACHE,
)
node.props[0] = {
type: NodeTypes.DIRECTIVE,
@ -210,17 +215,7 @@ describe('stringify static html', () => {
],
},
)
expect(ast.hoists).toMatchObject([
{
// the expression and the tree are still hoistable
// but should stay NodeTypes.VNODE_CALL
// if it's stringified it will be NodeTypes.JS_CALL_EXPRESSION
type: NodeTypes.VNODE_CALL,
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
])
expect(ast.cached).toMatchObject([cachedArrayBailedMatcher()])
expect(code).toMatchSnapshot()
})
@ -258,35 +253,19 @@ describe('stringify static html', () => {
],
},
)
expect(ast.hoists).toMatchObject([
{
// the hoisted node should be NodeTypes.JS_CALL_EXPRESSION
// of `createStaticVNode()` instead of dynamic NodeTypes.VNODE_CALL
type: NodeTypes.JS_CALL_EXPRESSION,
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
])
expect(ast.cached).toMatchObject([cachedArraySuccessMatcher()])
expect(code).toMatchSnapshot()
})
// #1128
test('should bail on non attribute bindings', () => {
test('should bail on non-attribute bindings', () => {
const { ast } = compileWithStringify(
`<div><div><input indeterminate>${repeat(
`<span class="foo">foo</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div></div>`,
)
expect(ast.hoists).toMatchObject([
{
type: NodeTypes.VNODE_CALL, // not CALL_EXPRESSION
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
])
expect(ast.cached).toMatchObject([cachedArrayBailedMatcher()])
const { ast: ast2 } = compileWithStringify(
`<div><div><input :indeterminate="true">${repeat(
@ -294,46 +273,23 @@ describe('stringify static html', () => {
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div></div>`,
)
expect(ast2.hoists).toMatchObject([
{
type: NodeTypes.VNODE_CALL, // not CALL_EXPRESSION
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
])
})
expect(ast2.cached).toMatchObject([cachedArrayBailedMatcher()])
test('should bail on non attribute bindings', () => {
const { ast } = compileWithStringify(
const { ast: ast3 } = compileWithStringify(
`<div><div>${repeat(
`<span class="foo">foo</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}<input indeterminate></div></div>`,
)
expect(ast.hoists).toMatchObject([
{
type: NodeTypes.VNODE_CALL, // not CALL_EXPRESSION
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
])
expect(ast3.cached).toMatchObject([cachedArrayBailedMatcher()])
const { ast: ast2 } = compileWithStringify(
const { ast: ast4 } = compileWithStringify(
`<div><div>${repeat(
`<span class="foo">foo</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}<input :indeterminate="true"></div></div>`,
)
expect(ast2.hoists).toMatchObject([
{
type: NodeTypes.VNODE_CALL, // not CALL_EXPRESSION
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
])
expect(ast4.cached).toMatchObject([cachedArrayBailedMatcher()])
})
test('should bail on tags that has placement constraints (eg.tables related tags)', () => {
@ -343,14 +299,7 @@ describe('stringify static html', () => {
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</tbody></table>`,
)
expect(ast.hoists).toMatchObject([
{
type: NodeTypes.VNODE_CALL, // not CALL_EXPRESSION
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
])
expect(ast.cached).toMatchObject([cachedArrayBailedMatcher()])
})
test('should bail inside slots', () => {
@ -360,14 +309,9 @@ describe('stringify static html', () => {
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</foo>`,
)
expect(ast.hoists.length).toBe(
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)
ast.hoists.forEach(node => {
expect(node).toMatchObject({
type: NodeTypes.VNODE_CALL, // not CALL_EXPRESSION
})
})
expect(ast.cached).toMatchObject([
cachedArrayBailedMatcher(StringifyThresholds.ELEMENT_WITH_BINDING_COUNT),
])
const { ast: ast2 } = compileWithStringify(
`<foo><template #foo>${repeat(
@ -375,14 +319,9 @@ describe('stringify static html', () => {
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</template></foo>`,
)
expect(ast2.hoists.length).toBe(
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)
ast2.hoists.forEach(node => {
expect(node).toMatchObject({
type: NodeTypes.VNODE_CALL, // not CALL_EXPRESSION
})
})
expect(ast2.cached).toMatchObject([
cachedArrayBailedMatcher(StringifyThresholds.ELEMENT_WITH_BINDING_COUNT),
])
})
test('should remove attribute for `null`', () => {
@ -392,19 +331,13 @@ describe('stringify static html', () => {
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div>`,
)
expect(ast.hoists[0]).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
`${repeat(
`<span></span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}`,
),
'5',
],
})
expect(ast.cached).toMatchObject([
cachedArrayStaticNodeMatcher(
repeat(`<span></span>`, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT),
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
),
])
})
// #6617
@ -415,19 +348,24 @@ describe('stringify static html', () => {
StringifyThresholds.NODE_COUNT,
)}`,
)
expect(ast.hoists[0]).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
`<button>enable</button>${repeat(
`<div></div>`,
StringifyThresholds.NODE_COUNT,
)}`,
),
'21',
],
})
expect(ast.cached).toMatchObject([
{
type: NodeTypes.JS_CACHE_EXPRESSION,
value: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
`<button>enable</button>${repeat(
`<div></div>`,
StringifyThresholds.NODE_COUNT,
)}`,
),
'21',
],
},
},
])
})
test('should stringify svg', () => {
@ -439,19 +377,16 @@ describe('stringify static html', () => {
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</svg></div>`,
)
expect(ast.hoists[0]).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
`${svg}${repeat(
repeated,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</svg>`,
),
'1',
],
})
expect(ast.cached).toMatchObject([
cachedArrayStaticNodeMatcher(
`${svg}${repeat(
repeated,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</svg>`,
1,
),
])
})
// #5439
@ -494,23 +429,14 @@ describe('stringify static html', () => {
)}</select></div>`,
)
// should be optimized now
expect(ast.hoists).toMatchObject([
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
`<select>${repeat(
`<option value="1"></option>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</select>`,
),
'1',
],
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
expect(ast.cached).toMatchObject([
cachedArrayStaticNodeMatcher(
`<select>${repeat(
`<option value="1"></option>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</select>`,
1,
),
])
expect(code).toMatchSnapshot()
})
@ -522,14 +448,7 @@ describe('stringify static html', () => {
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</select></div>`,
)
expect(ast.hoists).toMatchObject([
{
type: NodeTypes.VNODE_CALL,
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
])
expect(ast.cached).toMatchObject([cachedArrayBailedMatcher()])
expect(code).toMatchSnapshot()
})
})

View File

@ -268,7 +268,7 @@ describe('compiler-dom: transform v-on', () => {
prefixIdentifiers: true,
cacheHandlers: true,
})
expect(root.cached).toBe(1)
expect(root.cached.length).toBe(1)
// should not treat cached handler as dynamicProp, so it should have no
// dynamicProps flags and only the hydration flag
expect((root as any).children[0].codegenNode.patchFlag).toBe(

View File

@ -3,12 +3,12 @@
*/
import {
CREATE_STATIC,
type CacheExpression,
ConstantTypes,
type ElementNode,
ElementTypes,
type ExpressionNode,
type HoistTransform,
type JSChildNode,
Namespaces,
NodeTypes,
type PlainElementNode,
@ -16,11 +16,14 @@ import {
type TemplateChildNode,
type TextCallNode,
type TransformContext,
type VNodeCall,
createArrayExpression,
createCallExpression,
isStaticArgOf,
} from '@vue/compiler-core'
import {
escapeHtml,
isArray,
isBooleanAttr,
isKnownHtmlAttr,
isKnownSvgAttr,
@ -76,6 +79,14 @@ export const stringifyStatic: HoistTransform = (children, context, parent) => {
return
}
const isParentCached =
parent.type === NodeTypes.ELEMENT &&
parent.codegenNode &&
parent.codegenNode.type === NodeTypes.VNODE_CALL &&
parent.codegenNode.children &&
!isArray(parent.codegenNode.children) &&
parent.codegenNode.children.type === NodeTypes.JS_CACHE_EXPRESSION
let nc = 0 // current node count
let ec = 0 // current element with binding count
const currentChunk: StringifiableNode[] = []
@ -94,19 +105,31 @@ export const stringifyStatic: HoistTransform = (children, context, parent) => {
// will insert / hydrate
String(currentChunk.length),
])
// replace the first node's hoisted expression with the static vnode call
replaceHoist(currentChunk[0], staticCall, context)
if (currentChunk.length > 1) {
for (let i = 1; i < currentChunk.length; i++) {
// for the merged nodes, set their hoisted expression to null
replaceHoist(currentChunk[i], null, context)
if (isParentCached) {
;((parent.codegenNode as VNodeCall).children as CacheExpression).value =
createArrayExpression([staticCall])
} else {
// replace the first node's hoisted expression with the static vnode call
;(currentChunk[0].codegenNode as CacheExpression).value = staticCall
if (currentChunk.length > 1) {
// remove merged nodes from children
const deleteCount = currentChunk.length - 1
children.splice(currentIndex - currentChunk.length + 1, deleteCount)
// also adjust index for the remaining cache items
const cacheIndex = context.cached.indexOf(
currentChunk[currentChunk.length - 1]
.codegenNode as CacheExpression,
)
if (cacheIndex > -1) {
for (let i = cacheIndex; i < context.cached.length; i++) {
const c = context.cached[i]
if (c) c.index -= deleteCount
}
context.cached.splice(cacheIndex - deleteCount + 1, deleteCount)
}
return deleteCount
}
// also remove merged nodes from children
const deleteCount = currentChunk.length - 1
children.splice(currentIndex - currentChunk.length + 1, deleteCount)
return deleteCount
}
}
return 0
@ -115,16 +138,15 @@ export const stringifyStatic: HoistTransform = (children, context, parent) => {
let i = 0
for (; i < children.length; i++) {
const child = children[i]
const hoisted = getHoistedNode(child)
if (hoisted) {
// presence of hoisted means child must be a stringifiable node
const node = child as StringifiableNode
const result = analyzeNode(node)
const isCached = isParentCached || getCachedNode(child)
if (isCached) {
// presence of cached means child must be a stringifiable node
const result = analyzeNode(child as StringifiableNode)
if (result) {
// node is stringifiable, record state
nc += result[0]
ec += result[1]
currentChunk.push(node)
currentChunk.push(child as StringifiableNode)
continue
}
}
@ -141,12 +163,19 @@ export const stringifyStatic: HoistTransform = (children, context, parent) => {
stringifyCurrentChunk(i)
}
const getHoistedNode = (node: TemplateChildNode) =>
((node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.ELEMENT) ||
node.type == NodeTypes.TEXT_CALL) &&
node.codegenNode &&
node.codegenNode.type === NodeTypes.SIMPLE_EXPRESSION &&
node.codegenNode.hoisted
const getCachedNode = (
node: TemplateChildNode,
): CacheExpression | undefined => {
if (
((node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.ELEMENT) ||
node.type === NodeTypes.TEXT_CALL) &&
node.codegenNode &&
node.codegenNode.type === NodeTypes.JS_CACHE_EXPRESSION
) {
return node.codegenNode
}
}
const dataAriaRE = /^(data|aria)-/
const isStringifiableAttr = (name: string, ns: Namespaces) => {
@ -159,21 +188,12 @@ const isStringifiableAttr = (name: string, ns: Namespaces) => {
)
}
const replaceHoist = (
node: StringifiableNode,
replacement: JSChildNode | null,
context: TransformContext,
) => {
const hoistToReplace = (node.codegenNode as SimpleExpressionNode).hoisted!
context.hoists[context.hoists.indexOf(hoistToReplace)] = replacement
}
const isNonStringifiable = /*#__PURE__*/ makeMap(
`caption,thead,tr,th,tbody,td,tfoot,colgroup,col`,
)
/**
* for a hoisted node, analyze it and return:
* for a cached node, analyze it and return:
* - false: bailed (contains non-stringifiable props or runtime constant)
* - [nc, ec] where
* - nc is the number of nodes inside
@ -381,7 +401,7 @@ function evaluateConstant(exp: ExpressionNode): string {
} else if (c.type === NodeTypes.INTERPOLATION) {
res += toDisplayString(evaluateConstant(c.content))
} else {
res += evaluateConstant(c)
res += evaluateConstant(c as ExpressionNode)
}
})
return res

View File

@ -851,8 +851,6 @@ return (_ctx, _cache) => {
exports[`SFC compile <script setup> > inlineTemplate mode > should work 1`] = `
"import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = /*#__PURE__*/_createElementVNode("div", null, "static", -1 /* HOISTED */)
import { ref } from 'vue'
export default {
@ -863,7 +861,7 @@ export default {
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("div", null, _toDisplayString(count.value), 1 /* TEXT */),
_hoisted_1
_cache[0] || (_cache[0] = _createElementVNode("div", null, "static", -1 /* CACHED */))
], 64 /* STABLE_FRAGMENT */))
}
}

View File

@ -38,13 +38,11 @@ import _imports_0 from '@svg/file.svg'
const _hoisted_1 = _imports_0 + '#fragment'
const _hoisted_2 = /*#__PURE__*/_createElementVNode("use", { href: _hoisted_1 }, null, -1 /* HOISTED */)
const _hoisted_3 = /*#__PURE__*/_createElementVNode("use", { href: _hoisted_1 }, null, -1 /* HOISTED */)
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_hoisted_2,
_hoisted_3
_cache[0] || (_cache[0] = _createElementVNode("use", { href: _hoisted_1 }, null, -1 /* CACHED */)),
_cache[1] || (_cache[1] = _createElementVNode("use", { href: _hoisted_1 }, null, -1 /* CACHED */))
], 64 /* STABLE_FRAGMENT */))
}"
`;
@ -82,13 +80,10 @@ import _imports_0 from './bar.png'
import _imports_1 from '/bar.png'
const _hoisted_1 = /*#__PURE__*/_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)
const _hoisted_6 = [
_hoisted_1
]
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _hoisted_6))
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

@ -7,13 +7,11 @@ import _imports_0 from '@/logo.png'
const _hoisted_1 = _imports_0 + ', ' + _imports_0 + ' 2x'
const _hoisted_2 = _imports_0 + ' 1x, ' + "/foo/logo.png" + ' 2x'
const _hoisted_3 = /*#__PURE__*/_createElementVNode("img", { srcset: _hoisted_1 }, null, -1 /* HOISTED */)
const _hoisted_4 = /*#__PURE__*/_createElementVNode("img", { srcset: _hoisted_2 }, null, -1 /* HOISTED */)
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_hoisted_3,
_hoisted_4
_cache[0] || (_cache[0] = _createElementVNode("img", { srcset: _hoisted_1 }, null, -1 /* CACHED */)),
_cache[1] || (_cache[1] = _createElementVNode("img", { srcset: _hoisted_2 }, null, -1 /* CACHED */))
], 64 /* STABLE_FRAGMENT */))
}"
`;
@ -31,69 +29,57 @@ const _hoisted_5 = _imports_0 + ' 2x, ' + _imports_0
const _hoisted_6 = _imports_0 + ' 2x, ' + _imports_0 + ' 3x'
const _hoisted_7 = _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x'
const _hoisted_8 = "/logo.png" + ', ' + _imports_0 + ' 2x'
const _hoisted_9 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: ""
}, null, -1 /* HOISTED */)
const _hoisted_10 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_1
}, null, -1 /* HOISTED */)
const _hoisted_11 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_2
}, null, -1 /* HOISTED */)
const _hoisted_12 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_3
}, null, -1 /* HOISTED */)
const _hoisted_13 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_4
}, null, -1 /* HOISTED */)
const _hoisted_14 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_5
}, null, -1 /* HOISTED */)
const _hoisted_15 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_6
}, null, -1 /* HOISTED */)
const _hoisted_16 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_7
}, null, -1 /* HOISTED */)
const _hoisted_17 = /*#__PURE__*/_createElementVNode("img", {
src: "/logo.png",
srcset: "/logo.png, /logo.png 2x"
}, null, -1 /* HOISTED */)
const _hoisted_18 = /*#__PURE__*/_createElementVNode("img", {
src: "https://example.com/logo.png",
srcset: "https://example.com/logo.png, https://example.com/logo.png 2x"
}, null, -1 /* HOISTED */)
const _hoisted_19 = /*#__PURE__*/_createElementVNode("img", {
src: "/logo.png",
srcset: _hoisted_8
}, null, -1 /* HOISTED */)
const _hoisted_20 = /*#__PURE__*/_createElementVNode("img", {
src: "data:image/png;base64,i",
srcset: "data:image/png;base64,i 1x, data:image/png;base64,i 2x"
}, null, -1 /* HOISTED */)
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_hoisted_9,
_hoisted_10,
_hoisted_11,
_hoisted_12,
_hoisted_13,
_hoisted_14,
_hoisted_15,
_hoisted_16,
_hoisted_17,
_hoisted_18,
_hoisted_19,
_hoisted_20
_cache[0] || (_cache[0] = _createElementVNode("img", {
src: "./logo.png",
srcset: ""
}, null, -1 /* CACHED */)),
_cache[1] || (_cache[1] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_1
}, null, -1 /* CACHED */)),
_cache[2] || (_cache[2] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_2
}, null, -1 /* CACHED */)),
_cache[3] || (_cache[3] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_3
}, null, -1 /* CACHED */)),
_cache[4] || (_cache[4] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_4
}, null, -1 /* CACHED */)),
_cache[5] || (_cache[5] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_5
}, null, -1 /* CACHED */)),
_cache[6] || (_cache[6] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_6
}, null, -1 /* CACHED */)),
_cache[7] || (_cache[7] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_7
}, null, -1 /* CACHED */)),
_cache[8] || (_cache[8] = _createElementVNode("img", {
src: "/logo.png",
srcset: "/logo.png, /logo.png 2x"
}, null, -1 /* CACHED */)),
_cache[9] || (_cache[9] = _createElementVNode("img", {
src: "https://example.com/logo.png",
srcset: "https://example.com/logo.png, https://example.com/logo.png 2x"
}, null, -1 /* CACHED */)),
_cache[10] || (_cache[10] = _createElementVNode("img", {
src: "/logo.png",
srcset: _hoisted_8
}, null, -1 /* CACHED */)),
_cache[11] || (_cache[11] = _createElementVNode("img", {
src: "data:image/png;base64,i",
srcset: "data:image/png;base64,i 1x, data:image/png;base64,i 2x"
}, null, -1 /* CACHED */))
], 64 /* STABLE_FRAGMENT */))
}"
`;
@ -101,69 +87,56 @@ export function render(_ctx, _cache) {
exports[`compiler sfc: transform srcset > transform srcset w/ base 1`] = `
"import { createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: ""
}, null, -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png"
}, null, -1 /* HOISTED */)
const _hoisted_3 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png 2x"
}, null, -1 /* HOISTED */)
const _hoisted_4 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png 2x"
}, null, -1 /* HOISTED */)
const _hoisted_5 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png, /foo/logo.png 2x"
}, null, -1 /* HOISTED */)
const _hoisted_6 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png 2x, /foo/logo.png"
}, null, -1 /* HOISTED */)
const _hoisted_7 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png 2x, /foo/logo.png 3x"
}, null, -1 /* HOISTED */)
const _hoisted_8 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png, /foo/logo.png 2x, /foo/logo.png 3x"
}, null, -1 /* HOISTED */)
const _hoisted_9 = /*#__PURE__*/_createElementVNode("img", {
src: "/logo.png",
srcset: "/logo.png, /logo.png 2x"
}, null, -1 /* HOISTED */)
const _hoisted_10 = /*#__PURE__*/_createElementVNode("img", {
src: "https://example.com/logo.png",
srcset: "https://example.com/logo.png, https://example.com/logo.png 2x"
}, null, -1 /* HOISTED */)
const _hoisted_11 = /*#__PURE__*/_createElementVNode("img", {
src: "/logo.png",
srcset: "/logo.png, /foo/logo.png 2x"
}, null, -1 /* HOISTED */)
const _hoisted_12 = /*#__PURE__*/_createElementVNode("img", {
src: "data:image/png;base64,i",
srcset: "data:image/png;base64,i 1x, data:image/png;base64,i 2x"
}, null, -1 /* HOISTED */)
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_hoisted_1,
_hoisted_2,
_hoisted_3,
_hoisted_4,
_hoisted_5,
_hoisted_6,
_hoisted_7,
_hoisted_8,
_hoisted_9,
_hoisted_10,
_hoisted_11,
_hoisted_12
_cache[0] || (_cache[0] = _createElementVNode("img", {
src: "./logo.png",
srcset: ""
}, null, -1 /* CACHED */)),
_cache[1] || (_cache[1] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png"
}, null, -1 /* CACHED */)),
_cache[2] || (_cache[2] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png 2x"
}, null, -1 /* CACHED */)),
_cache[3] || (_cache[3] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png 2x"
}, null, -1 /* CACHED */)),
_cache[4] || (_cache[4] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png, /foo/logo.png 2x"
}, null, -1 /* CACHED */)),
_cache[5] || (_cache[5] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png 2x, /foo/logo.png"
}, null, -1 /* CACHED */)),
_cache[6] || (_cache[6] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png 2x, /foo/logo.png 3x"
}, null, -1 /* CACHED */)),
_cache[7] || (_cache[7] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png, /foo/logo.png 2x, /foo/logo.png 3x"
}, null, -1 /* CACHED */)),
_cache[8] || (_cache[8] = _createElementVNode("img", {
src: "/logo.png",
srcset: "/logo.png, /logo.png 2x"
}, null, -1 /* CACHED */)),
_cache[9] || (_cache[9] = _createElementVNode("img", {
src: "https://example.com/logo.png",
srcset: "https://example.com/logo.png, https://example.com/logo.png 2x"
}, null, -1 /* CACHED */)),
_cache[10] || (_cache[10] = _createElementVNode("img", {
src: "/logo.png",
srcset: "/logo.png, /foo/logo.png 2x"
}, null, -1 /* CACHED */)),
_cache[11] || (_cache[11] = _createElementVNode("img", {
src: "data:image/png;base64,i",
srcset: "data:image/png;base64,i 1x, data:image/png;base64,i 2x"
}, null, -1 /* CACHED */))
], 64 /* STABLE_FRAGMENT */))
}"
`;
@ -183,69 +156,57 @@ const _hoisted_6 = _imports_0 + ' 2x, ' + _imports_0 + ' 3x'
const _hoisted_7 = _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x'
const _hoisted_8 = _imports_1 + ', ' + _imports_1 + ' 2x'
const _hoisted_9 = _imports_1 + ', ' + _imports_0 + ' 2x'
const _hoisted_10 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: ""
}, null, -1 /* HOISTED */)
const _hoisted_11 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_1
}, null, -1 /* HOISTED */)
const _hoisted_12 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_2
}, null, -1 /* HOISTED */)
const _hoisted_13 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_3
}, null, -1 /* HOISTED */)
const _hoisted_14 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_4
}, null, -1 /* HOISTED */)
const _hoisted_15 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_5
}, null, -1 /* HOISTED */)
const _hoisted_16 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_6
}, null, -1 /* HOISTED */)
const _hoisted_17 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_7
}, null, -1 /* HOISTED */)
const _hoisted_18 = /*#__PURE__*/_createElementVNode("img", {
src: "/logo.png",
srcset: _hoisted_8
}, null, -1 /* HOISTED */)
const _hoisted_19 = /*#__PURE__*/_createElementVNode("img", {
src: "https://example.com/logo.png",
srcset: "https://example.com/logo.png, https://example.com/logo.png 2x"
}, null, -1 /* HOISTED */)
const _hoisted_20 = /*#__PURE__*/_createElementVNode("img", {
src: "/logo.png",
srcset: _hoisted_9
}, null, -1 /* HOISTED */)
const _hoisted_21 = /*#__PURE__*/_createElementVNode("img", {
src: "data:image/png;base64,i",
srcset: "data:image/png;base64,i 1x, data:image/png;base64,i 2x"
}, null, -1 /* HOISTED */)
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_hoisted_10,
_hoisted_11,
_hoisted_12,
_hoisted_13,
_hoisted_14,
_hoisted_15,
_hoisted_16,
_hoisted_17,
_hoisted_18,
_hoisted_19,
_hoisted_20,
_hoisted_21
_cache[0] || (_cache[0] = _createElementVNode("img", {
src: "./logo.png",
srcset: ""
}, null, -1 /* CACHED */)),
_cache[1] || (_cache[1] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_1
}, null, -1 /* CACHED */)),
_cache[2] || (_cache[2] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_2
}, null, -1 /* CACHED */)),
_cache[3] || (_cache[3] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_3
}, null, -1 /* CACHED */)),
_cache[4] || (_cache[4] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_4
}, null, -1 /* CACHED */)),
_cache[5] || (_cache[5] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_5
}, null, -1 /* CACHED */)),
_cache[6] || (_cache[6] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_6
}, null, -1 /* CACHED */)),
_cache[7] || (_cache[7] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_7
}, null, -1 /* CACHED */)),
_cache[8] || (_cache[8] = _createElementVNode("img", {
src: "/logo.png",
srcset: _hoisted_8
}, null, -1 /* CACHED */)),
_cache[9] || (_cache[9] = _createElementVNode("img", {
src: "https://example.com/logo.png",
srcset: "https://example.com/logo.png, https://example.com/logo.png 2x"
}, null, -1 /* CACHED */)),
_cache[10] || (_cache[10] = _createElementVNode("img", {
src: "/logo.png",
srcset: _hoisted_9
}, null, -1 /* CACHED */)),
_cache[11] || (_cache[11] = _createElementVNode("img", {
src: "data:image/png;base64,i",
srcset: "data:image/png;base64,i 1x, data:image/png;base64,i 2x"
}, null, -1 /* CACHED */))
], 64 /* STABLE_FRAGMENT */))
}"
`;
@ -265,12 +226,10 @@ const _hoisted_6 = _imports_0 + ' 2x, ' + _imports_0 + ' 3x'
const _hoisted_7 = _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x'
const _hoisted_8 = _imports_1 + ', ' + _imports_1 + ' 2x'
const _hoisted_9 = _imports_1 + ', ' + _imports_0 + ' 2x'
const _hoisted_10 = /*#__PURE__*/_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)
const _hoisted_22 = [
_hoisted_10
]
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _hoisted_22))
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

@ -9,8 +9,9 @@ import {
registerTS,
resolveTypeElements,
} from '../../src/script/resolveType'
import { UNKNOWN_TYPE } from '../../src/script/utils'
import ts from 'typescript'
registerTS(() => ts)
describe('resolveType', () => {
@ -128,7 +129,7 @@ describe('resolveType', () => {
defineProps<{ self: any } & Foo & Bar & Baz>()
`).props,
).toStrictEqual({
self: ['Unknown'],
self: [UNKNOWN_TYPE],
foo: ['Number'],
// both Bar & Baz has 'bar', but Baz['bar] is wider so it should be
// preferred
@ -136,6 +137,18 @@ describe('resolveType', () => {
})
})
test('intersection type with ignore', () => {
expect(
resolve(`
type Foo = { foo: number }
type Bar = { bar: string }
defineProps<Foo & /* @vue-ignore */ Bar>()
`).props,
).toStrictEqual({
foo: ['Number'],
})
})
// #7553
test('union type', () => {
expect(
@ -455,13 +468,13 @@ describe('resolveType', () => {
const { props } = resolve(
`
import { IMP } from './foo'
interface Foo { foo: 1, ${1}: 1 }
interface Foo { foo: 1, ${1}: 1 }
type Bar = { bar: 1 }
declare const obj: Bar
declare const set: Set<any>
declare const arr: Array<any>
defineProps<{
defineProps<{
imp: keyof IMP,
foo: keyof Foo,
bar: keyof Bar,
@ -483,6 +496,106 @@ describe('resolveType', () => {
})
})
test('keyof: index signature', () => {
const { props } = resolve(
`
declare const num: number;
interface Foo {
[key: symbol]: 1
[key: string]: 1
[key: typeof num]: 1,
}
type Test<T> = T
type Bar = {
[key: string]: 1
[key: Test<number>]: 1
}
defineProps<{
foo: keyof Foo
bar: keyof Bar
}>()
`,
)
expect(props).toStrictEqual({
foo: ['Symbol', 'String', 'Number'],
bar: [UNKNOWN_TYPE],
})
})
// #11129
test('keyof: intersection type', () => {
const { props } = resolve(`
type A = { name: string }
type B = A & { [key: number]: string }
defineProps<{
foo: keyof B
}>()`)
expect(props).toStrictEqual({
foo: ['String', 'Number'],
})
})
test('keyof: union type', () => {
const { props } = resolve(`
type A = { name: string }
type B = A | { [key: number]: string }
defineProps<{
foo: keyof B
}>()`)
expect(props).toStrictEqual({
foo: ['String', 'Number'],
})
})
test('keyof: utility type', () => {
const { props } = resolve(
`
type Foo = Record<symbol | string, any>
type Bar = { [key: string]: any }
type AnyRecord = Record<keyof any, any>
type Baz = { a: 1, ${1}: 2, b: 3}
defineProps<{
record: keyof Foo,
anyRecord: keyof AnyRecord
partial: keyof Partial<Bar>,
required: keyof Required<Bar>,
readonly: keyof Readonly<Bar>,
pick: keyof Pick<Baz, 'a' | 1>
extract: keyof Extract<keyof Baz, 'a' | 1>
}>()
`,
)
expect(props).toStrictEqual({
record: ['Symbol', 'String'],
anyRecord: ['String', 'Number', 'Symbol'],
partial: ['String'],
required: ['String'],
readonly: ['String'],
pick: ['String', 'Number'],
extract: ['String', 'Number'],
})
})
test('keyof: fallback to Unknown', () => {
const { props } = resolve(
`
interface Barr {}
interface Bar extends Barr {}
type Foo = keyof Bar
defineProps<{ foo: Foo }>()
`,
)
expect(props).toStrictEqual({
foo: [UNKNOWN_TYPE],
})
})
test('ExtractPropTypes (element-plus)', () => {
const { props, raw } = resolve(
`
@ -946,6 +1059,53 @@ describe('resolveType', () => {
expect(deps && [...deps]).toStrictEqual(['/user.ts'])
})
test('ts module resolve w/ project reference folder', () => {
const files = {
'/tsconfig.json': JSON.stringify({
references: [
{
path: './web',
},
{
path: './empty',
},
{
path: './noexists-should-ignore',
},
],
}),
'/web/tsconfig.json': JSON.stringify({
include: ['../**/*.ts', '../**/*.vue'],
compilerOptions: {
composite: true,
paths: {
bar: ['../user.ts'],
},
},
}),
// tsconfig with no include / paths defined, should match nothing
'/empty/tsconfig.json': JSON.stringify({
compilerOptions: {
composite: true,
},
}),
'/user.ts': 'export type User = { bar: string }',
}
const { props, deps } = resolve(
`
import { User } from 'bar'
defineProps<User>()
`,
files,
)
expect(props).toStrictEqual({
bar: ['String'],
})
expect(deps && [...deps]).toStrictEqual(['/user.ts'])
})
test('ts module resolve w/ path aliased vue file', () => {
const files = {
'/tsconfig.json': JSON.stringify({
@ -1122,6 +1282,37 @@ describe('resolveType', () => {
})
})
})
describe('template literals', () => {
test('mapped types with string type', () => {
expect(
resolve(`
type X = 'a' | 'b'
defineProps<{[K in X as \`\${K}_foo\`]: string}>()
`).props,
).toStrictEqual({
a_foo: ['String'],
b_foo: ['String'],
})
})
// #10962
test('mapped types with generic parameters', () => {
const { props } = resolve(`
type Breakpoints = 'sm' | 'md' | 'lg'
type BreakpointFactory<T extends string, V> = {
[K in Breakpoints as \`\${T}\${Capitalize<K>}\`]: V
}
type ColsBreakpoints = BreakpointFactory<'cols', number>
defineProps<ColsBreakpoints>()
`)
expect(props).toStrictEqual({
colsSm: ['Number'],
colsMd: ['Number'],
colsLg: ['Number'],
})
})
})
})
function resolve(

View File

@ -42,7 +42,7 @@
},
"homepage": "https://github.com/vuejs/core-vapor/tree/main/packages/compiler-sfc#readme",
"dependencies": {
"@babel/parser": "^7.24.6",
"@babel/parser": "^7.24.7",
"@vue/compiler-core": "workspace:*",
"@vue/compiler-dom": "workspace:*",
"@vue/compiler-ssr": "workspace:*",
@ -54,7 +54,7 @@
"source-map-js": "^1.2.0"
},
"devDependencies": {
"@babel/types": "^7.24.6",
"@babel/types": "^7.24.7",
"@vue/consolidate": "^1.0.0",
"hash-sum": "^2.0.0",
"lru-cache": "10.1.0",
@ -63,6 +63,6 @@
"postcss-modules": "^6.0.0",
"postcss-selector-parser": "^6.1.0",
"pug": "^3.0.3",
"sass": "^1.77.2"
"sass": "^1.77.4"
}
}

View File

@ -49,7 +49,7 @@ import {
} from './script/defineEmits'
import { DEFINE_EXPOSE, processDefineExpose } from './script/defineExpose'
import { DEFINE_OPTIONS, processDefineOptions } from './script/defineOptions'
import { processDefineSlots } from './script/defineSlots'
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'
@ -140,6 +140,16 @@ export interface ImportBinding {
isUsedInTemplate: boolean
}
const MACROS = [
DEFINE_PROPS,
DEFINE_EMITS,
DEFINE_EXPOSE,
DEFINE_OPTIONS,
DEFINE_SLOTS,
DEFINE_MODEL,
WITH_DEFAULTS,
]
/**
* Compile `<script setup>`
* It requires the whole SFC descriptor because we need to handle and merge
@ -323,15 +333,18 @@ export function compileScript(
const imported = getImportedName(specifier)
const source = node.source.value
const existing = ctx.userImports[local]
if (
source === 'vue' &&
(imported === DEFINE_PROPS ||
imported === DEFINE_EMITS ||
imported === DEFINE_EXPOSE)
) {
warnOnce(
`\`${imported}\` is a compiler macro and no longer needs to be imported.`,
)
if (source === 'vue' && MACROS.includes(imported)) {
if (local === imported) {
warnOnce(
`\`${imported}\` is a compiler macro and no longer needs to be imported.`,
)
} else {
ctx.error(
`\`${imported}\` is a compiler macro and cannot be aliased to ` +
`a different name.`,
specifier,
)
}
removeSpecifier(i)
} else if (existing) {
if (existing.source === source && existing.imported === imported) {
@ -1072,13 +1085,16 @@ function walkDeclaration(
// export const foo = ...
for (const { id, init: _init } of node.declarations) {
const init = _init && unwrapTSNode(_init)
const isDefineCall = !!(
const isConstMacroCall =
isConst &&
isCallOf(
init,
c => c === DEFINE_PROPS || c === DEFINE_EMITS || c === WITH_DEFAULTS,
c =>
c === DEFINE_PROPS ||
c === DEFINE_EMITS ||
c === WITH_DEFAULTS ||
c === DEFINE_SLOTS,
)
)
if (id.type === 'Identifier') {
let bindingType
const userReactiveBinding = userImportAliases['reactive']
@ -1095,7 +1111,7 @@ function walkDeclaration(
} else if (
// if a declaration is a const literal, we can mark it so that
// the generated render fn code doesn't need to unref() it
isDefineCall ||
isConstMacroCall ||
(isConst && canNeverBeRef(init!, userReactiveBinding))
) {
bindingType = isCallOf(init, DEFINE_PROPS)
@ -1127,9 +1143,9 @@ function walkDeclaration(
continue
}
if (id.type === 'ObjectPattern') {
walkObjectPattern(id, bindings, isConst, isDefineCall)
walkObjectPattern(id, bindings, isConst, isConstMacroCall)
} else if (id.type === 'ArrayPattern') {
walkArrayPattern(id, bindings, isConst, isDefineCall)
walkArrayPattern(id, bindings, isConst, isConstMacroCall)
}
}
}

View File

@ -198,11 +198,11 @@ function doCompileTemplate({
if (ssr && !ssrCssVars) {
warnOnce(
`compileTemplate is called with \`ssr: true\` but no ` +
`corresponding \`cssVars\` option.\`.`,
`corresponding \`cssVars\` option.`,
)
}
if (!id) {
warnOnce(`compileTemplate now requires the \`id\` option.\`.`)
warnOnce(`compileTemplate now requires the \`id\` option.`)
id = ''
}

View File

@ -176,14 +176,14 @@ export function resolveParserPlugins(
) {
plugins.push('importAttributes')
}
if (lang === 'jsx' || lang === 'tsx') {
if (lang === 'jsx' || lang === 'tsx' || lang === 'mtsx') {
plugins.push('jsx')
} else if (userPlugins) {
// If don't match the case of adding jsx
// should remove the jsx from user options
userPlugins = userPlugins.filter(p => p !== 'jsx')
}
if (lang === 'ts' || lang === 'tsx') {
if (lang === 'ts' || lang === 'mts' || lang === 'tsx' || lang === 'mtsx') {
plugins.push(['typescript', { dts }], 'explicitResourceManagement')
if (!userPlugins || !userPlugins.includes('decorators')) {
plugins.push('decorators-legacy')

View File

@ -165,6 +165,12 @@ function innerResolveTypeElements(
scope: TypeScope,
typeParameters?: Record<string, Node>,
): ResolvedElements {
if (
node.leadingComments &&
node.leadingComments.some(c => c.value.includes('@vue-ignore'))
) {
return { props: {} }
}
switch (node.type) {
case 'TSTypeLiteral':
return typeElementsToMap(ctx, node.members, scope, typeParameters)
@ -188,7 +194,7 @@ function innerResolveTypeElements(
node.type,
)
case 'TSMappedType':
return resolveMappedType(ctx, node, scope)
return resolveMappedType(ctx, node, scope, typeParameters)
case 'TSIndexedAccessType': {
const types = resolveIndexType(ctx, node, scope)
return mergeElements(
@ -414,12 +420,6 @@ function resolveInterfaceMembers(
)
if (node.extends) {
for (const ext of node.extends) {
if (
ext.leadingComments &&
ext.leadingComments.some(c => c.value.includes('@vue-ignore'))
) {
continue
}
try {
const { props, calls } = resolveTypeElements(ctx, ext, scope)
for (const key in props) {
@ -439,6 +439,7 @@ function resolveInterfaceMembers(
`Note: both in 3.2 or with the ignore, the properties in the base ` +
`type are treated as fallthrough attrs at runtime.`,
ext,
scope,
)
}
}
@ -450,9 +451,18 @@ function resolveMappedType(
ctx: TypeResolveContext,
node: TSMappedType,
scope: TypeScope,
typeParameters?: Record<string, Node>,
): ResolvedElements {
const res: ResolvedElements = { props: {} }
const keys = resolveStringType(ctx, node.typeParameter.constraint!, scope)
let keys: string[]
if (node.nameType) {
const { name, constraint } = node.typeParameter
scope = createChildScope(scope)
Object.assign(scope.types, { ...typeParameters, [name]: constraint })
keys = resolveStringType(ctx, node.nameType, scope)
} else {
keys = resolveStringType(ctx, node.typeParameter.constraint!, scope)
}
for (const key of keys) {
res.props[key] = createProperty(
{
@ -903,7 +913,7 @@ function importSourceToScope(
const filename = osSpecificJoinFn(dirname(scope.filename), source)
resolved = resolveExt(filename, fs)
} else if (source.startsWith('.')) {
} else if (source[0] === '.') {
// relative import - fast path
const filename = joinPaths(dirname(scope.filename), source)
resolved = resolveExt(filename, fs)
@ -1005,11 +1015,11 @@ function resolveWithTS(
(c.config.options.pathsBasePath as string) ||
dirname(c.config.options.configFilePath as string),
)
const included: string[] = c.config.raw?.include
const excluded: string[] = c.config.raw?.exclude
const included: string[] | undefined = c.config.raw?.include
const excluded: string[] | undefined = c.config.raw?.exclude
if (
(!included && (!base || containingFile.startsWith(base))) ||
included.some(p => isMatch(containingFile, joinPaths(base, p)))
included?.some(p => isMatch(containingFile, joinPaths(base, p)))
) {
if (
excluded &&
@ -1080,8 +1090,12 @@ function loadTSConfig(
const res = [config]
if (config.projectReferences) {
for (const ref of config.projectReferences) {
tsConfigRefMap.set(ref.path, configPath)
res.unshift(...loadTSConfig(ref.path, ts, fs))
const refPath = ts.resolveProjectReferencePath(ref)
if (!fs.fileExists(refPath)) {
continue
}
tsConfigRefMap.set(refPath, configPath)
res.unshift(...loadTSConfig(refPath, ts, fs))
}
}
return res
@ -1125,12 +1139,12 @@ function parseFile(
parserPlugins?: SFCScriptCompileOptions['babelParserPlugins'],
): Statement[] {
const ext = extname(filename)
if (ext === '.ts' || ext === '.tsx') {
if (ext === '.ts' || ext === '.mts' || ext === '.tsx' || ext === '.mtsx') {
return babelParse(content, {
plugins: resolveParserPlugins(
ext.slice(1),
parserPlugins,
filename.endsWith('.d.ts'),
/\.d\.m?ts$/.test(filename),
),
sourceType: 'module',
}).program.body
@ -1476,6 +1490,17 @@ export function inferRuntimeType(
m.key.type === 'NumericLiteral'
) {
types.add('Number')
} else if (m.type === 'TSIndexSignature') {
const annotation = m.parameters[0].typeAnnotation
if (annotation && annotation.type !== 'Noop') {
const type = inferRuntimeType(
ctx,
annotation.typeAnnotation,
scope,
)[0]
if (type === UNKNOWN_TYPE) return [UNKNOWN_TYPE]
types.add(type)
}
} else {
types.add('String')
}
@ -1489,7 +1514,9 @@ export function inferRuntimeType(
}
}
return types.size ? Array.from(types) : ['Object']
return types.size
? Array.from(types)
: [isKeyOf ? UNKNOWN_TYPE : 'Object']
}
case 'TSPropertySignature':
if (node.typeAnnotation) {
@ -1533,81 +1560,123 @@ export function inferRuntimeType(
case 'String':
case 'Array':
case 'ArrayLike':
case 'Parameters':
case 'ConstructorParameters':
case 'ReadonlyArray':
return ['String', 'Number']
default:
// TS built-in utility types
case 'Record':
case 'Partial':
case 'Required':
case 'Readonly':
if (node.typeParameters && node.typeParameters.params[0]) {
return inferRuntimeType(
ctx,
node.typeParameters.params[0],
scope,
true,
)
}
break
case 'Pick':
case 'Extract':
if (node.typeParameters && node.typeParameters.params[1]) {
return inferRuntimeType(
ctx,
node.typeParameters.params[1],
scope,
)
}
break
case 'Function':
case 'Object':
case 'Set':
case 'Map':
case 'WeakSet':
case 'WeakMap':
case 'Date':
case 'Promise':
case 'Error':
case 'Uppercase':
case 'Lowercase':
case 'Capitalize':
case 'Uncapitalize':
case 'ReadonlyMap':
case 'ReadonlySet':
return ['String']
}
}
} else {
switch (node.typeName.name) {
case 'Array':
case 'Function':
case 'Object':
case 'Set':
case 'Map':
case 'WeakSet':
case 'WeakMap':
case 'Date':
case 'Promise':
case 'Error':
return [node.typeName.name]
switch (node.typeName.name) {
case 'Array':
case 'Function':
case 'Object':
case 'Set':
case 'Map':
case 'WeakSet':
case 'WeakMap':
case 'Date':
case 'Promise':
case 'Error':
return [node.typeName.name]
// TS built-in utility types
// https://www.typescriptlang.org/docs/handbook/utility-types.html
case 'Partial':
case 'Required':
case 'Readonly':
case 'Record':
case 'Pick':
case 'Omit':
case 'InstanceType':
return ['Object']
// TS built-in utility types
// https://www.typescriptlang.org/docs/handbook/utility-types.html
case 'Partial':
case 'Required':
case 'Readonly':
case 'Record':
case 'Pick':
case 'Omit':
case 'InstanceType':
return ['Object']
case 'Uppercase':
case 'Lowercase':
case 'Capitalize':
case 'Uncapitalize':
return ['String']
case 'Uppercase':
case 'Lowercase':
case 'Capitalize':
case 'Uncapitalize':
return ['String']
case 'Parameters':
case 'ConstructorParameters':
case 'ReadonlyArray':
return ['Array']
case 'Parameters':
case 'ConstructorParameters':
case 'ReadonlyArray':
return ['Array']
case 'ReadonlyMap':
return ['Map']
case 'ReadonlySet':
return ['Set']
case 'ReadonlyMap':
return ['Map']
case 'ReadonlySet':
return ['Set']
case 'NonNullable':
if (node.typeParameters && node.typeParameters.params[0]) {
return inferRuntimeType(
ctx,
node.typeParameters.params[0],
scope,
).filter(t => t !== 'null')
}
break
case 'Extract':
if (node.typeParameters && node.typeParameters.params[1]) {
return inferRuntimeType(
ctx,
node.typeParameters.params[1],
scope,
)
}
break
case 'Exclude':
case 'OmitThisParameter':
if (node.typeParameters && node.typeParameters.params[0]) {
return inferRuntimeType(
ctx,
node.typeParameters.params[0],
scope,
)
}
break
case 'NonNullable':
if (node.typeParameters && node.typeParameters.params[0]) {
return inferRuntimeType(
ctx,
node.typeParameters.params[0],
scope,
).filter(t => t !== 'null')
}
break
case 'Extract':
if (node.typeParameters && node.typeParameters.params[1]) {
return inferRuntimeType(
ctx,
node.typeParameters.params[1],
scope,
)
}
break
case 'Exclude':
case 'OmitThisParameter':
if (node.typeParameters && node.typeParameters.params[0]) {
return inferRuntimeType(
ctx,
node.typeParameters.params[0],
scope,
)
}
break
}
}
}
// cannot infer, fallback to UNKNOWN: ThisParameterType
@ -1618,9 +1687,9 @@ export function inferRuntimeType(
return inferRuntimeType(ctx, node.typeAnnotation, scope)
case 'TSUnionType':
return flattenTypes(ctx, node.types, scope)
return flattenTypes(ctx, node.types, scope, isKeyOf)
case 'TSIntersectionType': {
return flattenTypes(ctx, node.types, scope).filter(
return flattenTypes(ctx, node.types, scope, isKeyOf).filter(
t => t !== UNKNOWN_TYPE,
)
}
@ -1674,6 +1743,13 @@ export function inferRuntimeType(
node.operator === 'keyof',
)
}
case 'TSAnyKeyword': {
if (isKeyOf) {
return ['String', 'Number', 'Symbol']
}
break
}
}
} catch (e) {
// always soft fail on failed runtime type inference
@ -1685,14 +1761,15 @@ function flattenTypes(
ctx: TypeResolveContext,
types: TSType[],
scope: TypeScope,
isKeyOf: boolean = false,
): string[] {
if (types.length === 1) {
return inferRuntimeType(ctx, types[0], scope)
return inferRuntimeType(ctx, types[0], scope, isKeyOf)
}
return [
...new Set(
([] as string[]).concat(
...types.map(t => inferRuntimeType(ctx, t, scope)),
...types.map(t => inferRuntimeType(ctx, t, scope, isKeyOf)),
),
),
]

View File

@ -1972,3 +1972,24 @@ createApp({}).component(
},
}),
)
const Comp = defineComponent({
props: {
actionText: {
type: {} as PropType<string>,
default: 'Become a sponsor',
},
},
__typeProps: {} as {
actionText?: string
},
})
const instance = new Comp()
function expectString(s: string) {}
// instance prop with default should be non-null
expectString(instance.actionText)
// public prop on $props should be optional
// @ts-expect-error
expectString(instance.$props.actionText)

View File

@ -1,4 +1,11 @@
import { type InjectionKey, type Ref, inject, provide, ref } from 'vue'
import {
type InjectionKey,
type Ref,
createApp,
inject,
provide,
ref,
} from 'vue'
import { expectType } from './utils'
// non-symbol keys
@ -40,3 +47,8 @@ provide<Cube>(injectionKeyRef, { size: 123 })
provide<Cube>('cube', { size: 'foo' })
// @ts-expect-error
provide<Cube>(123, { size: 'foo' })
// #10602
const app = createApp({})
// @ts-expect-error
app.provide(injectionKeyRef, ref({}))

View File

@ -120,3 +120,13 @@ describe('should unwrap extended Set correctly', () => {
expectType<string>(eset1.foo)
expectType<number>(eset1.bar)
})
describe('should not error when assignment', () => {
const arr = reactive([''])
let record: Record<number, string>
record = arr
expectType<string>(record[0])
let record2: { [key: number]: string }
record2 = arr
expectType<string>(record2[0])
})

View File

@ -1,7 +1,10 @@
import {
type ComputedRef,
type Ref,
computed,
defineComponent,
defineModel,
reactive,
ref,
shallowRef,
watch,
@ -12,8 +15,12 @@ const source = ref('foo')
const source2 = computed(() => source.value)
const source3 = () => 1
type Bar = Ref<string> | ComputedRef<string> | (() => number)
type Foo = readonly [Ref<string>, ComputedRef<string>, () => number]
type OnCleanup = (fn: () => void) => void
const readonlyArr: Foo = [source, source2, source3]
// lazy watcher will have consistent types for oldValue.
watch(source, (value, oldValue, onCleanup) => {
expectType<string>(value)
@ -32,6 +39,29 @@ watch([source, source2, source3] as const, (values, oldValues) => {
expectType<Readonly<[string, string, number]>>(oldValues)
})
// reactive array
watch(reactive([source, source2, source3]), (value, oldValues) => {
expectType<Bar[]>(value)
expectType<Bar[]>(oldValues)
})
// reactive w/ readonly tuple
watch(reactive([source, source2, source3] as const), (value, oldValues) => {
expectType<Foo>(value)
expectType<Foo>(oldValues)
})
// readonly array
watch(readonlyArr, (values, oldValues) => {
expectType<Readonly<[string, string, number]>>(values)
expectType<Readonly<[string, string, number]>>(oldValues)
})
// no type error, case from vueuse
declare const aAny: any
watch(aAny, (v, ov) => {})
watch(aAny, (v, ov) => {}, { immediate: true })
// immediate watcher's oldValue will be undefined on first run.
watch(
source,
@ -65,6 +95,34 @@ watch(
{ immediate: true },
)
// reactive array
watch(
reactive([source, source2, source3]),
(value, oldVals) => {
expectType<Bar[]>(value)
expectType<Bar[] | undefined>(oldVals)
},
{ immediate: true },
)
// reactive w/ readonly tuple
watch(reactive([source, source2, source3] as const), (value, oldVals) => {
expectType<Foo>(value)
expectType<Foo | undefined>(oldVals)
})
// readonly array
watch(
readonlyArr,
(values, oldValues) => {
expectType<Readonly<[string, string, number]>>(values)
expectType<
Readonly<[string | undefined, string | undefined, number | undefined]>
>(oldValues)
},
{ immediate: true },
)
// should provide correct ref.value inner type to callbacks
const nestedRefSource = ref({
foo: ref(1),

View File

@ -245,7 +245,7 @@ describe('reactivity/computed', () => {
])
})
it('debug: onTrigger', () => {
it('debug: onTrigger (reactive)', () => {
let events: DebuggerEvent[] = []
const onTrigger = vi.fn((e: DebuggerEvent) => {
events.push(e)
@ -852,4 +852,29 @@ describe('reactivity/computed', () => {
await nextTick()
expect(calls).toMatchObject(['b eval', 'mounted', 'b eval'])
})
it('debug: onTrigger (ref)', () => {
let events: DebuggerEvent[] = []
const onTrigger = vi.fn((e: DebuggerEvent) => {
events.push(e)
})
const obj = ref(1)
const c = computed(() => obj.value, { onTrigger })
// computed won't track until it has a subscriber
effect(() => c.value)
obj.value++
expect(c.value).toBe(2)
expect(onTrigger).toHaveBeenCalledTimes(1)
expect(events[0]).toEqual({
effect: c,
target: toRaw(obj),
type: TriggerOpTypes.SET,
key: 'value',
oldValue: 1,
newValue: 2,
})
})
})

View File

@ -258,6 +258,22 @@ describe('reactivity/effect', () => {
expect(dummy).toBe(undefined)
})
it('should not observe well-known symbol keyed properties in has operation', () => {
const key = Symbol.isConcatSpreadable
const obj = reactive({
[key]: true,
}) as any
const spy = vi.fn(() => {
key in obj
})
effect(spy)
expect(spy).toHaveBeenCalledTimes(1)
obj[key] = false
expect(spy).toHaveBeenCalledTimes(1)
})
it('should support manipulating an array while observing symbol keyed properties', () => {
const key = Symbol()
let dummy
@ -895,6 +911,31 @@ describe('reactivity/effect', () => {
expect(dummy).toBe(3)
})
it('stop with multiple dependencies', () => {
let dummy1, dummy2
const obj1 = reactive({ prop: 1 })
const obj2 = reactive({ prop: 1 })
const runner = effect(() => {
dummy1 = obj1.prop
dummy2 = obj2.prop
})
obj1.prop = 2
expect(dummy1).toBe(2)
obj2.prop = 3
expect(dummy2).toBe(3)
stop(runner)
obj1.prop = 4
obj2.prop = 5
// Check that both dependencies have been cleared
expect(dummy1).toBe(2)
expect(dummy2).toBe(3)
})
it('events: onStop', () => {
const onStop = vi.fn()
const runner = effect(() => {}, {

View File

@ -450,7 +450,7 @@ describe('reactivity/readonly', () => {
bar: markRaw({ b: 2 }),
})
expect(isReadonly(obj.foo)).toBe(true)
expect(isReactive(obj.bar)).toBe(false)
expect(isReadonly(obj.bar)).toBe(false)
})
test('should make ref readonly', () => {

View File

@ -47,11 +47,11 @@ export class ComputedRefImpl<T = any> implements Subscriber {
/**
* @internal
*/
readonly dep = new Dep(this);
readonly dep = new Dep(this)
/**
* @internal
*/
readonly [ReactiveFlags.IS_REF] = true;
readonly [ReactiveFlags.IS_REF] = true
/**
* @internal
*/

View File

@ -22,11 +22,3 @@ export enum ReactiveFlags {
RAW = '__v_raw',
IS_REF = '__v_isRef',
}
export enum DirtyLevels {
NotDirty = 0,
QueryingDirty = 1,
MaybeDirty_ComputedSideEffect = 2,
MaybeDirty = 3,
Dirty = 4,
}

View File

@ -37,6 +37,8 @@ export {
type DeepReadonly,
type ShallowReactive,
type UnwrapNestedRefs,
type Reactive,
type ReactiveMarker,
} from './reactive'
export {
computed,

View File

@ -58,6 +58,15 @@ function getTargetType(value: Target) {
// only unwrap nested ref
export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRefSimple<T>
declare const ReactiveMarkerSymbol: unique symbol
export declare class ReactiveMarker {
private [ReactiveMarkerSymbol]?: void
}
export type Reactive<T> = UnwrapNestedRefs<T> &
(T extends readonly any[] ? ReactiveMarker : {})
/**
* Returns a reactive proxy of the object.
*
@ -73,7 +82,7 @@ export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRefSimple<T>
* @param target - The source object.
* @see {@link https://vuejs.org/api/reactivity-core.html#reactive}
*/
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive<T extends object>(target: T): Reactive<T>
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (isReadonly(target)) {
@ -135,7 +144,7 @@ export function shallowReactive<T extends object>(
}
type Primitive = string | number | boolean | bigint | symbol | undefined | null
type Builtin = Primitive | Function | Date | Error | RegExp
export type Builtin = Primitive | Function | Date | Error | RegExp
export type DeepReadonly<T> = T extends Builtin
? T
: T extends Map<infer K, infer V>

View File

@ -7,6 +7,7 @@ import {
} from '@vue/shared'
import { Dep, getDepFromReactive } from './dep'
import {
type Builtin,
type ShallowReactiveMarker,
isProxy,
isReactive,
@ -450,11 +451,6 @@ function propertyToRef(
: (new ObjectRefImpl(source, key, defaultValue) as any)
}
// corner case when use narrows type
// Ex. type RelativePath = string & { __brand: unknown }
// RelativePath extends object -> true
type BaseTypes = string | number | boolean
/**
* This is a special exported interface for other packages to declare
* additional types that should bail out for ref unwrapping. For example
@ -471,10 +467,10 @@ type BaseTypes = string | number | boolean
export interface RefUnwrapBailTypes {}
export type ShallowUnwrapRef<T> = {
[K in keyof T]: DistrubuteRef<T[K]>
[K in keyof T]: DistributeRef<T[K]>
}
type DistrubuteRef<T> = T extends Ref<infer V> ? V : T
type DistributeRef<T> = T extends Ref<infer V> ? V : T
export type UnwrapRef<T> =
T extends ShallowRef<infer V>
@ -484,8 +480,7 @@ export type UnwrapRef<T> =
: UnwrapRefSimple<T>
export type UnwrapRefSimple<T> = T extends
| Function
| BaseTypes
| Builtin
| Ref
| RefUnwrapBailTypes[keyof RefUnwrapBailTypes]
| { [RawSymbol]?: true }

View File

@ -1,8 +1,10 @@
import {
KeepAlive,
TrackOpTypes,
h,
nextTick,
nodeOps,
onActivated,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
@ -407,4 +409,60 @@ describe('api: lifecycle hooks', () => {
await nextTick()
expect(fn).toHaveBeenCalledTimes(4)
})
it('immediately trigger unmount during rendering', async () => {
const fn = vi.fn()
const toggle = ref(false)
const Child = {
setup() {
onMounted(fn)
// trigger unmount immediately
toggle.value = false
return () => h('div')
},
}
const Comp = {
setup() {
return () => (toggle.value ? [h(Child)] : null)
},
}
render(h(Comp), nodeOps.createElement('div'))
toggle.value = true
await nextTick()
expect(fn).toHaveBeenCalledTimes(0)
})
it('immediately trigger unmount during rendering(with KeepAlive)', async () => {
const mountedSpy = vi.fn()
const activeSpy = vi.fn()
const toggle = ref(false)
const Child = {
setup() {
onMounted(mountedSpy)
onActivated(activeSpy)
// trigger unmount immediately
toggle.value = false
return () => h('div')
},
}
const Comp = {
setup() {
return () => h(KeepAlive, [toggle.value ? h(Child) : null])
},
}
render(h(Comp), nodeOps.createElement('div'))
toggle.value = true
await nextTick()
expect(mountedSpy).toHaveBeenCalledTimes(0)
expect(activeSpy).toHaveBeenCalledTimes(0)
})
})

View File

@ -749,4 +749,39 @@ describe('component props', () => {
expect(`Invalid prop name: "ref"`).toHaveBeenWarned()
expect(`Invalid prop name: "$foo"`).toHaveBeenWarned()
})
// #5517
test('events should not be props when component updating', async () => {
let props: any
function eventHandler() {}
const foo = ref(1)
const Child = defineComponent({
setup(_props) {
props = _props
},
emits: ['event'],
props: ['foo'],
template: `<div>{{ foo }}</div>`,
})
const Comp = defineComponent({
setup() {
return {
foo,
eventHandler,
}
},
components: { Child },
template: `<Child @event="eventHandler" :foo="foo" />`,
})
const root = document.createElement('div')
domRender(h(Comp), root)
expect(props).not.toHaveProperty('onEvent')
foo.value++
await nextTick()
expect(props).not.toHaveProperty('onEvent')
})
})

View File

@ -148,6 +148,17 @@ describe('v-memo', () => {
// should update
await nextTick()
expect(el.innerHTML).toBe(`<div>3 3</div>`)
vm.ok = true
await nextTick()
vm.ok = false
await nextTick()
expect(el.innerHTML).toBe(`<div>3 3</div>`)
vm.y++
// should update
await nextTick()
expect(el.innerHTML).toBe(`<div>4 3</div>`)
})
test('on v-for', async () => {

View File

@ -6,6 +6,7 @@ import {
h,
nextTick,
nodeOps,
ref,
render,
serializeInner,
triggerEvent,
@ -415,6 +416,53 @@ describe('hot module replacement', () => {
expect(mountSpy).toHaveBeenCalledTimes(1)
})
// #6930
test('reload: avoid infinite recursion', async () => {
const root = nodeOps.createElement('div')
const childId = 'test-child-6930'
const unmountSpy = vi.fn()
const mountSpy = vi.fn()
const Child: ComponentOptions = {
__hmrId: childId,
data() {
return { count: 0 }
},
expose: ['count'],
unmounted: unmountSpy,
render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
}
createRecord(childId, Child)
const Parent: ComponentOptions = {
setup() {
const com = ref()
const changeRef = (value: any) => {
com.value = value
}
return () => [h(Child, { ref: changeRef }), com.value?.count]
},
}
render(h(Parent), root)
await nextTick()
expect(serializeInner(root)).toBe(`<div>0</div>0`)
reload(childId, {
__hmrId: childId,
data() {
return { count: 1 }
},
mounted: mountSpy,
render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
})
await nextTick()
expect(serializeInner(root)).toBe(`<div>1</div>1`)
expect(unmountSpy).toHaveBeenCalledTimes(1)
expect(mountSpy).toHaveBeenCalledTimes(1)
})
// #1156 - static nodes should retain DOM element reference across updates
// when HMR is active
test('static el reference', async () => {

View File

@ -1122,7 +1122,7 @@ describe('SSR hydration', () => {
'input',
{ type: 'checkbox', indeterminate: '' },
null,
PatchFlags.HOISTED,
PatchFlags.CACHED,
),
)
expect((container.firstChild as any).indeterminate).toBe(true)

View File

@ -3,6 +3,7 @@ import {
NodeOpTypes,
type TestElement,
TestNodeTypes,
type VNode,
createBlock,
createCommentVNode,
createTextVNode,
@ -316,6 +317,64 @@ describe('renderer: fragment', () => {
)
})
// #10547
test('`template` fragment w/ render function', () => {
const renderFn = (vnode: VNode) => {
return (
openBlock(),
createBlock(
Fragment,
null,
[createTextVNode('text'), (openBlock(), createBlock(vnode))],
PatchFlags.STABLE_FRAGMENT,
)
)
}
const root = nodeOps.createElement('div')
const foo = h('div', ['foo'])
const bar = h('div', [h('div', 'bar')])
render(renderFn(foo), root)
expect(serializeInner(root)).toBe(`text<div>foo</div>`)
render(renderFn(bar), root)
expect(serializeInner(root)).toBe(`text<div><div>bar</div></div>`)
render(renderFn(foo), root)
expect(serializeInner(root)).toBe(`text<div>foo</div>`)
})
// #10547
test('`template` fragment w/ render function + keyed vnode', () => {
const renderFn = (vnode: VNode) => {
return (
openBlock(),
createBlock(
Fragment,
null,
[createTextVNode('text'), (openBlock(), createBlock(vnode))],
PatchFlags.STABLE_FRAGMENT,
)
)
}
const root = nodeOps.createElement('div')
const foo = h('div', { key: 1 }, [h('div', 'foo')])
const bar = h('div', { key: 2 }, [h('div', 'bar'), h('div', 'bar')])
render(renderFn(foo), root)
expect(serializeInner(root)).toBe(`text<div><div>foo</div></div>`)
render(renderFn(bar), root)
expect(serializeInner(root)).toBe(
`text<div><div>bar</div><div>bar</div></div>`,
)
render(renderFn(foo), root)
expect(serializeInner(root)).toBe(`text<div><div>foo</div></div>`)
})
// #6852
test('`template` keyed fragment w/ text', () => {
const root = nodeOps.createElement('div')

View File

@ -63,6 +63,17 @@ describe('vnode', () => {
})
})
test('create from an existing text vnode', () => {
const vnode1 = createVNode('div', null, 'text', PatchFlags.TEXT)
const vnode2 = createVNode(vnode1)
expect(vnode2).toMatchObject({
type: 'div',
patchFlag: PatchFlags.BAIL,
children: 'text',
shapeFlag: ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN,
})
})
test('vnode keys', () => {
for (const key of ['', 'a', 0, 1, NaN]) {
expect(createVNode('div', { key }).key).toBe(key)

View File

@ -2,7 +2,7 @@ import {
type Component,
type ComponentInternalInstance,
type ConcreteComponent,
getExposeProxy,
getComponentPublicInstance,
validateComponentName,
} from './component'
import type {
@ -55,7 +55,10 @@ export interface App<HostElement = any> {
): ComponentPublicInstance
unmount(): void
onUnmount(cb: () => void): void
provide<T>(key: InjectionKey<T> | string, value: T): this
provide<T, K = InjectionKey<T> | string | number>(
key: K,
value: K extends InjectionKey<infer V> ? V : T,
): this
/**
* Runs a function with the app as active instance. This allows using of `inject()` within the function to get access
@ -361,7 +364,7 @@ export function createAppAPI<HostElement>(
devtoolsInitApp(app, version)
}
return getExposeProxy(vnode.component!) || vnode.component!.proxy
return getComponentPublicInstance(vnode.component!)
} else if (__DEV__) {
warn(
`App has already been mounted.\n` +

View File

@ -31,9 +31,6 @@ export function injectHook(
const wrappedHook =
hook.__weh ||
(hook.__weh = (...args: unknown[]) => {
if (target.isUnmounted) {
return
}
// disable tracking inside all lifecycle hooks
// since they can potentially be called inside effects.
pauseTracking()

View File

@ -116,7 +116,7 @@ type BooleanKey<T, K extends keyof T = keyof T> = K extends any
* const emit = defineEmits<{
* // <eventName>: <expected arguments>
* change: []
* update: [value: string] // named tuple syntax
* update: [value: number] // named tuple syntax
* }>()
*
* emit('change')
@ -239,7 +239,7 @@ export function defineSlots<
return null as any
}
export type ModelRef<T, M extends string | number | symbol = string> = Ref<T> &
export type ModelRef<T, M extends PropertyKey = string> = Ref<T> &
[ModelRef<T, M>, Record<M, true | undefined>]
export type DefineModelOptions<T = any> = {
@ -280,24 +280,24 @@ export type DefineModelOptions<T = any> = {
* const count = defineModel<number>('count', { default: 0 })
* ```
*/
export function defineModel<T, M extends string | number | symbol = string>(
export function defineModel<T, M extends PropertyKey = string>(
options: { required: true } & PropOptions<T> & DefineModelOptions<T>,
): ModelRef<T, M>
export function defineModel<T, M extends string | number | symbol = string>(
export function defineModel<T, M extends PropertyKey = string>(
options: { default: any } & PropOptions<T> & DefineModelOptions<T>,
): ModelRef<T, M>
export function defineModel<T, M extends string | number | symbol = string>(
export function defineModel<T, M extends PropertyKey = string>(
options?: PropOptions<T> & DefineModelOptions<T>,
): ModelRef<T | undefined, M>
export function defineModel<T, M extends string | number | symbol = string>(
export function defineModel<T, M extends PropertyKey = string>(
name: string,
options: { required: true } & PropOptions<T> & DefineModelOptions<T>,
): ModelRef<T, M>
export function defineModel<T, M extends string | number | symbol = string>(
export function defineModel<T, M extends PropertyKey = string>(
name: string,
options: { default: any } & PropOptions<T> & DefineModelOptions<T>,
): ModelRef<T, M>
export function defineModel<T, M extends string | number | symbol = string>(
export function defineModel<T, M extends PropertyKey = string>(
name: string,
options?: PropOptions<T> & DefineModelOptions<T>,
): ModelRef<T | undefined, M>

View File

@ -3,6 +3,7 @@ import {
type BaseWatchOptions,
type ComputedRef,
type DebuggerOptions,
type ReactiveMarker,
type Ref,
baseWatch,
getCurrentScope,
@ -42,15 +43,13 @@ export type WatchCallback<V = any, OV = any> = (
onCleanup: OnCleanup,
) => any
type MaybeUndefined<T, I> = I extends true ? T | undefined : T
type MapSources<T, Immediate> = {
[K in keyof T]: T[K] extends WatchSource<infer V>
? Immediate extends true
? V | undefined
: V
? MaybeUndefined<V, Immediate>
: T[K] extends object
? Immediate extends true
? T[K] | undefined
: T[K]
? MaybeUndefined<T[K], Immediate>
: never
}
@ -103,7 +102,19 @@ type MultiWatchSources = (WatchSource<unknown> | object)[]
// overload: single source + cb
export function watch<T, Immediate extends Readonly<boolean> = false>(
source: WatchSource<T>,
cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
cb: WatchCallback<T, MaybeUndefined<T, Immediate>>,
options?: WatchOptions<Immediate>,
): WatchStopHandle
// overload: reactive array or tuple of multiple sources + cb
export function watch<
T extends Readonly<MultiWatchSources>,
Immediate extends Readonly<boolean> = false,
>(
sources: readonly [...T] | T,
cb: [T] extends [ReactiveMarker]
? WatchCallback<T, MaybeUndefined<T, Immediate>>
: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
options?: WatchOptions<Immediate>,
): WatchStopHandle
@ -117,25 +128,13 @@ export function watch<
options?: WatchOptions<Immediate>,
): WatchStopHandle
// overload: multiple sources w/ `as const`
// watch([foo, bar] as const, () => {})
// somehow [...T] breaks when the type is readonly
export function watch<
T extends Readonly<MultiWatchSources>,
Immediate extends Readonly<boolean> = false,
>(
source: T,
cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
options?: WatchOptions<Immediate>,
): WatchStopHandle
// overload: watching reactive object w/ cb
export function watch<
T extends object,
Immediate extends Readonly<boolean> = false,
>(
source: T,
cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
cb: WatchCallback<T, MaybeUndefined<T, Immediate>>,
options?: WatchOptions<Immediate>,
): WatchStopHandle

View File

@ -18,6 +18,15 @@ export function convertLegacyComponent(
// 2.x constructor
if (isFunction(comp) && comp.cid) {
// #7766
if (comp.render) {
// only necessary when compiled from SFC
comp.options.render = comp.render
}
// copy over internal properties set by the SFC compiler
comp.options.__file = comp.__file
comp.options.__hmrId = comp.__hmrId
comp.options.__scopeId = comp.__scopeId
comp = comp.options
}

View File

@ -102,11 +102,11 @@ export type CompatVue = Pick<App, 'version' | 'component' | 'directive'> & {
/**
* @deprecated Vue 3 no longer needs set() for adding new properties.
*/
set(target: any, key: string | number | symbol, value: any): void
set(target: any, key: PropertyKey, value: any): void
/**
* @deprecated Vue 3 no longer needs delete() for property deletions.
*/
delete(target: any, key: string | number | symbol): void
delete(target: any, key: PropertyKey): void
/**
* @deprecated use `reactive` instead.
*/

View File

@ -43,8 +43,15 @@ export type LegacyPublicInstance = ComponentPublicInstance &
LegacyPublicProperties
export interface LegacyPublicProperties {
$set(target: object, key: string, value: any): void
$delete(target: object, key: string): void
$set<T extends Record<keyof any, any>, K extends keyof T>(
target: T,
key: K,
value: T[K],
): void
$delete<T extends Record<keyof any, any>, K extends keyof T>(
target: T,
key: K,
): void
$mount(el?: string | Element): this
$destroy(): void
$scopedSlots: Slots

View File

@ -268,7 +268,7 @@ export type Component<
export type { ComponentOptions }
type LifecycleHook<TFn = Function> = TFn[] | null
export type LifecycleHook<TFn = Function> = (TFn & SchedulerJob)[] | null
// use `E extends any` to force evaluating type to fix #2362
export type SetupContext<
@ -279,7 +279,9 @@ export type SetupContext<
attrs: Data
slots: UnwrapSlotsType<S>
emit: EmitFn<E>
expose: (exposed?: Record<string, any>) => void
expose: <Exposed extends Record<string, any> = Record<string, any>>(
exposed?: Exposed,
) => void
}
: never
@ -369,7 +371,7 @@ export interface ComponentInternalInstance {
* after initialized (e.g. inline handlers)
* @internal
*/
renderCache: (Function | VNode)[]
renderCache: (Function | VNode | undefined)[]
/**
* Resolved component registry, only for components with mixins or extends
@ -614,6 +616,7 @@ export function createComponentInstance(
exposed: null,
exposeProxy: null,
withProxy: null,
provides: parent ? parent.provides : Object.create(appContext.provides),
accessCache: null!,
renderCache: [],
@ -1153,7 +1156,9 @@ export function createSetupContext(
}
}
export function getExposeProxy(instance: ComponentInternalInstance) {
export function getComponentPublicInstance(
instance: ComponentInternalInstance,
) {
if (instance.exposed) {
return (
instance.exposeProxy ||
@ -1170,6 +1175,8 @@ export function getExposeProxy(instance: ComponentInternalInstance) {
},
}))
)
} else {
return instance.proxy
}
}

View File

@ -1,7 +1,7 @@
import {
type Component,
type ComponentInternalInstance,
getExposeProxy,
getComponentPublicInstance,
isStatefulComponent,
} from './component'
import { nextTick, queueJob } from './scheduler'
@ -292,7 +292,7 @@ export type ComponentPublicInstance<
C extends ComputedOptions = {},
M extends MethodOptions = {},
E extends EmitsOptions = {},
PublicProps = P,
PublicProps = {},
Defaults = {},
MakeDefaultsOptional extends boolean = false,
Options = ComponentOptionsBase<any, any, any, any, any, any, any, any, any>,
@ -323,7 +323,11 @@ export type ComponentPublicInstance<
options?: WatchOptions,
): WatchStopHandle
} & ExposedKeys<
IfAny<P, P, Omit<P, keyof ShallowUnwrapRef<B>>> &
IfAny<
P,
P,
Readonly<Defaults> & Omit<P, keyof ShallowUnwrapRef<B> | keyof Defaults>
> &
ShallowUnwrapRef<B> &
UnwrapNestedRefs<D> &
ExtractComputedReturns<C> &
@ -347,7 +351,7 @@ const getPublicInstance = (
i: ComponentInternalInstance | null,
): ComponentPublicInstance | ComponentInternalInstance['exposed'] | null => {
if (!i) return null
if (isStatefulComponent(i)) return getExposeProxy(i) || i.proxy
if (isStatefulComponent(i)) return getComponentPublicInstance(i)
return getPublicInstance(i.parent)
}

View File

@ -136,6 +136,11 @@ export const BaseTransitionPropsValidators = {
onAppearCancelled: TransitionHookValidator,
}
const recursiveGetSubtree = (instance: ComponentInternalInstance): VNode => {
const subTree = instance.subTree
return subTree.component ? recursiveGetSubtree(subTree.component) : subTree
}
const BaseTransitionImpl: ComponentOptions = {
name: `BaseTransition`,
@ -179,11 +184,13 @@ const BaseTransitionImpl: ComponentOptions = {
return emptyPlaceholder(child)
}
const enterHooks = resolveTransitionHooks(
let enterHooks = resolveTransitionHooks(
innerChild,
rawProps,
state,
instance,
// #11061, ensure enterHooks is fresh after clone
hooks => (enterHooks = hooks),
)
setTransitionHooks(innerChild, enterHooks)
@ -194,7 +201,8 @@ const BaseTransitionImpl: ComponentOptions = {
if (
oldInnerChild &&
oldInnerChild.type !== Comment &&
!isSameVNodeType(innerChild, oldInnerChild)
!isSameVNodeType(innerChild, oldInnerChild) &&
recursiveGetSubtree(instance).type !== Comment
) {
const leavingHooks = resolveTransitionHooks(
oldInnerChild,
@ -303,6 +311,7 @@ export function resolveTransitionHooks(
props: BaseTransitionProps<any>,
state: TransitionState,
instance: ComponentInternalInstance,
postClone?: (hooks: TransitionHooks) => void,
): TransitionHooks {
const {
appear,
@ -443,7 +452,15 @@ export function resolveTransitionHooks(
},
clone(vnode) {
return resolveTransitionHooks(vnode, props, state, instance)
const hooks = resolveTransitionHooks(
vnode,
props,
state,
instance,
postClone,
)
if (postClone) postClone(hooks)
return hooks
},
}

View File

@ -38,6 +38,7 @@ import {
type RendererElement,
type RendererInternals,
type RendererNode,
invalidateMount,
queuePostRenderEffect,
} from '../renderer'
import { setTransitionHooks } from './BaseTransition'
@ -55,7 +56,7 @@ export interface KeepAliveProps {
max?: number | string
}
type CacheKey = string | number | symbol | ConcreteComponent
type CacheKey = PropertyKey | ConcreteComponent
type Cache = Map<CacheKey, VNode>
type Keys = Set<CacheKey>
@ -166,6 +167,9 @@ const KeepAliveImpl: ComponentOptions = {
sharedContext.deactivate = (vnode: VNode) => {
const instance = vnode.component!
invalidateMount(instance.m)
invalidateMount(instance.a)
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
queuePostRenderEffect(() => {
if (instance.da) {

View File

@ -438,6 +438,7 @@ export interface SuspenseBoundary {
registerDep(
instance: ComponentInternalInstance,
setupRenderEffect: SetupRenderEffectFn,
optimized: boolean,
): void
unmount(parentSuspense: SuspenseBoundary | null, doRemove?: boolean): void
}
@ -679,7 +680,7 @@ function createSuspenseBoundary(
return suspense.activeBranch && next(suspense.activeBranch)
},
registerDep(instance, setupRenderEffect) {
registerDep(instance, setupRenderEffect, optimized) {
const isInPendingSuspense = !!suspense.pendingBranch
if (isInPendingSuspense) {
suspense.deps++

View File

@ -15,7 +15,10 @@ import type { VNode } from './vnode'
import { EMPTY_OBJ, isBuiltInDirective, isFunction } from '@vue/shared'
import type { Data } from '@vue/runtime-shared'
import { warn } from './warning'
import { type ComponentInternalInstance, getExposeProxy } from './component'
import {
type ComponentInternalInstance,
getComponentPublicInstance,
} from './component'
import { currentRenderingInstance } from './componentRenderContext'
import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
import type { ComponentPublicInstance } from './componentPublicInstance'
@ -27,7 +30,7 @@ export interface DirectiveBinding<
Modifiers extends string = string,
Arg extends string = string,
> {
instance: ComponentPublicInstance | null
instance: ComponentPublicInstance | Record<string, any> | null
value: Value
oldValue: Value | null
arg?: Arg
@ -135,9 +138,7 @@ export function withDirectives<T extends VNode>(
__DEV__ && warn(`withDirectives can only be used inside render functions.`)
return vnode
}
const instance =
(getExposeProxy(currentRenderingInstance) as ComponentPublicInstance) ||
currentRenderingInstance.proxy
const instance = getComponentPublicInstance(currentRenderingInstance)
const bindings: DirectiveBinding[] = vnode.dirs || (vnode.dirs = [])
for (let i = 0; i < directives.length; i++) {
let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]

View File

@ -7,7 +7,7 @@ import type { NormalizedProps } from '../componentProps'
import { watchSyncEffect } from '../apiWatch'
export function useModel<
M extends string | number | symbol,
M extends PropertyKey,
T extends Record<string, any>,
K extends keyof T,
>(props: T, name: K, options?: DefineModelOptions<T[K]>): ModelRef<T[K], M>

View File

@ -15,6 +15,8 @@ export function withMemo(
// shallow clone
ret.memo = memo.slice()
ret.memoIndex = index
return (cache[index] = ret)
}

View File

@ -137,7 +137,11 @@ function reload(id: string, newComp: HMRComponent) {
// 4. Force the parent instance to re-render. This will cause all updated
// components to be unmounted and re-mounted. Queue the update so that we
// don't end up forcing the same parent to re-render multiple times.
queueJob(instance.parent.update)
queueJob(() => {
instance.parent!.update()
// #6930 avoid infinite recursion
hmrDirtyComponents.delete(oldComp)
})
} else if (instance.appContext.reload) {
// root instance mounted via createApp() has a reload method
instance.appContext.reload()

View File

@ -50,7 +50,15 @@ enum DOMNodeTypes {
COMMENT = 8,
}
let hasMismatch = false
let hasLoggedMismatchError = false
const logMismatchError = () => {
if (__TEST__ || hasLoggedMismatchError) {
return
}
// this error should show up in production
console.error('Hydration completed but contains mismatches.')
hasLoggedMismatchError = true
}
const isSVGContainer = (container: Element) =>
container.namespaceURI!.includes('svg') &&
@ -102,14 +110,10 @@ export function createHydrationFunctions(
container._vnode = vnode
return
}
hasMismatch = false
hydrateNode(container.firstChild!, vnode, null, null, null)
flushPostFlushCbs()
container._vnode = vnode
if (hasMismatch && !__TEST__) {
// this error should show up in production
console.error(`Hydration completed but contains mismatches.`)
}
}
const hydrateNode = (
@ -170,7 +174,6 @@ export function createHydrationFunctions(
}
} else {
if ((node as Text).data !== vnode.children) {
hasMismatch = true
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
warn(
`Hydration text mismatch in`,
@ -180,6 +183,7 @@ export function createHydrationFunctions(
)}` +
`\n - expected on client: ${JSON.stringify(vnode.children)}`,
)
logMismatchError()
;(node as Text).data = vnode.children as string
}
nextNode = nextSibling(node)
@ -366,7 +370,7 @@ export function createHydrationFunctions(
const forcePatch = type === 'input' || type === 'option'
// skip props & children if this is hoisted static nodes
// #5405 in dev, always hydrate children for HMR
if (__DEV__ || forcePatch || patchFlag !== PatchFlags.HOISTED) {
if (__DEV__ || forcePatch || patchFlag !== PatchFlags.CACHED) {
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'created')
}
@ -409,7 +413,6 @@ export function createHydrationFunctions(
)
let hasWarned = false
while (next) {
hasMismatch = true
if (
(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
!hasWarned
@ -421,6 +424,8 @@ export function createHydrationFunctions(
)
hasWarned = true
}
logMismatchError()
// The SSRed DOM contains more nodes than it should. Remove them.
const cur = next
next = next.nextSibling
@ -428,7 +433,6 @@ export function createHydrationFunctions(
}
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
if (el.textContent !== vnode.children) {
hasMismatch = true
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
warn(
`Hydration text content mismatch on`,
@ -436,6 +440,8 @@ export function createHydrationFunctions(
`\n - rendered on server: ${el.textContent}` +
`\n - expected on client: ${vnode.children as string}`,
)
logMismatchError()
el.textContent = vnode.children as string
}
}
@ -455,7 +461,7 @@ export function createHydrationFunctions(
(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
propHasMismatch(el, key, props[key], vnode, parentComponent)
) {
hasMismatch = true
logMismatchError()
}
if (
(forcePatch &&
@ -545,7 +551,6 @@ export function createHydrationFunctions(
// because server rendered HTML won't contain a text node
insert((vnode.el = createText('')), container)
} else {
hasMismatch = true
if (
(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
!hasWarned
@ -557,6 +562,8 @@ export function createHydrationFunctions(
)
hasWarned = true
}
logMismatchError()
// the SSRed DOM didn't contain enough nodes. Mount the missing ones.
patch(
null,
@ -603,7 +610,8 @@ export function createHydrationFunctions(
} else {
// fragment didn't hydrate successfully, since we didn't get a end anchor
// back. This should have led to node/children mismatch warnings.
hasMismatch = true
logMismatchError()
// since the anchor is missing, we need to create one and insert it
insert((vnode.anchor = createComment(`]`)), container, next)
return next
@ -618,7 +626,6 @@ export function createHydrationFunctions(
slotScopeIds: string[] | null,
isFragment: boolean,
): Node | null => {
hasMismatch = true
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
warn(
`Hydration node mismatch:\n- rendered on server:`,
@ -631,6 +638,8 @@ export function createHydrationFunctions(
`\n- expected on client:`,
vnode.type,
)
logMismatchError()
vnode.el = null
if (isFragment) {

View File

@ -214,6 +214,7 @@ export type {
DebuggerEvent,
DebuggerEventExtraInfo,
Raw,
Reactive,
} from '@vue/reactivity'
export type {
WatchEffect,
@ -375,7 +376,11 @@ export { transformVNodeArgs } from './vnode'
// **IMPORTANT** These APIs are exposed solely for @vue/server-renderer and may
// change without notice between versions. User code should never rely on them.
import { createComponentInstance, setupComponent } from './component'
import {
createComponentInstance,
getComponentPublicInstance,
setupComponent,
} from './component'
import { renderComponentRoot } from './componentRenderUtils'
import { setCurrentRenderingInstance } from './componentRenderContext'
import { isVNode, normalizeVNode } from './vnode'
@ -387,6 +392,7 @@ const _ssrUtils = {
setCurrentRenderingInstance,
isVNode,
normalizeVNode,
getComponentPublicInstance,
}
/**

View File

@ -16,6 +16,7 @@ import {
import {
type ComponentInternalInstance,
type ComponentOptions,
type LifecycleHook,
createComponentInstance,
setupComponent,
} from './component'
@ -1257,7 +1258,8 @@ function baseCreateRenderer(
// setup() is async. This component relies on async logic to be resolved
// before proceeding
if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect)
parentSuspense &&
parentSuspense.registerDep(instance, setupRenderEffect, optimized)
// Give it a placeholder if this is not hydration
// TODO handle self-defined fallback
@ -1921,7 +1923,7 @@ function baseCreateRenderer(
const s2 = i // next starting index
// 5.1 build key:index map for newChildren
const keyToNewIndexMap: Map<string | number | symbol, number> = new Map()
const keyToNewIndexMap: Map<PropertyKey, number> = new Map()
for (i = s2; i <= e2; i++) {
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
@ -2122,12 +2124,18 @@ function baseCreateRenderer(
shapeFlag,
patchFlag,
dirs,
memoIndex,
} = vnode
// unset ref
if (ref != null) {
setRef(ref, null, parentSuspense, vnode, true)
}
// #6593 should clean memo cache when unmount
if (memoIndex != null) {
parentComponent!.renderCache[memoIndex] = undefined
}
if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)
return
@ -2279,7 +2287,9 @@ function baseCreateRenderer(
unregisterHMR(instance)
}
const { bum, scope, job, subTree, um } = instance
const { bum, scope, job, subTree, um, m, a } = instance
invalidateMount(m)
invalidateMount(a)
// beforeUnmount hook
if (bum) {
@ -2483,7 +2493,8 @@ export function traverseStaticChildren(n1: VNode, n2: VNode, shallow = false) {
c2 = ch2[i] = cloneIfMounted(ch2[i] as VNode)
c2.el = c1.el
}
if (!shallow) traverseStaticChildren(c1, c2)
if (!shallow && c2.patchFlag !== PatchFlags.BAIL)
traverseStaticChildren(c1, c2)
}
// #6852 also inherit for text nodes
if (c2.type === Text) {
@ -2552,3 +2563,10 @@ function locateNonHydratedAsyncRoot(
}
}
}
export function invalidateMount(hooks: LifecycleHook) {
if (hooks) {
for (let i = 0; i < hooks.length; i++)
hooks[i].flags! |= SchedulerJobFlags.DISPOSED
}
}

View File

@ -10,12 +10,12 @@ import {
remove,
} from '@vue/shared'
import { isAsyncWrapper } from './apiAsyncComponent'
import { getExposeProxy } from './component'
import { warn } from './warning'
import { isRef } from '@vue/reactivity'
import { ErrorCodes, callWithErrorHandling } from './errorHandling'
import type { SchedulerJob } from './scheduler'
import { queuePostRenderEffect } from './renderer'
import { getComponentPublicInstance } from './component'
/**
* Function for handling a template ref
@ -48,7 +48,7 @@ export function setRef(
const refValue =
vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT
? getExposeProxy(vnode.component!) || vnode.component!.proxy
? getComponentPublicInstance(vnode.component!)
: vnode.el
const value = isUnmount ? null : refValue

View File

@ -170,14 +170,12 @@ export function flushPostFlushCbs(seen?: CountMap) {
postFlushIndex < activePostFlushCbs.length;
postFlushIndex++
) {
if (
__DEV__ &&
checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])
) {
const cb = activePostFlushCbs[postFlushIndex]
if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
continue
}
activePostFlushCbs[postFlushIndex]()
activePostFlushCbs[postFlushIndex].flags! &= ~SchedulerJobFlags.QUEUED
if (!(cb.flags! & SchedulerJobFlags.DISPOSED)) cb()
cb.flags! &= ~SchedulerJobFlags.QUEUED
}
activePostFlushCbs = null
postFlushIndex = 0

View File

@ -36,7 +36,10 @@ import {
isSuspense,
} from './components/Suspense'
import type { DirectiveBinding } from './directives'
import type { TransitionHooks } from './components/BaseTransition'
import {
type TransitionHooks,
setTransitionHooks,
} from './components/BaseTransition'
import { warn } from './warning'
import {
type Teleport,
@ -109,7 +112,7 @@ export type VNodeHook =
// https://github.com/microsoft/TypeScript/issues/33099
export type VNodeProps = {
key?: string | number | symbol
key?: PropertyKey
ref?: VNodeRef
ref_for?: boolean
ref_key?: string
@ -159,7 +162,7 @@ export interface VNode<
type: VNodeTypes
props: (VNodeProps & ExtraProps) | null
key: string | number | symbol | null
key: PropertyKey | null
ref: VNodeNormalizedRef | null
/**
* SFC only. This is assigned on vnode creation using currentScopeId
@ -225,6 +228,10 @@ export interface VNode<
* @internal attached by v-memo
*/
memo?: any[]
/**
* @internal index for cleaning v-memo cache
*/
memoIndex?: number
/**
* @internal __COMPAT__ only
*/
@ -546,7 +553,7 @@ function _createVNode(
currentBlock.push(cloned)
}
}
cloned.patchFlag |= PatchFlags.BAIL
cloned.patchFlag = PatchFlags.BAIL
return cloned
}
@ -650,7 +657,7 @@ export function cloneVNode<T, U>(
scopeId: vnode.scopeId,
slotScopeIds: vnode.slotScopeIds,
children:
__DEV__ && patchFlag === PatchFlags.HOISTED && isArray(children)
__DEV__ && patchFlag === PatchFlags.CACHED && isArray(children)
? (children as VNode[]).map(deepCloneVNode)
: children,
target: vnode.target,
@ -663,7 +670,7 @@ export function cloneVNode<T, U>(
// fast paths only.
patchFlag:
extraProps && vnode.type !== Fragment
? patchFlag === PatchFlags.HOISTED // hoisted node
? patchFlag === PatchFlags.CACHED // hoisted node
? PatchFlags.FULL_PROPS
: patchFlag | PatchFlags.FULL_PROPS
: patchFlag,
@ -691,7 +698,10 @@ export function cloneVNode<T, U>(
// to clone the transition to ensure that the vnode referenced within
// the transition hooks is fresh.
if (transition && cloneTransition) {
cloned.transition = transition.clone(cloned as VNode)
setTransitionHooks(
cloned as VNode,
transition.clone(cloned as VNode) as TransitionHooks,
)
}
if (__COMPAT__) {
@ -772,7 +782,7 @@ export function normalizeVNode(child: VNodeChild): VNode {
// optimized normalization for template-compiled render fns
export function cloneIfMounted(child: VNode): VNode {
return (child.el === null && child.patchFlag !== PatchFlags.HOISTED) ||
return (child.el === null && child.patchFlag !== PatchFlags.CACHED) ||
child.memo
? child
: cloneVNode(child)

View File

@ -342,6 +342,23 @@ describe('defineCustomElement', () => {
expect(el.maxAge).toBe(50)
expect(el.shadowRoot.innerHTML).toBe('max age: 50/type: number')
})
test('support direct setup function syntax with extra options', () => {
const E = defineCustomElement(
props => {
return () => props.text
},
{
props: {
text: String,
},
},
)
customElements.define('my-el-setup-with-props', E)
container.innerHTML = `<my-el-setup-with-props text="hello"></my-el-setup-with-props>`
const e = container.childNodes[0] as VueElement
expect(e.shadowRoot!.innerHTML).toBe('hello')
})
})
describe('attrs', () => {

View File

@ -18,6 +18,20 @@ describe('runtime-dom: node-ops', () => {
expect(option2.selected).toBe(true)
})
test('create custom elements', () => {
const spyCreateElement = vi.spyOn(document, 'createElement')
nodeOps.createElement('custom-element')
expect(spyCreateElement).toHaveBeenLastCalledWith('custom-element')
nodeOps.createElement('custom-element', undefined, 'li')
expect(spyCreateElement).toHaveBeenLastCalledWith('custom-element', {
is: 'li',
})
spyCreateElement.mockClear()
})
describe('insertStaticContent', () => {
test('fresh insertion', () => {
const content = `<div>one</div><div>two</div>three`

View File

@ -53,4 +53,20 @@ describe('runtime-dom: attrs patching', () => {
patchProp(el, 'onwards', 'a', null)
expect(el.getAttribute('onwards')).toBe(null)
})
// #10597
test('should allow setting attribute to symbol', () => {
const el = document.createElement('div')
const symbol = Symbol('foo')
patchProp(el, 'foo', null, symbol)
expect(el.getAttribute('foo')).toBe(symbol.toString())
})
// #10598
test('should allow setting value to symbol', () => {
const el = document.createElement('input')
const symbol = Symbol('foo')
patchProp(el, 'value', null, symbol)
expect(el.value).toBe(symbol.toString())
})
})

View File

@ -51,6 +51,7 @@
"dependencies": {
"@vue/shared": "workspace:*",
"@vue/runtime-core": "workspace:*",
"@vue/reactivity": "workspace:*",
"csstype": "^3.1.3"
}
}

View File

@ -38,10 +38,16 @@ export type VueElementConstructor<P = {}> = {
// overload 1: direct setup function
export function defineCustomElement<Props, RawBindings = object>(
setup: (
props: Readonly<Props>,
ctx: SetupContext,
) => RawBindings | RenderFunction,
setup: (props: Props, ctx: SetupContext) => RawBindings | RenderFunction,
options?: Pick<ComponentOptions, 'name' | 'inheritAttrs' | 'emits'> & {
props?: (keyof Props)[]
},
): VueElementConstructor<Props>
export function defineCustomElement<Props, RawBindings = object>(
setup: (props: Props, ctx: SetupContext) => RawBindings | RenderFunction,
options?: Pick<ComponentOptions, 'name' | 'inheritAttrs' | 'emits'> & {
props?: ComponentObjectPropsOptions<Props>
},
): VueElementConstructor<Props>
// overload 2: defineCustomElement with options object, infer props from options
@ -127,9 +133,13 @@ export function defineCustomElement<P>(
/*! #__NO_SIDE_EFFECTS__ */
export function defineCustomElement(
options: any,
extraOptions?: ComponentOptions,
/**
* @internal
*/
hydrate?: RootHydrateFunction,
): VueElementConstructor {
const Comp = defineComponent(options) as any
const Comp = defineComponent(options, extraOptions) as any
class VueCustomElement extends VueElement {
static def = Comp
constructor(initialProps?: Record<string, any>) {
@ -141,9 +151,12 @@ export function defineCustomElement(
}
/*! #__NO_SIDE_EFFECTS__ */
export const defineSSRCustomElement = ((options: any) => {
export const defineSSRCustomElement = ((
options: any,
extraOptions?: ComponentOptions,
) => {
// @ts-expect-error
return defineCustomElement(options, hydrate)
return defineCustomElement(options, extraOptions, hydrate)
}) as typeof defineCustomElement
const BaseClass = (

View File

@ -85,7 +85,11 @@ export const vModelText: ModelDirective<
mounted(el, { value }) {
el.value = value == null ? '' : value
},
beforeUpdate(el, { value, modifiers: { lazy, trim, number } }, vnode) {
beforeUpdate(
el,
{ value, oldValue, modifiers: { lazy, trim, number } },
vnode,
) {
el[assignKey] = getModelAssigner(vnode)
// avoid clearing unresolved text. #2302
if ((el as any).composing) return
@ -100,7 +104,8 @@ export const vModelText: ModelDirective<
}
if (document.activeElement === el && el.type !== 'range') {
if (lazy) {
// #8546
if (lazy && value === oldValue) {
return
}
if (trim && el.value.trim() === newValue) {

View File

@ -1390,7 +1390,7 @@ type EventHandlers<E> = {
import type { VNodeRef } from '@vue/runtime-core'
export type ReservedProps = {
key?: string | number | symbol
key?: PropertyKey
ref?: VNodeRef
ref_for?: boolean
ref_key?: string

View File

@ -18,6 +18,7 @@ export function patchAttr(
value: any,
isSVG: boolean,
instance?: ComponentInternalInstance | null,
isBoolean = isSpecialBooleanAttr(key),
) {
if (isSVG && key.startsWith('xlink:')) {
if (value == null) {
@ -32,11 +33,11 @@ export function patchAttr(
// note we are only checking boolean attributes that don't have a
// corresponding dom prop of the same name here.
const isBoolean = isSpecialBooleanAttr(key)
if (value == null || (isBoolean && !includeBooleanAttr(value))) {
el.removeAttribute(key)
} else {
el.setAttribute(key, isBoolean ? '' : value)
// attribute value is a string https://html.spec.whatwg.org/multipage/dom.html#attributes
el.setAttribute(key, isBoolean ? '' : String(value))
}
}
}
@ -75,12 +76,13 @@ export function compatCoerceAttr(
} else if (
value === false &&
!isSpecialBooleanAttr(key) &&
compatUtils.softAssertCompatEnabled(
compatUtils.isCompatEnabled(DeprecationTypes.ATTR_FALSE_VALUE, instance)
) {
compatUtils.warnDeprecation(
DeprecationTypes.ATTR_FALSE_VALUE,
instance,
key,
)
) {
el.removeAttribute(key)
return true
}

View File

@ -38,7 +38,7 @@ export function patchDOMProp(
// compare against its attribute value instead.
const oldValue =
tag === 'OPTION' ? el.getAttribute('value') || '' : el.value
const newValue = value == null ? '' : value
const newValue = value == null ? '' : String(value)
if (oldValue !== newValue || !('_value' in el)) {
el.value = newValue
}

View File

@ -25,7 +25,9 @@ export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
? doc.createElementNS(svgNS, tag)
: namespace === 'mathml'
? doc.createElementNS(mathmlNS, tag)
: doc.createElement(tag, is ? { is } : undefined)
: is
? doc.createElement(tag, { is })
: doc.createElement(tag)
if (tag === 'select' && props && props.multiple != null) {
;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)

View File

@ -51,6 +51,11 @@ export const patchProp: DOMRendererOptions['patchProp'] = (
parentSuspense,
unmountChildren,
)
// #6007 also set form state as attributes so they work with
// <input type="reset"> or libs / extensions that expect attributes
if (key === 'value' || key === 'checked' || key === 'selected') {
patchAttr(el, key, nextValue, isSVG, parentComponent, key !== 'value')
}
} else {
// special case for <input v-model type="checkbox"> with
// :true-value & :false-value

View File

@ -2,7 +2,10 @@ import { renderToString } from '../src/renderToString'
import {
createApp,
h,
mergeProps,
ref,
resolveDirective,
unref,
vModelCheckbox,
vModelDynamic,
vModelRadio,
@ -542,4 +545,44 @@ describe('ssr: directives', () => {
),
).toBe(`<div id="foo-arg-true"></div>`)
})
// #7499
test('custom directive w/ getSSRProps (expose)', async () => {
let exposeVars: null | string | undefined = null
const useTestDirective = () => ({
vTest: {
getSSRProps({ instance }: any) {
if (instance) {
exposeVars = instance.x
}
return { id: exposeVars }
},
},
})
const { vTest } = useTestDirective()
const renderString = await renderToString(
createApp({
setup(props, { expose }) {
const x = ref('foo')
expose({ x })
const __returned__ = { useTestDirective, vTest, ref, x }
Object.defineProperty(__returned__, '__isScriptSetup', {
enumerable: false,
value: true,
})
return __returned__
},
ssrRender(_ctx, _push, _parent, _attrs) {
_push(
`<div${ssrRenderAttrs(
mergeProps(_attrs!, ssrGetDirectiveProps(_ctx, unref(vTest))),
)}></div>`,
)
},
}),
)
expect(renderString).toBe(`<div id="foo"></div>`)
expect(exposeVars).toBe('foo')
})
})

View File

@ -179,4 +179,94 @@ describe('ssr: scopedId runtime behavior', () => {
const result = await renderToString(createApp(Comp)) // output: `<div></div>`
expect(result).toBe(`<div parent></div>`)
})
// #6093
test(':slotted on forwarded slots on component', async () => {
const Wrapper = {
__scopeId: 'wrapper',
ssrRender: (ctx: any, push: any, parent: any, attrs: any) => {
// <div class="wrapper"><slot/></div>
push(
`<div${ssrRenderAttrs(
mergeProps({ class: 'wrapper' }, attrs),
)} wrapper>`,
)
ssrRenderSlot(
ctx.$slots,
'default',
{},
null,
push,
parent,
'wrapper-s',
)
push(`</div>`)
},
}
const Slotted = {
__scopeId: 'slotted',
ssrRender: (ctx: any, push: any, parent: any, attrs: any) => {
// <Wrapper><slot/></Wrapper>
push(
ssrRenderComponent(
Wrapper,
attrs,
{
default: withCtx(
(_: any, push: any, parent: any, scopeId: string) => {
ssrRenderSlot(
ctx.$slots,
'default',
{},
null,
push,
parent,
'slotted-s' + scopeId,
)
},
),
_: 1,
} as any,
parent,
),
)
},
}
const Child = {
ssrRender: (ctx: any, push: any, parent: any, attrs: any) => {
push(`<div${ssrRenderAttrs(attrs)}></div>`)
},
}
const Root = {
__scopeId: 'root',
// <Slotted><Child></Child></Slotted>
ssrRender: (_: any, push: any, parent: any, attrs: any) => {
push(
ssrRenderComponent(
Slotted,
attrs,
{
default: withCtx(
(_: any, push: any, parent: any, scopeId: string) => {
push(ssrRenderComponent(Child, null, null, parent, scopeId))
},
),
_: 1,
} as any,
parent,
),
)
},
}
const result = await renderToString(createApp(Root))
expect(result).toBe(
`<div class="wrapper" root slotted wrapper>` +
`<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--]-->` +
`</div>`,
)
})
})

Some files were not shown because too many files have changed in this diff Show More