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: env:
PUPPETEER_SKIP_DOWNLOAD: 'true' PUPPETEER_SKIP_DOWNLOAD: 'true'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4.1.0 uses: pnpm/action-setup@v4.1.0
@ -31,4 +31,4 @@ jobs:
- name: Run prettier - name: Run prettier
run: pnpm run format 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 runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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:" "vite": "catalog:"
}, },
"dependencies": { "dependencies": {
"@vue/repl": "^4.5.1", "@vue/repl": "^4.7.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"vue": "workspace:*" "vue": "workspace:*"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -478,7 +478,10 @@ describe('compiler: transform component slots', () => {
}) })
test('should only force dynamic slots when actually using scope vars w/ prefixIdentifiers: true', () => { 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 }) const { root } = parseWithSlots(template, { prefixIdentifiers: true })
let flag: any let flag: any
if (root.children[0].type === NodeTypes.FOR) { if (root.children[0].type === NodeTypes.FOR) {
@ -491,8 +494,8 @@ describe('compiler: transform component slots', () => {
.children[0] as ComponentNode .children[0] as ComponentNode
flag = (innerComp.codegenNode as VNodeCall).patchFlag flag = (innerComp.codegenNode as VNodeCall).patchFlag
} }
if (shouldForce) { if (expectedPatchFlag) {
expect(flag).toBe(PatchFlags.DYNAMIC_SLOTS) expect(flag).toBe(expectedPatchFlag)
} else { } else {
expect(flag).toBeUndefined() expect(flag).toBeUndefined()
} }
@ -502,14 +505,13 @@ describe('compiler: transform component slots', () => {
`<div v-for="i in list"> `<div v-for="i in list">
<Comp v-slot="bar">foo</Comp> <Comp v-slot="bar">foo</Comp>
</div>`, </div>`,
false,
) )
assertDynamicSlots( assertDynamicSlots(
`<div v-for="i in list"> `<div v-for="i in list">
<Comp v-slot="bar">{{ i }}</Comp> <Comp v-slot="bar">{{ i }}</Comp>
</div>`, </div>`,
true, PatchFlags.DYNAMIC_SLOTS,
) )
// reference the component's own slot variable should not force 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="foo">
<Comp v-slot="bar">{{ bar }}</Comp> <Comp v-slot="bar">{{ bar }}</Comp>
</Comp>`, </Comp>`,
false,
) )
assertDynamicSlots( assertDynamicSlots(
`<Comp v-slot="foo"> `<Comp v-slot="foo">
<Comp v-slot="bar">{{ foo }}</Comp> <Comp v-slot="bar">{{ foo }}</Comp>
</Comp>`, </Comp>`,
true, PatchFlags.DYNAMIC_SLOTS,
) )
// #2564 // #2564
@ -532,14 +533,35 @@ describe('compiler: transform component slots', () => {
`<div v-for="i in list"> `<div v-for="i in list">
<Comp v-slot="bar"><button @click="fn(i)" /></Comp> <Comp v-slot="bar"><button @click="fn(i)" /></Comp>
</div>`, </div>`,
true, PatchFlags.DYNAMIC_SLOTS,
) )
assertDynamicSlots( assertDynamicSlots(
`<div v-for="i in list"> `<div v-for="i in list">
<Comp v-slot="bar"><button @click="fn()" /></Comp> <Comp v-slot="bar"><button @click="fn()" /></Comp>
</div>`, </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 { type Position, createSimpleExpression } from '../src/ast'
import { import {
advancePositionWithClone, advancePositionWithClone,
@ -115,3 +120,18 @@ test('toValidAssetId', () => {
'_component_test_2797935797_1', '_component_test_2797935797_1',
) )
}) })
describe('isReferencedIdentifier', () => {
test('identifiers in function parameters should not be inferred as references', () => {
expect.assertions(4)
const ast = babelParse(`(({ title }) => [])`)
walkIdentifiers(
ast.program.body[0],
(node, parent, parentStack, isReference) => {
expect(isReference).toBe(false)
expect(isReferencedIdentifier(node, parent, parentStack)).toBe(false)
},
true,
)
})
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -131,9 +131,17 @@ export function buildSlots(
// since it likely uses a scope variable. // since it likely uses a scope variable.
let hasDynamicSlots = context.scopes.vSlot > 0 || context.scopes.vFor > 0 let hasDynamicSlots = context.scopes.vSlot > 0 || context.scopes.vFor > 0
// with `prefixIdentifiers: true`, this can be further optimized to make // 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) { 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. // 1. Check for slot with slotProps on component itself.
@ -215,7 +223,7 @@ export function buildSlots(
), ),
) )
} else if ( } else if (
(vElse = findDir(slotElement, /^else(-if)?$/, true /* allowEmpty */)) (vElse = findDir(slotElement, /^else(?:-if)?$/, true /* allowEmpty */))
) { ) {
// find adjacent v-if // find adjacent v-if
let j = i let j = i
@ -226,7 +234,7 @@ export function buildSlots(
break break
} }
} }
if (prev && isTemplateNode(prev) && findDir(prev, /^(else-)?if$/)) { if (prev && isTemplateNode(prev) && findDir(prev, /^(?:else-)?if$/)) {
__TEST__ && assert(dynamicSlots.length > 0) __TEST__ && assert(dynamicSlots.length > 0)
// attach this slot to previous conditional // attach this slot to previous conditional
let conditional = dynamicSlots[ 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 => export const isSimpleIdentifier = (name: string): boolean =>
!nonIdentifierRE.test(name) !nonIdentifierRE.test(name)
@ -189,7 +189,7 @@ export const isMemberExpression: (
) => boolean = __BROWSER__ ? isMemberExpressionBrowser : isMemberExpressionNode ) => boolean = __BROWSER__ ? isMemberExpressionBrowser : isMemberExpressionNode
const fnExpRE = 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 => export const isFnExpressionBrowser: (exp: ExpressionNode) => boolean = exp =>
fnExpRE.test(getExpSource(exp)) fnExpRE.test(getExpSource(exp))
@ -343,6 +343,10 @@ export function isText(
return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT
} }
export function isVPre(p: ElementNode['props'][0]): p is DirectiveNode {
return p.type === NodeTypes.DIRECTIVE && p.name === 'pre'
}
export function isVSlot(p: ElementNode['props'][0]): p is DirectiveNode { export function isVSlot(p: ElementNode['props'][0]): p is DirectiveNode {
return p.type === NodeTypes.DIRECTIVE && p.name === 'slot' return p.type === NodeTypes.DIRECTIVE && p.name === 'slot'
} }

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compiler-dom", "name": "@vue/compiler-dom",
"version": "3.5.16", "version": "3.5.21",
"description": "@vue/compiler-dom", "description": "@vue/compiler-dom",
"main": "index.js", "main": "index.js",
"module": "dist/compiler-dom.esm-bundler.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__) { if (DOMErrorCodes.X_V_HTML_NO_EXPRESSION < ErrorCodes.__EXTEND_POINT__) {
throw new Error( throw new Error(
`DOMErrorCodes need to be updated to ${ `DOMErrorCodes need to be updated to ${
ErrorCodes.__EXTEND_POINT__ + 1 ErrorCodes.__EXTEND_POINT__
} to match extension point from core ErrorCodes.`, } to match extension point from core ErrorCodes.`,
) )
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -808,30 +808,4 @@ const props = defineProps({ foo: String })
expect(content).toMatch(`foo: { default: 5.5, type: Number }`) expect(content).toMatch(`foo: { default: 5.5, type: Number }`)
assertCode(content) 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({ expect(props).toStrictEqual({
foo: ['Symbol', 'String', 'Number'], 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', () => { describe('generics', () => {
test('generic with type literal', () => { test('generic with type literal', () => {
expect( expect(
@ -1155,6 +1198,45 @@ describe('resolveType', () => {
expect(deps && [...deps]).toStrictEqual(['/user.ts']) 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', () => { test('ts module resolve w/ project reference folder', () => {
const files = { const files = {
'/tsconfig.json': JSON.stringify({ '/tsconfig.json': JSON.stringify({
@ -1299,6 +1381,33 @@ describe('resolveType', () => {
expect(deps && [...deps]).toStrictEqual(Object.keys(files)) 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', () => { test('global types with ambient references', () => {
const files = { const files = {
// with references // with references

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1029,6 +1029,14 @@ function resolveWithTS(
if (configs.length === 1) { if (configs.length === 1) {
matchedConfig = configs[0] matchedConfig = configs[0]
} else { } 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 // resolve which config matches the current file
for (const c of configs) { for (const c of configs) {
const base = normalizePath( const base = normalizePath(
@ -1039,11 +1047,11 @@ function resolveWithTS(
const excluded: string[] | undefined = c.config.raw?.exclude const excluded: string[] | undefined = c.config.raw?.exclude
if ( if (
(!included && (!base || containingFile.startsWith(base))) || (!included && (!base || containingFile.startsWith(base))) ||
included?.some(p => isMatch(containingFile, joinPaths(base, p))) included?.some(p => isMatch(containingFile, getPattern(base, p)))
) { ) {
if ( if (
excluded && excluded &&
excluded.some(p => isMatch(containingFile, joinPaths(base, p))) excluded.some(p => isMatch(containingFile, getPattern(base, p)))
) { ) {
continue continue
} }
@ -1296,7 +1304,12 @@ function recordTypes(
} }
} else if (stmt.type === 'TSModuleDeclaration' && stmt.global) { } else if (stmt.type === 'TSModuleDeclaration' && stmt.global) {
for (const s of (stmt.body as TSModuleBlock).body) { 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 { } else {
@ -1500,6 +1513,7 @@ export function inferRuntimeType(
node: Node & MaybeWithScope, node: Node & MaybeWithScope,
scope: TypeScope = node._ownerScope || ctxToScope(ctx), scope: TypeScope = node._ownerScope || ctxToScope(ctx),
isKeyOf = false, isKeyOf = false,
typeParameters?: Record<string, Node>,
): string[] { ): string[] {
try { try {
switch (node.type) { switch (node.type) {
@ -1589,17 +1603,42 @@ export function inferRuntimeType(
const resolved = resolveTypeReference(ctx, node, scope) const resolved = resolveTypeReference(ctx, node, scope)
if (resolved) { if (resolved) {
if (resolved.type === 'TSTypeAliasDeclaration') { if (resolved.type === 'TSTypeAliasDeclaration') {
return inferRuntimeType( // #13240
ctx, // Special case for function type aliases to ensure correct runtime behavior
resolved.typeAnnotation, // other type aliases still fallback to unknown as before
resolved._ownerScope, if (resolved.typeAnnotation.type === 'TSFunctionType') {
isKeyOf, 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) return inferRuntimeType(ctx, resolved, resolved._ownerScope, isKeyOf)
} }
if (node.typeName.type === 'Identifier') { if (node.typeName.type === 'Identifier') {
if (typeParameters && typeParameters[node.typeName.name]) {
return inferRuntimeType(
ctx,
typeParameters[node.typeName.name],
scope,
isKeyOf,
typeParameters,
)
}
if (isKeyOf) { if (isKeyOf) {
switch (node.typeName.name) { switch (node.typeName.name) {
case 'String': case 'String':
@ -1732,11 +1771,15 @@ export function inferRuntimeType(
return inferRuntimeType(ctx, node.typeAnnotation, scope) return inferRuntimeType(ctx, node.typeAnnotation, scope)
case 'TSUnionType': case 'TSUnionType':
return flattenTypes(ctx, node.types, scope, isKeyOf) return flattenTypes(ctx, node.types, scope, isKeyOf, typeParameters)
case 'TSIntersectionType': { case 'TSIntersectionType': {
return flattenTypes(ctx, node.types, scope, isKeyOf).filter( return flattenTypes(
t => t !== UNKNOWN_TYPE, ctx,
) node.types,
scope,
isKeyOf,
typeParameters,
).filter(t => t !== UNKNOWN_TYPE)
} }
case 'TSEnumDeclaration': case 'TSEnumDeclaration':
@ -1807,14 +1850,17 @@ function flattenTypes(
types: TSType[], types: TSType[],
scope: TypeScope, scope: TypeScope,
isKeyOf: boolean = false, isKeyOf: boolean = false,
typeParameters: Record<string, Node> | undefined = undefined,
): string[] { ): string[] {
if (types.length === 1) { if (types.length === 1) {
return inferRuntimeType(ctx, types[0], scope, isKeyOf) return inferRuntimeType(ctx, types[0], scope, isKeyOf, typeParameters)
} }
return [ return [
...new Set( ...new Set(
([] as string[]).concat( ([] as string[]).concat(
...types.map(t => inferRuntimeType(ctx, t, scope, isKeyOf)), ...types.map(t =>
inferRuntimeType(ctx, t, scope, isKeyOf, typeParameters),
),
), ),
), ),
] ]

View File

@ -121,3 +121,8 @@ export const propNameEscapeSymbolsRE: RegExp =
export function getEscapedPropName(key: string): string { export function getEscapedPropName(key: string): string {
return propNameEscapeSymbolsRE.test(key) ? JSON.stringify(key) : key 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 return `{\n ${vars
.map( .map(
key => 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}` .join(',\n ')}\n}`
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -166,6 +166,132 @@ describe('ssr: v-model', () => {
_push(\`</optgroup></select></div>\`) _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">', () => { test('<input type="radio">', () => {

View File

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

View File

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

View File

@ -17,7 +17,7 @@ import {
// Plugin for the first transform pass, which simply constructs the AST node // Plugin for the first transform pass, which simply constructs the AST node
export const ssrTransformIf: NodeTransform = createStructuralDirectiveTransform( export const ssrTransformIf: NodeTransform = createStructuralDirectiveTransform(
/^(if|else|else-if)$/, /^(?:if|else|else-if)$/,
processIf, 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) { function processOption(plainNode: PlainElementNode) {
if (plainNode.tag === 'option') { if (plainNode.tag === 'option') {
if (plainNode.props.findIndex(p => p.name === 'selected') === -1) { 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') { } else if (plainNode.tag === 'optgroup') {
plainNode.children.forEach(option => processSelectChildren(plainNode.children)
processOption(option as PlainElementNode),
)
} }
} }
@ -163,18 +173,7 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
checkDuplicatedValue() checkDuplicatedValue()
node.children = [createInterpolation(model, model.loc)] node.children = [createInterpolation(model, model.loc)]
} else if (node.tag === 'select') { } else if (node.tag === 'select') {
const processChildren = (children: TemplateChildNode[]) => { processSelectChildren(node.children)
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)
} else { } else {
context.onError( context.onError(
createDOMCompilerError( createDOMCompilerError(

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/reactivity", "name": "@vue/reactivity",
"version": "3.5.16", "version": "3.5.21",
"description": "@vue/reactivity", "description": "@vue/reactivity",
"main": "index.js", "main": "index.js",
"module": "dist/reactivity.esm-bundler.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) return reactiveReadArray(this).join(separator)
}, },
// keys() iterator only reads `length`, no optimisation required // keys() iterator only reads `length`, no optimization required
lastIndexOf(...args: unknown[]) { lastIndexOf(...args: unknown[]) {
return searchProxy(this, 'lastIndexOf', args) return searchProxy(this, 'lastIndexOf', args)
@ -200,7 +200,7 @@ function iterator(
wrapValue: (value: any) => unknown, wrapValue: (value: any) => unknown,
) { ) {
// note that taking ARRAY_ITERATE dependency here is not strictly equivalent // 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: // creating the iterator does not access any array property:
// it is only when .next() is called that length and indexes are accessed. // 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, // 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 (!isArray(target) && isRef(oldValue) && !isRef(value)) {
if (isOldValueReadonly) { if (isOldValueReadonly) {
return false if (__DEV__) {
warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target[key],
)
}
return true
} else { } else {
oldValue.value = value oldValue.value = value
return true return true

View File

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

View File

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

View File

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

View File

@ -1689,6 +1689,57 @@ describe('api: watch', () => {
expect(cb).toHaveBeenCalledTimes(4) 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 () => { test('pause / resume', async () => {
const count = ref(0) const count = ref(0)
const cb = vi.fn() const cb = vi.fn()

View File

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

View File

@ -6,6 +6,8 @@ import {
nodeOps, nodeOps,
ref, ref,
render, render,
serializeInner,
useSlots,
} from '@vue/runtime-test' } from '@vue/runtime-test'
import { createBlock, normalizeVNode } from '../src/vnode' import { createBlock, normalizeVNode } from '../src/vnode'
import { createSlots } from '../src/helpers/createSlots' import { createSlots } from '../src/helpers/createSlots'
@ -42,6 +44,25 @@ describe('component: slots', () => {
expect(slots).toMatchObject({}) 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)', () => { test('initSlots: should normalize object slots (when value is null, string, array)', () => {
const { slots } = renderWithSlots({ const { slots } = renderWithSlots({
_inner: '_inner', _inner: '_inner',
@ -50,6 +71,10 @@ describe('component: slots', () => {
footer: ['f1', 'f2'], footer: ['f1', 'f2'],
}) })
expect(
'[Vue warn]: Non-function value encountered for slot "_inner". Prefer function slots for better performance.',
).toHaveBeenWarned()
expect( expect(
'[Vue warn]: Non-function value encountered for slot "header". Prefer function slots for better performance.', '[Vue warn]: Non-function value encountered for slot "header". Prefer function slots for better performance.',
).toHaveBeenWarned() ).toHaveBeenWarned()
@ -58,8 +83,8 @@ describe('component: slots', () => {
'[Vue warn]: Non-function value encountered for slot "footer". Prefer function slots for better performance.', '[Vue warn]: Non-function value encountered for slot "footer". Prefer function slots for better performance.',
).toHaveBeenWarned() ).toHaveBeenWarned()
expect(slots).not.toHaveProperty('_inner')
expect(slots).not.toHaveProperty('foo') expect(slots).not.toHaveProperty('foo')
expect(slots._inner()).toMatchObject([normalizeVNode('_inner')])
expect(slots.header()).toMatchObject([normalizeVNode('header')]) expect(slots.header()).toMatchObject([normalizeVNode('header')])
expect(slots.footer()).toMatchObject([ expect(slots.footer()).toMatchObject([
normalizeVNode('f1'), normalizeVNode('f1'),
@ -418,4 +443,22 @@ describe('component: slots', () => {
'Slot "default" invoked outside of the render function', 'Slot "default" invoked outside of the render function',
).toHaveBeenWarned() ).toHaveBeenWarned()
}) })
test('slot name starts with underscore', () => {
const Comp = {
setup(_: any, { slots }: any) {
return () => slots._foo()
},
}
const App = {
setup() {
return () => h(Comp, null, { _foo: () => 'foo' })
},
}
const root = nodeOps.createElement('div')
createApp(App).mount(root)
expect(serializeInner(root)).toBe('foo')
})
}) })

View File

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

View File

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

View File

@ -894,4 +894,150 @@ describe('hot module replacement', () => {
await timeout() await timeout()
expect(serializeInner(root)).toBe('<div>bar</div>') 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 () => { test('hydrate safely when property used by async setup changed before render', async () => {
const toggle = ref(true) const toggle = ref(true)
@ -1677,6 +1740,35 @@ describe('SSR hydration', () => {
expect(`mismatch`).not.toHaveBeenWarned() expect(`mismatch`).not.toHaveBeenWarned()
}) })
// #13394
test('transition appear work with empty content', async () => {
const show = ref(true)
const { vnode, container } = mountWithHydration(
`<template><!----></template>`,
function (this: any) {
return h(
Transition,
{ appear: true },
{
default: () =>
show.value
? renderSlot(this.$slots, 'default')
: createTextVNode('foo'),
},
)
},
)
// empty slot render as a comment node
expect(container.firstChild!.nodeType).toBe(Node.COMMENT_NODE)
expect(vnode.el).toBe(container.firstChild)
expect(`mismatch`).not.toHaveBeenWarned()
show.value = false
await nextTick()
expect(container.innerHTML).toBe('foo')
})
test('transition appear with v-if', () => { test('transition appear with v-if', () => {
const show = false const show = false
const { vnode, container } = mountWithHydration( const { vnode, container } = mountWithHydration(
@ -2265,6 +2357,30 @@ describe('SSR hydration', () => {
expect(`Hydration style mismatch`).not.toHaveBeenWarned() 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', () => { test('escape css var name', () => {
const container = document.createElement('div') const container = document.createElement('div')
container.innerHTML = `<div style="padding: 4px;--foo\\.bar:red;"></div>` container.innerHTML = `<div style="padding: 4px;--foo\\.bar:red;"></div>`

View File

@ -10,6 +10,7 @@ import {
render, render,
serializeInner, serializeInner,
shallowRef, shallowRef,
watch,
} from '@vue/runtime-test' } from '@vue/runtime-test'
describe('api: template refs', () => { describe('api: template refs', () => {
@ -179,6 +180,89 @@ describe('api: template refs', () => {
expect(el.value).toBe(null) 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 () => { it('unset old ref when new ref is absent', async () => {
const root1 = nodeOps.createElement('div') const root1 = nodeOps.createElement('div')
const root2 = nodeOps.createElement('div') const root2 = nodeOps.createElement('div')

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/runtime-core", "name": "@vue/runtime-core",
"version": "3.5.16", "version": "3.5.21",
"description": "@vue/runtime-core", "description": "@vue/runtime-core",
"main": "index.js", "main": "index.js",
"module": "dist/runtime-core.esm-bundler.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 => export const isAsyncWrapper = (i: ComponentInternalInstance | VNode): boolean =>
!!(i.type as ComponentOptions).__asyncLoader !!(i.type as ComponentOptions).__asyncLoader
/*! #__NO_SIDE_EFFECTS__ */ /*@__NO_SIDE_EFFECTS__*/
export function defineAsyncComponent< export function defineAsyncComponent<
T extends Component = { new (): ComponentPublicInstance }, T extends Component = { new (): ComponentPublicInstance },
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T { >(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
@ -123,28 +123,30 @@ export function defineAsyncComponent<
__asyncHydrate(el, instance, hydrate) { __asyncHydrate(el, instance, hydrate) {
let patched = false 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 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 => const teardown = hydrateStrategy(performHydrate, cb =>
forEachElement(el, cb), forEachElement(el, cb),
) )
if (teardown) { if (teardown) {
;(instance.bum || (instance.bum = [])).push(teardown) ;(instance.bum || (instance.bum = [])).push(teardown)
} }
;(instance.u || (instance.u = [])).push(() => (patched = true))
} }
: hydrate : performHydrate
if (resolvedComp) { if (resolvedComp) {
doHydrate() doHydrate()
} else { } else {

View File

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

View File

@ -1,6 +1,5 @@
import { isFunction } from '@vue/shared' import { isFunction } from '@vue/shared'
import { currentInstance } from './component' import { currentInstance, getCurrentInstance } from './component'
import { currentRenderingInstance } from './componentRenderContext'
import { currentApp } from './apiCreateApp' import { currentApp } from './apiCreateApp'
import { warn } from './warning' import { warn } from './warning'
@ -51,7 +50,7 @@ export function inject(
) { ) {
// fallback to `currentRenderingInstance` so that this can be called in // fallback to `currentRenderingInstance` so that this can be called in
// a functional component // a functional component
const instance = currentInstance || currentRenderingInstance const instance = getCurrentInstance()
// also support looking up from app-level provides w/ `app.runWithContext()` // also support looking up from app-level provides w/ `app.runWithContext()`
if (instance || currentApp) { if (instance || currentApp) {
@ -90,5 +89,5 @@ export function inject(
* user. One example is `useRoute()` in `vue-router`. * user. One example is `useRoute()` in `vue-router`.
*/ */
export function hasInjectionContext(): boolean { 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'] { export function useSlots(): SetupContext['slots'] {
return getContext().slots return getContext('useSlots').slots
} }
export function useAttrs(): SetupContext['attrs'] { export function useAttrs(): SetupContext['attrs'] {
return getContext().attrs return getContext('useAttrs').attrs
} }
function getContext(): SetupContext { function getContext(calledFunctionName: string): SetupContext {
const i = getCurrentInstance()! const i = getCurrentInstance()!
if (__DEV__ && !i) { if (__DEV__ && !i) {
warn(`useContext() called without active instance.`) warn(`${calledFunctionName}() called without active instance.`)
} }
return i.setupContext || (i.setupContext = createSetupContext(i)) return i.setupContext || (i.setupContext = createSetupContext(i))
} }

View File

@ -536,7 +536,7 @@ function installCompatMount(
if (__DEV__) { if (__DEV__) {
for (let i = 0; i < container.attributes.length; i++) { for (let i = 0; i < container.attributes.length; i++) {
const attr = container.attributes[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) warnDeprecation(DeprecationTypes.GLOBAL_MOUNT_CONTAINER, null)
break break
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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