chore: Merge branch 'main' into edison/fix/13460

This commit is contained in:
daiwei 2025-09-18 15:10:07 +08:00
commit 1aa2a6b12a
138 changed files with 3958 additions and 2128 deletions

View File

@ -11,7 +11,7 @@ jobs:
env:
PUPPETEER_SKIP_DOWNLOAD: 'true'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
@ -31,4 +31,4 @@ jobs:
- name: Run prettier
run: pnpm run format
- uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27

View File

@ -1,33 +0,0 @@
name: canary minor release
on:
# Runs every Monday at 1 AM UTC (9:00 AM in Singapore)
schedule:
- cron: 0 1 * * MON
workflow_dispatch:
jobs:
canary:
# prevents this action from running on forks
if: github.repository == 'vuejs/core'
runs-on: ubuntu-latest
environment: Release
steps:
- uses: actions/checkout@v4
with:
ref: minor
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.node-version'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
- run: pnpm install
- run: pnpm release --canary --publish --tag minor
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@ -1,31 +0,0 @@
name: canary release
on:
# Runs every Monday at 1 AM UTC (9:00 AM in Singapore)
schedule:
- cron: 0 1 * * MON
workflow_dispatch:
jobs:
canary:
# prevents this action from running on forks
if: github.repository == 'vuejs/core'
runs-on: ubuntu-latest
environment: Release
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.node-version'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
- run: pnpm install
- run: pnpm release --canary --publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,100 @@
## [3.5.21](https://github.com/vuejs/core/compare/v3.5.20...v3.5.21) (2025-09-02)
### Bug Fixes
* **compiler-core:** force dynamic slots when slot referencing scope vars ([#9427](https://github.com/vuejs/core/issues/9427)) ([99d54b2](https://github.com/vuejs/core/commit/99d54b28b46dbea006205dff71c383a31dd1b87a)), closes [#9380](https://github.com/vuejs/core/issues/9380)
* **compiler-sfc:** check lang before attempt to compile script ([#13508](https://github.com/vuejs/core/issues/13508)) ([55922ff](https://github.com/vuejs/core/commit/55922ff3168a1397ad72f18946eb1c4051cdab3b)), closes [#8368](https://github.com/vuejs/core/issues/8368)
* **compiler-sfc:** support `${configDir}` in paths for TypeScript 5.5+ ([#13491](https://github.com/vuejs/core/issues/13491)) ([8696e34](https://github.com/vuejs/core/commit/8696e346b4780d88247464490f1a992cc0c3658c)), closes [#13484](https://github.com/vuejs/core/issues/13484)
* **compiler-sfc:** support global augments with named exports ([#13789](https://github.com/vuejs/core/issues/13789)) ([35da3c6](https://github.com/vuejs/core/commit/35da3c6dcb30030ef60fa22e30aa83a56e396c60))
* **custom-element:** prevent defineCustomElement from mutating the options object ([#13791](https://github.com/vuejs/core/issues/13791)) ([e322436](https://github.com/vuejs/core/commit/e322436887549c129e61eb58a0084167103451bb))
* **hmr:** prevent `__VUE_HMR_RUNTIME__` from being overwritten by vue runtime in 3rd-party libraries ([#13817](https://github.com/vuejs/core/issues/13817)) ([1392734](https://github.com/vuejs/core/commit/1392734ae5d5a3b2be124753e198eafa324f6815)), closes [vitejs/vite-plugin-vue#644](https://github.com/vitejs/vite-plugin-vue/issues/644)
* **hmr:** prevent update unmounting component during HMR reload ([#13815](https://github.com/vuejs/core/issues/13815)) ([ef20b86](https://github.com/vuejs/core/commit/ef20b86b36a127e317f8981df970dc8efd277053)), closes [vitejs/vite-plugin-vue#599](https://github.com/vitejs/vite-plugin-vue/issues/599)
* **runtime-core:** disable tracking block in h function ([#8213](https://github.com/vuejs/core/issues/8213)) ([8f6b505](https://github.com/vuejs/core/commit/8f6b5050518441a5047d128138da44f798836002)), closes [#6913](https://github.com/vuejs/core/issues/6913)
* **runtime-core:** use separate emits caches for components and mixins ([#11661](https://github.com/vuejs/core/issues/11661)) ([15fc75f](https://github.com/vuejs/core/commit/15fc75f4031dea805c3bbb67a75e48a9dc307c11))
* **Suspence:** handle Suspense + KeepAlive HMR updating edge case ([#13076](https://github.com/vuejs/core/issues/13076)) ([5d75a17](https://github.com/vuejs/core/commit/5d75a170c8d23acd11ef2513173d4cbc4d0b54de)), closes [#13075](https://github.com/vuejs/core/issues/13075)
* **Teleport:** hydrate disabled Teleport with undefined target ([#11235](https://github.com/vuejs/core/issues/11235)) ([00978f7](https://github.com/vuejs/core/commit/00978f7d14e85b49d9d334ea92fa8c03733ce64c)), closes [#11230](https://github.com/vuejs/core/issues/11230)
* **templateRef:** prevent unnecessary set ref on dynamic ref change or component unmount ([#12642](https://github.com/vuejs/core/issues/12642)) ([93ba107](https://github.com/vuejs/core/commit/93ba10767230872fcdca974a1e19e8bd69b7eb6a)), closes [#12639](https://github.com/vuejs/core/issues/12639)
* **watch:** use maximum depth for duplicates ([#13434](https://github.com/vuejs/core/issues/13434)) ([f2699a5](https://github.com/vuejs/core/commit/f2699a5cb376ffa452a54feb171c14411c67287c))
### Performance Improvements
* improve regexp performance with non-capturing groups ([#13567](https://github.com/vuejs/core/issues/13567)) ([1e8b65a](https://github.com/vuejs/core/commit/1e8b65aa4934c94ef6142b4f49cdfb13ba5e6ce5))
## [3.5.20](https://github.com/vuejs/core/compare/v3.5.19...v3.5.20) (2025-08-25)
### Bug Fixes
* **runtime-dom:** add name to vShow for prop mismatch check ([#13806](https://github.com/vuejs/core/issues/13806)) ([1031e8d](https://github.com/vuejs/core/commit/1031e8de08b735059217b1ad0057f62565c99c4f)), closes [#13805](https://github.com/vuejs/core/issues/13805) re-fix [#13744](https://github.com/vuejs/core/issues/13744) revert [#13777](https://github.com/vuejs/core/issues/13777)
## [3.5.19](https://github.com/vuejs/core/compare/v3.5.18...v3.5.19) (2025-08-21)
### Bug Fixes
* **compiler-core:** adjacent v-else should cause a compiler error ([#13699](https://github.com/vuejs/core/issues/13699)) ([911e670](https://github.com/vuejs/core/commit/911e67045e2a63e0ecbd198ed4f567530f6d1c17)), closes [#13698](https://github.com/vuejs/core/issues/13698)
* **compiler-core:** prevent cached array children from retaining detached dom nodes ([#13691](https://github.com/vuejs/core/issues/13691)) ([7f60ef8](https://github.com/vuejs/core/commit/7f60ef83e735dbd29d323347acecf69f22b06d53)), closes [element-plus/element-plus#21408](https://github.com/element-plus/element-plus/issues/21408) [#13211](https://github.com/vuejs/core/issues/13211)
* **compiler-sfc:** improve type inference for generic type aliases types ([#12876](https://github.com/vuejs/core/issues/12876)) ([d9dd628](https://github.com/vuejs/core/commit/d9dd628800ae32e673bdfabfe79f1988037991d0)), closes [#12872](https://github.com/vuejs/core/issues/12872)
* **compiler-sfc:** throw mismatched script langs error before invoking babel ([#13194](https://github.com/vuejs/core/issues/13194)) ([0562548](https://github.com/vuejs/core/commit/0562548ab3a040073386021222225e0e9d43c632)), closes [#13193](https://github.com/vuejs/core/issues/13193)
* **compiler-ssr:** disable v-memo transform in ssr vdom fallback branch ([#13725](https://github.com/vuejs/core/issues/13725)) ([0a202d8](https://github.com/vuejs/core/commit/0a202d890ff2a564b1fab51e4ac621708640818e)), closes [#13724](https://github.com/vuejs/core/issues/13724)
* **devtools:** clear performance measures ([#13701](https://github.com/vuejs/core/issues/13701)) ([c875019](https://github.com/vuejs/core/commit/c875019d49b4c36a88d929ccadc31ad414747c7b)), closes [#13700](https://github.com/vuejs/core/issues/13700)
* **hmr:** prevent updating unmounting component during HMR rerender ([#13775](https://github.com/vuejs/core/issues/13775)) ([6e5143d](https://github.com/vuejs/core/commit/6e5143d9635dac3f20fb394a827109df30e232ae)), closes [#13771](https://github.com/vuejs/core/issues/13771) [#13772](https://github.com/vuejs/core/issues/13772)
* **hydration:** also set vShow name if `__FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__` flag is enabled ([#13777](https://github.com/vuejs/core/issues/13777)) ([439e1a5](https://github.com/vuejs/core/commit/439e1a543e62de4dbf7658d78d05c358c9677c86)), closes [#13744](https://github.com/vuejs/core/issues/13744)
* **reactivity:** warn on nested readonly ref update during unwrapping ([#12141](https://github.com/vuejs/core/issues/12141)) ([1498821](https://github.com/vuejs/core/commit/1498821ed9eeb22a0767e53ddc1f6a2840598a29))
* **runtime-core:** avoid setting direct ref of useTemplateRef in dev ([#13449](https://github.com/vuejs/core/issues/13449)) ([4a2953f](https://github.com/vuejs/core/commit/4a2953f57b90dfc24e34ff1a87cc1ebb0b97636d))
* **runtime-core:** improve consistency of `PublicInstanceProxyHandlers.has` ([#13507](https://github.com/vuejs/core/issues/13507)) ([d7283f3](https://github.com/vuejs/core/commit/d7283f3b7f0631c8b8a4a31a05983dac9f078c4f))
* **suspense:** don't immediately resolve suspense on last dep unmount ([#13456](https://github.com/vuejs/core/issues/13456)) ([a871315](https://github.com/vuejs/core/commit/a8713159ee24602c7c2b70c5fd52d2e5cd37dca5)), closes [#13453](https://github.com/vuejs/core/issues/13453)
* **transition:** handle KeepAlive + transition leaving edge case ([#13152](https://github.com/vuejs/core/issues/13152)) ([3190b17](https://github.com/vuejs/core/commit/3190b179b0545a3dc4549737793eec630cf9f0d1)), closes [#13153](https://github.com/vuejs/core/issues/13153)
## [3.5.18](https://github.com/vuejs/core/compare/v3.5.17...v3.5.18) (2025-07-23)
### Bug Fixes
* **compiler-core:** avoid cached text vnodes retaining detached DOM nodes ([#13662](https://github.com/vuejs/core/issues/13662)) ([00695a5](https://github.com/vuejs/core/commit/00695a5b41b2d032deaeada83831ff83aa6bfd4e)), closes [#13661](https://github.com/vuejs/core/issues/13661)
* **compiler-core:** avoid self updates of `v-pre` ([#12556](https://github.com/vuejs/core/issues/12556)) ([21b685a](https://github.com/vuejs/core/commit/21b685ad9d9d0e6060fc7d07b719bf35f2d9ae1f))
* **compiler-core:** identifiers in function parameters should not be inferred as references ([#13548](https://github.com/vuejs/core/issues/13548)) ([9b02923](https://github.com/vuejs/core/commit/9b029239edf88558465b941e1e4c085f92b1ebff))
* **compiler-core:** recognize empty string as non-identifier ([#12553](https://github.com/vuejs/core/issues/12553)) ([ce93339](https://github.com/vuejs/core/commit/ce933390ad1c72bed258f7ad959a78f0e8acdf57))
* **compiler-core:** transform empty `v-bind` dynamic argument content correctly ([#12554](https://github.com/vuejs/core/issues/12554)) ([d3af67e](https://github.com/vuejs/core/commit/d3af67e878790892f9d34cfea15d13625aabe733))
* **compiler-sfc:** transform empty srcset w/ includeAbsolute: true ([#13639](https://github.com/vuejs/core/issues/13639)) ([d8e40ef](https://github.com/vuejs/core/commit/d8e40ef7e1c20ee86b294e7cf78e2de60d12830e)), closes [vitejs/vite-plugin-vue#631](https://github.com/vitejs/vite-plugin-vue/issues/631)
* **css-vars:** nullish v-bind in style should not lead to unexpected inheritance ([#12461](https://github.com/vuejs/core/issues/12461)) ([c85f1b5](https://github.com/vuejs/core/commit/c85f1b5a132eb8ec25f71b250e25e65a5c20964f)), closes [#12434](https://github.com/vuejs/core/issues/12434) [#12439](https://github.com/vuejs/core/issues/12439) [#7474](https://github.com/vuejs/core/issues/7474) [#7475](https://github.com/vuejs/core/issues/7475)
* **custom-element:** ensure exposed methods are accessible from custom elements by making them enumerable ([#13634](https://github.com/vuejs/core/issues/13634)) ([90573b0](https://github.com/vuejs/core/commit/90573b06bf6fb6c14c6bbff6c4e34e0ab108953a)), closes [#13632](https://github.com/vuejs/core/issues/13632)
* **hydration:** prevent lazy hydration for updated components ([#13511](https://github.com/vuejs/core/issues/13511)) ([a9269c6](https://github.com/vuejs/core/commit/a9269c642bf944560bc29adb5dae471c11cd9ee8)), closes [#13510](https://github.com/vuejs/core/issues/13510)
* **runtime-core:** ensure correct anchor el for unresolved async components ([#13560](https://github.com/vuejs/core/issues/13560)) ([7f29943](https://github.com/vuejs/core/commit/7f2994393dcdb82cacbf62e02b5ba5565f32588b)), closes [#13559](https://github.com/vuejs/core/issues/13559)
* **slots:** refine internal key checking to support slot names starting with an underscore ([#13612](https://github.com/vuejs/core/issues/13612)) ([c5f7db1](https://github.com/vuejs/core/commit/c5f7db11542bb2246363aef78c88a8e6cef0ee93)), closes [#13611](https://github.com/vuejs/core/issues/13611)
* **ssr:** ensure empty slots render as a comment node in Transition ([#13396](https://github.com/vuejs/core/issues/13396)) ([8cfc10a](https://github.com/vuejs/core/commit/8cfc10a80b9cbf5d801ab149e49b8506d192e7e1)), closes [#13394](https://github.com/vuejs/core/issues/13394)
## [3.5.17](https://github.com/vuejs/core/compare/v3.5.16...v3.5.17) (2025-06-18)
### Bug Fixes
* **compat:** allow v-model built in modifiers on component ([#12654](https://github.com/vuejs/core/issues/12654)) ([cb14b86](https://github.com/vuejs/core/commit/cb14b860f150c4a83bcd52cd26096b7a5aa3a2bf)), closes [#12652](https://github.com/vuejs/core/issues/12652)
* **compile-sfc:** handle mapped types work with omit and pick ([#12648](https://github.com/vuejs/core/issues/12648)) ([4eb46e4](https://github.com/vuejs/core/commit/4eb46e443f1878199755cb73d481d318a9714392)), closes [#12647](https://github.com/vuejs/core/issues/12647)
* **compiler-core:** do not increase newlines in `InEntity` state ([#13362](https://github.com/vuejs/core/issues/13362)) ([f05a8d6](https://github.com/vuejs/core/commit/f05a8d613bd873b811cfdb9979ccac8382dba322))
* **compiler-core:** ignore whitespace when matching adjacent v-if ([#12321](https://github.com/vuejs/core/issues/12321)) ([10ebcef](https://github.com/vuejs/core/commit/10ebcef8c870dbc042b0ea49b1424b2e8f692145)), closes [#9173](https://github.com/vuejs/core/issues/9173)
* **compiler-core:** prevent comments from blocking static node hoisting ([#13345](https://github.com/vuejs/core/issues/13345)) ([55dad62](https://github.com/vuejs/core/commit/55dad625acd9e9ddd5a933d5e323ecfdec1a612f)), closes [#13344](https://github.com/vuejs/core/issues/13344)
* **compiler-sfc:** improved type resolution for function type aliases ([#13452](https://github.com/vuejs/core/issues/13452)) ([f3479aa](https://github.com/vuejs/core/commit/f3479aac9625f4459e650d1c0a70e73863147903)), closes [#13444](https://github.com/vuejs/core/issues/13444)
* **custom-element:** ensure configureApp is applied to async component ([#12607](https://github.com/vuejs/core/issues/12607)) ([5ba1afb](https://github.com/vuejs/core/commit/5ba1afba09c3ea56c1c17484f5d8aeae210ce52a)), closes [#12448](https://github.com/vuejs/core/issues/12448)
* **custom-element:** prevent injecting child styles if shadowRoot is false ([#12769](https://github.com/vuejs/core/issues/12769)) ([73055d8](https://github.com/vuejs/core/commit/73055d8d9578d485e3fe846726b50666e1aa56f5)), closes [#12630](https://github.com/vuejs/core/issues/12630)
* **reactivity:** add `__v_skip` flag to `Dep` to prevent reactive conversion ([#12804](https://github.com/vuejs/core/issues/12804)) ([e8d8f5f](https://github.com/vuejs/core/commit/e8d8f5f604e821acc46b4200d5b06979c05af1c2)), closes [#12803](https://github.com/vuejs/core/issues/12803)
* **runtime-core:** unset old ref during patching when new ref is absent ([#12900](https://github.com/vuejs/core/issues/12900)) ([47ddf98](https://github.com/vuejs/core/commit/47ddf986021dff8de68b0da72787e53a6c19de4c)), closes [#12898](https://github.com/vuejs/core/issues/12898)
* **slots:** make cache indexes marker non-enumerable ([#13469](https://github.com/vuejs/core/issues/13469)) ([919c447](https://github.com/vuejs/core/commit/919c44744bba1f0c661c87d2059c3b429611aa7e)), closes [#13468](https://github.com/vuejs/core/issues/13468)
* **ssr:** handle initial selected state for select with v-model + v-for/v-if option ([#13487](https://github.com/vuejs/core/issues/13487)) ([1552095](https://github.com/vuejs/core/commit/15520954f9f1c7f834175938a50dba5d4be0e6c4)), closes [#13486](https://github.com/vuejs/core/issues/13486)
* **types:** typo of `vOnce` and `vSlot` ([#13343](https://github.com/vuejs/core/issues/13343)) ([762fae4](https://github.com/vuejs/core/commit/762fae4b57ad60602e5c84465a3bff562785b314))
## [3.5.16](https://github.com/vuejs/core/compare/v3.5.15...v3.5.16) (2025-05-29)
@ -232,7 +329,7 @@
* **compiler-core:** fix handling of delimiterOpen in VPre ([#11915](https://github.com/vuejs/core/issues/11915)) ([706d4ac](https://github.com/vuejs/core/commit/706d4ac1d0210b2d9134b3228280187fe02fc971)), closes [#11913](https://github.com/vuejs/core/issues/11913)
* **compiler-dom:** fix stringify static edge for partially eligible chunks in cached parent ([1d99d61](https://github.com/vuejs/core/commit/1d99d61c1bd77f9ea6743f6214a82add8346a121)), closes [#11879](https://github.com/vuejs/core/issues/11879) [#11890](https://github.com/vuejs/core/issues/11890)
* **compiler-dom:** should ignore leading newline in <textarea> per spec ([3c4bf76](https://github.com/vuejs/core/commit/3c4bf7627649ec1e3220f8c4e4163c20d2afb367))
* **compiler-dom:** should ignore leading newline in `<textarea>` per spec ([3c4bf76](https://github.com/vuejs/core/commit/3c4bf7627649ec1e3220f8c4e4163c20d2afb367))
* **compiler-sfc:** nested css supports atrule and comment ([#11899](https://github.com/vuejs/core/issues/11899)) ([0e7bc71](https://github.com/vuejs/core/commit/0e7bc717e6640644f062957ec5031506f0dab215)), closes [#11896](https://github.com/vuejs/core/issues/11896)
* **custom-element:** handle nested customElement mount w/ shadowRoot false ([#11861](https://github.com/vuejs/core/issues/11861)) ([f2d8019](https://github.com/vuejs/core/commit/f2d801918841e7673ff3f048d0d895592a2f7e23)), closes [#11851](https://github.com/vuejs/core/issues/11851) [#11871](https://github.com/vuejs/core/issues/11871)
* **hmr:** reload async child wrapped in Suspense + KeepAlive ([#11907](https://github.com/vuejs/core/issues/11907)) ([10a2c60](https://github.com/vuejs/core/commit/10a2c6053bd30d160d0214bb3566f540187e6874)), closes [#11868](https://github.com/vuejs/core/issues/11868)

View File

@ -1,7 +1,7 @@
{
"private": true,
"version": "3.5.16",
"packageManager": "pnpm@10.11.1",
"version": "3.5.21",
"packageManager": "pnpm@10.15.0",
"type": "module",
"scripts": {
"dev": "node scripts/dev.js",
@ -17,11 +17,11 @@
"format": "prettier --write --cache .",
"format-check": "prettier --check --cache .",
"test": "vitest",
"test-unit": "vitest --project unit",
"test-unit": "vitest --project unit*",
"test-e2e": "node scripts/build.js vue -f global -d && vitest --project e2e",
"test-dts": "run-s build-dts test-dts-only",
"test-dts-only": "tsc -p packages-private/dts-built-test/tsconfig.json && tsc -p ./packages-private/dts-test/tsconfig.test.json",
"test-coverage": "vitest run --project unit --coverage",
"test-coverage": "vitest run --project unit* --coverage",
"prebench": "node scripts/build.js -pf esm-browser reactivity",
"prebench-compare": "node scripts/build.js -pf esm-browser reactivity",
"bench": "vitest bench --project=unit --outputJson=temp/bench.json",
@ -65,21 +65,21 @@
"@babel/parser": "catalog:",
"@babel/types": "catalog:",
"@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-commonjs": "^28.0.3",
"@rollup/plugin-commonjs": "^28.0.6",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-replace": "5.0.4",
"@swc/core": "^1.11.29",
"@swc/core": "^1.13.5",
"@types/hash-sum": "^1.0.2",
"@types/node": "^22.15.29",
"@types/node": "^22.17.2",
"@types/semver": "^7.7.0",
"@types/serve-handler": "^6.1.4",
"@vitest/coverage-v8": "^3.1.4",
"@vitest/eslint-plugin": "^1.2.1",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/eslint-plugin": "^1.3.5",
"@vue/consolidate": "1.0.0",
"conventional-changelog-cli": "^5.0.0",
"enquirer": "^2.4.1",
"esbuild": "^0.25.5",
"esbuild": "^0.25.9",
"esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^9.27.0",
"eslint-plugin-import-x": "^4.13.1",
@ -87,18 +87,18 @@
"jsdom": "^26.1.0",
"lint-staged": "^16.0.0",
"lodash": "^4.17.21",
"magic-string": "^0.30.17",
"magic-string": "^0.30.18",
"markdown-table": "^3.0.4",
"marked": "13.0.3",
"npm-run-all2": "^7.0.2",
"npm-run-all2": "^8.0.4",
"picocolors": "^1.1.1",
"prettier": "^3.5.3",
"pretty-bytes": "^6.1.1",
"pug": "^3.0.3",
"puppeteer": "~24.9.0",
"puppeteer": "~24.17.1",
"rimraf": "^6.0.1",
"rollup": "^4.41.1",
"rollup-plugin-dts": "^6.2.1",
"rollup": "^4.50.0",
"rollup-plugin-dts": "^6.2.3",
"rollup-plugin-esbuild": "^6.2.1",
"rollup-plugin-polyfill-node": "^0.13.0",
"semver": "^7.7.2",
@ -110,6 +110,6 @@
"typescript": "~5.6.2",
"typescript-eslint": "^8.32.1",
"vite": "catalog:",
"vitest": "^3.1.4"
"vitest": "^3.2.4"
}
}

View File

@ -0,0 +1,31 @@
import { nextTick } from 'vue'
import { describe, expectType } from './utils'
describe('nextTick', async () => {
expectType<Promise<void>>(nextTick())
expectType<Promise<string>>(nextTick(() => 'foo'))
expectType<Promise<string>>(nextTick(() => Promise.resolve('foo')))
expectType<Promise<string>>(
nextTick(() => Promise.resolve(Promise.resolve('foo'))),
)
expectType<void>(await nextTick())
expectType<string>(await nextTick(() => 'foo'))
expectType<string>(await nextTick(() => Promise.resolve('foo')))
expectType<string>(
await nextTick(() => Promise.resolve(Promise.resolve('foo'))),
)
nextTick().then(value => {
expectType<void>(value)
})
nextTick(() => 'foo').then(value => {
expectType<string>(value)
})
nextTick(() => Promise.resolve('foo')).then(value => {
expectType<string>(value)
})
nextTick(() => Promise.resolve(Promise.resolve('foo'))).then(value => {
expectType<string>(value)
})
})

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@
"enableNonBrowserBranches": true
},
"dependencies": {
"monaco-editor": "^0.52.2",
"monaco-editor": "^0.53.0",
"source-map-js": "^1.2.1"
}
}

View File

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

View File

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

View File

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

View File

@ -478,7 +478,10 @@ describe('compiler: transform component slots', () => {
})
test('should only force dynamic slots when actually using scope vars w/ prefixIdentifiers: true', () => {
function assertDynamicSlots(template: string, shouldForce: boolean) {
function assertDynamicSlots(
template: string,
expectedPatchFlag?: PatchFlags,
) {
const { root } = parseWithSlots(template, { prefixIdentifiers: true })
let flag: any
if (root.children[0].type === NodeTypes.FOR) {
@ -491,8 +494,8 @@ describe('compiler: transform component slots', () => {
.children[0] as ComponentNode
flag = (innerComp.codegenNode as VNodeCall).patchFlag
}
if (shouldForce) {
expect(flag).toBe(PatchFlags.DYNAMIC_SLOTS)
if (expectedPatchFlag) {
expect(flag).toBe(expectedPatchFlag)
} else {
expect(flag).toBeUndefined()
}
@ -502,14 +505,13 @@ describe('compiler: transform component slots', () => {
`<div v-for="i in list">
<Comp v-slot="bar">foo</Comp>
</div>`,
false,
)
assertDynamicSlots(
`<div v-for="i in list">
<Comp v-slot="bar">{{ i }}</Comp>
</div>`,
true,
PatchFlags.DYNAMIC_SLOTS,
)
// reference the component's own slot variable should not force dynamic slots
@ -517,14 +519,13 @@ describe('compiler: transform component slots', () => {
`<Comp v-slot="foo">
<Comp v-slot="bar">{{ bar }}</Comp>
</Comp>`,
false,
)
assertDynamicSlots(
`<Comp v-slot="foo">
<Comp v-slot="bar">{{ foo }}</Comp>
</Comp>`,
true,
PatchFlags.DYNAMIC_SLOTS,
)
// #2564
@ -532,14 +533,35 @@ describe('compiler: transform component slots', () => {
`<div v-for="i in list">
<Comp v-slot="bar"><button @click="fn(i)" /></Comp>
</div>`,
true,
PatchFlags.DYNAMIC_SLOTS,
)
assertDynamicSlots(
`<div v-for="i in list">
<Comp v-slot="bar"><button @click="fn()" /></Comp>
</div>`,
false,
)
// #9380
assertDynamicSlots(
`<div v-for="i in list">
<Comp :i="i">foo</Comp>
</div>`,
PatchFlags.PROPS,
)
assertDynamicSlots(
`<div v-for="i in list">
<Comp v-slot="{ value = i }"><button @click="fn()" /></Comp>
</div>`,
PatchFlags.DYNAMIC_SLOTS,
)
assertDynamicSlots(
`<div v-for="i in list">
<Comp v-slot:[i]><button @click="fn()" /></Comp>
</div>`,
PatchFlags.DYNAMIC_SLOTS,
)
})

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-core",
"version": "3.5.16",
"version": "3.5.21",
"description": "@vue/compiler-core",
"main": "index.js",
"module": "dist/compiler-core.esm-bundler.js",

View File

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

View File

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

View File

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

View File

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

View File

@ -36,7 +36,7 @@ import { findDir, findProp, getMemoedVNodeCall, injectProp } from '../utils'
import { PatchFlags } from '@vue/shared'
export const transformIf: NodeTransform = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
/^(?:if|else|else-if)$/,
(node, dir, context) => {
return processIf(node, dir, context, (ifNode, branch, isRoot) => {
// #1587: We need to dynamically increment the key based on the current
@ -141,9 +141,9 @@ export function processIf(
}
if (sibling && sibling.type === NodeTypes.IF) {
// Check if v-else was followed by v-else-if
// Check if v-else was followed by v-else-if or there are two adjacent v-else
if (
dir.name === 'else-if' &&
(dir.name === 'else-if' || dir.name === 'else') &&
sibling.branches[sibling.branches.length - 1].condition === undefined
) {
context.onError(

View File

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

View File

@ -131,9 +131,17 @@ export function buildSlots(
// since it likely uses a scope variable.
let hasDynamicSlots = context.scopes.vSlot > 0 || context.scopes.vFor > 0
// with `prefixIdentifiers: true`, this can be further optimized to make
// it dynamic only when the slot actually uses the scope variables.
// it dynamic when
// 1. the slot arg or exp uses the scope variables.
// 2. the slot children use the scope variables.
if (!__BROWSER__ && !context.ssr && context.prefixIdentifiers) {
hasDynamicSlots = hasScopeRef(node, context.identifiers)
hasDynamicSlots =
node.props.some(
prop =>
isVSlot(prop) &&
(hasScopeRef(prop.arg, context.identifiers) ||
hasScopeRef(prop.exp, context.identifiers)),
) || children.some(child => hasScopeRef(child, context.identifiers))
}
// 1. Check for slot with slotProps on component itself.
@ -215,7 +223,7 @@ export function buildSlots(
),
)
} else if (
(vElse = findDir(slotElement, /^else(-if)?$/, true /* allowEmpty */))
(vElse = findDir(slotElement, /^else(?:-if)?$/, true /* allowEmpty */))
) {
// find adjacent v-if
let j = i
@ -226,7 +234,7 @@ export function buildSlots(
break
}
}
if (prev && isTemplateNode(prev) && findDir(prev, /^(else-)?if$/)) {
if (prev && isTemplateNode(prev) && findDir(prev, /^(?:else-)?if$/)) {
__TEST__ && assert(dynamicSlots.length > 0)
// attach this slot to previous conditional
let conditional = dynamicSlots[

View File

@ -63,7 +63,7 @@ export function isCoreComponent(tag: string): symbol | void {
}
}
const nonIdentifierRE = /^\d|[^\$\w\xA0-\uFFFF]/
const nonIdentifierRE = /^$|^\d|[^\$\w\xA0-\uFFFF]/
export const isSimpleIdentifier = (name: string): boolean =>
!nonIdentifierRE.test(name)
@ -189,7 +189,7 @@ export const isMemberExpression: (
) => boolean = __BROWSER__ ? isMemberExpressionBrowser : isMemberExpressionNode
const fnExpRE =
/^\s*(async\s*)?(\([^)]*?\)|[\w$_]+)\s*(:[^=]+)?=>|^\s*(async\s+)?function(?:\s+[\w$]+)?\s*\(/
/^\s*(?:async\s*)?(?:\([^)]*?\)|[\w$_]+)\s*(?::[^=]+)?=>|^\s*(?:async\s+)?function(?:\s+[\w$]+)?\s*\(/
export const isFnExpressionBrowser: (exp: ExpressionNode) => boolean = exp =>
fnExpRE.test(getExpSource(exp))
@ -343,6 +343,10 @@ export function isText(
return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT
}
export function isVPre(p: ElementNode['props'][0]): p is DirectiveNode {
return p.type === NodeTypes.DIRECTIVE && p.name === 'pre'
}
export function isVSlot(p: ElementNode['props'][0]): p is DirectiveNode {
return p.type === NodeTypes.DIRECTIVE && p.name === 'slot'
}

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-dom",
"version": "3.5.16",
"version": "3.5.21",
"description": "@vue/compiler-dom",
"main": "index.js",
"module": "dist/compiler-dom.esm-bundler.js",

View File

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

View File

@ -184,7 +184,7 @@ const getCachedNode = (
}
}
const dataAriaRE = /^(data|aria)-/
const dataAriaRE = /^(?:data|aria)-/
const isStringifiableAttr = (name: string, ns: Namespaces) => {
return (
(ns === Namespaces.HTML

View File

@ -884,9 +884,9 @@ export default {
return (_ctx, _push, _parent, _attrs) => {
const _cssVars = { style: {
"--xxxxxxxx-count": (count.value),
"--xxxxxxxx-style\\\\.color": (style.color),
"--xxxxxxxx-height\\\\ \\\\+\\\\ \\\\\\"px\\\\\\"": (height.value + "px")
":--xxxxxxxx-count": (count.value),
":--xxxxxxxx-style\\\\.color": (style.color),
":--xxxxxxxx-height\\\\ \\\\+\\\\ \\\\\\"px\\\\\\"": (height.value + "px")
}}
_push(\`<!--[--><div\${
_ssrRenderAttrs(_cssVars)

View File

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

View File

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

View File

@ -652,10 +652,10 @@ describe('SFC compile <script setup>', () => {
expect(content).toMatch(`return (_ctx, _push`)
expect(content).toMatch(`ssrInterpolate`)
expect(content).not.toMatch(`useCssVars`)
expect(content).toMatch(`"--${mockId}-count": (count.value)`)
expect(content).toMatch(`"--${mockId}-style\\\\.color": (style.color)`)
expect(content).toMatch(`":--${mockId}-count": (count.value)`)
expect(content).toMatch(`":--${mockId}-style\\\\.color": (style.color)`)
expect(content).toMatch(
`"--${mockId}-height\\\\ \\\\+\\\\ \\\\\\"px\\\\\\"": (height.value + "px")`,
`":--${mockId}-height\\\\ \\\\+\\\\ \\\\\\"px\\\\\\"": (height.value + "px")`,
)
assertCode(content)
})
@ -913,6 +913,13 @@ describe('SFC compile <script setup>', () => {
expect(() =>
compile(`<script>foo()</script><script setup lang="ts">bar()</script>`),
).toThrow(`<script> and <script setup> must have the same language type`)
// #13193 must check lang before parsing with babel
expect(() =>
compile(
`<script lang="ts">const a = 1</script><script setup lang="tsx">const Comp = () => <p>test</p></script>`,
),
).toThrow(`<script> and <script setup> must have the same language type`)
})
const moduleErrorMsg = `cannot contain ES module exports`
@ -1543,4 +1550,19 @@ describe('compileScript', () => {
)
assertCode(content)
})
test('should not compile unrecognized language', () => {
const { content, lang, scriptAst } = compile(
`<script lang="coffee">
export default
data: ->
myVal: 0
</script>`,
)
expect(content).toMatch(`export default
data: ->
myVal: 0`)
expect(lang).toBe('coffee')
expect(scriptAst).not.toBeDefined()
})
})

View File

@ -148,27 +148,6 @@ export default /*@__PURE__*/_defineComponent({
return { }
}
})"
`;
exports[`defineProps > w/ TSTypeAliasDeclaration 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
type FunFoo<O> = (item: O) => boolean;
type FunBar = FunFoo<number>;
export default /*@__PURE__*/_defineComponent({
props: {
foo: { type: Function, required: false, default: () => true },
bar: { type: Function, required: false, default: () => true }
},
setup(__props: any, { expose: __expose }) {
__expose();
return { }
}

View File

@ -808,30 +808,4 @@ const props = defineProps({ foo: String })
expect(content).toMatch(`foo: { default: 5.5, type: Number }`)
assertCode(content)
})
test('w/ TSTypeAliasDeclaration', () => {
const { content } = compile(`
<script setup lang="ts">
type FunFoo<O> = (item: O) => boolean;
type FunBar = FunFoo<number>;
withDefaults(
defineProps<{
foo?: FunFoo<number>;
bar?: FunBar;
}>(),
{
foo: () => true,
bar: () => true,
},
);
</script>
`)
assertCode(content)
expect(content).toMatch(
`foo: { type: Function, required: false, default: () => true }`,
)
expect(content).toMatch(
`bar: { type: Function, required: false, default: () => true }`,
)
})
})

View File

@ -538,7 +538,7 @@ describe('resolveType', () => {
expect(props).toStrictEqual({
foo: ['Symbol', 'String', 'Number'],
bar: [UNKNOWN_TYPE],
bar: ['String', 'Number'],
})
})
@ -731,6 +731,49 @@ describe('resolveType', () => {
})
})
describe('type alias declaration', () => {
// #13240
test('function type', () => {
expect(
resolve(`
type FunFoo<O> = (item: O) => boolean;
type FunBar = FunFoo<number>;
defineProps<{
foo?: FunFoo<number>;
bar?: FunBar;
}>()
`).props,
).toStrictEqual({
foo: ['Function'],
bar: ['Function'],
})
})
test('with intersection type', () => {
expect(
resolve(`
type Brand<T> = T & {};
defineProps<{
foo: Brand<string>;
}>()
`).props,
).toStrictEqual({
foo: ['String', 'Object'],
})
})
test('with union type', () => {
expect(
resolve(`
type Wrapped<T> = T | symbol | number
defineProps<{foo?: Wrapped<boolean>}>()
`).props,
).toStrictEqual({
foo: ['Boolean', 'Symbol', 'Number'],
})
})
})
describe('generics', () => {
test('generic with type literal', () => {
expect(
@ -1155,6 +1198,45 @@ describe('resolveType', () => {
expect(deps && [...deps]).toStrictEqual(['/user.ts'])
})
// #13484
test('ts module resolve w/ project reference & extends & ${configDir}', () => {
const files = {
'/tsconfig.json': JSON.stringify({
files: [],
references: [{ path: './tsconfig.app.json' }],
}),
'/tsconfig.app.json': JSON.stringify({
extends: ['./tsconfigs/base.json'],
}),
'/tsconfigs/base.json': JSON.stringify({
compilerOptions: {
paths: {
'@/*': ['${configDir}/src/*'],
},
},
include: ['${configDir}/src/**/*.ts', '${configDir}/src/**/*.vue'],
}),
'/src/types.ts':
'export type BaseProps = { foo?: string, bar?: string }',
}
const { props, deps } = resolve(
`
import { BaseProps } from '@/types.ts';
defineProps<BaseProps>()
`,
files,
{},
'/src/components/Foo.vue',
)
expect(props).toStrictEqual({
foo: ['String'],
bar: ['String'],
})
expect(deps && [...deps]).toStrictEqual(['/src/types.ts'])
})
test('ts module resolve w/ project reference folder', () => {
const files = {
'/tsconfig.json': JSON.stringify({
@ -1299,6 +1381,33 @@ describe('resolveType', () => {
expect(deps && [...deps]).toStrictEqual(Object.keys(files))
})
test('global types with named exports', () => {
const files = {
'/global.d.ts': `
declare global {
export interface ExportedInterface { foo: number }
export type ExportedType = { bar: boolean }
}
export {}
`,
}
const globalTypeFiles = { globalTypeFiles: Object.keys(files) }
expect(
resolve(`defineProps<ExportedInterface>()`, files, globalTypeFiles)
.props,
).toStrictEqual({
foo: ['Number'],
})
expect(
resolve(`defineProps<ExportedType>()`, files, globalTypeFiles).props,
).toStrictEqual({
bar: ['Boolean'],
})
})
test('global types with ambient references', () => {
const files = {
// with references

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-sfc",
"version": "3.5.16",
"version": "3.5.21",
"description": "@vue/compiler-sfc",
"main": "dist/compiler-sfc.cjs.js",
"module": "dist/compiler-sfc.esm-browser.js",
@ -49,7 +49,7 @@
"@vue/shared": "workspace:*",
"estree-walker": "catalog:",
"magic-string": "catalog:",
"postcss": "^8.5.3",
"postcss": "^8.5.6",
"source-map-js": "catalog:"
},
"devDependencies": {
@ -58,10 +58,10 @@
"hash-sum": "^2.0.0",
"lru-cache": "10.1.0",
"merge-source-map": "^1.1.0",
"minimatch": "~10.0.1",
"minimatch": "~10.0.3",
"postcss-modules": "^6.0.1",
"postcss-selector-parser": "^7.1.0",
"pug": "^3.0.3",
"sass": "^1.89.1"
"sass": "^1.90.0"
}
}

View File

@ -18,6 +18,7 @@ import type {
Declaration,
ExportSpecifier,
Identifier,
LVal,
Node,
ObjectPattern,
Statement,
@ -55,7 +56,13 @@ import { DEFINE_EXPOSE, processDefineExpose } from './script/defineExpose'
import { DEFINE_OPTIONS, processDefineOptions } from './script/defineOptions'
import { DEFINE_SLOTS, processDefineSlots } from './script/defineSlots'
import { DEFINE_MODEL, processDefineModel } from './script/defineModel'
import { getImportedName, isCallOf, isLiteralNode } from './script/utils'
import {
getImportedName,
isCallOf,
isJS,
isLiteralNode,
isTS,
} from './script/utils'
import { analyzeScriptBindings } from './script/analyzeScriptBindings'
import { isImportUsed } from './script/importUsageCheck'
import { processAwait } from './script/topLevelAwait'
@ -167,33 +174,43 @@ export function compileScript(
)
}
const ctx = new ScriptCompileContext(sfc, options)
const { script, scriptSetup, source, filename } = sfc
const hoistStatic = options.hoistStatic !== false && !script
const scopeId = options.id ? options.id.replace(/^data-v-/, '') : ''
const scriptLang = script && script.lang
const scriptSetupLang = scriptSetup && scriptSetup.lang
const isJSOrTS =
isJS(scriptLang, scriptSetupLang) || isTS(scriptLang, scriptSetupLang)
if (!scriptSetup) {
if (!script) {
throw new Error(`[@vue/compiler-sfc] SFC contains no <script> tags.`)
}
// normal <script> only
return processNormalScript(ctx, scopeId)
}
if (script && scriptLang !== scriptSetupLang) {
if (script && scriptSetup && scriptLang !== scriptSetupLang) {
throw new Error(
`[@vue/compiler-sfc] <script> and <script setup> must have the same ` +
`language type.`,
)
}
if (scriptSetupLang && !ctx.isJS && !ctx.isTS) {
if (!scriptSetup) {
if (!script) {
throw new Error(`[@vue/compiler-sfc] SFC contains no <script> tags.`)
}
// normal <script> only
if (script.lang && !isJSOrTS) {
// do not process non js/ts script blocks
return script
}
const ctx = new ScriptCompileContext(sfc, options)
return processNormalScript(ctx, scopeId)
}
if (scriptSetupLang && !isJSOrTS) {
// do not process non js/ts script blocks
return scriptSetup
}
const ctx = new ScriptCompileContext(sfc, options)
// metadata that needs to be returned
// const ctx.bindingMetadata: BindingMetadata = {}
const scriptBindings: Record<string, BindingTypes> = Object.create(null)
@ -540,7 +557,7 @@ export function compileScript(
}
// defineProps
const isDefineProps = processDefineProps(ctx, init, decl.id)
const isDefineProps = processDefineProps(ctx, init, decl.id as LVal)
if (ctx.propsDestructureRestId) {
setupBindings[ctx.propsDestructureRestId] =
BindingTypes.SETUP_REACTIVE_CONST
@ -548,10 +565,10 @@ export function compileScript(
// defineEmits
const isDefineEmits =
!isDefineProps && processDefineEmits(ctx, init, decl.id)
!isDefineProps && processDefineEmits(ctx, init, decl.id as LVal)
!isDefineEmits &&
(processDefineSlots(ctx, init, decl.id) ||
processDefineModel(ctx, init, decl.id))
(processDefineSlots(ctx, init, decl.id as LVal) ||
processDefineModel(ctx, init, decl.id as LVal))
if (
isDefineProps &&

View File

@ -9,6 +9,7 @@ import type { BindingMetadata } from '../../../compiler-core/src'
import MagicString from 'magic-string'
import type { TypeScope } from './resolveType'
import { warn } from '../warn'
import { isJS, isTS } from './utils'
export class ScriptCompileContext {
isJS: boolean
@ -87,16 +88,8 @@ export class ScriptCompileContext {
const scriptLang = script && script.lang
const scriptSetupLang = scriptSetup && scriptSetup.lang
this.isJS =
scriptLang === 'js' ||
scriptLang === 'jsx' ||
scriptSetupLang === 'js' ||
scriptSetupLang === 'jsx'
this.isTS =
scriptLang === 'ts' ||
scriptLang === 'tsx' ||
scriptSetupLang === 'ts' ||
scriptSetupLang === 'tsx'
this.isJS = isJS(scriptLang, scriptSetupLang)
this.isTS = isTS(scriptLang, scriptSetupLang)
const customElement = options.customElement
const filename = this.descriptor.filename

View File

@ -114,7 +114,7 @@ export function processDefineModel(
return true
}
export function genModelProps(ctx: ScriptCompileContext) {
export function genModelProps(ctx: ScriptCompileContext): string | undefined {
if (!ctx.hasDefineModelCall) return
const isProd = !!ctx.options.isProd

View File

@ -12,10 +12,6 @@ export function processNormalScript(
scopeId: string,
): SFCScriptBlock {
const script = ctx.descriptor.script!
if (script.lang && !ctx.isJS && !ctx.isTS) {
// do not process non js/ts script blocks
return script
}
try {
let content = script.content
let map = script.map

View File

@ -1029,6 +1029,14 @@ function resolveWithTS(
if (configs.length === 1) {
matchedConfig = configs[0]
} else {
const [major, minor] = ts.versionMajorMinor.split('.').map(Number)
const getPattern = (base: string, p: string) => {
// ts 5.5+ supports ${configDir} in paths
const supportsConfigDir = major > 5 || (major === 5 && minor >= 5)
return p.startsWith('${configDir}') && supportsConfigDir
? normalizePath(p.replace('${configDir}', dirname(configPath!)))
: joinPaths(base, p)
}
// resolve which config matches the current file
for (const c of configs) {
const base = normalizePath(
@ -1039,11 +1047,11 @@ function resolveWithTS(
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, getPattern(base, p)))
) {
if (
excluded &&
excluded.some(p => isMatch(containingFile, joinPaths(base, p)))
excluded.some(p => isMatch(containingFile, getPattern(base, p)))
) {
continue
}
@ -1296,7 +1304,12 @@ function recordTypes(
}
} else if (stmt.type === 'TSModuleDeclaration' && stmt.global) {
for (const s of (stmt.body as TSModuleBlock).body) {
recordType(s, types, declares)
if (s.type === 'ExportNamedDeclaration' && s.declaration) {
// Handle export declarations inside declare global
recordType(s.declaration, types, declares)
} else {
recordType(s, types, declares)
}
}
}
} else {
@ -1500,6 +1513,7 @@ export function inferRuntimeType(
node: Node & MaybeWithScope,
scope: TypeScope = node._ownerScope || ctxToScope(ctx),
isKeyOf = false,
typeParameters?: Record<string, Node>,
): string[] {
try {
switch (node.type) {
@ -1589,17 +1603,42 @@ export function inferRuntimeType(
const resolved = resolveTypeReference(ctx, node, scope)
if (resolved) {
if (resolved.type === 'TSTypeAliasDeclaration') {
return inferRuntimeType(
ctx,
resolved.typeAnnotation,
resolved._ownerScope,
isKeyOf,
)
// #13240
// Special case for function type aliases to ensure correct runtime behavior
// other type aliases still fallback to unknown as before
if (resolved.typeAnnotation.type === 'TSFunctionType') {
return ['Function']
}
if (node.typeParameters) {
const typeParams: Record<string, Node> = Object.create(null)
if (resolved.typeParameters) {
resolved.typeParameters.params.forEach((p, i) => {
typeParams![p.name] = node.typeParameters!.params[i]
})
}
return inferRuntimeType(
ctx,
resolved.typeAnnotation,
resolved._ownerScope,
isKeyOf,
typeParams,
)
}
}
return inferRuntimeType(ctx, resolved, resolved._ownerScope, isKeyOf)
}
if (node.typeName.type === 'Identifier') {
if (typeParameters && typeParameters[node.typeName.name]) {
return inferRuntimeType(
ctx,
typeParameters[node.typeName.name],
scope,
isKeyOf,
typeParameters,
)
}
if (isKeyOf) {
switch (node.typeName.name) {
case 'String':
@ -1732,11 +1771,15 @@ export function inferRuntimeType(
return inferRuntimeType(ctx, node.typeAnnotation, scope)
case 'TSUnionType':
return flattenTypes(ctx, node.types, scope, isKeyOf)
return flattenTypes(ctx, node.types, scope, isKeyOf, typeParameters)
case 'TSIntersectionType': {
return flattenTypes(ctx, node.types, scope, isKeyOf).filter(
t => t !== UNKNOWN_TYPE,
)
return flattenTypes(
ctx,
node.types,
scope,
isKeyOf,
typeParameters,
).filter(t => t !== UNKNOWN_TYPE)
}
case 'TSEnumDeclaration':
@ -1807,14 +1850,17 @@ function flattenTypes(
types: TSType[],
scope: TypeScope,
isKeyOf: boolean = false,
typeParameters: Record<string, Node> | undefined = undefined,
): string[] {
if (types.length === 1) {
return inferRuntimeType(ctx, types[0], scope, isKeyOf)
return inferRuntimeType(ctx, types[0], scope, isKeyOf, typeParameters)
}
return [
...new Set(
([] as string[]).concat(
...types.map(t => inferRuntimeType(ctx, t, scope, isKeyOf)),
...types.map(t =>
inferRuntimeType(ctx, t, scope, isKeyOf, typeParameters),
),
),
),
]

View File

@ -121,3 +121,8 @@ export const propNameEscapeSymbolsRE: RegExp =
export function getEscapedPropName(key: string): string {
return propNameEscapeSymbolsRE.test(key) ? JSON.stringify(key) : key
}
export const isJS = (...langs: (string | null | undefined)[]): boolean =>
langs.some(lang => lang === 'js' || lang === 'jsx')
export const isTS = (...langs: (string | null | undefined)[]): boolean =>
langs.some(lang => lang === 'ts' || lang === 'tsx')

View File

@ -23,7 +23,12 @@ export function genCssVarsFromList(
return `{\n ${vars
.map(
key =>
`"${isSSR ? `--` : ``}${genVarName(id, key, isProd, isSSR)}": (${key})`,
// The `:` prefix here is used in `ssrRenderStyle` to distinguish whether
// a custom property comes from `ssrCssVars`. If it does, we need to reset
// its value to `initial` on the component instance to avoid unintentionally
// inheriting the same property value from a different instance of the same
// component in the outer scope.
`"${isSSR ? `:--` : ``}${genVarName(id, key, isProd, isSSR)}": (${key})`,
)
.join(',\n ')}\n}`
}

View File

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

View File

@ -6,7 +6,7 @@ export function isRelativeUrl(url: string): boolean {
return firstChar === '.' || firstChar === '~' || firstChar === '@'
}
const externalRE = /^(https?:)?\/\//
const externalRE = /^(?:https?:)?\/\//
export function isExternalUrl(url: string): boolean {
return externalRE.test(url)
}

View File

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

View File

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

View File

@ -166,6 +166,132 @@ describe('ssr: v-model', () => {
_push(\`</optgroup></select></div>\`)
}"
`)
expect(
compileWithWrapper(`
<select multiple v-model="model">
<optgroup>
<option v-for="item in items" :value="item">{{item}}</option>
</optgroup>
</select>`).code,
).toMatchInlineSnapshot(`
"const { ssrRenderAttr: _ssrRenderAttr, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}><select multiple><optgroup><!--[-->\`)
_ssrRenderList(_ctx.items, (item) => {
_push(\`<option\${
_ssrRenderAttr("value", item)
}\${
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
? _ssrLooseContain(_ctx.model, item)
: _ssrLooseEqual(_ctx.model, item))) ? " selected" : ""
}>\${
_ssrInterpolate(item)
}</option>\`)
})
_push(\`<!--]--></optgroup></select></div>\`)
}"
`)
expect(
compileWithWrapper(`
<select multiple v-model="model">
<optgroup>
<option v-if="true" :value="item">{{item}}</option>
</optgroup>
</select>`).code,
).toMatchInlineSnapshot(`
"const { ssrRenderAttr: _ssrRenderAttr, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}><select multiple><optgroup>\`)
if (true) {
_push(\`<option\${
_ssrRenderAttr("value", _ctx.item)
}\${
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
? _ssrLooseContain(_ctx.model, _ctx.item)
: _ssrLooseEqual(_ctx.model, _ctx.item))) ? " selected" : ""
}>\${
_ssrInterpolate(_ctx.item)
}</option>\`)
} else {
_push(\`<!---->\`)
}
_push(\`</optgroup></select></div>\`)
}"
`)
expect(
compileWithWrapper(`
<select multiple v-model="model">
<optgroup>
<template v-if="ok">
<option v-for="item in items" :value="item">{{item}}</option>
</template>
</optgroup>
</select>`).code,
).toMatchInlineSnapshot(`
"const { ssrRenderAttr: _ssrRenderAttr, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}><select multiple><optgroup>\`)
if (_ctx.ok) {
_push(\`<!--[-->\`)
_ssrRenderList(_ctx.items, (item) => {
_push(\`<option\${
_ssrRenderAttr("value", item)
}\${
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
? _ssrLooseContain(_ctx.model, item)
: _ssrLooseEqual(_ctx.model, item))) ? " selected" : ""
}>\${
_ssrInterpolate(item)
}</option>\`)
})
_push(\`<!--]-->\`)
} else {
_push(\`<!---->\`)
}
_push(\`</optgroup></select></div>\`)
}"
`)
expect(
compileWithWrapper(`
<select multiple v-model="model">
<optgroup>
<template v-for="item in items" :value="item">
<option v-if="item===1" :value="item">{{item}}</option>
</template>
</optgroup>
</select>`).code,
).toMatchInlineSnapshot(`
"const { ssrRenderAttr: _ssrRenderAttr, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}><select multiple><optgroup><!--[-->\`)
_ssrRenderList(_ctx.items, (item) => {
_push(\`<!--[-->\`)
if (item===1) {
_push(\`<option\${
_ssrRenderAttr("value", item)
}\${
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
? _ssrLooseContain(_ctx.model, item)
: _ssrLooseEqual(_ctx.model, item))) ? " selected" : ""
}>\${
_ssrInterpolate(item)
}</option>\`)
} else {
_push(\`<!---->\`)
}
_push(\`<!--]-->\`)
})
_push(\`<!--]--></optgroup></select></div>\`)
}"
`)
})
test('<input type="radio">', () => {

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-ssr",
"version": "3.5.16",
"version": "3.5.21",
"description": "@vue/compiler-ssr",
"main": "dist/compiler-ssr.cjs.js",
"types": "dist/compiler-ssr.d.ts",

View File

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

View File

@ -17,7 +17,7 @@ import {
// Plugin for the first transform pass, which simply constructs the AST node
export const ssrTransformIf: NodeTransform = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
/^(?:if|else|else-if)$/,
processIf,
)

View File

@ -39,6 +39,18 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
}
}
const processSelectChildren = (children: TemplateChildNode[]) => {
children.forEach(child => {
if (child.type === NodeTypes.ELEMENT) {
processOption(child as PlainElementNode)
} else if (child.type === NodeTypes.FOR) {
processSelectChildren(child.children)
} else if (child.type === NodeTypes.IF) {
child.branches.forEach(b => processSelectChildren(b.children))
}
})
}
function processOption(plainNode: PlainElementNode) {
if (plainNode.tag === 'option') {
if (plainNode.props.findIndex(p => p.name === 'selected') === -1) {
@ -65,9 +77,7 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
)
}
} else if (plainNode.tag === 'optgroup') {
plainNode.children.forEach(option =>
processOption(option as PlainElementNode),
)
processSelectChildren(plainNode.children)
}
}
@ -163,18 +173,7 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
checkDuplicatedValue()
node.children = [createInterpolation(model, model.loc)]
} else if (node.tag === 'select') {
const processChildren = (children: TemplateChildNode[]) => {
children.forEach(child => {
if (child.type === NodeTypes.ELEMENT) {
processOption(child as PlainElementNode)
} else if (child.type === NodeTypes.FOR) {
processChildren(child.children)
} else if (child.type === NodeTypes.IF) {
child.branches.forEach(b => processChildren(b.children))
}
})
}
processChildren(node.children)
processSelectChildren(node.children)
} else {
context.onError(
createDOMCompilerError(

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@vue/reactivity",
"version": "3.5.16",
"version": "3.5.21",
"description": "@vue/reactivity",
"main": "index.js",
"module": "dist/reactivity.esm-bundler.js",

View File

@ -107,7 +107,7 @@ export const arrayInstrumentations: Record<string | symbol, Function> = <any>{
return reactiveReadArray(this).join(separator)
},
// keys() iterator only reads `length`, no optimisation required
// keys() iterator only reads `length`, no optimization required
lastIndexOf(...args: unknown[]) {
return searchProxy(this, 'lastIndexOf', args)
@ -200,7 +200,7 @@ function iterator(
wrapValue: (value: any) => unknown,
) {
// note that taking ARRAY_ITERATE dependency here is not strictly equivalent
// to calling iterate on the proxified array.
// to calling iterate on the proxied array.
// creating the iterator does not access any array property:
// it is only when .next() is called that length and indexes are accessed.
// pushed to the extreme, an iterator could be created in one effect scope,

View File

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

View File

@ -125,7 +125,7 @@ function createInstrumentations(
get size() {
const target = (this as unknown as IterableCollections)[ReactiveFlags.RAW]
!readonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.get(target, 'size', target)
return target.size
},
has(this: CollectionTypes, key: unknown): boolean {
const target = this[ReactiveFlags.RAW]

View File

@ -275,7 +275,7 @@ export function proxyRefs<T extends object>(
objectWithRefs: T,
): ShallowUnwrapRef<T> {
return isReactive(objectWithRefs)
? objectWithRefs
? (objectWithRefs as ShallowUnwrapRef<T>)
: new Proxy(objectWithRefs, shallowUnwrapHandlers)
}

View File

@ -331,17 +331,17 @@ export function watch(
export function traverse(
value: unknown,
depth: number = Infinity,
seen?: Set<unknown>,
seen?: Map<unknown, number>,
): unknown {
if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
return value
}
seen = seen || new Set()
if (seen.has(value)) {
seen = seen || new Map()
if ((seen.get(value) || 0) >= depth) {
return value
}
seen.add(value)
seen.set(value, depth)
depth--
if (isRef(value)) {
traverse(value.value, depth, seen)

View File

@ -1689,6 +1689,57 @@ describe('api: watch', () => {
expect(cb).toHaveBeenCalledTimes(4)
})
test('watching the same object at different depths', async () => {
const arr1: any[] = reactive([[[{ foo: {} }]]])
const arr2 = arr1[0]
const arr3 = arr2[0]
const obj = arr3[0]
arr1.push(arr3)
const cb1 = vi.fn()
const cb2 = vi.fn()
const cb3 = vi.fn()
const cb4 = vi.fn()
watch(arr1, cb1, { deep: 1 })
watch(arr1, cb2, { deep: 2 })
watch(arr1, cb3, { deep: 3 })
watch(arr1, cb4, { deep: 4 })
await nextTick()
expect(cb1).toHaveBeenCalledTimes(0)
expect(cb2).toHaveBeenCalledTimes(0)
expect(cb3).toHaveBeenCalledTimes(0)
expect(cb4).toHaveBeenCalledTimes(0)
obj.foo = {}
await nextTick()
expect(cb1).toHaveBeenCalledTimes(0)
expect(cb2).toHaveBeenCalledTimes(0)
expect(cb3).toHaveBeenCalledTimes(1)
expect(cb4).toHaveBeenCalledTimes(1)
obj.foo.bar = 1
await nextTick()
expect(cb1).toHaveBeenCalledTimes(0)
expect(cb2).toHaveBeenCalledTimes(0)
expect(cb3).toHaveBeenCalledTimes(1)
expect(cb4).toHaveBeenCalledTimes(2)
arr3.push(obj.foo)
await nextTick()
expect(cb1).toHaveBeenCalledTimes(0)
expect(cb2).toHaveBeenCalledTimes(1)
expect(cb3).toHaveBeenCalledTimes(2)
expect(cb4).toHaveBeenCalledTimes(3)
obj.foo.bar = 2
await nextTick()
expect(cb1).toHaveBeenCalledTimes(0)
expect(cb2).toHaveBeenCalledTimes(1)
expect(cb3).toHaveBeenCalledTimes(3)
expect(cb4).toHaveBeenCalledTimes(4)
})
test('pause / resume', async () => {
const count = ref(0)
const cb = vi.fn()

View File

@ -3,6 +3,7 @@
import {
type ComponentPublicInstance,
createApp,
defineComponent,
h,
nextTick,
@ -598,4 +599,45 @@ describe('component: emit', () => {
render(h(ComponentC), el)
expect(renderFn).toHaveBeenCalledTimes(1)
})
test('merging emits for a component that is also used as a mixin', () => {
const render = () => h('div')
const CompA = {
render,
}
const validateByMixin = vi.fn(() => true)
const validateByGlobalMixin = vi.fn(() => true)
const mixin = {
emits: {
one: validateByMixin,
},
}
const CompB = defineComponent({
mixins: [mixin, CompA],
created(this) {
this.$emit('one', 1)
},
render,
})
const app = createApp({
render() {
return [h(CompA), h(CompB)]
},
})
app.mixin({
emits: {
one: validateByGlobalMixin,
two: null,
},
})
const root = nodeOps.createElement('div')
app.mount(root)
expect(validateByMixin).toHaveBeenCalledTimes(1)
expect(validateByGlobalMixin).not.toHaveBeenCalled()
})
})

View File

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

View File

@ -6,6 +6,8 @@ import {
nodeOps,
ref,
render,
serializeInner,
useSlots,
} from '@vue/runtime-test'
import { createBlock, normalizeVNode } from '../src/vnode'
import { createSlots } from '../src/helpers/createSlots'
@ -42,6 +44,25 @@ describe('component: slots', () => {
expect(slots).toMatchObject({})
})
test('initSlots: ensure compiler marker non-enumerable', () => {
const Comp = {
render() {
const slots = useSlots()
// Only user-defined slots should be enumerable
expect(Object.keys(slots)).toEqual(['foo'])
// Internal compiler markers must still exist but be non-enumerable
expect(slots).toHaveProperty('_')
expect(Object.getOwnPropertyDescriptor(slots, '_')!.enumerable).toBe(
false,
)
return h('div')
},
}
const slots = { foo: () => {}, _: 1 }
render(createBlock(Comp, null, slots), nodeOps.createElement('div'))
})
test('initSlots: should normalize object slots (when value is null, string, array)', () => {
const { slots } = renderWithSlots({
_inner: '_inner',
@ -50,6 +71,10 @@ describe('component: slots', () => {
footer: ['f1', 'f2'],
})
expect(
'[Vue warn]: Non-function value encountered for slot "_inner". Prefer function slots for better performance.',
).toHaveBeenWarned()
expect(
'[Vue warn]: Non-function value encountered for slot "header". Prefer function slots for better performance.',
).toHaveBeenWarned()
@ -58,8 +83,8 @@ describe('component: slots', () => {
'[Vue warn]: Non-function value encountered for slot "footer". Prefer function slots for better performance.',
).toHaveBeenWarned()
expect(slots).not.toHaveProperty('_inner')
expect(slots).not.toHaveProperty('foo')
expect(slots._inner()).toMatchObject([normalizeVNode('_inner')])
expect(slots.header()).toMatchObject([normalizeVNode('header')])
expect(slots.footer()).toMatchObject([
normalizeVNode('f1'),
@ -418,4 +443,22 @@ describe('component: slots', () => {
'Slot "default" invoked outside of the render function',
).toHaveBeenWarned()
})
test('slot name starts with underscore', () => {
const Comp = {
setup(_: any, { slots }: any) {
return () => slots._foo()
},
}
const App = {
setup() {
return () => h(Comp, null, { _foo: () => 'foo' })
},
}
const root = nodeOps.createElement('div')
createApp(App).mount(root)
expect(serializeInner(root)).toBe('foo')
})
})

View File

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

View File

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

View File

@ -894,4 +894,150 @@ describe('hot module replacement', () => {
await timeout()
expect(serializeInner(root)).toBe('<div>bar</div>')
})
test('multi reload child wrapped in Suspense + KeepAlive', async () => {
const id = 'test-child-reload-3'
const Child: ComponentOptions = {
__hmrId: id,
setup() {
const count = ref(0)
return { count }
},
render: compileToFunction(`<div>{{ count }}</div>`),
}
createRecord(id, Child)
const appId = 'test-app-id'
const App: ComponentOptions = {
__hmrId: appId,
components: { Child },
render: compileToFunction(`
<KeepAlive>
<Suspense>
<Child />
</Suspense>
</KeepAlive>
`),
}
const root = nodeOps.createElement('div')
render(h(App), root)
expect(serializeInner(root)).toBe('<div>0</div>')
await timeout()
reload(id, {
__hmrId: id,
setup() {
const count = ref(1)
return { count }
},
render: compileToFunction(`<div>{{ count }}</div>`),
})
await timeout()
expect(serializeInner(root)).toBe('<div>1</div>')
reload(id, {
__hmrId: id,
setup() {
const count = ref(2)
return { count }
},
render: compileToFunction(`<div>{{ count }}</div>`),
})
await timeout()
expect(serializeInner(root)).toBe('<div>2</div>')
})
test('rerender for nested component', () => {
const id = 'child-nested-rerender'
const Foo: ComponentOptions = {
__hmrId: id,
render() {
return this.$slots.default()
},
}
createRecord(id, Foo)
const parentId = 'parent-nested-rerender'
const Parent: ComponentOptions = {
__hmrId: parentId,
render() {
return h(Foo, null, {
default: () => this.$slots.default(),
_: 3 /* FORWARDED */,
})
},
}
const appId = 'app-nested-rerender'
const App: ComponentOptions = {
__hmrId: appId,
render: () =>
h(Parent, null, {
default: () => [
h(Foo, null, {
default: () => ['foo'],
}),
],
}),
}
createRecord(parentId, App)
const root = nodeOps.createElement('div')
render(h(App), root)
expect(serializeInner(root)).toBe('foo')
rerender(id, () => 'bar')
expect(serializeInner(root)).toBe('bar')
})
// https://github.com/vitejs/vite-plugin-vue/issues/599
// Both Outer and Inner are reloaded when './server.js' changes
test('reload nested components from single update', async () => {
const innerId = 'nested-reload-inner'
const outerId = 'nested-reload-outer'
let Inner = {
__hmrId: innerId,
render() {
return h('div', 'foo')
},
}
let Outer = {
__hmrId: outerId,
render() {
return h(Inner)
},
}
createRecord(innerId, Inner)
createRecord(outerId, Outer)
const App = {
render: () => h(Outer),
}
const root = nodeOps.createElement('div')
render(h(App), root)
expect(serializeInner(root)).toBe('<div>foo</div>')
Inner = {
__hmrId: innerId,
render() {
return h('div', 'bar')
},
}
Outer = {
__hmrId: outerId,
render() {
return h(Inner)
},
}
// trigger reload for both Outer and Inner
reload(outerId, Outer)
reload(innerId, Inner)
await nextTick()
expect(serializeInner(root)).toBe('<div>bar</div>')
})
})

View File

@ -1160,6 +1160,69 @@ describe('SSR hydration', () => {
)
})
// #13510
test('update async component after parent mount before async component resolve', async () => {
const Comp = {
props: ['toggle'],
render(this: any) {
return h('h1', [
this.toggle ? 'Async component' : 'Updated async component',
])
},
}
let serverResolve: any
let AsyncComp = defineAsyncComponent(
() =>
new Promise(r => {
serverResolve = r
}),
)
const toggle = ref(true)
const App = {
setup() {
onMounted(() => {
// change state, after mount and before async component resolve
nextTick(() => (toggle.value = false))
})
return () => {
return h(AsyncComp, { toggle: toggle.value })
}
},
}
// server render
const htmlPromise = renderToString(h(App))
serverResolve(Comp)
const html = await htmlPromise
expect(html).toMatchInlineSnapshot(`"<h1>Async component</h1>"`)
// hydration
let clientResolve: any
AsyncComp = defineAsyncComponent(
() =>
new Promise(r => {
clientResolve = r
}),
)
const container = document.createElement('div')
container.innerHTML = html
createSSRApp(App).mount(container)
// resolve
clientResolve(Comp)
await new Promise(r => setTimeout(r))
// prevent lazy hydration since the component has been patched
expect('Skipping lazy hydration for component').toHaveBeenWarned()
expect(`Hydration node mismatch`).not.toHaveBeenWarned()
expect(container.innerHTML).toMatchInlineSnapshot(
`"<h1>Updated async component</h1>"`,
)
})
test('hydrate safely when property used by async setup changed before render', async () => {
const toggle = ref(true)
@ -1677,6 +1740,35 @@ describe('SSR hydration', () => {
expect(`mismatch`).not.toHaveBeenWarned()
})
// #13394
test('transition appear work with empty content', async () => {
const show = ref(true)
const { vnode, container } = mountWithHydration(
`<template><!----></template>`,
function (this: any) {
return h(
Transition,
{ appear: true },
{
default: () =>
show.value
? renderSlot(this.$slots, 'default')
: createTextVNode('foo'),
},
)
},
)
// empty slot render as a comment node
expect(container.firstChild!.nodeType).toBe(Node.COMMENT_NODE)
expect(vnode.el).toBe(container.firstChild)
expect(`mismatch`).not.toHaveBeenWarned()
show.value = false
await nextTick()
expect(container.innerHTML).toBe('foo')
})
test('transition appear with v-if', () => {
const show = false
const { vnode, container } = mountWithHydration(
@ -2265,6 +2357,30 @@ describe('SSR hydration', () => {
expect(`Hydration style mismatch`).not.toHaveBeenWarned()
})
test('with disabled teleport + undefined target', async () => {
const container = document.createElement('div')
const isOpen = ref(false)
const App = {
setup() {
return { isOpen }
},
template: `
<Teleport :to="undefined" :disabled="true">
<div v-if="isOpen">
Menu is open...
</div>
</Teleport>`,
}
container.innerHTML = await renderToString(h(App))
const app = createSSRApp(App)
app.mount(container)
isOpen.value = true
await nextTick()
expect(container.innerHTML).toBe(
`<!--teleport start--><div> Menu is open... </div><!--teleport end-->`,
)
})
test('escape css var name', () => {
const container = document.createElement('div')
container.innerHTML = `<div style="padding: 4px;--foo\\.bar:red;"></div>`

View File

@ -10,6 +10,7 @@ import {
render,
serializeInner,
shallowRef,
watch,
} from '@vue/runtime-test'
describe('api: template refs', () => {
@ -179,6 +180,89 @@ describe('api: template refs', () => {
expect(el.value).toBe(null)
})
// #12639
it('update and unmount child in the same tick', async () => {
const root = nodeOps.createElement('div')
const el = ref(null)
const toggle = ref(true)
const show = ref(true)
const Comp = defineComponent({
emits: ['change'],
props: ['show'],
setup(props, { emit }) {
watch(
() => props.show,
() => {
emit('change')
},
)
return () => h('div', 'hi')
},
})
const App = {
setup() {
return {
refKey: el,
}
},
render() {
return toggle.value
? h(Comp, {
ref: 'refKey',
show: show.value,
onChange: () => (toggle.value = false),
})
: null
},
}
render(h(App), root)
expect(el.value).not.toBe(null)
show.value = false
await nextTick()
expect(el.value).toBe(null)
})
it('set and change ref in the same tick', async () => {
const root = nodeOps.createElement('div')
const show = ref(false)
const refName = ref('a')
const Child = defineComponent({
setup() {
refName.value = 'b'
return () => {}
},
})
const Comp = {
render() {
return h(Child, {
ref: refName.value,
})
},
updated(this: any) {
expect(this.$refs.a).toBe(null)
expect(this.$refs.b).not.toBe(null)
},
}
const App = {
render() {
return show.value ? h(Comp) : null
},
}
render(h(App), root)
expect(refName.value).toBe('a')
show.value = true
await nextTick()
expect(refName.value).toBe('b')
})
it('unset old ref when new ref is absent', async () => {
const root1 = nodeOps.createElement('div')
const root2 = nodeOps.createElement('div')

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@vue/runtime-core",
"version": "3.5.16",
"version": "3.5.21",
"description": "@vue/runtime-core",
"main": "index.js",
"module": "dist/runtime-core.esm-bundler.js",

View File

@ -43,7 +43,7 @@ export interface AsyncComponentOptions<T = any> {
export const isAsyncWrapper = (i: ComponentInternalInstance | VNode): boolean =>
!!(i.type as ComponentOptions).__asyncLoader
/*! #__NO_SIDE_EFFECTS__ */
/*@__NO_SIDE_EFFECTS__*/
export function defineAsyncComponent<
T extends Component = { new (): ComponentPublicInstance },
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
@ -123,28 +123,30 @@ export function defineAsyncComponent<
__asyncHydrate(el, instance, hydrate) {
let patched = false
;(instance.bu || (instance.bu = [])).push(() => (patched = true))
const performHydrate = () => {
// skip hydration if the component has been patched
if (patched) {
if (__DEV__) {
warn(
`Skipping lazy hydration for component '${getComponentName(resolvedComp!) || resolvedComp!.__file}': ` +
`it was updated before lazy hydration performed.`,
)
}
return
}
hydrate()
}
const doHydrate = hydrateStrategy
? () => {
const performHydrate = () => {
// skip hydration if the component has been patched
if (__DEV__ && patched) {
warn(
`Skipping lazy hydration for component '${getComponentName(resolvedComp!)}': ` +
`it was updated before lazy hydration performed.`,
)
return
}
hydrate()
}
const teardown = hydrateStrategy(performHydrate, cb =>
forEachElement(el, cb),
)
if (teardown) {
;(instance.bum || (instance.bum = [])).push(teardown)
}
;(instance.u || (instance.u = [])).push(() => (patched = true))
}
: hydrate
: performHydrate
if (resolvedComp) {
doHydrate()
} else {

View File

@ -301,7 +301,7 @@ export function defineComponent<
>
// implementation, close to no-op
/*! #__NO_SIDE_EFFECTS__ */
/*@__NO_SIDE_EFFECTS__*/
export function defineComponent(
options: unknown,
extraOptions?: ComponentOptions,

View File

@ -1,6 +1,5 @@
import { isFunction } from '@vue/shared'
import { currentInstance } from './component'
import { currentRenderingInstance } from './componentRenderContext'
import { currentInstance, getCurrentInstance } from './component'
import { currentApp } from './apiCreateApp'
import { warn } from './warning'
@ -51,7 +50,7 @@ export function inject(
) {
// fallback to `currentRenderingInstance` so that this can be called in
// a functional component
const instance = currentInstance || currentRenderingInstance
const instance = getCurrentInstance()
// also support looking up from app-level provides w/ `app.runWithContext()`
if (instance || currentApp) {
@ -90,5 +89,5 @@ export function inject(
* user. One example is `useRoute()` in `vue-router`.
*/
export function hasInjectionContext(): boolean {
return !!(currentInstance || currentRenderingInstance || currentApp)
return !!(getCurrentInstance() || currentApp)
}

View File

@ -382,17 +382,17 @@ export function withDefaults<
}
export function useSlots(): SetupContext['slots'] {
return getContext().slots
return getContext('useSlots').slots
}
export function useAttrs(): SetupContext['attrs'] {
return getContext().attrs
return getContext('useAttrs').attrs
}
function getContext(): SetupContext {
function getContext(calledFunctionName: string): SetupContext {
const i = getCurrentInstance()!
if (__DEV__ && !i) {
warn(`useContext() called without active instance.`)
warn(`${calledFunctionName}() called without active instance.`)
}
return i.setupContext || (i.setupContext = createSetupContext(i))
}

View File

@ -536,7 +536,7 @@ function installCompatMount(
if (__DEV__) {
for (let i = 0; i < container.attributes.length; i++) {
const attr = container.attributes[i]
if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
if (attr.name !== 'v-cloak' && /^(?:v-|:|@)/.test(attr.name)) {
warnDeprecation(DeprecationTypes.GLOBAL_MOUNT_CONTAINER, null)
break
}

View File

@ -33,7 +33,7 @@ import {
legacyPrependModifier,
legacyRenderSlot,
legacyRenderStatic,
legacyresolveScopedSlots,
legacyResolveScopedSlots,
} from './renderHelpers'
import { resolveFilter } from '../helpers/resolveAssets'
import type { Slots } from '../componentSlots'
@ -183,7 +183,7 @@ export function installCompatInstanceProperties(
_b: () => legacyBindObjectProps,
_v: () => createTextVNode,
_e: () => createCommentVNode,
_u: () => legacyresolveScopedSlots,
_u: () => legacyResolveScopedSlots,
_g: () => legacyBindObjectListeners,
_d: () => legacyBindDynamicKeys,
_p: () => legacyPrependModifier,

View File

@ -87,7 +87,7 @@ type LegacyScopedSlotsData = Array<
| LegacyScopedSlotsData
>
export function legacyresolveScopedSlots(
export function legacyResolveScopedSlots(
fns: LegacyScopedSlotsData,
raw?: Record<string, Slot>,
// the following are added in 2.6

View File

@ -585,13 +585,13 @@ export interface ComponentInternalInstance {
* For updating css vars on contained teleports
* @internal
*/
ut?: (vars?: Record<string, string>) => void
ut?: (vars?: Record<string, unknown>) => void
/**
* dev only. For style v-bind hydration mismatch checks
* @internal
*/
getCssVars?: () => Record<string, string>
getCssVars?: () => Record<string, unknown>
/**
* v2 compat only, for caching mutated $options
@ -1203,7 +1203,7 @@ export function getComponentPublicInstance(
}
}
const classifyRE = /(?:^|[-_])(\w)/g
const classifyRE = /(?:^|[-_])\w/g
const classify = (str: string): string =>
str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '')

View File

@ -151,10 +151,14 @@ export function emit(
}
let args = rawArgs
const isModelListener = event.startsWith('update:')
const isCompatModelListener =
__COMPAT__ && compatModelEventPrefix + event in props
const isModelListener = isCompatModelListener || event.startsWith('update:')
const modifiers = isCompatModelListener
? props.modelModifiers
: isModelListener && getModelModifiers(props, event.slice(7))
// for v-model update:xxx events, apply modifiers on args
const modifiers = isModelListener && getModelModifiers(props, event.slice(7))
if (modifiers) {
if (modifiers.trim) {
args = rawArgs.map(a => (isString(a) ? a.trim() : a))
@ -228,12 +232,14 @@ export function emit(
}
}
const mixinEmitsCache = new WeakMap<ConcreteComponent, ObjectEmitsOptions>()
export function normalizeEmitsOptions(
comp: ConcreteComponent,
appContext: AppContext,
asMixin = false,
): ObjectEmitsOptions | null {
const cache = appContext.emitsCache
const cache =
__FEATURE_OPTIONS_API__ && asMixin ? mixinEmitsCache : appContext.emitsCache
const cached = cache.get(comp)
if (cached !== undefined) {
return cached

View File

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

View File

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

View File

@ -79,14 +79,10 @@ export type RawSlots = {
* @internal
*/
_?: SlotFlags
/**
* cache indexes for slot content
* @internal
*/
__?: number[]
}
const isInternalKey = (key: string) => key[0] === '_' || key === '$stable'
const isInternalKey = (key: string) =>
key === '_' || key === '_ctx' || key === '$stable'
const normalizeSlotValue = (value: unknown): VNode[] =>
isArray(value)

View File

@ -24,7 +24,7 @@ import { SchedulerJobFlags } from '../scheduler'
type Hook<T = () => void> = T | T[]
const leaveCbKey: unique symbol = Symbol('_leaveCb')
export const leaveCbKey: unique symbol = Symbol('_leaveCb')
const enterCbKey: unique symbol = Symbol('_enterCb')
export interface BaseTransitionProps<HostElement = RendererElement> {
@ -204,7 +204,7 @@ const BaseTransitionImpl: ComponentOptions = {
if (
oldInnerChild &&
oldInnerChild.type !== Comment &&
!isSameVNodeType(innerChild, oldInnerChild) &&
!isSameVNodeType(oldInnerChild, innerChild) &&
recursiveGetSubtree(instance).type !== Comment
) {
let leavingHooks = resolveTransitionHooks(

View File

@ -235,7 +235,7 @@ function patchSuspense(
const { activeBranch, pendingBranch, isInFallback, isHydrating } = suspense
if (pendingBranch) {
suspense.pendingBranch = newBranch
if (isSameVNodeType(newBranch, pendingBranch)) {
if (isSameVNodeType(pendingBranch, newBranch)) {
// same root type but content may have changed.
patch(
pendingBranch,
@ -321,7 +321,7 @@ function patchSuspense(
)
setActiveBranch(suspense, newFallback)
}
} else if (activeBranch && isSameVNodeType(newBranch, activeBranch)) {
} else if (activeBranch && isSameVNodeType(activeBranch, newBranch)) {
// toggled "back" to current active branch
patch(
activeBranch,
@ -355,7 +355,7 @@ function patchSuspense(
}
}
} else {
if (activeBranch && isSameVNodeType(newBranch, activeBranch)) {
if (activeBranch && isSameVNodeType(activeBranch, newBranch)) {
// root did not change, just normal patch
patch(
activeBranch,

View File

@ -406,29 +406,43 @@ function hydrateTeleport(
optimized: boolean,
) => Node | null,
): Node | null {
function hydrateDisabledTeleport(
node: Node,
vnode: VNode,
targetStart: Node | null,
targetAnchor: Node | null,
) {
vnode.anchor = hydrateChildren(
nextSibling(node),
vnode,
parentNode(node)!,
parentComponent,
parentSuspense,
slotScopeIds,
optimized,
)
vnode.targetStart = targetStart
vnode.targetAnchor = targetAnchor
}
const target = (vnode.target = resolveTarget<Element>(
vnode.props,
querySelector,
))
const disabled = isTeleportDisabled(vnode.props)
if (target) {
const disabled = isTeleportDisabled(vnode.props)
// if multiple teleports rendered to the same target element, we need to
// pick up from where the last teleport finished instead of the first node
const targetNode =
(target as TeleportTargetElement)._lpa || target.firstChild
if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
if (disabled) {
vnode.anchor = hydrateChildren(
nextSibling(node),
hydrateDisabledTeleport(
node,
vnode,
parentNode(node)!,
parentComponent,
parentSuspense,
slotScopeIds,
optimized,
targetNode,
targetNode && nextSibling(targetNode),
)
vnode.targetStart = targetNode
vnode.targetAnchor = targetNode && nextSibling(targetNode)
} else {
vnode.anchor = nextSibling(node)
@ -470,6 +484,10 @@ function hydrateTeleport(
}
}
updateCssVars(vnode, disabled)
} else if (disabled) {
if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
hydrateDisabledTeleport(node, vnode, node, nextSibling(node))
}
}
return vnode.anchor && nextSibling(vnode.anchor as Node)
}

View File

@ -125,7 +125,7 @@ export const devtoolsComponentRemoved = (
type DevtoolsComponentHook = (component: ComponentInternalInstance) => void
/*! #__NO_SIDE_EFFECTS__ */
/*@__NO_SIDE_EFFECTS__*/
function createDevtoolsComponentHook(
hook: DevtoolsHooks,
): DevtoolsComponentHook {

View File

@ -7,6 +7,7 @@ import {
type VNodeProps,
createVNode,
isVNode,
setBlockTracking,
} from './vnode'
import type { Teleport, TeleportProps } from './components/Teleport'
import type { Suspense, SuspenseProps } from './components/Suspense'
@ -201,25 +202,31 @@ export function h<P>(
// Actual implementation
export function h(type: any, propsOrChildren?: any, children?: any): VNode {
const l = arguments.length
if (l === 2) {
if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
// single vnode without props
if (isVNode(propsOrChildren)) {
return createVNode(type, null, [propsOrChildren])
try {
// #6913 disable tracking block in h function
setBlockTracking(-1)
const l = arguments.length
if (l === 2) {
if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
// single vnode without props
if (isVNode(propsOrChildren)) {
return createVNode(type, null, [propsOrChildren])
}
// props without children
return createVNode(type, propsOrChildren)
} else {
// omit props
return createVNode(type, null, propsOrChildren)
}
// props without children
return createVNode(type, propsOrChildren)
} else {
// omit props
return createVNode(type, null, propsOrChildren)
if (l > 3) {
children = Array.prototype.slice.call(arguments, 2)
} else if (l === 3 && isVNode(children)) {
children = [children]
}
return createVNode(type, propsOrChildren, children)
}
} else {
if (l > 3) {
children = Array.prototype.slice.call(arguments, 2)
} else if (l === 3 && isVNode(children)) {
children = [children]
}
return createVNode(type, propsOrChildren, children)
} finally {
setBlockTracking(1)
}
}

View File

@ -7,7 +7,7 @@ import {
type InternalRenderFunction,
isClassComponent,
} from './component'
import { queueJob, queuePostFlushCb } from './scheduler'
import { SchedulerJobFlags, queueJob, queuePostFlushCb } from './scheduler'
import { extend, getGlobalThis } from '@vue/shared'
type HMRComponent = ComponentOptions | ClassComponent
@ -31,11 +31,17 @@ export interface HMRRuntime {
// Note: for a component to be eligible for HMR it also needs the __hmrId option
// to be set so that its instances can be registered / removed.
if (__DEV__) {
getGlobalThis().__VUE_HMR_RUNTIME__ = {
createRecord: tryWrap(createRecord),
rerender: tryWrap(rerender),
reload: tryWrap(reload),
} as HMRRuntime
const g = getGlobalThis()
// vite-plugin-vue/issues/644, #13202
// custom-element libraries bundle Vue to simplify usage outside Vue projects but
// it overwrite __VUE_HMR_RUNTIME__, causing HMR to break.
if (!g.__VUE_HMR_RUNTIME__) {
g.__VUE_HMR_RUNTIME__ = {
createRecord: tryWrap(createRecord),
rerender: tryWrap(rerender),
reload: tryWrap(reload),
} as HMRRuntime
}
}
const map: Map<
@ -96,7 +102,10 @@ function rerender(id: string, newRender?: Function): void {
instance.renderCache = []
// this flag forces child components with slot content to update
isHmrUpdating = true
instance.update()
// #13771 don't update if the job is already disposed
if (!(instance.job.flags! & SchedulerJobFlags.DISPOSED)) {
instance.update()
}
isHmrUpdating = false
})
}
@ -144,11 +153,15 @@ function reload(id: string, newComp: HMRComponent): void {
// components to be unmounted and re-mounted. Queue the update so that we
// don't end up forcing the same parent to re-render multiple times.
queueJob(() => {
isHmrUpdating = true
instance.parent!.update()
isHmrUpdating = false
// #6930, #11248 avoid infinite recursion
dirtyInstances.delete(instance)
// vite-plugin-vue/issues/599
// don't update if the job is already disposed
if (!(instance.job.flags! & SchedulerJobFlags.DISPOSED)) {
isHmrUpdating = true
instance.parent!.update()
isHmrUpdating = false
// #6930, #11248 avoid infinite recursion
dirtyInstances.delete(instance)
}
})
} else if (instance.appContext.reload) {
// root instance mounted via createApp() has a reload method

View File

@ -28,6 +28,7 @@ import {
isReservedProp,
isString,
normalizeClass,
normalizeCssVarValue,
normalizeStyle,
stringifyStyle,
} from '@vue/shared'
@ -945,10 +946,8 @@ function resolveCssVars(
) {
const cssVars = instance.getCssVars()
for (const key in cssVars) {
expectedMap.set(
`--${getEscapedCssVarName(key, false)}`,
String(cssVars[key]),
)
const value = normalizeCssVarValue(cssVars[key])
expectedMap.set(`--${getEscapedCssVarName(key, false)}`, value)
}
}
if (vnode === root && instance.parent) {

View File

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

View File

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

View File

@ -1,7 +1,13 @@
import type { SuspenseBoundary } from './components/Suspense'
import type { VNode, VNodeNormalizedRef, VNodeNormalizedRefAtom } from './vnode'
import type {
VNode,
VNodeNormalizedRef,
VNodeNormalizedRefAtom,
VNodeRef,
} from './vnode'
import {
EMPTY_OBJ,
NO,
ShapeFlags,
hasOwn,
isArray,
@ -13,11 +19,12 @@ import { isAsyncWrapper } from './apiAsyncComponent'
import { warn } from './warning'
import { isRef, toRaw } from '@vue/reactivity'
import { ErrorCodes, callWithErrorHandling } from './errorHandling'
import type { SchedulerJob } from './scheduler'
import { type SchedulerJob, SchedulerJobFlags } from './scheduler'
import { queuePostRenderEffect } from './renderer'
import { type ComponentOptions, getComponentPublicInstance } from './component'
import { knownTemplateRefs } from './helpers/useTemplateRef'
const pendingSetRefMap = new WeakMap<VNodeNormalizedRef, SchedulerJob>()
/**
* Function for handling a template ref
*/
@ -77,7 +84,7 @@ export function setRef(
const rawSetupState = toRaw(setupState)
const canSetSetupRef =
setupState === EMPTY_OBJ
? () => false
? NO
: (key: string) => {
if (__DEV__) {
if (hasOwn(rawSetupState, key) && !isRef(rawSetupState[key])) {
@ -94,15 +101,26 @@ export function setRef(
return hasOwn(rawSetupState, key)
}
const canSetRef = (ref: VNodeRef) => {
return !__DEV__ || !knownTemplateRefs.has(ref as any)
}
// dynamic ref changed. unset old ref
if (oldRef != null && oldRef !== ref) {
invalidatePendingSetRef(oldRawRef!)
if (isString(oldRef)) {
refs[oldRef] = null
if (canSetSetupRef(oldRef)) {
setupState[oldRef] = null
}
} else if (isRef(oldRef)) {
oldRef.value = null
if (canSetRef(oldRef)) {
oldRef.value = null
}
// this type assertion is valid since `oldRef` has already been asserted to be non-null
const oldRawRefAtom = oldRawRef as VNodeNormalizedRefAtom
if (oldRawRefAtom.k) refs[oldRawRefAtom.k] = null
}
}
@ -119,7 +137,9 @@ export function setRef(
? canSetSetupRef(ref)
? setupState[ref]
: refs[ref]
: ref.value
: canSetRef(ref) || !rawRef.k
? ref.value
: refs[rawRef.k]
if (isUnmount) {
isArray(existing) && remove(existing, refValue)
} else {
@ -130,8 +150,11 @@ export function setRef(
setupState[ref] = refs[ref]
}
} else {
ref.value = [refValue]
if (rawRef.k) refs[rawRef.k] = ref.value
const newVal = [refValue]
if (canSetRef(ref)) {
ref.value = newVal
}
if (rawRef.k) refs[rawRef.k] = newVal
}
} else if (!existing.includes(refValue)) {
existing.push(refValue)
@ -143,7 +166,9 @@ export function setRef(
setupState[ref] = value
}
} else if (_isRef) {
ref.value = value
if (canSetRef(ref)) {
ref.value = value
}
if (rawRef.k) refs[rawRef.k] = value
} else if (__DEV__) {
warn('Invalid template ref type:', ref, `(${typeof ref})`)
@ -153,9 +178,15 @@ export function setRef(
// #1789: for non-null values, set them after render
// null values means this is unmount and it should not overwrite another
// ref with the same key
;(doSet as SchedulerJob).id = -1
queuePostRenderEffect(doSet, parentSuspense)
const job: SchedulerJob = () => {
doSet()
pendingSetRefMap.delete(rawRef)
}
job.id = -1
pendingSetRefMap.set(rawRef, job)
queuePostRenderEffect(job, parentSuspense)
} else {
invalidatePendingSetRef(rawRef)
doSet()
}
} else if (__DEV__) {
@ -163,3 +194,11 @@ export function setRef(
}
}
}
function invalidatePendingSetRef(rawRef: VNodeNormalizedRef) {
const pendingSetRef = pendingSetRefMap.get(rawRef)
if (pendingSetRef) {
pendingSetRef.flags! |= SchedulerJobFlags.DISPOSED
pendingSetRefMap.delete(rawRef)
}
}

View File

@ -53,10 +53,15 @@ let currentFlushPromise: Promise<void> | null = null
const RECURSION_LIMIT = 100
type CountMap = Map<SchedulerJob, number>
export function nextTick<T = void, R = void>(
export function nextTick(): Promise<void>
export function nextTick<T, R>(
this: T,
fn?: (this: T) => R,
): Promise<Awaited<R>> {
fn: (this: T) => R | Promise<R>,
): Promise<R>
export function nextTick<T, R>(
this: T,
fn?: (this: T) => R | Promise<R>,
): Promise<void | R> {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p
}

View File

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

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