Merge branch 'main' into compat-array-watch

# Conflicts:
#	packages/reactivity/src/watch.ts
This commit is contained in:
skirtle 2025-05-25 01:36:53 +01:00
commit 5d04c062d6
158 changed files with 4880 additions and 1929 deletions

View File

@ -1,18 +1,17 @@
{ {
$schema: 'https://docs.renovatebot.com/renovate-schema.json', $schema: 'https://docs.renovatebot.com/renovate-schema.json',
extends: ['config:base', 'schedule:weekly', 'group:allNonMajor'], extends: ['config:recommended', 'schedule:weekly', 'group:allNonMajor'],
labels: ['dependencies'], labels: ['dependencies'],
ignorePaths: ['**/__tests__/**'], ignorePaths: ['**/__tests__/**'],
rangeStrategy: 'bump', rangeStrategy: 'bump',
packageRules: [ packageRules: [
{ {
depTypeList: ['peerDependencies'], matchDepTypes: ['peerDependencies'],
enabled: false, enabled: false,
}, },
{ {
groupName: 'test', groupName: 'test',
matchPackageNames: ['vitest', 'jsdom', 'puppeteer'], matchPackageNames: ['vitest', 'jsdom', 'puppeteer', '@vitest{/,}**'],
matchPackagePrefixes: ['@vitest'],
}, },
{ {
groupName: 'playground', groupName: 'playground',
@ -23,18 +22,28 @@
}, },
{ {
groupName: 'compiler', groupName: 'compiler',
matchPackageNames: ['magic-string'], matchPackageNames: ['magic-string', '@babel{/,}**', 'postcss{/,}**'],
matchPackagePrefixes: ['@babel', 'postcss'],
}, },
{ {
groupName: 'build', groupName: 'build',
matchPackageNames: ['vite', '@swc/core'], matchPackageNames: [
matchPackagePrefixes: ['rollup', 'esbuild', '@rollup', '@vitejs'], 'vite',
'@swc/core',
'rollup{/,}**',
'esbuild{/,}**',
'@rollup{/,}**',
'@vitejs{/,}**',
],
}, },
{ {
groupName: 'lint', groupName: 'lint',
matchPackageNames: ['simple-git-hooks', 'lint-staged'], matchPackageNames: [
matchPackagePrefixes: ['typescript-eslint', 'eslint', 'prettier'], 'simple-git-hooks',
'lint-staged',
'typescript-eslint{/,}**',
'eslint{/,}**',
'prettier{/,}**',
],
}, },
], ],
ignoreDeps: [ ignoreDeps: [

View File

@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4.0.0 uses: pnpm/action-setup@v4.1.0
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@ -31,4 +31,4 @@ jobs:
- name: Run prettier - name: Run prettier
run: pnpm run format run: pnpm run format
- uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c - uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef

View File

@ -17,7 +17,7 @@ jobs:
ref: minor ref: minor
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4.0.0 uses: pnpm/action-setup@v4.1.0
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4

View File

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

View File

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

View File

@ -25,7 +25,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4.0.0 uses: pnpm/action-setup@v4.1.0
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@ -37,7 +37,7 @@ jobs:
run: pnpm install run: pnpm install
- name: Download Size Data - name: Download Size Data
uses: dawidd6/action-download-artifact@v6 uses: dawidd6/action-download-artifact@v9
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@v6 uses: dawidd6/action-download-artifact@v9
with: with:
branch: ${{ steps.pr-base.outputs.content }} branch: ${{ steps.pr-base.outputs.content }}
workflow: size-data.yml workflow: size-data.yml

View File

@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4.0.0 uses: pnpm/action-setup@v4.1.0
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@ -35,7 +35,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4.0.0 uses: pnpm/action-setup@v4.1.0
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@ -63,7 +63,7 @@ jobs:
key: chromium-${{ hashFiles('pnpm-lock.yaml') }} key: chromium-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4.0.0 uses: pnpm/action-setup@v4.1.0
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@ -88,7 +88,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4.0.0 uses: pnpm/action-setup@v4.1.0
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@ -104,5 +104,8 @@ jobs:
- name: Run prettier - name: Run prettier
run: pnpm run format-check run: pnpm run format-check
- name: Run tsc
run: pnpm run check
- name: Run type declaration tests - name: Run type declaration tests
run: pnpm run test-dts run: pnpm run test-dts

View File

@ -1 +1 @@
20 22.14.0

View File

@ -13,5 +13,6 @@
}, },
"[json]": { "[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
} },
"editor.formatOnSave": true
} }

View File

@ -0,0 +1 @@
https://vuejs.org/funding.json

View File

@ -1,3 +1,74 @@
## [3.5.14](https://github.com/vuejs/core/compare/v3.5.13...v3.5.14) (2025-05-15)
### Bug Fixes
* **compat:** correct deprecation message for v-bind.sync usage ([#13137](https://github.com/vuejs/core/issues/13137)) ([466b30f](https://github.com/vuejs/core/commit/466b30f4049ec89fb282624ec17d1a93472ab93f)), closes [#13133](https://github.com/vuejs/core/issues/13133)
* **compiler-core:** remove slot cache from parent renderCache during unmounting ([#13215](https://github.com/vuejs/core/issues/13215)) ([5d166f3](https://github.com/vuejs/core/commit/5d166f3796a03a497435fc079c6a83a4e9c6cf52))
* **compiler-sfc:** fix scope handling for props destructure in function parameters and catch clauses ([8e34357](https://github.com/vuejs/core/commit/8e3435779a667de485cf9efd78667d0ca14c5f84)), closes [#12790](https://github.com/vuejs/core/issues/12790)
* **compiler-sfc:** treat the return value of `useTemplateRef` as a definite ref ([#13197](https://github.com/vuejs/core/issues/13197)) ([8ae1122](https://github.com/vuejs/core/commit/8ae11226e8ee938615e17c7b81dc38ae3f7cefb9))
* **compiler:** fix spelling error in domTagConfig ([#13043](https://github.com/vuejs/core/issues/13043)) ([388295b](https://github.com/vuejs/core/commit/388295b27f3cc69eba25d325bbe60a36a3df831a))
* **customFormatter:** properly accessing ref value during debugger ([#12948](https://github.com/vuejs/core/issues/12948)) ([fdbd026](https://github.com/vuejs/core/commit/fdbd02658301dd794fe0c84f0018d080a07fca9f))
* **hmr/teleport:** adjust static children traversal for HMR in dev mode ([#12819](https://github.com/vuejs/core/issues/12819)) ([5e37dd0](https://github.com/vuejs/core/commit/5e37dd009562bcd8080a200c32abde2d6e4f0305)), closes [#12816](https://github.com/vuejs/core/issues/12816)
* **hmr:** avoid hydration for hmr root reload ([#12450](https://github.com/vuejs/core/issues/12450)) ([1f98a9c](https://github.com/vuejs/core/commit/1f98a9c493d01c21befa90107f0593bc92a58932)), closes [vitejs/vite-plugin-vue#146](https://github.com/vitejs/vite-plugin-vue/issues/146) [vitejs/vite-plugin-vue#477](https://github.com/vitejs/vite-plugin-vue/issues/477)
* **hmr:** avoid hydration for hmr updating ([#12262](https://github.com/vuejs/core/issues/12262)) ([9c4dbbc](https://github.com/vuejs/core/commit/9c4dbbc5185125835ad3e49baba303bd54676111)), closes [#7706](https://github.com/vuejs/core/issues/7706) [#8170](https://github.com/vuejs/core/issues/8170)
* **reactivity:** ensure markRaw objects are not reactive ([#12824](https://github.com/vuejs/core/issues/12824)) ([295b5ec](https://github.com/vuejs/core/commit/295b5ec19b6a52c4a56652cc4d6e93a4ea7c14ed)), closes [#12807](https://github.com/vuejs/core/issues/12807)
* **reactivity:** ensure multiple effectScope on() and off() calls maintains correct active scope ([22dcbf3](https://github.com/vuejs/core/commit/22dcbf3e20eb84f69c8952f6f70d9990136a4a68)), closes [#12631](https://github.com/vuejs/core/issues/12631) [#12632](https://github.com/vuejs/core/issues/12632) [#12641](https://github.com/vuejs/core/issues/12641)
* **reactivity:** should not recompute if computed does not track reactive data ([#12341](https://github.com/vuejs/core/issues/12341)) ([0b23fd2](https://github.com/vuejs/core/commit/0b23fd23833cf085e7e112bf4435cfc9b360d072)), closes [#12337](https://github.com/vuejs/core/issues/12337)
* **runtime-core:** stop tracking deps in setRef during unmount ([#13210](https://github.com/vuejs/core/issues/13210)) ([016c472](https://github.com/vuejs/core/commit/016c472bd2e7604b21c69dee1da8545ce26e4d2f))
* **runtime-core:** update __vnode of static nodes when patching along the optimized path ([#13223](https://github.com/vuejs/core/issues/13223)) ([b3ecee3](https://github.com/vuejs/core/commit/b3ecee3da8ed5c55dea89ce6b4b376b2b722b018))
* **runtime-core:** inherit comment nodes during block patch in production build ([#10748](https://github.com/vuejs/core/issues/10748)) ([6264505](https://github.com/vuejs/core/commit/626450590d81f79117b34d2a73073b1dc8f551bd)), closes [#10747](https://github.com/vuejs/core/issues/10747) [#12650](https://github.com/vuejs/core/issues/12650)
* **runtime-core:** prevent unmounted vnode from being inserted during transition leave ([#12862](https://github.com/vuejs/core/issues/12862)) ([d6a6ec1](https://github.com/vuejs/core/commit/d6a6ec13ce521683bfb2a22932778ef7b51f8600)), closes [#12860](https://github.com/vuejs/core/issues/12860)
* **runtime-core:** respect immutability for readonly reactive arrays in `v-for` ([#13091](https://github.com/vuejs/core/issues/13091)) ([3f27c58](https://github.com/vuejs/core/commit/3f27c58ffbd4309df369bc89493fdc284dc540bb)), closes [#13087](https://github.com/vuejs/core/issues/13087)
* **runtime-dom:** always treat autocorrect as attribute ([#13001](https://github.com/vuejs/core/issues/13001)) ([1499135](https://github.com/vuejs/core/commit/1499135c227236e037bb746beeb777941b0b58ff)), closes [#5705](https://github.com/vuejs/core/issues/5705)
* **slots:** properly warn if slot invoked in setup ([#12195](https://github.com/vuejs/core/issues/12195)) ([9196222](https://github.com/vuejs/core/commit/9196222ae1d63b52b35ac5fbf5e71494587ccf05)), closes [#12194](https://github.com/vuejs/core/issues/12194)
* **ssr:** properly init slots during ssr rendering ([#12441](https://github.com/vuejs/core/issues/12441)) ([2206cd2](https://github.com/vuejs/core/commit/2206cd235a1627c540e795e378b7564a55b47313)), closes [#12438](https://github.com/vuejs/core/issues/12438)
* **transition:** fix KeepAlive with transition out-in mode behavior in production ([#12468](https://github.com/vuejs/core/issues/12468)) ([343c891](https://github.com/vuejs/core/commit/343c89122448719bd6ed6bd9de986dfb2721d6bf)), closes [#12465](https://github.com/vuejs/core/issues/12465)
* **TransitionGroup:** reset prevChildren to prevent memory leak ([#13183](https://github.com/vuejs/core/issues/13183)) ([8b848cb](https://github.com/vuejs/core/commit/8b848cbbd2af337d23e19e202f9ab433f8580855)), closes [#13181](https://github.com/vuejs/core/issues/13181)
* **types:** allow return any for Options API lifecycle hooks ([#5914](https://github.com/vuejs/core/issues/5914)) ([06310e8](https://github.com/vuejs/core/commit/06310e82f5bed62d1b9733dcb18cd8d6edc988de))
* **types:** the directive's modifiers should be optional ([#12605](https://github.com/vuejs/core/issues/12605)) ([10e54dc](https://github.com/vuejs/core/commit/10e54dcc86a7967f3196d96200bcbd1d3d42082f))
* **typos:** fix comments referencing transformElement.ts ([#12551](https://github.com/vuejs/core/issues/12551))[ci-skip] ([11c053a](https://github.com/vuejs/core/commit/11c053a5429ad0d27a0e2c78b6b026ea00ace116))
### Features
* **types:** add type TemplateRef ([#12645](https://github.com/vuejs/core/issues/12645)) ([636a861](https://github.com/vuejs/core/commit/636a8619f06c71dfd79f7f6412fd130c4f84226f))
## [3.5.13](https://github.com/vuejs/core/compare/v3.5.12...v3.5.13) (2024-11-15)
### Bug Fixes
* **compiler-core:** handle v-memo + v-for with functional key ([#12014](https://github.com/vuejs/core/issues/12014)) ([99009ee](https://github.com/vuejs/core/commit/99009eee0efc238392daba93792d478525b21afa)), closes [#12013](https://github.com/vuejs/core/issues/12013)
* **compiler-dom:** properly stringify template string style ([#12392](https://github.com/vuejs/core/issues/12392)) ([2d78539](https://github.com/vuejs/core/commit/2d78539da35322aea5f821b3cf9b02d006abac72)), closes [#12391](https://github.com/vuejs/core/issues/12391)
* **custom-element:** avoid triggering mutationObserver when relecting props ([352bc88](https://github.com/vuejs/core/commit/352bc88c1bd2fda09c61ab17ea1a5967ffcd7bc0)), closes [#12214](https://github.com/vuejs/core/issues/12214) [#12215](https://github.com/vuejs/core/issues/12215)
* **deps:** update dependency postcss to ^8.4.48 ([#12356](https://github.com/vuejs/core/issues/12356)) ([b5ff930](https://github.com/vuejs/core/commit/b5ff930089985a58c3553977ef999cec2a6708a4))
* **hydration:** the component vnode's el should be updated when a mismatch occurs. ([#12255](https://github.com/vuejs/core/issues/12255)) ([a20a4cb](https://github.com/vuejs/core/commit/a20a4cb36a3e717d1f8f259d0d59f133f508ff0a)), closes [#12253](https://github.com/vuejs/core/issues/12253)
* **reactivity:** avoid unnecessary watcher effect removal from inactive scope ([2193284](https://github.com/vuejs/core/commit/21932840eae72ffcd357a62ec596aaecc7ec224a)), closes [#5783](https://github.com/vuejs/core/issues/5783) [#5806](https://github.com/vuejs/core/issues/5806)
* **reactivity:** release nested effects/scopes on effect scope stop ([#12373](https://github.com/vuejs/core/issues/12373)) ([bee2f5e](https://github.com/vuejs/core/commit/bee2f5ee62dc0cd04123b737779550726374dd0a)), closes [#12370](https://github.com/vuejs/core/issues/12370)
* **runtime-dom:** set css vars before user onMounted hooks ([2d5c5e2](https://github.com/vuejs/core/commit/2d5c5e25e9b7a56e883674fb434135ac514429b5)), closes [#11533](https://github.com/vuejs/core/issues/11533)
* **runtime-dom:** set css vars on update to handle child forcing reflow in onMount ([#11561](https://github.com/vuejs/core/issues/11561)) ([c4312f9](https://github.com/vuejs/core/commit/c4312f9c715c131a09e552ba46e9beb4b36d55e6))
* **ssr:** avoid updating subtree of async component if it is resolved ([#12363](https://github.com/vuejs/core/issues/12363)) ([da7ad5e](https://github.com/vuejs/core/commit/da7ad5e3d24f3e108401188d909d27a4910da095)), closes [#12362](https://github.com/vuejs/core/issues/12362)
* **ssr:** ensure v-text updates correctly with custom directives in SSR output ([#12311](https://github.com/vuejs/core/issues/12311)) ([1f75d4e](https://github.com/vuejs/core/commit/1f75d4e6dfe18121ebe443cd3e8105d54f727893)), closes [#12309](https://github.com/vuejs/core/issues/12309)
* **ssr:** handle initial selected state for select with v-model + v-for option ([#12399](https://github.com/vuejs/core/issues/12399)) ([4f8d807](https://github.com/vuejs/core/commit/4f8d8078221ee52deed266677a227ad2a6d8dd22)), closes [#12395](https://github.com/vuejs/core/issues/12395)
* **teleport:** handle deferred teleport update before mounted ([#12168](https://github.com/vuejs/core/issues/12168)) ([8bff142](https://github.com/vuejs/core/commit/8bff142f99b646e9dd15897ec75368fbf34f1534)), closes [#12161](https://github.com/vuejs/core/issues/12161)
* **templateRef:** set ref on cached async component which wrapped in KeepAlive ([#12290](https://github.com/vuejs/core/issues/12290)) ([983eb50](https://github.com/vuejs/core/commit/983eb50a17eac76f1bba4394ad0316c62b72191d)), closes [#4999](https://github.com/vuejs/core/issues/4999) [#5004](https://github.com/vuejs/core/issues/5004)
* **test:** update snapshot ([#12169](https://github.com/vuejs/core/issues/12169)) ([828d4a4](https://github.com/vuejs/core/commit/828d4a443919fa2aa4e2e92fbd03a5f04b258eea))
* **Transition:** fix transition memory leak edge case ([#12182](https://github.com/vuejs/core/issues/12182)) ([660132d](https://github.com/vuejs/core/commit/660132df6c6a8c14bf75e593dc47d2fdada30322)), closes [#12181](https://github.com/vuejs/core/issues/12181)
* **transition:** reflow before leave-active class after leave-from ([#12288](https://github.com/vuejs/core/issues/12288)) ([4b479db](https://github.com/vuejs/core/commit/4b479db61d233b054561402ae94ef08550073ea1)), closes [#2593](https://github.com/vuejs/core/issues/2593)
* **types:** defineEmits w/ interface declaration ([#12343](https://github.com/vuejs/core/issues/12343)) ([1022eab](https://github.com/vuejs/core/commit/1022eabaa1aaf8436876f5ec5573cb1e4b3959a6)), closes [#8457](https://github.com/vuejs/core/issues/8457)
* **v-once:** setting hasOnce to current block only when in v-once ([#12374](https://github.com/vuejs/core/issues/12374)) ([37300fc](https://github.com/vuejs/core/commit/37300fc26190a7299efddbf98800ffd96d5cad96)), closes [#12371](https://github.com/vuejs/core/issues/12371)
### Performance Improvements
* **reactivity:** do not track inner key `__v_skip`` ([#11690](https://github.com/vuejs/core/issues/11690)) ([d637bd6](https://github.com/vuejs/core/commit/d637bd6c0164c2883e6eabd3c2f1f8c258dedfb1))
* **runtime-core:** use feature flag for call to resolveMergedOptions ([#12163](https://github.com/vuejs/core/issues/12163)) ([1755ac0](https://github.com/vuejs/core/commit/1755ac0a108ba3486bd8397e56d3bdcd69196594))
## [3.5.12](https://github.com/vuejs/core/compare/v3.5.11...v3.5.12) (2024-10-11) ## [3.5.12](https://github.com/vuejs/core/compare/v3.5.11...v3.5.12) (2024-10-11)

View File

@ -34,7 +34,8 @@ Please make sure to respect issue requirements and use [the new issue helper](ht
## Stay In Touch ## Stay In Touch
- [Twitter](https://twitter.com/vuejs) - [X](https://x.com/vuejs)
- [Bluesky](https://bsky.app/profile/vuejs.org)
- [Blog](https://blog.vuejs.org/) - [Blog](https://blog.vuejs.org/)
- [Job Board](https://vuejobs.com/?ref=vuejs) - [Job Board](https://vuejobs.com/?ref=vuejs)
@ -44,7 +45,9 @@ Please make sure to read the [Contributing Guide](https://github.com/vuejs/core/
Thank you to all the people who already contributed to Vue! Thank you to all the people who already contributed to Vue!
<a href="https://github.com/vuejs/core/graphs/contributors"><img src="https://opencollective.com/vuejs/contributors.svg?width=890" /></a> <a href="https://github.com/vuejs/core/graphs/contributors"><img src="https://opencollective.com/vuejs/contributors.svg?width=890&limit=500" /></a>
<sub>_Note: Showing the first 500 contributors only due to GitHub image size limitations_</sub>
## License ## License

View File

@ -741,17 +741,6 @@ Note that this is a type-only breaking change in a minor release, which adheres
## [3.3.13](https://github.com/vuejs/core/compare/v3.3.12...v3.3.13) (2023-12-19)
### Bug Fixes
* **compiler-core:** fix v-on with modifiers on inline expression of undefined ([#9866](https://github.com/vuejs/core/issues/9866)) ([bae79dd](https://github.com/vuejs/core/commit/bae79ddf8564a2da4a5365cfeb8d811990f42335)), closes [#9865](https://github.com/vuejs/core/issues/9865)
* **runtime-dom:** cache event handlers by key/modifiers ([#9851](https://github.com/vuejs/core/issues/9851)) ([04d2c05](https://github.com/vuejs/core/commit/04d2c05054c26b02fbc1d84839b0ed5cd36455b6)), closes [#9849](https://github.com/vuejs/core/issues/9849)
* **types:** extract properties from extended collections ([#9854](https://github.com/vuejs/core/issues/9854)) ([24b1c1d](https://github.com/vuejs/core/commit/24b1c1dd57fd55d998aa231a147500e010b10219)), closes [#9852](https://github.com/vuejs/core/issues/9852)
# [3.4.0-beta.3](https://github.com/vuejs/core/compare/v3.3.12...v3.4.0-beta.3) (2023-12-16) # [3.4.0-beta.3](https://github.com/vuejs/core/compare/v3.3.12...v3.4.0-beta.3) (2023-12-16)
@ -764,19 +753,6 @@ Note that this is a type-only breaking change in a minor release, which adheres
## [3.3.12](https://github.com/vuejs/core/compare/v3.3.11...v3.3.12) (2023-12-16)
### Bug Fixes
* **hydration:** handle appear transition before patch props ([#9837](https://github.com/vuejs/core/issues/9837)) ([e70f4c4](https://github.com/vuejs/core/commit/e70f4c47c553b6e16d8fad70743271ca23802fe7)), closes [#9832](https://github.com/vuejs/core/issues/9832)
* **sfc/cssVars:** fix loss of CSS v-bind variables when setting inline style with string value ([#9824](https://github.com/vuejs/core/issues/9824)) ([0a387df](https://github.com/vuejs/core/commit/0a387dfb1d04afb6eae4296b6da76dfdaca77af4)), closes [#9821](https://github.com/vuejs/core/issues/9821)
* **ssr:** fix suspense hydration of fallback content ([#7188](https://github.com/vuejs/core/issues/7188)) ([60415b5](https://github.com/vuejs/core/commit/60415b5d67df55f1fd6b176615299c08640fa142))
* **types:** add `xmlns:xlink` to `SVGAttributes` ([#9300](https://github.com/vuejs/core/issues/9300)) ([0d61b42](https://github.com/vuejs/core/commit/0d61b429ecf63591d31e09702058fa4c7132e1a7)), closes [#9299](https://github.com/vuejs/core/issues/9299)
* **types:** fix `shallowRef` type error ([#9839](https://github.com/vuejs/core/issues/9839)) ([9a57158](https://github.com/vuejs/core/commit/9a571582b53220270e498d8712ea59312c0bef3a))
* **types:** support for generic keyof slots ([#8374](https://github.com/vuejs/core/issues/8374)) ([213eba4](https://github.com/vuejs/core/commit/213eba479ce080efc1053fe636f6be4a4c889b44))
# [3.4.0-beta.2](https://github.com/vuejs/core/compare/v3.4.0-beta.1...v3.4.0-beta.2) (2023-12-14) # [3.4.0-beta.2](https://github.com/vuejs/core/compare/v3.4.0-beta.1...v3.4.0-beta.2) (2023-12-14)
@ -836,22 +812,6 @@ default.
## [3.3.11](https://github.com/vuejs/core/compare/v3.3.10...v3.3.11) (2023-12-08)
### Bug Fixes
* **custom-element:** correctly handle number type props in prod ([#8989](https://github.com/vuejs/core/issues/8989)) ([d74d364](https://github.com/vuejs/core/commit/d74d364d62db8e48881af6b5a75ce4fb5f36cc35))
* **reactivity:** fix mutation on user proxy of reactive Array ([6ecbd5c](https://github.com/vuejs/core/commit/6ecbd5ce2a7f59314a8326a1d193874b87f4d8c8)), closes [#9742](https://github.com/vuejs/core/issues/9742) [#9751](https://github.com/vuejs/core/issues/9751) [#9750](https://github.com/vuejs/core/issues/9750)
* **runtime-dom:** fix width and height prop check condition ([5b00286](https://github.com/vuejs/core/commit/5b002869c533220706f9788b496b8ca8d8e98609)), closes [#9762](https://github.com/vuejs/core/issues/9762)
* **shared:** handle Map with symbol keys in toDisplayString ([#9731](https://github.com/vuejs/core/issues/9731)) ([364821d](https://github.com/vuejs/core/commit/364821d6bdb1775e2f55a69bcfb9f40f7acf1506)), closes [#9727](https://github.com/vuejs/core/issues/9727)
* **shared:** handle more Symbol cases in toDisplayString ([983d45d](https://github.com/vuejs/core/commit/983d45d4f8eb766b5a16b7ea93b86d3c51618fa6))
* **Suspense:** properly get anchor when mount fallback vnode ([#9770](https://github.com/vuejs/core/issues/9770)) ([b700328](https://github.com/vuejs/core/commit/b700328342e17dc16b19316c2e134a26107139d2)), closes [#9769](https://github.com/vuejs/core/issues/9769)
* **types:** ref() return type should not be any when initial value is any ([#9768](https://github.com/vuejs/core/issues/9768)) ([cdac121](https://github.com/vuejs/core/commit/cdac12161ec27b45ded48854c3d749664b6d4a6d))
* **watch:** should not fire pre watcher on child component unmount ([#7181](https://github.com/vuejs/core/issues/7181)) ([6784f0b](https://github.com/vuejs/core/commit/6784f0b1f8501746ea70d87d18ed63a62cf6b76d)), closes [#7030](https://github.com/vuejs/core/issues/7030)
# [3.4.0-alpha.4](https://github.com/vuejs/core/compare/v3.3.10...v3.4.0-alpha.4) (2023-12-04) # [3.4.0-alpha.4](https://github.com/vuejs/core/compare/v3.3.10...v3.4.0-alpha.4) (2023-12-04)
@ -873,37 +833,6 @@ default.
## [3.3.10](https://github.com/vuejs/core/compare/v3.3.9...v3.3.10) (2023-12-04)
### Bug Fixes
* **app:** prevent template from being cached between apps with different options ([#9724](https://github.com/vuejs/core/issues/9724)) ([ec71585](https://github.com/vuejs/core/commit/ec715854ca12520b2afc9e9b3981cbae05ae5206)), closes [#9618](https://github.com/vuejs/core/issues/9618)
* **compiler-sfc:** avoid passing forEach index to genMap ([f12db7f](https://github.com/vuejs/core/commit/f12db7fb564a534cef2e5805cc9f54afe5d72fbf))
* **compiler-sfc:** deindent pug/jade templates ([6345197](https://github.com/vuejs/core/commit/634519720a21fb5a6871454e1cadad7053a568b8)), closes [#3231](https://github.com/vuejs/core/issues/3231) [#3842](https://github.com/vuejs/core/issues/3842) [#7723](https://github.com/vuejs/core/issues/7723)
* **compiler-sfc:** fix :where and :is selector in scoped mode with multiple selectors ([#9735](https://github.com/vuejs/core/issues/9735)) ([c3e2c55](https://github.com/vuejs/core/commit/c3e2c556b532656b50b8ab5cd2d9eabc26622d63)), closes [#9707](https://github.com/vuejs/core/issues/9707)
* **compiler-sfc:** generate more treeshaking friendly code ([#9507](https://github.com/vuejs/core/issues/9507)) ([8d74ca0](https://github.com/vuejs/core/commit/8d74ca0e6fa2738ca6854b7e879ff59419f948c7)), closes [#9500](https://github.com/vuejs/core/issues/9500)
* **compiler-sfc:** support inferring generic types ([#8511](https://github.com/vuejs/core/issues/8511)) ([eb5e307](https://github.com/vuejs/core/commit/eb5e307c0be62002e62c4c800d0dfacb39b0d4ca)), closes [#8482](https://github.com/vuejs/core/issues/8482)
* **compiler-sfc:** support resolving components from props ([#8785](https://github.com/vuejs/core/issues/8785)) ([7cbcee3](https://github.com/vuejs/core/commit/7cbcee3d831241a8bd3588ae92d3f27e3641e25f))
* **compiler-sfc:** throw error when failing to load TS during type resolution ([#8883](https://github.com/vuejs/core/issues/8883)) ([4936d2e](https://github.com/vuejs/core/commit/4936d2e11a8d0ca3704bfe408548cb26bb3fd5e9))
* **cssVars:** cssVar names should be double-escaped when generating code for ssr ([#8824](https://github.com/vuejs/core/issues/8824)) ([5199a12](https://github.com/vuejs/core/commit/5199a12f8855cd06f24bf355708b5a2134f63176)), closes [#7823](https://github.com/vuejs/core/issues/7823)
* **deps:** update compiler to ^7.23.4 ([#9681](https://github.com/vuejs/core/issues/9681)) ([31f6ebc](https://github.com/vuejs/core/commit/31f6ebc4df84490ed29fb75e7bf4259200eb51f0))
* **runtime-core:** Suspense get anchor properly in Transition ([#9309](https://github.com/vuejs/core/issues/9309)) ([65f3fe2](https://github.com/vuejs/core/commit/65f3fe273127a8b68e1222fbb306d28d85f01757)), closes [#8105](https://github.com/vuejs/core/issues/8105)
* **runtime-dom:** set width/height with units as attribute ([#8781](https://github.com/vuejs/core/issues/8781)) ([bfc1838](https://github.com/vuejs/core/commit/bfc1838f31199de3f189198a3c234fa7bae91386))
* **ssr:** avoid computed being accidentally cached before server render ([#9688](https://github.com/vuejs/core/issues/9688)) ([30d5d93](https://github.com/vuejs/core/commit/30d5d93a92b2154406ec04f8aca6b217fa01177c)), closes [#5300](https://github.com/vuejs/core/issues/5300)
* **types:** expose emits as props in functional components ([#9234](https://github.com/vuejs/core/issues/9234)) ([887e54c](https://github.com/vuejs/core/commit/887e54c347ea9eac4c721b5e2288f054873d1d30))
* **types:** fix reactive collection types ([#8960](https://github.com/vuejs/core/issues/8960)) ([ad27473](https://github.com/vuejs/core/commit/ad274737015c36906d76f3189203093fa3a2e4e7)), closes [#8904](https://github.com/vuejs/core/issues/8904)
* **types:** improve return type withKeys and withModifiers ([#9734](https://github.com/vuejs/core/issues/9734)) ([43c3cfd](https://github.com/vuejs/core/commit/43c3cfdec5ae5d70fa2a21e857abc2d73f1a0d07))
### Performance Improvements
* optimize on* prop check ([38aaa8c](https://github.com/vuejs/core/commit/38aaa8c88648c54fe2616ad9c0961288092fcb44))
* **runtime-dom:** cache modifier wrapper functions ([da4a4fb](https://github.com/vuejs/core/commit/da4a4fb5e8eee3c6d31f24ebd79a9d0feca56cb2)), closes [#8882](https://github.com/vuejs/core/issues/8882)
* **v-on:** constant handlers with modifiers should not be treated as dynamic ([4d94ebf](https://github.com/vuejs/core/commit/4d94ebfe75174b340d2b794e699cad1add3600a9))
# [3.4.0-alpha.3](https://github.com/vuejs/core/compare/v3.4.0-alpha.2...v3.4.0-alpha.3) (2023-11-28) # [3.4.0-alpha.3](https://github.com/vuejs/core/compare/v3.4.0-alpha.2...v3.4.0-alpha.3) (2023-11-28)
@ -960,55 +889,6 @@ default.
## [3.3.9](https://github.com/vuejs/core/compare/v3.3.8...v3.3.9) (2023-11-25)
### Bug Fixes
* **compiler-core:** avoid rewriting scope variables in inline for loops ([#7245](https://github.com/vuejs/core/issues/7245)) ([a2d810e](https://github.com/vuejs/core/commit/a2d810eb40cef631f61991ca68b426ee9546aba0)), closes [#7238](https://github.com/vuejs/core/issues/7238)
* **compiler-core:** fix `resolveParserPlugins` decorators check ([#9566](https://github.com/vuejs/core/issues/9566)) ([9d0eba9](https://github.com/vuejs/core/commit/9d0eba916f3bf6fb5c03222400edae1a2db7444f)), closes [#9560](https://github.com/vuejs/core/issues/9560)
* **compiler-sfc:** consistently escape type-only prop names ([#8654](https://github.com/vuejs/core/issues/8654)) ([3e08d24](https://github.com/vuejs/core/commit/3e08d246dfd8523c54fb8e7a4a6fd5506ffb1bcc)), closes [#8635](https://github.com/vuejs/core/issues/8635) [#8910](https://github.com/vuejs/core/issues/8910) [vitejs/vite-plugin-vue#184](https://github.com/vitejs/vite-plugin-vue/issues/184)
* **compiler-sfc:** malformed filename on windows using path.posix.join() ([#9478](https://github.com/vuejs/core/issues/9478)) ([f18a174](https://github.com/vuejs/core/commit/f18a174979626b3429db93c5d5b7ae5448917c70)), closes [#8671](https://github.com/vuejs/core/issues/8671) [#9583](https://github.com/vuejs/core/issues/9583) [#9446](https://github.com/vuejs/core/issues/9446) [#9473](https://github.com/vuejs/core/issues/9473)
* **compiler-sfc:** support `:is` and `:where` selector in scoped css rewrite ([#8929](https://github.com/vuejs/core/issues/8929)) ([3227e50](https://github.com/vuejs/core/commit/3227e50b32105f8893f7dff2f29278c5b3a9f621))
* **compiler-sfc:** support resolve extends interface for defineEmits ([#8470](https://github.com/vuejs/core/issues/8470)) ([9e1b74b](https://github.com/vuejs/core/commit/9e1b74bcd5fa4151f5d1bc02c69fbbfa4762f577)), closes [#8465](https://github.com/vuejs/core/issues/8465)
* **hmr/transition:** fix kept-alive component inside transition disappearing after hmr ([#7126](https://github.com/vuejs/core/issues/7126)) ([d11e978](https://github.com/vuejs/core/commit/d11e978fc98dcc83526c167e603b8308f317f786)), closes [#7121](https://github.com/vuejs/core/issues/7121)
* **hydration:** force hydration for v-bind with .prop modifier ([364f319](https://github.com/vuejs/core/commit/364f319d214226770d97c98d8fcada80c9e8dde3)), closes [#7490](https://github.com/vuejs/core/issues/7490)
* **hydration:** properly hydrate indeterminate prop ([34b5a5d](https://github.com/vuejs/core/commit/34b5a5da4ae9c9faccac237acd7acc8e7e017571)), closes [#7476](https://github.com/vuejs/core/issues/7476)
* **reactivity:** clear method on readonly collections should return undefined ([#7316](https://github.com/vuejs/core/issues/7316)) ([657476d](https://github.com/vuejs/core/commit/657476dcdb964be4fbb1277c215c073f3275728e))
* **reactivity:** onCleanup also needs to be cleaned ([#8655](https://github.com/vuejs/core/issues/8655)) ([73fd810](https://github.com/vuejs/core/commit/73fd810eebdd383a2b4629f67736c4db1f428abd)), closes [#5151](https://github.com/vuejs/core/issues/5151) [#7695](https://github.com/vuejs/core/issues/7695)
* **ssr:** hydration `__vnode` missing for devtools ([#9328](https://github.com/vuejs/core/issues/9328)) ([5156ac5](https://github.com/vuejs/core/commit/5156ac5b38cfa80d3db26f2c9bf40cb22a7521cb))
* **types:** allow falsy value types in `StyleValue` ([#7954](https://github.com/vuejs/core/issues/7954)) ([17aa92b](https://github.com/vuejs/core/commit/17aa92b79b31d8bb8b5873ddc599420cb9806db8)), closes [#7955](https://github.com/vuejs/core/issues/7955)
* **types:** defineCustomElement using defineComponent return type with emits ([#7937](https://github.com/vuejs/core/issues/7937)) ([5d932a8](https://github.com/vuejs/core/commit/5d932a8e6d14343c9d7fc7c2ecb58ac618b2f938)), closes [#7782](https://github.com/vuejs/core/issues/7782)
* **types:** fix `unref` and `toValue` when input union type contains ComputedRef ([#8748](https://github.com/vuejs/core/issues/8748)) ([176d476](https://github.com/vuejs/core/commit/176d47671271b1abc21b1508e9a493c7efca6451)), closes [#8747](https://github.com/vuejs/core/issues/8747) [#8857](https://github.com/vuejs/core/issues/8857)
* **types:** fix instance type when props type is incompatible with setup returned type ([#7338](https://github.com/vuejs/core/issues/7338)) ([0e1e8f9](https://github.com/vuejs/core/commit/0e1e8f919e5a74cdaadf9c80ee135088b25e7fa3)), closes [#5885](https://github.com/vuejs/core/issues/5885)
* **types:** fix shallowRef return type with union value type ([#7853](https://github.com/vuejs/core/issues/7853)) ([7c44800](https://github.com/vuejs/core/commit/7c448000b0def910c2cfabfdf7ff20a3d6bc844f)), closes [#7852](https://github.com/vuejs/core/issues/7852)
* **types:** more precise types for class bindings ([#8012](https://github.com/vuejs/core/issues/8012)) ([46e3374](https://github.com/vuejs/core/commit/46e33744c890bd49482c5e5c5cdea44e00ec84d5))
* **types:** remove optional properties from defineProps return type ([#6421](https://github.com/vuejs/core/issues/6421)) ([94c049d](https://github.com/vuejs/core/commit/94c049d930d922069e38ea8700d7ff0970f71e61)), closes [#6420](https://github.com/vuejs/core/issues/6420)
* **types:** return type of withDefaults should be readonly ([#8601](https://github.com/vuejs/core/issues/8601)) ([f15debc](https://github.com/vuejs/core/commit/f15debc01acb22d23f5acee97e6f02db88cef11a))
* **types:** revert class type restrictions ([5d077c8](https://github.com/vuejs/core/commit/5d077c8754cc14f85d2d6d386df70cf8c0d93842)), closes [#8012](https://github.com/vuejs/core/issues/8012)
* **types:** update jsx type definitions ([#8607](https://github.com/vuejs/core/issues/8607)) ([58e2a94](https://github.com/vuejs/core/commit/58e2a94871ae06a909c5f8bad07fb401193e6a38))
* **types:** widen ClassValue type ([2424013](https://github.com/vuejs/core/commit/242401305944422d0c361b16101a4d18908927af))
* **v-model:** avoid overwriting number input with same value ([#7004](https://github.com/vuejs/core/issues/7004)) ([40f4b77](https://github.com/vuejs/core/commit/40f4b77bb570868cb6e47791078767797e465989)), closes [#7003](https://github.com/vuejs/core/issues/7003)
* **v-model:** unnecessary value binding error should apply to dynamic instead of static binding ([2859b65](https://github.com/vuejs/core/commit/2859b653c9a22460e60233cac10fe139e359b046)), closes [#3596](https://github.com/vuejs/core/issues/3596)
## [3.3.8](https://github.com/vuejs/core/compare/v3.3.7...v3.3.8) (2023-11-06)
### Bug Fixes
* **compile-sfc:** support `Error` type in `defineProps` ([#5955](https://github.com/vuejs/core/issues/5955)) ([a989345](https://github.com/vuejs/core/commit/a9893458ec519aae442e1b99e64e6d74685cd22c))
* **compiler-core:** known global should be shadowed by local variables in expression rewrite ([#9492](https://github.com/vuejs/core/issues/9492)) ([a75d1c5](https://github.com/vuejs/core/commit/a75d1c5c6242e91a73cc5ba01e6da620dea0b3d9)), closes [#9482](https://github.com/vuejs/core/issues/9482)
* **compiler-sfc:** fix dynamic directive arguments usage check for slots ([#9495](https://github.com/vuejs/core/issues/9495)) ([b39fa1f](https://github.com/vuejs/core/commit/b39fa1f8157647859331ce439c42ae016a49b415)), closes [#9493](https://github.com/vuejs/core/issues/9493)
* **deps:** update dependency @vue/repl to ^2.6.2 ([#9536](https://github.com/vuejs/core/issues/9536)) ([5cef325](https://github.com/vuejs/core/commit/5cef325f41e3b38657c72fa1a38dedeee1c7a60a))
* **deps:** update dependency @vue/repl to ^2.6.3 ([#9540](https://github.com/vuejs/core/issues/9540)) ([176d590](https://github.com/vuejs/core/commit/176d59058c9aecffe9da4d4311e98496684f06d4))
* **hydration:** fix tagName access error on comment/text node hydration mismatch ([dd8a0cf](https://github.com/vuejs/core/commit/dd8a0cf5dcde13d2cbd899262a0e07f16e14e489)), closes [#9531](https://github.com/vuejs/core/issues/9531)
* **types:** avoid exposing lru-cache types in generated dts ([462aeb3](https://github.com/vuejs/core/commit/462aeb3b600765e219ded2ee9a0ed1e74df61de0)), closes [#9521](https://github.com/vuejs/core/issues/9521)
* **warn:** avoid warning on empty children with Suspense ([#3962](https://github.com/vuejs/core/issues/3962)) ([405f345](https://github.com/vuejs/core/commit/405f34587a63a5f1e3d147b9848219ea98acc22d))
# [3.4.0-alpha.1](https://github.com/vuejs/core/compare/v3.3.7...v3.4.0-alpha.1) (2023-10-28) # [3.4.0-alpha.1](https://github.com/vuejs/core/compare/v3.3.7...v3.4.0-alpha.1) (2023-10-28)

View File

@ -1,3 +1,3 @@
[build.environment] [build.environment]
NODE_VERSION = "18" NODE_VERSION = "22"
NPM_FLAGS = "--version" # prevent Netlify npm install NPM_FLAGS = "--version" # prevent Netlify npm install

View File

@ -1,7 +1,7 @@
{ {
"private": true, "private": true,
"version": "3.5.12", "version": "3.5.14",
"packageManager": "pnpm@9.12.2", "packageManager": "pnpm@10.11.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "node scripts/dev.js", "dev": "node scripts/dev.js",
@ -22,7 +22,10 @@
"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",
"test-bench": "vitest bench", "prebench": "node scripts/build.js -pf esm-browser reactivity",
"prebench-compare": "node scripts/build.js -pf esm-browser reactivity",
"bench": "vitest bench --project=unit --outputJson=temp/bench.json",
"bench-compare": "vitest bench --project=unit --compare=temp/bench.json",
"release": "node scripts/release.js", "release": "node scripts/release.js",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"dev-esm": "node scripts/dev.js -if esm-bundler-runtime", "dev-esm": "node scripts/dev.js -if esm-bundler-runtime",
@ -62,62 +65,51 @@
"@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.1", "@rollup/plugin-commonjs": "^28.0.3",
"@rollup/plugin-json": "^6.1.0", "@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.3.0", "@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-replace": "5.0.4", "@rollup/plugin-replace": "5.0.4",
"@swc/core": "^1.7.36", "@swc/core": "^1.11.24",
"@types/hash-sum": "^1.0.2", "@types/hash-sum": "^1.0.2",
"@types/node": "^20.16.13", "@types/node": "^22.15.21",
"@types/semver": "^7.5.8", "@types/semver": "^7.7.0",
"@types/serve-handler": "^6.1.4", "@types/serve-handler": "^6.1.4",
"@vitest/coverage-v8": "^2.1.1", "@vitest/coverage-v8": "^3.1.4",
"@vitest/eslint-plugin": "^1.2.0",
"@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.24.0", "esbuild": "^0.25.4",
"esbuild-plugin-polyfill-node": "^0.3.0", "esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^9.13.0", "eslint": "^9.27.0",
"eslint-plugin-import-x": "^4.3.1", "eslint-plugin-import-x": "^4.12.2",
"@vitest/eslint-plugin": "^1.0.1",
"estree-walker": "catalog:", "estree-walker": "catalog:",
"jsdom": "^25.0.0", "jsdom": "^26.1.0",
"lint-staged": "^15.2.10", "lint-staged": "^15.5.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"magic-string": "^0.30.12", "magic-string": "^0.30.17",
"markdown-table": "^3.0.3", "markdown-table": "^3.0.4",
"marked": "13.0.3", "marked": "13.0.3",
"npm-run-all2": "^6.2.6", "npm-run-all2": "^7.0.2",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"prettier": "^3.3.3", "prettier": "^3.5.3",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"pug": "^3.0.3", "pug": "^3.0.3",
"puppeteer": "~23.3.0", "puppeteer": "~24.9.0",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"rollup": "^4.24.0", "rollup": "^4.41.0",
"rollup-plugin-dts": "^6.1.1", "rollup-plugin-dts": "^6.2.1",
"rollup-plugin-esbuild": "^6.1.1", "rollup-plugin-esbuild": "^6.2.1",
"rollup-plugin-polyfill-node": "^0.13.0", "rollup-plugin-polyfill-node": "^0.13.0",
"semver": "^7.6.3", "semver": "^7.7.2",
"serve": "^14.2.4", "serve": "^14.2.4",
"serve-handler": "^6.1.6", "serve-handler": "^6.1.6",
"simple-git-hooks": "^2.11.1", "simple-git-hooks": "^2.13.0",
"todomvc-app-css": "^2.4.3", "todomvc-app-css": "^2.4.3",
"tslib": "^2.8.0", "tslib": "^2.8.1",
"typescript": "~5.6.2", "typescript": "~5.6.2",
"typescript-eslint": "^8.10.0", "typescript-eslint": "^8.32.1",
"vite": "catalog:", "vite": "catalog:",
"vitest": "^2.1.1" "vitest": "^3.1.4"
},
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {
"typescript-eslint>eslint": "^9.0.0",
"@typescript-eslint/eslint-plugin>eslint": "^9.0.0",
"@typescript-eslint/parser>eslint": "^9.0.0",
"@typescript-eslint/type-utils>eslint": "^9.0.0",
"@typescript-eslint/utils>eslint": "^9.0.0"
}
}
} }
} }

View File

@ -9,7 +9,7 @@ app.directive<HTMLElement, string, 'prevent' | 'stop', 'arg1' | 'arg2'>(
mounted(el, binding) { mounted(el, binding) {
expectType<HTMLElement>(el) expectType<HTMLElement>(el)
expectType<string>(binding.value) expectType<string>(binding.value)
expectType<{ prevent: boolean; stop: boolean }>(binding.modifiers) expectType<{ prevent?: boolean; stop?: boolean }>(binding.modifiers)
expectType<'arg1' | 'arg2'>(binding.arg!) expectType<'arg1' | 'arg2'>(binding.arg!)
// @ts-expect-error not any // @ts-expect-error not any

View File

@ -12,8 +12,11 @@ app.use(PluginWithoutType, 2)
app.use(PluginWithoutType, { anything: 'goes' }, true) app.use(PluginWithoutType, { anything: 'goes' }, true)
type PluginOptions = { type PluginOptions = {
/** option1 */
option1?: string option1?: string
/** option2 */
option2: number option2: number
/** option3 */
option3: boolean option3: boolean
} }
@ -25,6 +28,20 @@ const PluginWithObjectOptions = {
}, },
} }
const objectPluginOptional = {
install(app: App, options?: PluginOptions) {},
}
app.use(objectPluginOptional)
app.use(
objectPluginOptional,
// Test JSDoc and `go to definition` for options
{
option1: 'foo',
option2: 1,
option3: true,
},
)
for (const Plugin of [ for (const Plugin of [
PluginWithObjectOptions, PluginWithObjectOptions,
PluginWithObjectOptions.install, PluginWithObjectOptions.install,
@ -92,7 +109,27 @@ const PluginTyped: Plugin<PluginOptions> = (app, options) => {}
// @ts-expect-error: needs options // @ts-expect-error: needs options
app.use(PluginTyped) app.use(PluginTyped)
app.use(PluginTyped, { option2: 2, option3: true }) app.use(
PluginTyped,
// Test autocomplete for options
{
option1: '',
option2: 2,
option3: true,
},
)
const functionPluginOptional = (app: App, options?: PluginOptions) => {}
app.use(functionPluginOptional)
app.use(functionPluginOptional, { option2: 2, option3: true })
// type optional params
const functionPluginOptional2: Plugin<[options?: PluginOptions]> = (
app,
options,
) => {}
app.use(functionPluginOptional2)
app.use(functionPluginOptional2, { option2: 2, option3: true })
// vuetify usage // vuetify usage
const key: string = '' const key: string = ''

View File

@ -137,3 +137,18 @@ describe('Generic component', () => {
expectType<string | number>(comp.msg) expectType<string | number>(comp.msg)
expectType<Array<string | number>>(comp.list) expectType<Array<string | number>>(comp.list)
}) })
// #12751
{
const Comp = defineComponent({
__typeEmits: {} as {
'update:visible': [value?: boolean]
},
})
const comp: ComponentInstance<typeof Comp> = {} as any
expectType<((value?: boolean) => any) | undefined>(comp['onUpdate:visible'])
expectType<{ 'onUpdate:visible'?: (value?: boolean) => any }>(comp['$props'])
// @ts-expect-error
comp['$props']['$props']
}

View File

@ -20,6 +20,9 @@ import { type IsAny, type IsUnion, describe, expectType } from './utils'
describe('with object props', () => { describe('with object props', () => {
interface ExpectedProps { interface ExpectedProps {
a?: number | undefined a?: number | undefined
aa: number
aaa: number | null
aaaa: number | undefined
b: string b: string
e?: Function e?: Function
h: boolean h: boolean
@ -53,6 +56,19 @@ describe('with object props', () => {
const props = { const props = {
a: Number, a: Number,
aa: {
type: Number as PropType<number | undefined>,
default: 1,
},
aaa: {
type: Number as PropType<number | null>,
default: 1,
},
aaaa: {
type: Number as PropType<number | undefined>,
// `as const` prevents widening to `boolean` (keeps literal `true` type)
required: true as const,
},
// required should make property non-void // required should make property non-void
b: { b: {
type: String, type: String,
@ -146,6 +162,13 @@ describe('with object props', () => {
setup(props) { setup(props) {
// type assertion. See https://github.com/SamVerschueren/tsd // type assertion. See https://github.com/SamVerschueren/tsd
expectType<ExpectedProps['a']>(props.a) expectType<ExpectedProps['a']>(props.a)
expectType<ExpectedProps['aa']>(props.aa)
expectType<ExpectedProps['aaa']>(props.aaa)
// @ts-expect-error should included `undefined`
expectType<number>(props.aaaa)
expectType<ExpectedProps['aaaa']>(props.aaaa)
expectType<ExpectedProps['b']>(props.b) expectType<ExpectedProps['b']>(props.b)
expectType<ExpectedProps['e']>(props.e) expectType<ExpectedProps['e']>(props.e)
expectType<ExpectedProps['h']>(props.h) expectType<ExpectedProps['h']>(props.h)
@ -198,6 +221,8 @@ describe('with object props', () => {
render() { render() {
const props = this.$props const props = this.$props
expectType<ExpectedProps['a']>(props.a) expectType<ExpectedProps['a']>(props.a)
expectType<ExpectedProps['aa']>(props.aa)
expectType<ExpectedProps['aaa']>(props.aaa)
expectType<ExpectedProps['b']>(props.b) expectType<ExpectedProps['b']>(props.b)
expectType<ExpectedProps['e']>(props.e) expectType<ExpectedProps['e']>(props.e)
expectType<ExpectedProps['h']>(props.h) expectType<ExpectedProps['h']>(props.h)
@ -225,6 +250,8 @@ describe('with object props', () => {
// should also expose declared props on `this` // should also expose declared props on `this`
expectType<ExpectedProps['a']>(this.a) expectType<ExpectedProps['a']>(this.a)
expectType<ExpectedProps['aa']>(this.aa)
expectType<ExpectedProps['aaa']>(this.aaa)
expectType<ExpectedProps['b']>(this.b) expectType<ExpectedProps['b']>(this.b)
expectType<ExpectedProps['e']>(this.e) expectType<ExpectedProps['e']>(this.e)
expectType<ExpectedProps['h']>(this.h) expectType<ExpectedProps['h']>(this.h)
@ -269,6 +296,7 @@ describe('with object props', () => {
expectType<JSX.Element>( expectType<JSX.Element>(
<MyComponent <MyComponent
a={1} a={1}
aaaa={1}
b="b" b="b"
bb="bb" bb="bb"
e={() => {}} e={() => {}}
@ -295,6 +323,7 @@ describe('with object props', () => {
expectType<Component>( expectType<Component>(
<MyComponent <MyComponent
aaaa={1}
b="b" b="b"
dd={{ n: 1 }} dd={{ n: 1 }}
ddd={['ddd']} ddd={['ddd']}

View File

@ -29,7 +29,7 @@ describe('custom', () => {
value: number value: number
oldValue: number | null oldValue: number | null
arg?: 'Arg' arg?: 'Arg'
modifiers: Record<'a' | 'b', boolean> modifiers: Partial<Record<'a' | 'b', boolean>>
}>(testDirective<number, 'a' | 'b', 'Arg'>()) }>(testDirective<number, 'a' | 'b', 'Arg'>())
expectType<{ expectType<{

View File

@ -4,6 +4,7 @@ import {
type MaybeRefOrGetter, type MaybeRefOrGetter,
type Ref, type Ref,
type ShallowRef, type ShallowRef,
type TemplateRef,
type ToRefs, type ToRefs,
type WritableComputedRef, type WritableComputedRef,
computed, computed,
@ -535,7 +536,7 @@ expectType<string>(toValue(unref2))
// useTemplateRef // useTemplateRef
const tRef = useTemplateRef('foo') const tRef = useTemplateRef('foo')
expectType<Readonly<ShallowRef<unknown>>>(tRef) expectType<TemplateRef>(tRef)
const tRef2 = useTemplateRef<HTMLElement>('bar') const tRef2 = useTemplateRef<HTMLElement>('bar')
expectType<Readonly<ShallowRef<HTMLElement | null>>>(tRef2) expectType<TemplateRef<HTMLElement>>(tRef2)

View File

@ -306,6 +306,14 @@ describe('defineEmits w/ type declaration', () => {
emit2('baz') emit2('baz')
}) })
describe('defineEmits w/ interface declaration', () => {
interface Emits {
foo: [value: string]
}
const emit = defineEmits<Emits>()
emit('foo', 'hi')
})
describe('defineEmits w/ alt type declaration', () => { describe('defineEmits w/ alt type declaration', () => {
const emit = defineEmits<{ const emit = defineEmits<{
foo: [id: string] foo: [id: string]

View File

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

View File

@ -165,8 +165,9 @@ onMounted(() => {
body { body {
font-size: 13px; font-size: 13px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, font-family:
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
margin: 0; margin: 0;
--base: #444; --base: #444;
--nav-height: 50px; --nav-height: 50px;

View File

@ -46,6 +46,7 @@ function resetVueVersion() {
async function copyLink(e: MouseEvent) { async function copyLink(e: MouseEvent) {
if (e.metaKey) { if (e.metaKey) {
resetVueVersion()
// hidden logic for going to local debug from play.vuejs.org // hidden logic for going to local debug from play.vuejs.org
window.location.href = 'http://localhost:5173/' + window.location.hash window.location.href = 'http://localhost:5173/' + window.location.hash
return return

View File

@ -17,7 +17,10 @@ export async function downloadProject(store: ReplStore) {
// basic structure // basic structure
zip.file('index.html', index) zip.file('index.html', index)
zip.file('package.json', pkg) zip.file(
'package.json',
pkg.replace(`"vue": "latest"`, `"vue": "${store.vueVersion || 'latest'}"`),
)
zip.file('vite.config.js', config) zip.file('vite.config.js', config)
zip.file('README.md', readme) zip.file('README.md', readme)

View File

@ -8,10 +8,10 @@
"serve": "vite preview" "serve": "vite preview"
}, },
"dependencies": { "dependencies": {
"vue": "^3.4.0" "vue": "latest"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.1.4", "@vitejs/plugin-vue": "^5.2.4",
"vite": "^5.4.9" "vite": "^6.3.5"
} }
} }

View File

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

View File

@ -1,8 +1,9 @@
body { body {
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, font-family:
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
--bg: #1d1f21; --bg: #1d1f21;
--border: #333; --border: #333;
} }

View File

@ -1,5 +1,23 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`compiler: v-memo transform > element v-for key expression prefixing + v-memo 1`] = `
"import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, isMemoSame as _isMemoSame, withMemo as _withMemo } from "vue"
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.tableData, (data, __, ___, _cached) => {
const _memo = (_ctx.getLetter(data))
if (_cached && _cached.key === _ctx.getId(data) && _isMemoSame(_cached, _memo)) return _cached
const _item = (_openBlock(), _createElementBlock("span", {
key: _ctx.getId(data)
}))
_item.memo = _memo
return _item
}, _cache, 0), 128 /* KEYED_FRAGMENT */))
]))
}"
`;
exports[`compiler: v-memo transform > on component 1`] = ` exports[`compiler: v-memo transform > on component 1`] = `
"import { resolveComponent as _resolveComponent, createVNode as _createVNode, withMemo as _withMemo, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" "import { resolveComponent as _resolveComponent, createVNode as _createVNode, withMemo as _withMemo, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

View File

@ -8,7 +8,7 @@ return function render(_ctx, _cache) {
const { setBlockTracking: _setBlockTracking, createElementVNode: _createElementVNode } = _Vue const { setBlockTracking: _setBlockTracking, createElementVNode: _createElementVNode } = _Vue
return _cache[0] || ( return _cache[0] || (
_setBlockTracking(-1), _setBlockTracking(-1, true),
(_cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0, (_cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0,
_setBlockTracking(1), _setBlockTracking(1),
_cache[0] _cache[0]
@ -28,7 +28,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [ return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || ( _cache[0] || (
_setBlockTracking(-1), _setBlockTracking(-1, true),
(_cache[0] = _createVNode(_component_Comp, { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0, (_cache[0] = _createVNode(_component_Comp, { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0,
_setBlockTracking(1), _setBlockTracking(1),
_cache[0] _cache[0]
@ -47,7 +47,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [ return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || ( _cache[0] || (
_setBlockTracking(-1), _setBlockTracking(-1, true),
(_cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0, (_cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0,
_setBlockTracking(1), _setBlockTracking(1),
_cache[0] _cache[0]
@ -66,7 +66,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [ return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || ( _cache[0] || (
_setBlockTracking(-1), _setBlockTracking(-1, true),
(_cache[0] = _renderSlot($slots, "default")).cacheIndex = 0, (_cache[0] = _renderSlot($slots, "default")).cacheIndex = 0,
_setBlockTracking(1), _setBlockTracking(1),
_cache[0] _cache[0]
@ -85,7 +85,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [ return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || ( _cache[0] || (
_setBlockTracking(-1), _setBlockTracking(-1, true),
(_cache[0] = _createElementVNode("div")).cacheIndex = 0, (_cache[0] = _createElementVNode("div")).cacheIndex = 0,
_setBlockTracking(1), _setBlockTracking(1),
_cache[0] _cache[0]

View File

@ -170,6 +170,11 @@ describe('compiler: cacheStatic transform', () => {
{ {
/* _ slot flag */ /* _ slot flag */
}, },
{
type: NodeTypes.JS_PROPERTY,
key: { content: '__' },
value: { content: '[0]' },
},
], ],
}) })
}) })
@ -197,6 +202,11 @@ describe('compiler: cacheStatic transform', () => {
{ {
/* _ slot flag */ /* _ slot flag */
}, },
{
type: NodeTypes.JS_PROPERTY,
key: { content: '__' },
value: { content: '[0]' },
},
], ],
}) })
}) })

View File

@ -53,4 +53,12 @@ describe('compiler: v-memo transform', () => {
), ),
).toMatchSnapshot() ).toMatchSnapshot()
}) })
test('element v-for key expression prefixing + v-memo', () => {
expect(
compile(
`<span v-for="data of tableData" :key="getId(data)" v-memo="getLetter(data)"></span>`,
),
).toMatchSnapshot()
})
}) })

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compiler-core", "name": "@vue/compiler-core",
"version": "3.5.12", "version": "3.5.14",
"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

@ -418,6 +418,7 @@ export interface CacheExpression extends Node {
index: number index: number
value: JSChildNode value: JSChildNode
needPauseTracking: boolean needPauseTracking: boolean
inVOnce: boolean
needArraySpread: boolean needArraySpread: boolean
} }
@ -774,12 +775,14 @@ export function createCacheExpression(
index: number, index: number,
value: JSChildNode, value: JSChildNode,
needPauseTracking: boolean = false, needPauseTracking: boolean = false,
inVOnce: boolean = false,
): CacheExpression { ): CacheExpression {
return { return {
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index, index,
value, value,
needPauseTracking: needPauseTracking, needPauseTracking: needPauseTracking,
inVOnce,
needArraySpread: false, needArraySpread: false,
loc: locStub, loc: locStub,
} }

View File

@ -188,7 +188,9 @@ function createCodegenContext(
name = content name = content
} }
} }
addMapping(node.loc.start, name) if (node.loc.source) {
addMapping(node.loc.start, name)
}
} }
if (newlineIndex === NewlineType.Unknown) { if (newlineIndex === NewlineType.Unknown) {
// multiple newlines, full iteration // multiple newlines, full iteration
@ -225,7 +227,7 @@ function createCodegenContext(
context.column = code.length - newlineIndex context.column = code.length - newlineIndex
} }
} }
if (node && node.loc !== locStub) { if (node && node.loc !== locStub && node.loc.source) {
addMapping(node.loc.end) addMapping(node.loc.end)
} }
} }
@ -1017,7 +1019,9 @@ function genCacheExpression(node: CacheExpression, context: CodegenContext) {
push(`_cache[${node.index}] || (`) push(`_cache[${node.index}] || (`)
if (needPauseTracking) { if (needPauseTracking) {
indent() indent()
push(`${helper(SET_BLOCK_TRACKING)}(-1),`) push(`${helper(SET_BLOCK_TRACKING)}(-1`)
if (node.inVOnce) push(`, true`)
push(`),`)
newline() newline()
push(`(`) push(`(`)
} }

View File

@ -388,7 +388,7 @@ const tokenizer = new Tokenizer(stack, {
CompilerDeprecationTypes.COMPILER_V_BIND_SYNC, CompilerDeprecationTypes.COMPILER_V_BIND_SYNC,
currentOptions, currentOptions,
currentProp.loc, currentProp.loc,
currentProp.rawName, currentProp.arg!.loc.source,
) )
) { ) {
currentProp.name = 'model' currentProp.name = 'model'

View File

@ -116,7 +116,7 @@ export interface TransformContext
addIdentifiers(exp: ExpressionNode | string): void addIdentifiers(exp: ExpressionNode | string): void
removeIdentifiers(exp: ExpressionNode | string): void removeIdentifiers(exp: ExpressionNode | string): void
hoist(exp: string | JSChildNode | ArrayExpression): SimpleExpressionNode hoist(exp: string | JSChildNode | ArrayExpression): SimpleExpressionNode
cache(exp: JSChildNode, isVNode?: boolean): CacheExpression cache(exp: JSChildNode, isVNode?: boolean, inVOnce?: boolean): CacheExpression
constantCache: WeakMap<TemplateChildNode, ConstantTypes> constantCache: WeakMap<TemplateChildNode, ConstantTypes>
// 2.x Compat only // 2.x Compat only
@ -297,11 +297,12 @@ export function createTransformContext(
identifier.hoisted = exp identifier.hoisted = exp
return identifier return identifier
}, },
cache(exp, isVNode = false) { cache(exp, isVNode = false, inVOnce = false) {
const cacheExp = createCacheExpression( const cacheExp = createCacheExpression(
context.cached.length, context.cached.length,
exp, exp,
isVNode, isVNode,
inVOnce,
) )
context.cached.push(cacheExp) context.cached.push(cacheExp)
return cacheExp return cacheExp

View File

@ -12,11 +12,14 @@ 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'
@ -140,6 +143,7 @@ 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 &&
@ -163,6 +167,7 @@ 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[]),
) )
@ -186,6 +191,7 @@ 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[]),
) )
@ -196,10 +202,31 @@ 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

View File

@ -594,11 +594,9 @@ export function buildProps(
hasDynamicKeys = true hasDynamicKeys = true
if (exp) { if (exp) {
if (isVBind) { if (isVBind) {
// #10696 in case a v-bind object contains ref
pushRefVForMarker()
// have to merge early for compat build check
pushMergeArg()
if (__COMPAT__) { if (__COMPAT__) {
// have to merge early for compat build check
pushMergeArg()
// 2.x v-bind object order compat // 2.x v-bind object order compat
if (__DEV__) { if (__DEV__) {
const hasOverridableKeys = mergeArgs.some(arg => { const hasOverridableKeys = mergeArgs.some(arg => {
@ -641,6 +639,9 @@ export function buildProps(
} }
} }
// #10696 in case a v-bind object contains ref
pushRefVForMarker()
pushMergeArg()
mergeArgs.push(exp) mergeArgs.push(exp)
} else { } else {
// v-on="obj" -> toHandlers(obj) // v-on="obj" -> toHandlers(obj)

View File

@ -24,7 +24,7 @@ import {
isStaticPropertyKey, isStaticPropertyKey,
walkIdentifiers, walkIdentifiers,
} from '../babelUtils' } from '../babelUtils'
import { advancePositionWithClone, isSimpleIdentifier } from '../utils' import { advancePositionWithClone, findDir, isSimpleIdentifier } from '../utils'
import { import {
genPropsAccessExp, genPropsAccessExp,
hasOwn, hasOwn,
@ -54,6 +54,7 @@ export const transformExpression: NodeTransform = (node, context) => {
) )
} else if (node.type === NodeTypes.ELEMENT) { } else if (node.type === NodeTypes.ELEMENT) {
// handle directives on element // handle directives on element
const memo = findDir(node, 'memo')
for (let i = 0; i < node.props.length; i++) { for (let i = 0; i < node.props.length; i++) {
const dir = node.props[i] const dir = node.props[i]
// do not process for v-on & v-for since they are special handled // do not process for v-on & v-for since they are special handled
@ -65,7 +66,14 @@ export const transformExpression: NodeTransform = (node, context) => {
if ( if (
exp && exp &&
exp.type === NodeTypes.SIMPLE_EXPRESSION && exp.type === NodeTypes.SIMPLE_EXPRESSION &&
!(dir.name === 'on' && arg) !(dir.name === 'on' && arg) &&
// key has been processed in transformFor(vMemo + vFor)
!(
memo &&
arg &&
arg.type === NodeTypes.SIMPLE_EXPRESSION &&
arg.content === 'key'
)
) { ) {
dir.exp = processExpression( dir.exp = processExpression(
exp, exp,

View File

@ -12,7 +12,7 @@ import { camelize } from '@vue/shared'
import { CAMELIZE } from '../runtimeHelpers' import { CAMELIZE } from '../runtimeHelpers'
import { processExpression } from './transformExpression' import { processExpression } from './transformExpression'
// v-bind without arg is handled directly in ./transformElements.ts due to it affecting // v-bind without arg is handled directly in ./transformElement.ts due to its affecting
// codegen for the entire props object. This transform here is only for v-bind // codegen for the entire props object. This transform here is only for v-bind
// *with* args. // *with* args.
export const transformBind: DirectiveTransform = (dir, _node, context) => { export const transformBind: DirectiveTransform = (dir, _node, context) => {

View File

@ -63,17 +63,27 @@ export const transformFor: NodeTransform = createStructuralDirectiveTransform(
const isTemplate = isTemplateNode(node) const isTemplate = isTemplateNode(node)
const memo = findDir(node, 'memo') const memo = findDir(node, 'memo')
const keyProp = findProp(node, `key`, false, true) const keyProp = findProp(node, `key`, false, true)
if (keyProp && keyProp.type === NodeTypes.DIRECTIVE && !keyProp.exp) { const isDirKey = keyProp && keyProp.type === NodeTypes.DIRECTIVE
if (isDirKey && !keyProp.exp) {
// resolve :key shorthand #10882 // resolve :key shorthand #10882
transformBindShorthand(keyProp, context) transformBindShorthand(keyProp, context)
} }
const keyExp = let keyExp =
keyProp && keyProp &&
(keyProp.type === NodeTypes.ATTRIBUTE (keyProp.type === NodeTypes.ATTRIBUTE
? keyProp.value ? keyProp.value
? createSimpleExpression(keyProp.value.content, true) ? createSimpleExpression(keyProp.value.content, true)
: undefined : undefined
: keyProp.exp) : keyProp.exp)
if (memo && keyExp && isDirKey) {
if (!__BROWSER__) {
keyProp.exp = keyExp = processExpression(
keyExp as SimpleExpressionNode,
context,
)
}
}
const keyProperty = const keyProperty =
keyProp && keyExp ? createObjectProperty(`key`, keyExp) : null keyProp && keyExp ? createObjectProperty(`key`, keyExp) : null

View File

@ -17,7 +17,7 @@ import { hasScopeRef, isFnExpression, isMemberExpression } from '../utils'
import { TO_HANDLER_KEY } from '../runtimeHelpers' import { TO_HANDLER_KEY } from '../runtimeHelpers'
export interface VOnDirectiveNode extends DirectiveNode { export interface VOnDirectiveNode extends DirectiveNode {
// v-on without arg is handled directly in ./transformElements.ts due to it affecting // v-on without arg is handled directly in ./transformElement.ts due to its affecting
// codegen for the entire props object. This transform here is only for v-on // codegen for the entire props object. This transform here is only for v-on
// *with* args. // *with* args.
arg: ExpressionNode arg: ExpressionNode

View File

@ -17,7 +17,11 @@ export const transformOnce: NodeTransform = (node, context) => {
context.inVOnce = false context.inVOnce = false
const cur = context.currentNode as ElementNode | IfNode | ForNode const cur = context.currentNode as ElementNode | IfNode | ForNode
if (cur.codegenNode) { if (cur.codegenNode) {
cur.codegenNode = context.cache(cur.codegenNode, true /* isVNode */) cur.codegenNode = context.cache(
cur.codegenNode,
true /* isVNode */,
true /* inVOnce */,
)
} }
} }
} }

View File

@ -342,7 +342,6 @@ export function buildSlots(
: hasForwardedSlots(node.children) : hasForwardedSlots(node.children)
? SlotFlags.FORWARDED ? SlotFlags.FORWARDED
: SlotFlags.STABLE : SlotFlags.STABLE
let slots = createObjectExpression( let slots = createObjectExpression(
slotsProperties.concat( slotsProperties.concat(
createObjectProperty( createObjectProperty(

View File

@ -32,6 +32,16 @@ return function render(_ctx, _cache) {
}" }"
`; `;
exports[`stringify static html > serializing template string style 1`] = `
"const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createStaticVNode("<div style=\\"color:red;\\"><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span></div>", 1)
])))
}"
`;
exports[`stringify static html > should bail for <option> elements with null values 1`] = ` exports[`stringify static html > should bail for <option> elements with null values 1`] = `
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue "const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue

View File

@ -162,6 +162,27 @@ describe('stringify static html', () => {
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
// #12391
test('serializing template string style', () => {
const { ast, code } = compileWithStringify(
`<div><div :style="\`color:red;\`">${repeat(
`<span :class="[{ foo: true }, { bar: true }]">{{ 1 }} + {{ false }}</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div></div>`,
)
// should be optimized now
expect(ast.cached).toMatchObject([
cachedArrayStaticNodeMatcher(
`<div style="color:red;">${repeat(
`<span class="foo bar">1 + false</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div>`,
1,
),
])
expect(code).toMatchSnapshot()
})
test('escape', () => { test('escape', () => {
const { ast, code } = compileWithStringify( const { ast, code } = compileWithStringify(
`<div><div>${repeat( `<div><div>${repeat(

View File

@ -1,4 +1,5 @@
import { type CompilerError, compile } from '../../src' import { type CompilerError, compile } from '../../src'
import { isValidHTMLNesting } from '../../src/htmlNesting'
describe('validate html nesting', () => { describe('validate html nesting', () => {
it('should warn with p > div', () => { it('should warn with p > div', () => {
@ -17,4 +18,185 @@ describe('validate html nesting', () => {
}) })
expect(err).toBeUndefined() expect(err).toBeUndefined()
}) })
// #13318
it('should not warn when parent tag is template', () => {
let err: CompilerError | undefined
compile(`<template><tr/></template>`, {
onWarn: e => (err = e),
})
expect(err).toBeUndefined()
})
})
/**
* Copied from https://github.com/MananTank/validate-html-nesting
* with ISC license
*/
describe('isValidHTMLNesting', () => {
test('form', () => {
// invalid
expect(isValidHTMLNesting('form', 'form')).toBe(false)
// valid
expect(isValidHTMLNesting('form', 'div')).toBe(true)
expect(isValidHTMLNesting('form', 'input')).toBe(true)
expect(isValidHTMLNesting('form', 'select')).toBe(true)
expect(isValidHTMLNesting('form', 'button')).toBe(true)
expect(isValidHTMLNesting('form', 'label')).toBe(true)
expect(isValidHTMLNesting('form', 'h1')).toBe(true)
})
test('p', () => {
// invalid
expect(isValidHTMLNesting('p', 'p')).toBe(false)
expect(isValidHTMLNesting('p', 'div')).toBe(false)
expect(isValidHTMLNesting('p', 'hr')).toBe(false)
expect(isValidHTMLNesting('p', 'blockquote')).toBe(false)
expect(isValidHTMLNesting('p', 'pre')).toBe(false)
// valid
expect(isValidHTMLNesting('p', 'a')).toBe(true)
expect(isValidHTMLNesting('p', 'span')).toBe(true)
expect(isValidHTMLNesting('p', 'abbr')).toBe(true)
expect(isValidHTMLNesting('p', 'button')).toBe(true)
expect(isValidHTMLNesting('p', 'b')).toBe(true)
expect(isValidHTMLNesting('p', 'i')).toBe(true)
expect(isValidHTMLNesting('p', 'input')).toBe(true)
expect(isValidHTMLNesting('p', 'label')).toBe(true)
})
test('a', () => {
// invalid
expect(isValidHTMLNesting('a', 'a')).toBe(false)
// valid
expect(isValidHTMLNesting('a', 'div')).toBe(true)
expect(isValidHTMLNesting('a', 'span')).toBe(true)
})
test('button', () => {
// invalid
expect(isValidHTMLNesting('button', 'button')).toBe(false)
// valid
expect(isValidHTMLNesting('button', 'div')).toBe(true)
expect(isValidHTMLNesting('button', 'span')).toBe(true)
})
test('table', () => {
// invalid
expect(isValidHTMLNesting('table', 'tr')).toBe(false)
expect(isValidHTMLNesting('table', 'table')).toBe(false)
expect(isValidHTMLNesting('table', 'td')).toBe(false)
// valid
expect(isValidHTMLNesting('table', 'thead')).toBe(true)
expect(isValidHTMLNesting('table', 'tbody')).toBe(true)
expect(isValidHTMLNesting('table', 'tfoot')).toBe(true)
expect(isValidHTMLNesting('table', 'caption')).toBe(true)
expect(isValidHTMLNesting('table', 'colgroup')).toBe(true)
})
test('td', () => {
// valid
expect(isValidHTMLNesting('td', 'span')).toBe(true)
expect(isValidHTMLNesting('tr', 'td')).toBe(true)
// invalid
expect(isValidHTMLNesting('td', 'td')).toBe(false)
expect(isValidHTMLNesting('div', 'td')).toBe(false)
})
test('tbody', () => {
// invalid
expect(isValidHTMLNesting('tbody', 'td')).toBe(false)
// valid
expect(isValidHTMLNesting('tbody', 'tr')).toBe(true)
})
test('tr', () => {
// invalid
expect(isValidHTMLNesting('tr', 'tr')).toBe(false)
expect(isValidHTMLNesting('table', 'tr')).toBe(false)
// valid
expect(isValidHTMLNesting('tbody', 'tr')).toBe(true)
expect(isValidHTMLNesting('thead', 'tr')).toBe(true)
expect(isValidHTMLNesting('tfoot', 'tr')).toBe(true)
expect(isValidHTMLNesting('tr', 'td')).toBe(true)
expect(isValidHTMLNesting('tr', 'th')).toBe(true)
})
test('li', () => {
// invalid
expect(isValidHTMLNesting('li', 'li')).toBe(false)
// valid
expect(isValidHTMLNesting('li', 'div')).toBe(true)
expect(isValidHTMLNesting('li', 'ul')).toBe(true)
})
test('headings', () => {
// invalid
expect(isValidHTMLNesting('h1', 'h1')).toBe(false)
expect(isValidHTMLNesting('h2', 'h1')).toBe(false)
expect(isValidHTMLNesting('h3', 'h1')).toBe(false)
expect(isValidHTMLNesting('h1', 'h6')).toBe(false)
// valid
expect(isValidHTMLNesting('h1', 'div')).toBe(true)
})
describe('SVG', () => {
test('svg', () => {
// invalid non-svg tags as children
expect(isValidHTMLNesting('svg', 'div')).toBe(false)
expect(isValidHTMLNesting('svg', 'img')).toBe(false)
expect(isValidHTMLNesting('svg', 'p')).toBe(false)
expect(isValidHTMLNesting('svg', 'h2')).toBe(false)
expect(isValidHTMLNesting('svg', 'span')).toBe(false)
// valid non-svg tags as children
expect(isValidHTMLNesting('svg', 'a')).toBe(true)
expect(isValidHTMLNesting('svg', 'textarea')).toBe(true)
expect(isValidHTMLNesting('svg', 'input')).toBe(true)
expect(isValidHTMLNesting('svg', 'select')).toBe(true)
// valid svg tags as children
expect(isValidHTMLNesting('svg', 'g')).toBe(true)
expect(isValidHTMLNesting('svg', 'ellipse')).toBe(true)
expect(isValidHTMLNesting('svg', 'feOffset')).toBe(true)
})
test('foreignObject', () => {
// valid
expect(isValidHTMLNesting('foreignObject', 'g')).toBe(true)
expect(isValidHTMLNesting('foreignObject', 'div')).toBe(true)
expect(isValidHTMLNesting('foreignObject', 'a')).toBe(true)
expect(isValidHTMLNesting('foreignObject', 'textarea')).toBe(true)
})
test('g', () => {
// valid
expect(isValidHTMLNesting('g', 'div')).toBe(true)
expect(isValidHTMLNesting('g', 'p')).toBe(true)
expect(isValidHTMLNesting('g', 'a')).toBe(true)
expect(isValidHTMLNesting('g', 'textarea')).toBe(true)
expect(isValidHTMLNesting('g', 'g')).toBe(true)
})
test('dl', () => {
// valid
expect(isValidHTMLNesting('dl', 'dt')).toBe(true)
expect(isValidHTMLNesting('dl', 'dd')).toBe(true)
expect(isValidHTMLNesting('dl', 'div')).toBe(true)
expect(isValidHTMLNesting('div', 'dt')).toBe(true)
expect(isValidHTMLNesting('div', 'dd')).toBe(true)
// invalid
expect(isValidHTMLNesting('span', 'dt')).toBe(false)
expect(isValidHTMLNesting('span', 'dd')).toBe(false)
})
})
}) })

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compiler-dom", "name": "@vue/compiler-dom",
"version": "3.5.12", "version": "3.5.14",
"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

@ -11,6 +11,11 @@
* returns true if given parent-child nesting is valid HTML * returns true if given parent-child nesting is valid HTML
*/ */
export function isValidHTMLNesting(parent: string, child: string): boolean { export function isValidHTMLNesting(parent: string, child: string): boolean {
// if the parent is a template, it can have any child
if (parent === 'template') {
return true
}
// if we know the list of children that are the only valid children for the given parent // if we know the list of children that are the only valid children for the given parent
if (parent in onlyValidChildren) { if (parent in onlyValidChildren) {
return onlyValidChildren[parent].has(child) return onlyValidChildren[parent].has(child)

View File

@ -1,5 +1,11 @@
import { BindingTypes } from '@vue/compiler-core' import { BindingTypes } from '@vue/compiler-core'
import { assertCode, compileSFCScript as compile, mockId } from './utils' import {
assertCode,
compileSFCScript as compile,
getPositionInCode,
mockId,
} from './utils'
import { type RawSourceMap, SourceMapConsumer } from 'source-map-js'
describe('SFC compile <script setup>', () => { describe('SFC compile <script setup>', () => {
test('should compile JS syntax', () => { test('should compile JS syntax', () => {
@ -690,6 +696,27 @@ describe('SFC compile <script setup>', () => {
expect(content).toMatch(`new (_unref(Foo)).Bar()`) expect(content).toMatch(`new (_unref(Foo)).Bar()`)
assertCode(content) assertCode(content)
}) })
// #12682
test('source map', () => {
const source = `
<script setup>
const count = ref(0)
</script>
<template>
<button @click="throw new Error(\`msg\`);"></button>
</template>
`
const { content, map } = compile(source, { inlineTemplate: true })
expect(map).not.toBeUndefined()
const consumer = new SourceMapConsumer(map as RawSourceMap)
expect(
consumer.originalPositionFor(getPositionInCode(content, 'count')),
).toMatchObject(getPositionInCode(source, `count`))
expect(
consumer.originalPositionFor(getPositionInCode(content, 'Error')),
).toMatchObject(getPositionInCode(source, `Error`))
})
}) })
describe('with TypeScript', () => { describe('with TypeScript', () => {
@ -980,7 +1007,7 @@ describe('SFC compile <script setup>', () => {
expect(() => expect(() =>
compile(`<script setup> compile(`<script setup>
let bar = 1 let bar = 1
defineModel({ const model = defineModel({
default: () => bar default: () => bar
}) })
</script>`), </script>`),
@ -990,7 +1017,7 @@ describe('SFC compile <script setup>', () => {
expect(() => expect(() =>
compile(`<script setup> compile(`<script setup>
const bar = 1 const bar = 1
defineModel({ const model = defineModel({
default: () => bar default: () => bar
}) })
</script>`), </script>`),
@ -1000,7 +1027,7 @@ describe('SFC compile <script setup>', () => {
expect(() => expect(() =>
compile(`<script setup> compile(`<script setup>
let bar = 1 let bar = 1
defineModel({ const model = defineModel({
get: () => bar, get: () => bar,
set: () => bar set: () => bar
}) })

View File

@ -148,6 +148,27 @@ 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

@ -192,6 +192,25 @@ return () => {}
}" }"
`; `;
exports[`sfc reactive props destructure > handle function parameters with same name as destructured props 1`] = `
"
export default {
setup(__props) {
function test(value) {
try {
} catch {
}
}
console.log(__props.value)
return () => {}
}
}"
`;
exports[`sfc reactive props destructure > multi-variable declaration 1`] = ` exports[`sfc reactive props destructure > multi-variable declaration 1`] = `
" "
export default { export default {

View File

@ -269,4 +269,16 @@ describe('defineModel()', () => {
modelValue: BindingTypes.SETUP_REF, modelValue: BindingTypes.SETUP_REF,
}) })
}) })
test('error when defineModel is not assigned to a variable', () => {
expect(() =>
compile(`
<script setup>
defineModel()
</script>
`),
).toThrow(
'defineModel() must be assigned to a variable. For example: const model = defineModel()',
)
})
}) })

View File

@ -808,4 +808,30 @@ 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

@ -358,6 +358,22 @@ describe('sfc reactive props destructure', () => {
expect(content).toMatch(`props: ['item'],`) expect(content).toMatch(`props: ['item'],`)
}) })
test('handle function parameters with same name as destructured props', () => {
const { content } = compile(`
<script setup>
const { value } = defineProps()
function test(value) {
try {
} catch {
}
}
console.log(value)
</script>
`)
assertCode(content)
expect(content).toMatch(`console.log(__props.value)`)
})
test('defineProps/defineEmits in multi-variable declaration (full removal)', () => { test('defineProps/defineEmits in multi-variable declaration (full removal)', () => {
const { content } = compile(` const { content } = compile(`
<script setup> <script setup>

View File

@ -1434,6 +1434,29 @@ describe('resolveType', () => {
colsLg: ['Number'], colsLg: ['Number'],
}) })
}) })
test('allowArbitraryExtensions', () => {
const files = {
'/foo.d.vue.ts': 'export type Foo = number;',
'/foo.vue': '<template><div /></template>',
'/bar.d.css.ts': 'export type Bar = string;',
'/bar.css': ':root { --color: red; }',
}
const { props } = resolve(
`
import { Foo } from './foo.vue'
import { Bar } from './bar.css'
defineProps<{ foo: Foo; bar: Bar }>()
`,
files,
)
expect(props).toStrictEqual({
foo: ['Number'],
bar: ['String'],
})
})
}) })
}) })

View File

@ -211,38 +211,42 @@ color: red
expect( expect(
compileScoped(`.div { color: red; } .div:where(:hover) { color: blue; }`), compileScoped(`.div { color: red; } .div:where(:hover) { color: blue; }`),
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
".div[data-v-test] { color: red; ".div[data-v-test] { color: red;
} }
.div[data-v-test]:where(:hover) { color: blue; .div[data-v-test]:where(:hover) { color: blue;
}"`) }"
`)
expect( expect(
compileScoped(`.div { color: red; } .div:is(:hover) { color: blue; }`), compileScoped(`.div { color: red; } .div:is(:hover) { color: blue; }`),
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
".div[data-v-test] { color: red; ".div[data-v-test] { color: red;
} }
.div[data-v-test]:is(:hover) { color: blue; .div[data-v-test]:is(:hover) { color: blue;
}"`) }"
`)
expect( expect(
compileScoped( compileScoped(
`.div { color: red; } .div:where(.foo:hover) { color: blue; }`, `.div { color: red; } .div:where(.foo:hover) { color: blue; }`,
), ),
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
".div[data-v-test] { color: red; ".div[data-v-test] { color: red;
} }
.div[data-v-test]:where(.foo:hover) { color: blue; .div[data-v-test]:where(.foo:hover) { color: blue;
}"`) }"
`)
expect( expect(
compileScoped( compileScoped(
`.div { color: red; } .div:is(.foo:hover) { color: blue; }`, `.div { color: red; } .div:is(.foo:hover) { color: blue; }`,
), ),
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
".div[data-v-test] { color: red; ".div[data-v-test] { color: red;
} }
.div[data-v-test]:is(.foo:hover) { color: blue; .div[data-v-test]:is(.foo:hover) { color: blue;
}"`) }"
`)
}) })
test('media query', () => { test('media query', () => {
@ -489,7 +493,31 @@ describe('SFC style preprocessors', () => {
}" }"
`) `)
expect(compileScoped(`.foo * { color: red; }`)).toMatchInlineSnapshot(` expect(compileScoped(`.foo * { color: red; }`)).toMatchInlineSnapshot(`
".foo[data-v-test] * { color: red; ".foo[data-v-test] [data-v-test] { color: red;
}"
`)
expect(compileScoped(`.foo :active { color: red; }`))
.toMatchInlineSnapshot(`
".foo[data-v-test] :active { color: red;
}"
`)
expect(compileScoped(`.foo *:active { color: red; }`))
.toMatchInlineSnapshot(`
".foo[data-v-test] [data-v-test]:active { color: red;
}"
`)
expect(compileScoped(`.foo * .bar { color: red; }`)).toMatchInlineSnapshot(`
".foo * .bar[data-v-test] { color: red;
}"
`)
expect(compileScoped(`:last-child * { color: red; }`))
.toMatchInlineSnapshot(`
"[data-v-test]:last-child [data-v-test] { color: red;
}"
`)
expect(compileScoped(`:last-child *:active { color: red; }`))
.toMatchInlineSnapshot(`
"[data-v-test]:last-child [data-v-test]:active { color: red;
}" }"
`) `)
}) })

View File

@ -6,6 +6,7 @@ import {
} from '../src/compileTemplate' } from '../src/compileTemplate'
import { type SFCTemplateBlock, parse } from '../src/parse' import { type SFCTemplateBlock, parse } from '../src/parse'
import { compileScript } from '../src' import { compileScript } from '../src'
import { getPositionInCode } from './utils'
function compile(opts: Omit<SFCTemplateCompileOptions, 'id'>) { function compile(opts: Omit<SFCTemplateCompileOptions, 'id'>) {
return compileTemplate({ return compileTemplate({
@ -157,6 +158,35 @@ test('source map', () => {
).toMatchObject(getPositionInCode(template.content, `foobar`)) ).toMatchObject(getPositionInCode(template.content, `foobar`))
}) })
test('source map: v-if generated comment should not have original position', () => {
const template = parse(
`
<template>
<div v-if="true"></div>
</template>
`,
{ filename: 'example.vue', sourceMap: true },
).descriptor.template!
const { code, map } = compile({
filename: 'example.vue',
source: template.content,
})
expect(map!.sources).toEqual([`example.vue`])
expect(map!.sourcesContent).toEqual([template.content])
const consumer = new SourceMapConsumer(map as RawSourceMap)
const commentNode = code.match(/_createCommentVNode\("v-if", true\)/)
expect(commentNode).not.toBeNull()
const commentPosition = getPositionInCode(code, commentNode![0])
const originalPosition = consumer.originalPositionFor(commentPosition)
// the comment node should not be mapped to the original source
expect(originalPosition.column).toBeNull()
expect(originalPosition.line).toBeNull()
expect(originalPosition.source).toBeNull()
})
test('should work w/ AST from descriptor', () => { test('should work w/ AST from descriptor', () => {
const source = ` const source = `
<template> <template>
@ -482,36 +512,3 @@ test('non-identifier expression in legacy filter syntax', () => {
babelParse(compilationResult.code, { sourceType: 'module' }) babelParse(compilationResult.code, { sourceType: 'module' })
}).not.toThrow() }).not.toThrow()
}) })
interface Pos {
line: number
column: number
name?: string
}
function getPositionInCode(
code: string,
token: string,
expectName: string | boolean = false,
): Pos {
const generatedOffset = code.indexOf(token)
let line = 1
let lastNewLinePos = -1
for (let i = 0; i < generatedOffset; i++) {
if (code.charCodeAt(i) === 10 /* newline char code */) {
line++
lastNewLinePos = i
}
}
const res: Pos = {
line,
column:
lastNewLinePos === -1
? generatedOffset
: generatedOffset - lastNewLinePos - 1,
}
if (expectName) {
res.name = typeof expectName === 'string' ? expectName : token
}
return res
}

View File

@ -81,7 +81,7 @@ font-weight: bold;
const consumer = new SourceMapConsumer(script!.map!) const consumer = new SourceMapConsumer(script!.map!)
consumer.eachMapping(mapping => { consumer.eachMapping(mapping => {
expect(mapping.originalLine - mapping.generatedLine).toBe(padding) expect(mapping.originalLine! - mapping.generatedLine).toBe(padding)
}) })
}) })
@ -100,8 +100,8 @@ font-weight: bold;
const consumer = new SourceMapConsumer(template.map!) const consumer = new SourceMapConsumer(template.map!)
consumer.eachMapping(mapping => { consumer.eachMapping(mapping => {
expect(mapping.originalLine - mapping.generatedLine).toBe(padding) expect(mapping.originalLine! - mapping.generatedLine).toBe(padding)
expect(mapping.originalColumn - mapping.generatedColumn).toBe(2) expect(mapping.originalColumn! - mapping.generatedColumn).toBe(2)
}) })
}) })
@ -115,7 +115,7 @@ font-weight: bold;
const consumer = new SourceMapConsumer(custom!.map!) const consumer = new SourceMapConsumer(custom!.map!)
consumer.eachMapping(mapping => { consumer.eachMapping(mapping => {
expect(mapping.originalLine - mapping.generatedLine).toBe(padding) expect(mapping.originalLine! - mapping.generatedLine).toBe(padding)
}) })
}) })
}) })

View File

@ -40,3 +40,36 @@ export function assertCode(code: string): void {
} }
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
} }
interface Pos {
line: number
column: number
name?: string
}
export function getPositionInCode(
code: string,
token: string,
expectName: string | boolean = false,
): Pos {
const generatedOffset = code.indexOf(token)
let line = 1
let lastNewLinePos = -1
for (let i = 0; i < generatedOffset; i++) {
if (code.charCodeAt(i) === 10 /* newline char code */) {
line++
lastNewLinePos = i
}
}
const res: Pos = {
line,
column:
lastNewLinePos === -1
? generatedOffset
: generatedOffset - lastNewLinePos - 1,
}
if (expectName) {
res.name = typeof expectName === 'string' ? expectName : token
}
return res
}

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compiler-sfc", "name": "@vue/compiler-sfc",
"version": "3.5.12", "version": "3.5.14",
"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.4.47", "postcss": "^8.5.3",
"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": "~9.0.5", "minimatch": "~10.0.1",
"postcss-modules": "^6.0.0", "postcss-modules": "^6.0.1",
"postcss-selector-parser": "^6.1.2", "postcss-selector-parser": "^7.1.0",
"pug": "^3.0.3", "pug": "^3.0.3",
"sass": "^1.80.3" "sass": "^1.89.0"
} }
} }

View File

@ -23,7 +23,11 @@ import type {
Statement, Statement,
} from '@babel/types' } from '@babel/types'
import { walk } from 'estree-walker' import { walk } from 'estree-walker'
import type { RawSourceMap } from 'source-map-js' import {
type RawSourceMap,
SourceMapConsumer,
SourceMapGenerator,
} from 'source-map-js'
import { import {
normalScriptDefaultVar, normalScriptDefaultVar,
processNormalScript, processNormalScript,
@ -170,8 +174,6 @@ export function compileScript(
const scriptLang = script && script.lang const scriptLang = script && script.lang
const scriptSetupLang = scriptSetup && scriptSetup.lang const scriptSetupLang = scriptSetup && scriptSetup.lang
let refBindings: string[] | undefined
if (!scriptSetup) { if (!scriptSetup) {
if (!script) { if (!script) {
throw new Error(`[@vue/compiler-sfc] SFC contains no <script> tags.`) throw new Error(`[@vue/compiler-sfc] SFC contains no <script> tags.`)
@ -740,12 +742,6 @@ export function compileScript(
for (const key in setupBindings) { for (const key in setupBindings) {
ctx.bindingMetadata[key] = setupBindings[key] ctx.bindingMetadata[key] = setupBindings[key]
} }
// known ref bindings
if (refBindings) {
for (const key of refBindings) {
ctx.bindingMetadata[key] = BindingTypes.SETUP_REF
}
}
// 7. inject `useCssVars` calls // 7. inject `useCssVars` calls
if ( if (
@ -817,6 +813,7 @@ export function compileScript(
args += `, { ${destructureElements.join(', ')} }` args += `, { ${destructureElements.join(', ')} }`
} }
let templateMap
// 9. generate return statement // 9. generate return statement
let returned let returned
if ( if (
@ -866,7 +863,7 @@ export function compileScript(
} }
// inline render function mode - we are going to compile the template and // inline render function mode - we are going to compile the template and
// inline it right here // inline it right here
const { code, ast, preamble, tips, errors } = compileTemplate({ const { code, ast, preamble, tips, errors, map } = compileTemplate({
filename, filename,
ast: sfc.template.ast, ast: sfc.template.ast,
source: sfc.template.content, source: sfc.template.content,
@ -884,6 +881,7 @@ export function compileScript(
bindingMetadata: ctx.bindingMetadata, bindingMetadata: ctx.bindingMetadata,
}, },
}) })
templateMap = map
if (tips.length) { if (tips.length) {
tips.forEach(warnOnce) tips.forEach(warnOnce)
} }
@ -1022,19 +1020,28 @@ export function compileScript(
) )
} }
const content = ctx.s.toString()
let map =
options.sourceMap !== false
? (ctx.s.generateMap({
source: filename,
hires: true,
includeContent: true,
}) as unknown as RawSourceMap)
: undefined
// merge source maps of the script setup and template in inline mode
if (templateMap && map) {
const offset = content.indexOf(returned)
const templateLineOffset =
content.slice(0, offset).split(/\r?\n/).length - 1
map = mergeSourceMaps(map, templateMap, templateLineOffset)
}
return { return {
...scriptSetup, ...scriptSetup,
bindings: ctx.bindingMetadata, bindings: ctx.bindingMetadata,
imports: ctx.userImports, imports: ctx.userImports,
content: ctx.s.toString(), content,
map: map,
options.sourceMap !== false
? (ctx.s.generateMap({
source: filename,
hires: true,
includeContent: true,
}) as unknown as RawSourceMap)
: undefined,
scriptAst: scriptAst?.body, scriptAst: scriptAst?.body,
scriptSetupAst: scriptSetupAst?.body, scriptSetupAst: scriptSetupAst?.body,
deps: ctx.deps ? [...ctx.deps] : undefined, deps: ctx.deps ? [...ctx.deps] : undefined,
@ -1112,6 +1119,7 @@ function walkDeclaration(
m === userImportAliases['shallowRef'] || m === userImportAliases['shallowRef'] ||
m === userImportAliases['customRef'] || m === userImportAliases['customRef'] ||
m === userImportAliases['toRef'] || m === userImportAliases['toRef'] ||
m === userImportAliases['useTemplateRef'] ||
m === DEFINE_MODEL, m === DEFINE_MODEL,
) )
) { ) {
@ -1291,3 +1299,42 @@ function isStaticNode(node: Node): boolean {
} }
return false return false
} }
export function mergeSourceMaps(
scriptMap: RawSourceMap,
templateMap: RawSourceMap,
templateLineOffset: number,
): RawSourceMap {
const generator = new SourceMapGenerator()
const addMapping = (map: RawSourceMap, lineOffset = 0) => {
const consumer = new SourceMapConsumer(map)
;(consumer as any).sources.forEach((sourceFile: string) => {
;(generator as any)._sources.add(sourceFile)
const sourceContent = consumer.sourceContentFor(sourceFile)
if (sourceContent != null) {
generator.setSourceContent(sourceFile, sourceContent)
}
})
consumer.eachMapping(m => {
if (m.originalLine == null) return
generator.addMapping({
generated: {
line: m.generatedLine + lineOffset,
column: m.generatedColumn,
},
original: {
line: m.originalLine,
column: m.originalColumn!,
},
source: m.source,
name: m.name,
})
})
}
addMapping(scriptMap)
addMapping(templateMap, templateLineOffset)
;(generator as any)._sourceRoot = scriptMap.sourceRoot
;(generator as any)._file = scriptMap.file
return (generator as any).toJSON()
}

View File

@ -289,7 +289,7 @@ function mapLines(oldMap: RawSourceMap, newMap: RawSourceMap): RawSourceMap {
const origPosInOldMap = oldMapConsumer.originalPositionFor({ const origPosInOldMap = oldMapConsumer.originalPositionFor({
line: m.originalLine, line: m.originalLine,
column: m.originalColumn, column: m.originalColumn!,
}) })
if (origPosInOldMap.source == null) { if (origPosInOldMap.source == null) {
@ -305,7 +305,7 @@ function mapLines(oldMap: RawSourceMap, newMap: RawSourceMap): RawSourceMap {
line: origPosInOldMap.line, // map line line: origPosInOldMap.line, // map line
// use current column, since the oldMap produced by @vue/compiler-sfc // use current column, since the oldMap produced by @vue/compiler-sfc
// does not // does not
column: m.originalColumn, column: m.originalColumn!,
}, },
source: origPosInOldMap.source, source: origPosInOldMap.source,
name: origPosInOldMap.name, name: origPosInOldMap.name,

View File

@ -39,7 +39,7 @@ export function rewriteDefaultAST(
ast.forEach(node => { ast.forEach(node => {
if (node.type === 'ExportDefaultDeclaration') { if (node.type === 'ExportDefaultDeclaration') {
if (node.declaration.type === 'ClassDeclaration' && node.declaration.id) { if (node.declaration.type === 'ClassDeclaration' && node.declaration.id) {
let start: number = const start: number =
node.declaration.decorators && node.declaration.decorators.length > 0 node.declaration.decorators && node.declaration.decorators.length > 0
? node.declaration.decorators[ ? node.declaration.decorators[
node.declaration.decorators.length - 1 node.declaration.decorators.length - 1

View File

@ -22,6 +22,13 @@ export function processDefineModel(
return false return false
} }
if (!declId) {
ctx.error(
'defineModel() must be assigned to a variable. For example: const model = defineModel()',
node,
)
}
ctx.hasDefineModelCall = true ctx.hasDefineModelCall = true
const type = const type =

View File

@ -291,7 +291,8 @@ export function transformDestructuredProps(
parent && parentStack.pop() parent && parentStack.pop()
if ( if (
(node.type === 'BlockStatement' && !isFunctionType(parent!)) || (node.type === 'BlockStatement' && !isFunctionType(parent!)) ||
isFunctionType(node) isFunctionType(node) ||
node.type === 'CatchClause'
) { ) {
popScope() popScope()
} }

View File

@ -860,13 +860,13 @@ function resolveFS(ctx: TypeResolveContext): FS | undefined {
} }
return (ctx.fs = { return (ctx.fs = {
fileExists(file) { fileExists(file) {
if (file.endsWith('.vue.ts')) { if (file.endsWith('.vue.ts') && !file.endsWith('.d.vue.ts')) {
file = file.replace(/\.ts$/, '') file = file.replace(/\.ts$/, '')
} }
return fs.fileExists(file) return fs.fileExists(file)
}, },
readFile(file) { readFile(file) {
if (file.endsWith('.vue.ts')) { if (file.endsWith('.vue.ts') && !file.endsWith('.d.vue.ts')) {
file = file.replace(/\.ts$/, '') file = file.replace(/\.ts$/, '')
} }
return fs.readFile(file) return fs.readFile(file)
@ -1059,7 +1059,7 @@ function resolveWithTS(
if (res.resolvedModule) { if (res.resolvedModule) {
let filename = res.resolvedModule.resolvedFileName let filename = res.resolvedModule.resolvedFileName
if (filename.endsWith('.vue.ts')) { if (filename.endsWith('.vue.ts') && !filename.endsWith('.d.vue.ts')) {
filename = filename.replace(/\.ts$/, '') filename = filename.replace(/\.ts$/, '')
} }
return fs.realpath ? fs.realpath(filename) : filename return fs.realpath ? fs.realpath(filename) : filename
@ -1129,7 +1129,7 @@ export function fileToScope(
// fs should be guaranteed to exist here // fs should be guaranteed to exist here
const fs = resolveFS(ctx)! const fs = resolveFS(ctx)!
const source = fs.readFile(filename) || '' const source = fs.readFile(filename) || ''
const body = parseFile(filename, source, ctx.options.babelParserPlugins) const body = parseFile(filename, source, fs, ctx.options.babelParserPlugins)
const scope = new TypeScope(filename, source, 0, recordImports(body)) const scope = new TypeScope(filename, source, 0, recordImports(body))
recordTypes(ctx, body, scope, asGlobal) recordTypes(ctx, body, scope, asGlobal)
fileToScopeCache.set(filename, scope) fileToScopeCache.set(filename, scope)
@ -1139,6 +1139,7 @@ export function fileToScope(
function parseFile( function parseFile(
filename: string, filename: string,
content: string, content: string,
fs: FS,
parserPlugins?: SFCScriptCompileOptions['babelParserPlugins'], parserPlugins?: SFCScriptCompileOptions['babelParserPlugins'],
): Statement[] { ): Statement[] {
const ext = extname(filename) const ext = extname(filename)
@ -1151,7 +1152,21 @@ function parseFile(
), ),
sourceType: 'module', sourceType: 'module',
}).program.body }).program.body
} else if (ext === '.vue') { }
// simulate `allowArbitraryExtensions` on TypeScript >= 5.0
const isUnknownTypeSource = !/\.[cm]?[tj]sx?$/.test(filename)
const arbitraryTypeSource = `${filename.slice(0, -ext.length)}.d${ext}.ts`
const hasArbitraryTypeDeclaration =
isUnknownTypeSource && fs.fileExists(arbitraryTypeSource)
if (hasArbitraryTypeDeclaration) {
return babelParse(fs.readFile(arbitraryTypeSource)!, {
plugins: resolveParserPlugins('ts', parserPlugins, true),
sourceType: 'module',
}).program.body
}
if (ext === '.vue') {
const { const {
descriptor: { script, scriptSetup }, descriptor: { script, scriptSetup },
} = parse(content) } = parse(content)
@ -1554,6 +1569,14 @@ export function inferRuntimeType(
case 'TSTypeReference': { case 'TSTypeReference': {
const resolved = resolveTypeReference(ctx, node, scope) const resolved = resolveTypeReference(ctx, node, scope)
if (resolved) { if (resolved) {
if (resolved.type === 'TSTypeAliasDeclaration') {
return inferRuntimeType(
ctx,
resolved.typeAnnotation,
resolved._ownerScope,
isKeyOf,
)
}
return inferRuntimeType(ctx, resolved, resolved._ownerScope, isKeyOf) return inferRuntimeType(ctx, resolved, resolved._ownerScope, isKeyOf)
} }

View File

@ -102,6 +102,7 @@ function rewriteSelector(
slotted = false, slotted = false,
) { ) {
let node: selectorParser.Node | null = null let node: selectorParser.Node | null = null
let starNode: selectorParser.Node | null = null
let shouldInject = !deep let shouldInject = !deep
// find the last child node to insert attribute selector // find the last child node to insert attribute selector
selector.each(n => { selector.each(n => {
@ -189,8 +190,7 @@ function rewriteSelector(
// global: replace with inner selector and do not inject [id]. // global: replace with inner selector and do not inject [id].
// ::v-global(.foo) -> .foo // ::v-global(.foo) -> .foo
if (value === ':global' || value === '::v-global') { if (value === ':global' || value === '::v-global') {
selectorRoot.insertAfter(selector, n.nodes[0]) selector.replaceWith(n.nodes[0])
selectorRoot.removeChild(selector)
return false return false
} }
} }
@ -217,17 +217,21 @@ function rewriteSelector(
return false return false
} }
} }
// .foo * -> .foo[xxxxxxx] * // store the universal selector so it can be rewritten later
if (node) return // .foo * -> .foo[xxxxxxx] [xxxxxxx]
starNode = n
} }
if ( if (
(n.type !== 'pseudo' && n.type !== 'combinator') || (n.type !== 'pseudo' &&
n.type !== 'combinator' &&
n.type !== 'universal') ||
(n.type === 'pseudo' && (n.type === 'pseudo' &&
(n.value === ':is' || n.value === ':where') && (n.value === ':is' || n.value === ':where') &&
!node) !node)
) { ) {
node = n node = n
starNode = null
} }
}) })
@ -275,6 +279,20 @@ function rewriteSelector(
quoteMark: `"`, quoteMark: `"`,
}), }),
) )
// Used for trailing universal selectors (#12906)
// `.foo * {}` -> `.foo[xxxxxxx] [xxxxxxx] {}`
if (starNode) {
selector.insertBefore(
starNode,
selectorParser.attribute({
attribute: idToAdd,
value: idToAdd,
raws: {},
quoteMark: `"`,
}),
)
selector.removeChild(starNode)
}
} }
} }

View File

@ -337,6 +337,39 @@ describe('ssr: element', () => {
`) `)
}) })
test('custom dir with v-text', () => {
expect(getCompiledString(`<div v-xxx v-text="foo" />`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_ssrGetDirectiveProps(_ctx, _directive_xxx))
}>\${
_ssrInterpolate(_ctx.foo)
}</div>\`"
`)
})
test('custom dir with v-text and normal attrs', () => {
expect(getCompiledString(`<div class="test" v-xxx v-text="foo" />`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_mergeProps({ class: "test" }, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
}>\${
_ssrInterpolate(_ctx.foo)
}</div>\`"
`)
})
test('mulptiple custom dirs with v-text', () => {
expect(getCompiledString(`<div v-xxx v-yyy v-text="foo" />`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_mergeProps(_ssrGetDirectiveProps(_ctx, _directive_xxx), _ssrGetDirectiveProps(_ctx, _directive_yyy)))
}>\${
_ssrInterpolate(_ctx.foo)
}</div>\`"
`)
})
test('custom dir with object v-bind', () => { test('custom dir with object v-bind', () => {
expect(getCompiledString(`<div v-bind="x" v-xxx />`)) expect(getCompiledString(`<div v-bind="x" v-xxx />`))
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compiler-ssr", "name": "@vue/compiler-ssr",
"version": "3.5.12", "version": "3.5.14",
"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

@ -28,6 +28,7 @@ import {
createSequenceExpression, createSequenceExpression,
createSimpleExpression, createSimpleExpression,
createTemplateLiteral, createTemplateLiteral,
findDir,
hasDynamicKeyVBind, hasDynamicKeyVBind,
isStaticArgOf, isStaticArgOf,
isStaticExp, isStaticExp,
@ -164,24 +165,28 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
] ]
} }
} else if (directives.length && !node.children.length) { } else if (directives.length && !node.children.length) {
const tempId = `_temp${context.temps++}` // v-text directive has higher priority than the merged props
propsExp.arguments = [ const vText = findDir(node, 'text')
createAssignmentExpression( if (!vText) {
createSimpleExpression(tempId, false), const tempId = `_temp${context.temps++}`
mergedProps, propsExp.arguments = [
), createAssignmentExpression(
] createSimpleExpression(tempId, false),
rawChildrenMap.set( mergedProps,
node, ),
createConditionalExpression( ]
createSimpleExpression(`"textContent" in ${tempId}`, false), rawChildrenMap.set(
createCallExpression(context.helper(SSR_INTERPOLATE), [ node,
createSimpleExpression(`${tempId}.textContent`, false), createConditionalExpression(
]), createSimpleExpression(`"textContent" in ${tempId}`, false),
createSimpleExpression(`${tempId}.innerHTML ?? ''`, false), createCallExpression(context.helper(SSR_INTERPOLATE), [
false, createSimpleExpression(`${tempId}.textContent`, false),
), ]),
) createSimpleExpression(`${tempId}.innerHTML ?? ''`, false),
false,
),
)
}
} }
if (needTagForRuntime) { if (needTagForRuntime) {

View File

@ -5,6 +5,7 @@ import {
type ExpressionNode, type ExpressionNode,
NodeTypes, NodeTypes,
type PlainElementNode, type PlainElementNode,
type TemplateChildNode,
createCallExpression, createCallExpression,
createConditionalExpression, createConditionalExpression,
createDOMCompilerError, createDOMCompilerError,
@ -162,11 +163,18 @@ 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') {
node.children.forEach(child => { const processChildren = (children: TemplateChildNode[]) => {
if (child.type === NodeTypes.ELEMENT) { children.forEach(child => {
processOption(child as PlainElementNode) 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

@ -1,5 +1,10 @@
import { bench, describe } from 'vitest' import { bench, describe } from 'vitest'
import { type ComputedRef, type Ref, computed, effect, ref } from '../src' import type { ComputedRef, Ref } from '../src'
import { computed, effect, ref } from '../dist/reactivity.esm-browser.prod'
declare module '../dist/reactivity.esm-browser.prod' {
function computed(...args: any[]): any
}
describe('computed', () => { describe('computed', () => {
bench('create computed', () => { bench('create computed', () => {

View File

@ -1,5 +1,6 @@
import { bench, describe } from 'vitest' import { bench, describe } from 'vitest'
import { type Ref, effect, ref } from '../src' import type { Ref } from '../src'
import { effect, ref } from '../dist/reactivity.esm-browser.prod'
describe('effect', () => { describe('effect', () => {
{ {

View File

@ -1,5 +1,9 @@
import { bench } from 'vitest' import { bench } from 'vitest'
import { effect, reactive, shallowReadArray } from '../src' import {
effect,
reactive,
shallowReadArray,
} from '../dist/reactivity.esm-browser.prod'
for (let amount = 1e1; amount < 1e4; amount *= 10) { for (let amount = 1e1; amount < 1e4; amount *= 10) {
{ {

View File

@ -1,5 +1,6 @@
import { bench } from 'vitest' import { bench } from 'vitest'
import { type ComputedRef, computed, reactive } from '../src' import type { ComputedRef } from '../src'
import { computed, reactive } from '../dist/reactivity.esm-browser.prod'
function createMap(obj: Record<string, any>) { function createMap(obj: Record<string, any>) {
const map = new Map() const map = new Map()

View File

@ -1,5 +1,5 @@
import { bench } from 'vitest' import { bench } from 'vitest'
import { reactive } from '../src' import { reactive } from '../dist/reactivity.esm-browser.prod'
bench('create reactive obj', () => { bench('create reactive obj', () => {
reactive({ a: 1 }) reactive({ a: 1 })

View File

@ -1,5 +1,5 @@
import { bench, describe } from 'vitest' import { bench, describe } from 'vitest'
import { ref } from '../src/index' import { ref } from '../dist/reactivity.esm-browser.prod'
describe('ref', () => { describe('ref', () => {
bench('create ref', () => { bench('create ref', () => {

View File

@ -1012,6 +1012,17 @@ describe('reactivity/computed', () => {
expect(cValue.value).toBe(1) expect(cValue.value).toBe(1)
}) })
test('should not recompute if computed does not track reactive data', async () => {
const spy = vi.fn()
const c1 = computed(() => spy())
c1.value
ref(0).value++ // update globalVersion
c1.value
expect(spy).toBeCalledTimes(1)
})
test('computed should remain live after losing all subscribers', () => { test('computed should remain live after losing all subscribers', () => {
const state = reactive({ a: 1 }) const state = reactive({ a: 1 })
const p = computed(() => state.a + 1) const p = computed(() => state.a + 1)

View File

@ -176,7 +176,7 @@ describe('reactivity/effect/scope', () => {
expect('[Vue warn] cannot run an inactive effect scope.').toHaveBeenWarned() expect('[Vue warn] cannot run an inactive effect scope.').toHaveBeenWarned()
expect(scope.effects.length).toBe(1) expect(scope.effects.length).toBe(0)
counter.num = 7 counter.num = 7
expect(dummy).toBe(0) expect(dummy).toBe(0)
@ -322,4 +322,44 @@ describe('reactivity/effect/scope', () => {
scope.resume() scope.resume()
expect(fnSpy).toHaveBeenCalledTimes(3) expect(fnSpy).toHaveBeenCalledTimes(3)
}) })
test('removing a watcher while stopping its effectScope', async () => {
const count = ref(0)
const scope = effectScope()
let watcherCalls = 0
let cleanupCalls = 0
scope.run(() => {
const stop1 = watch(count, () => {
watcherCalls++
})
watch(count, (val, old, onCleanup) => {
watcherCalls++
onCleanup(() => {
cleanupCalls++
stop1()
})
})
watch(count, () => {
watcherCalls++
})
})
expect(watcherCalls).toBe(0)
expect(cleanupCalls).toBe(0)
count.value++
await nextTick()
expect(watcherCalls).toBe(3)
expect(cleanupCalls).toBe(0)
scope.stop()
count.value++
await nextTick()
expect(watcherCalls).toBe(3)
expect(cleanupCalls).toBe(1)
expect(scope.effects.length).toBe(0)
expect(scope.cleanups.length).toBe(0)
})
}) })

View File

@ -1,4 +1,4 @@
import { isRef, ref } from '../src/ref' import { isRef, ref, shallowRef } from '../src/ref'
import { import {
isProxy, isProxy,
isReactive, isReactive,
@ -301,6 +301,13 @@ describe('reactivity/reactive', () => {
expect(() => markRaw(obj)).not.toThrowError() expect(() => markRaw(obj)).not.toThrowError()
}) })
test('should not markRaw object as reactive', () => {
const a = reactive({ a: 1 })
const b = reactive({ b: 2 }) as any
b.a = markRaw(toRaw(a))
expect(b.a === a).toBe(false)
})
test('should not observe non-extensible objects', () => { test('should not observe non-extensible objects', () => {
const obj = reactive({ const obj = reactive({
foo: Object.preventExtensions({ a: 1 }), foo: Object.preventExtensions({ a: 1 }),
@ -419,4 +426,17 @@ describe('reactivity/reactive', () => {
map.set(void 0, 1) map.set(void 0, 1)
expect(c.value).toBe(1) expect(c.value).toBe(1)
}) })
test('should return true for reactive objects', () => {
expect(isReactive(reactive({}))).toBe(true)
expect(isReactive(readonly(reactive({})))).toBe(true)
expect(isReactive(ref({}).value)).toBe(true)
expect(isReactive(readonly(ref({})).value)).toBe(true)
expect(isReactive(shallowReactive({}))).toBe(true)
})
test('should return false for non-reactive objects', () => {
expect(isReactive(ref(true))).toBe(false)
expect(isReactive(shallowRef({}).value)).toBe(false)
})
}) })

View File

@ -277,4 +277,16 @@ describe('watch', () => {
expect(dummy).toEqual([1, 2, 3]) expect(dummy).toEqual([1, 2, 3])
}) })
test('watch with immediate reset and sync flush', () => {
const value = ref(false)
watch(value, () => {
value.value = false
})
value.value = true
value.value = true
expect(value.value).toBe(false)
})
}) })

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/reactivity", "name": "@vue/reactivity",
"version": "3.5.12", "version": "3.5.14",
"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

@ -53,6 +53,8 @@ class BaseReactiveHandler implements ProxyHandler<Target> {
) {} ) {}
get(target: Target, key: string | symbol, receiver: object): any { get(target: Target, key: string | symbol, receiver: object): any {
if (key === ReactiveFlags.SKIP) return target[ReactiveFlags.SKIP]
const isReadonly = this._isReadonly, const isReadonly = this._isReadonly,
isShallow = this._isShallow isShallow = this._isShallow
if (key === ReactiveFlags.IS_REACTIVE) { if (key === ReactiveFlags.IS_REACTIVE) {

View File

@ -49,6 +49,7 @@ export enum EffectFlags {
DIRTY = 1 << 4, DIRTY = 1 << 4,
ALLOW_RECURSE = 1 << 5, ALLOW_RECURSE = 1 << 5,
PAUSED = 1 << 6, PAUSED = 1 << 6,
EVALUATED = 1 << 7,
} }
/** /**
@ -377,22 +378,22 @@ export function refreshComputed(computed: ComputedRefImpl): undefined {
} }
computed.globalVersion = globalVersion computed.globalVersion = globalVersion
const dep = computed.dep
computed.flags |= EffectFlags.RUNNING
// In SSR there will be no render effect, so the computed has no subscriber // In SSR there will be no render effect, so the computed has no subscriber
// and therefore tracks no deps, thus we cannot rely on the dirty check. // and therefore tracks no deps, thus we cannot rely on the dirty check.
// Instead, computed always re-evaluate and relies on the globalVersion // Instead, computed always re-evaluate and relies on the globalVersion
// fast path above for caching. // fast path above for caching.
// #12337 if computed has no deps (does not rely on any reactive data) and evaluated,
// there is no need to re-evaluate.
if ( if (
dep.version > 0 &&
!computed.isSSR && !computed.isSSR &&
computed.deps && computed.flags & EffectFlags.EVALUATED &&
!isDirty(computed) ((!computed.deps && !(computed as any)._dirty) || !isDirty(computed))
) { ) {
computed.flags &= ~EffectFlags.RUNNING
return return
} }
computed.flags |= EffectFlags.RUNNING
const dep = computed.dep
const prevSub = activeSub const prevSub = activeSub
const prevShouldTrack = shouldTrack const prevShouldTrack = shouldTrack
activeSub = computed activeSub = computed
@ -402,6 +403,7 @@ export function refreshComputed(computed: ComputedRefImpl): undefined {
prepareDeps(computed) prepareDeps(computed)
const value = computed.fn(computed._value) const value = computed.fn(computed._value)
if (dep.version === 0 || hasChanged(value, computed._value)) { if (dep.version === 0 || hasChanged(value, computed._value)) {
computed.flags |= EffectFlags.EVALUATED
computed._value = value computed._value = value
dep.version++ dep.version++
} }

View File

@ -8,6 +8,10 @@ export class EffectScope {
* @internal * @internal
*/ */
private _active = true private _active = true
/**
* @internal track `on` calls, allow `on` call multiple times
*/
private _on = 0
/** /**
* @internal * @internal
*/ */
@ -99,12 +103,16 @@ export class EffectScope {
} }
} }
prevScope: EffectScope | undefined
/** /**
* This should only be called on non-detached scopes * This should only be called on non-detached scopes
* @internal * @internal
*/ */
on(): void { on(): void {
activeEffectScope = this if (++this._on === 1) {
this.prevScope = activeEffectScope
activeEffectScope = this
}
} }
/** /**
@ -112,23 +120,33 @@ export class EffectScope {
* @internal * @internal
*/ */
off(): void { off(): void {
activeEffectScope = this.parent if (this._on > 0 && --this._on === 0) {
activeEffectScope = this.prevScope
this.prevScope = undefined
}
} }
stop(fromParent?: boolean): void { stop(fromParent?: boolean): void {
if (this._active) { if (this._active) {
this._active = false
let i, l let i, l
for (i = 0, l = this.effects.length; i < l; i++) { for (i = 0, l = this.effects.length; i < l; i++) {
this.effects[i].stop() this.effects[i].stop()
} }
this.effects.length = 0
for (i = 0, l = this.cleanups.length; i < l; i++) { for (i = 0, l = this.cleanups.length; i < l; i++) {
this.cleanups[i]() this.cleanups[i]()
} }
this.cleanups.length = 0
if (this.scopes) { if (this.scopes) {
for (i = 0, l = this.scopes.length; i < l; i++) { for (i = 0, l = this.scopes.length; i < l; i++) {
this.scopes[i].stop(true) this.scopes[i].stop(true)
} }
this.scopes.length = 0
} }
// nested scope, dereference from parent to avoid memory leaks // nested scope, dereference from parent to avoid memory leaks
if (!this.detached && this.parent && !fromParent) { if (!this.detached && this.parent && !fromParent) {
// optimized O(1) removal // optimized O(1) removal
@ -139,7 +157,6 @@ export class EffectScope {
} }
} }
this.parent = undefined this.parent = undefined
this._active = false
} }
} }
} }

View File

@ -108,9 +108,9 @@ export declare const ShallowReactiveMarker: unique symbol
export type ShallowReactive<T> = T & { [ShallowReactiveMarker]?: true } export type ShallowReactive<T> = T & { [ShallowReactiveMarker]?: true }
/** /**
* Shallow version of {@link reactive()}. * Shallow version of {@link reactive}.
* *
* Unlike {@link reactive()}, there is no deep conversion: only root-level * Unlike {@link reactive}, there is no deep conversion: only root-level
* properties are reactive for a shallow reactive object. Property values are * properties are reactive for a shallow reactive object. Property values are
* stored and exposed as-is - this also means properties with ref values will * stored and exposed as-is - this also means properties with ref values will
* not be automatically unwrapped. * not be automatically unwrapped.
@ -178,7 +178,7 @@ export type DeepReadonly<T> = T extends Builtin
* the original. * the original.
* *
* A readonly proxy is deep: any nested property accessed will be readonly as * A readonly proxy is deep: any nested property accessed will be readonly as
* well. It also has the same ref-unwrapping behavior as {@link reactive()}, * well. It also has the same ref-unwrapping behavior as {@link reactive},
* except the unwrapped values will also be made readonly. * except the unwrapped values will also be made readonly.
* *
* @example * @example
@ -215,9 +215,9 @@ export function readonly<T extends object>(
} }
/** /**
* Shallow version of {@link readonly()}. * Shallow version of {@link readonly}.
* *
* Unlike {@link readonly()}, there is no deep conversion: only root-level * Unlike {@link readonly}, there is no deep conversion: only root-level
* properties are made readonly. Property values are stored and exposed as-is - * properties are made readonly. Property values are stored and exposed as-is -
* this also means properties with ref values will not be automatically * this also means properties with ref values will not be automatically
* unwrapped. * unwrapped.
@ -279,16 +279,16 @@ function createReactiveObject(
) { ) {
return target return target
} }
// target already has corresponding Proxy
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only specific value types can be observed. // only specific value types can be observed.
const targetType = getTargetType(target) const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) { if (targetType === TargetType.INVALID) {
return target return target
} }
// target already has corresponding Proxy
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
const proxy = new Proxy( const proxy = new Proxy(
target, target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
@ -298,8 +298,8 @@ function createReactiveObject(
} }
/** /**
* Checks if an object is a proxy created by {@link reactive()} or * Checks if an object is a proxy created by {@link reactive} or
* {@link shallowReactive()} (or {@link ref()} in some cases). * {@link shallowReactive} (or {@link ref} in some cases).
* *
* @example * @example
* ```js * ```js
@ -327,7 +327,7 @@ export function isReactive(value: unknown): boolean {
* readonly object can change, but they can't be assigned directly via the * readonly object can change, but they can't be assigned directly via the
* passed object. * passed object.
* *
* The proxies created by {@link readonly()} and {@link shallowReadonly()} are * The proxies created by {@link readonly} and {@link shallowReadonly} are
* both considered readonly, as is a computed ref without a set function. * both considered readonly, as is a computed ref without a set function.
* *
* @param value - The value to check. * @param value - The value to check.
@ -343,7 +343,7 @@ export function isShallow(value: unknown): boolean {
/** /**
* Checks if an object is a proxy created by {@link reactive}, * Checks if an object is a proxy created by {@link reactive},
* {@link readonly}, {@link shallowReactive} or {@link shallowReadonly()}. * {@link readonly}, {@link shallowReactive} or {@link shallowReadonly}.
* *
* @param value - The value to check. * @param value - The value to check.
* @see {@link https://vuejs.org/api/reactivity-utilities.html#isproxy} * @see {@link https://vuejs.org/api/reactivity-utilities.html#isproxy}
@ -356,8 +356,8 @@ export function isProxy(value: any): boolean {
* Returns the raw, original object of a Vue-created proxy. * Returns the raw, original object of a Vue-created proxy.
* *
* `toRaw()` can return the original object from proxies created by * `toRaw()` can return the original object from proxies created by
* {@link reactive()}, {@link readonly()}, {@link shallowReactive()} or * {@link reactive}, {@link readonly}, {@link shallowReactive} or
* {@link shallowReadonly()}. * {@link shallowReadonly}.
* *
* This is an escape hatch that can be used to temporarily read without * This is an escape hatch that can be used to temporarily read without
* incurring proxy access / tracking overhead or write without triggering * incurring proxy access / tracking overhead or write without triggering
@ -397,7 +397,7 @@ export type Raw<T> = T & { [RawSymbol]?: true }
* ``` * ```
* *
* **Warning:** `markRaw()` together with the shallow APIs such as * **Warning:** `markRaw()` together with the shallow APIs such as
* {@link shallowReactive()} allow you to selectively opt-out of the default * {@link shallowReactive} allow you to selectively opt-out of the default
* deep reactive/readonly conversion and embed raw, non-proxied objects in your * deep reactive/readonly conversion and embed raw, non-proxied objects in your
* state graph. * state graph.
* *

View File

@ -67,7 +67,7 @@ export type ShallowRef<T = any, S = T> = Ref<T, S> & {
} }
/** /**
* Shallow version of {@link ref()}. * Shallow version of {@link ref}.
* *
* @example * @example
* ```js * ```js
@ -229,7 +229,7 @@ export function unref<T>(ref: MaybeRef<T> | ComputedRef<T>): T {
/** /**
* Normalizes values / refs / getters to values. * Normalizes values / refs / getters to values.
* This is similar to {@link unref()}, except that it also normalizes getters. * This is similar to {@link unref}, except that it also normalizes getters.
* If the argument is a getter, it will be invoked and its return value will * If the argument is a getter, it will be invoked and its return value will
* be returned. * be returned.
* *
@ -331,7 +331,7 @@ export type ToRefs<T = any> = {
/** /**
* Converts a reactive object to a plain object where each property of the * Converts a reactive object to a plain object where each property of the
* resulting object is a ref pointing to the corresponding property of the * resulting object is a ref pointing to the corresponding property of the
* original object. Each individual ref is created using {@link toRef()}. * original object. Each individual ref is created using {@link toRef}.
* *
* @param object - Reactive object to be made into an object of linked refs. * @param object - Reactive object to be made into an object of linked refs.
* @see {@link https://vuejs.org/api/reactivity-utilities.html#torefs} * @see {@link https://vuejs.org/api/reactivity-utilities.html#torefs}

View File

@ -213,7 +213,7 @@ export function watch(
const scope = getCurrentScope() const scope = getCurrentScope()
const watchHandle: WatchHandle = () => { const watchHandle: WatchHandle = () => {
effect.stop() effect.stop()
if (scope) { if (scope && scope.active) {
remove(scope.effects, effect) remove(scope.effects, effect)
} }
} }
@ -273,11 +273,11 @@ export function watch(
} }
} }
oldValue = newValue
call call
? call(cb!, WatchErrorCodes.WATCH_CALLBACK, args) ? call(cb!, WatchErrorCodes.WATCH_CALLBACK, args)
: // @ts-expect-error : // @ts-expect-error
cb!(...args) cb!(...args)
oldValue = newValue
} finally { } finally {
activeWatcher = currentWatcher activeWatcher = currentWatcher
} }

View File

@ -25,13 +25,13 @@ import {
} from '@vue/runtime-test' } from '@vue/runtime-test'
import { import {
type DebuggerEvent, type DebuggerEvent,
EffectFlags,
ITERATE_KEY, ITERATE_KEY,
type Ref, type Ref,
type ShallowRef, type ShallowRef,
TrackOpTypes, TrackOpTypes,
TriggerOpTypes, TriggerOpTypes,
effectScope, effectScope,
onScopeDispose,
shallowReactive, shallowReactive,
shallowRef, shallowRef,
toRef, toRef,
@ -1341,7 +1341,7 @@ describe('api: watch', () => {
await nextTick() await nextTick()
await nextTick() await nextTick()
expect(instance!.scope.effects[0].flags & EffectFlags.ACTIVE).toBeFalsy() expect(instance!.scope.effects.length).toBe(0)
}) })
test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => { test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => {
@ -1983,4 +1983,31 @@ describe('api: watch', () => {
expect(spy1).toHaveBeenCalled() expect(spy1).toHaveBeenCalled()
expect(spy2).toHaveBeenCalled() expect(spy2).toHaveBeenCalled()
}) })
// #12631
test('this.$watch w/ onScopeDispose', () => {
const onCleanup = vi.fn()
const toggle = ref(true)
const Comp = defineComponent({
render() {},
created(this: any) {
this.$watch(
() => 1,
function () {},
)
onScopeDispose(onCleanup)
},
})
const App = defineComponent({
render() {
return toggle.value ? h(Comp) : null
},
})
const root = nodeOps.createElement('div')
createApp(App).mount(root)
expect(onCleanup).toBeCalledTimes(0)
})
}) })

View File

@ -324,4 +324,98 @@ describe('component: slots', () => {
'Slot "default" invoked outside of the render function', 'Slot "default" invoked outside of the render function',
).not.toHaveBeenWarned() ).not.toHaveBeenWarned()
}) })
test('basic warn', () => {
const Comp = {
setup(_: any, { slots }: any) {
slots.default && slots.default()
return () => null
},
}
const App = {
setup() {
return () => h(Comp, () => h('div'))
},
}
createApp(App).mount(nodeOps.createElement('div'))
expect(
'Slot "default" invoked outside of the render function',
).toHaveBeenWarned()
})
test('basic warn when mounting another app in setup', () => {
const Comp = {
setup(_: any, { slots }: any) {
slots.default?.()
return () => null
},
}
const mountComp = () => {
createApp({
setup() {
return () => h(Comp, () => 'msg')
},
}).mount(nodeOps.createElement('div'))
}
const App = {
setup() {
mountComp()
return () => null
},
}
createApp(App).mount(nodeOps.createElement('div'))
expect(
'Slot "default" invoked outside of the render function',
).toHaveBeenWarned()
})
test('should not warn when render in setup', () => {
const container = {
setup(_: any, { slots }: any) {
return () => slots.default && slots.default()
},
}
const comp = h(container, null, () => h('div'))
const App = {
setup() {
render(h(comp), nodeOps.createElement('div'))
return () => null
},
}
createApp(App).mount(nodeOps.createElement('div'))
expect(
'Slot "default" invoked outside of the render function',
).not.toHaveBeenWarned()
})
test('basic warn when render in setup', () => {
const container = {
setup(_: any, { slots }: any) {
slots.default && slots.default()
return () => null
},
}
const comp = h(container, null, () => h('div'))
const App = {
setup() {
render(h(comp), nodeOps.createElement('div'))
return () => null
},
}
createApp(App).mount(nodeOps.createElement('div'))
expect(
'Slot "default" invoked outside of the render function',
).toHaveBeenWarned()
})
}) })

View File

@ -1198,4 +1198,51 @@ describe('BaseTransition', () => {
test('should not error on KeepAlive w/ function children', () => { test('should not error on KeepAlive w/ function children', () => {
expect(() => mount({}, () => () => h('div'), true)).not.toThrow() expect(() => mount({}, () => () => h('div'), true)).not.toThrow()
}) })
// #12465
test('mode: "out-in" w/ KeepAlive + fallthrough attrs (prod mode)', async () => {
__DEV__ = false
async function testOutIn({ trueBranch, falseBranch }: ToggleOptions) {
const toggle = ref(true)
const { props, cbs } = mockProps({ mode: 'out-in' }, true)
const root = nodeOps.createElement('div')
const App = {
render() {
return h(
BaseTransition,
{
...props,
class: 'test',
},
() =>
h(KeepAlive, null, toggle.value ? trueBranch() : falseBranch()),
)
},
}
render(h(App), root)
expect(serializeInner(root)).toBe(`<div class="test">0</div>`)
// trigger toggle
toggle.value = false
await nextTick()
expect(props.onBeforeLeave).toHaveBeenCalledTimes(1)
expect(serialize((props.onBeforeLeave as any).mock.calls[0][0])).toBe(
`<div class="test">0</div>`,
)
expect(props.onLeave).toHaveBeenCalledTimes(1)
expect(serialize((props.onLeave as any).mock.calls[0][0])).toBe(
`<div class="test">0</div>`,
)
expect(props.onAfterLeave).not.toHaveBeenCalled()
// enter should not have started
expect(props.onBeforeEnter).not.toHaveBeenCalled()
expect(props.onEnter).not.toHaveBeenCalled()
expect(props.onAfterEnter).not.toHaveBeenCalled()
cbs.doneLeave[`<div class="test">0</div>`]()
expect(serializeInner(root)).toBe(`<span class="test">0</span>`)
}
await runTestWithKeepAlive(testOutIn)
__DEV__ = true
})
}) })

View File

@ -10,14 +10,29 @@ import {
markRaw, markRaw,
nextTick, nextTick,
nodeOps, nodeOps,
onMounted,
h as originalH, h as originalH,
ref, ref,
render, render,
serialize,
serializeInner, serializeInner,
useModel,
withDirectives, withDirectives,
} from '@vue/runtime-test' } from '@vue/runtime-test'
import { Fragment, createCommentVNode, createVNode } from '../../src/vnode' import {
Fragment,
createBlock,
createCommentVNode,
createTextVNode,
createVNode,
openBlock,
} from '../../src/vnode'
import { toDisplayString } from '@vue/shared'
import { compile, createApp as createDOMApp, render as domRender } from 'vue' import { compile, createApp as createDOMApp, render as domRender } from 'vue'
import type { HMRRuntime } from '../../src/hmr'
declare var __VUE_HMR_RUNTIME__: HMRRuntime
const { rerender, createRecord } = __VUE_HMR_RUNTIME__
describe('renderer: teleport', () => { describe('renderer: teleport', () => {
describe('eager mode', () => { describe('eager mode', () => {
@ -87,6 +102,105 @@ describe('renderer: teleport', () => {
`</div>`, `</div>`,
) )
}) })
test('update before mounted with defer', async () => {
const root = document.createElement('div')
document.body.appendChild(root)
const show = ref(false)
const foo = ref('foo')
const Header = {
props: { foo: String },
setup(props: any) {
return () => h('div', props.foo)
},
}
const Footer = {
setup() {
foo.value = 'bar'
return () => h('div', 'Footer')
},
}
createDOMApp({
render() {
return show.value
? [
h(
Teleport,
{ to: '#targetId', defer: true },
h(Header, { foo: foo.value }),
),
h(Footer),
h('div', { id: 'targetId' }),
]
: [h('div')]
},
}).mount(root)
expect(root.innerHTML).toMatchInlineSnapshot(`"<div></div>"`)
show.value = true
await nextTick()
expect(root.innerHTML).toMatchInlineSnapshot(
`"<!--teleport start--><!--teleport end--><div>Footer</div><div id="targetId"><div>bar</div></div>"`,
)
})
// #13349
test('handle deferred teleport updates before and after mount', async () => {
const root = document.createElement('div')
document.body.appendChild(root)
const show = ref(false)
const data2 = ref('2')
const data3 = ref('3')
const Comp = {
props: {
modelValue: {},
modelModifiers: {},
},
emits: ['update:modelValue'],
setup(props: any) {
const data2 = useModel(props, 'modelValue')
data2.value = '2+'
return () => h('span')
},
}
createDOMApp({
setup() {
setTimeout(() => (show.value = true), 5)
setTimeout(() => (data3.value = '3+'), 10)
},
render() {
return h(Fragment, null, [
h('span', { id: 'targetId001' }),
show.value
? h(Fragment, null, [
h(Teleport, { to: '#targetId001', defer: true }, [
createTextVNode(String(data3.value)),
]),
h(Comp, {
modelValue: data2.value,
'onUpdate:modelValue': (event: any) =>
(data2.value = event),
}),
])
: createCommentVNode('v-if'),
])
},
}).mount(root)
expect(root.innerHTML).toMatchInlineSnapshot(
`"<span id="targetId001"></span><!--v-if-->"`,
)
await new Promise(r => setTimeout(r, 10))
expect(root.innerHTML).toMatchInlineSnapshot(
`"<span id="targetId001">3+</span><!--teleport start--><!--teleport end--><span></span>"`,
)
})
}) })
function runSharedTests(deferMode: boolean) { function runSharedTests(deferMode: boolean) {
@ -200,6 +314,39 @@ describe('renderer: teleport', () => {
expect(serializeInner(target)).toBe(`teleported`) expect(serializeInner(target)).toBe(`teleported`)
}) })
test('should traverse comment node after updating in optimize mode', async () => {
const target = nodeOps.createElement('div')
const root = nodeOps.createElement('div')
const count = ref(0)
let teleport
__DEV__ = false
render(
h(() => {
teleport =
(openBlock(),
createBlock(Teleport, { to: target }, [
createCommentVNode('comment in teleport'),
]))
return h('div', null, [
createTextVNode(toDisplayString(count.value)),
teleport,
])
}),
root,
)
const commentNode = teleport!.children[0].el
expect(serializeInner(root)).toBe(`<div>0</div>`)
expect(serializeInner(target)).toBe(`<!--comment in teleport-->`)
expect(serialize(commentNode)).toBe(`<!--comment in teleport-->`)
count.value = 1
await nextTick()
__DEV__ = true
expect(serializeInner(root)).toBe(`<div>1</div>`)
expect(teleport!.children[0].el).toBe(commentNode)
})
test('should remove children when unmounted', () => { test('should remove children when unmounted', () => {
const target = nodeOps.createElement('div') const target = nodeOps.createElement('div')
const root = nodeOps.createElement('div') const root = nodeOps.createElement('div')
@ -226,6 +373,34 @@ describe('renderer: teleport', () => {
testUnmount({ to: null, disabled: true }) testUnmount({ to: null, disabled: true })
}) })
// #10747
test('should unmount correctly when using top level comment in teleport', async () => {
const target = nodeOps.createElement('div')
const root = nodeOps.createElement('div')
const count = ref(0)
__DEV__ = false
render(
h(() => {
return h('div', null, [
createTextVNode(toDisplayString(count.value)),
(openBlock(),
createBlock(Teleport, { to: target }, [
createCommentVNode('comment in teleport'),
])),
])
}),
root,
)
count.value = 1
await nextTick()
__DEV__ = true
render(null, root)
expect(root.children.length).toBe(0)
})
test('component with multi roots should be removed when unmounted', () => { test('component with multi roots should be removed when unmounted', () => {
const target = nodeOps.createElement('div') const target = nodeOps.createElement('div')
const root = nodeOps.createElement('div') const root = nodeOps.createElement('div')
@ -698,4 +873,56 @@ describe('renderer: teleport', () => {
expect(tRefInMounted).toBe(target.children[1]) expect(tRefInMounted).toBe(target.children[1])
}) })
} }
test('handle update and hmr rerender', async () => {
const target = document.createElement('div')
const root = document.createElement('div')
const Comp = {
setup() {
const cls = ref('foo')
onMounted(() => {
// trigger update
cls.value = 'bar'
})
return { cls, target }
},
template: `
<Teleport :to="target">
<div :class="cls">
<div>
<slot></slot>
</div>
</div>
</Teleport>
`,
}
const appId = 'test-app-id'
const App = {
__hmrId: appId,
components: { Comp },
render() {
return originalH(Comp, null, { default: () => originalH('div', 'foo') })
},
}
createRecord(appId, App)
domRender(originalH(App), root)
expect(target.innerHTML).toBe(
'<div class="foo"><div><div>foo</div></div></div>',
)
await nextTick()
expect(target.innerHTML).toBe(
'<div class="bar"><div><div>foo</div></div></div>',
)
rerender(appId, () =>
originalH(Comp, null, { default: () => originalH('div', 'bar') }),
)
await nextTick()
expect(target.innerHTML).toBe(
'<div class="bar"><div><div>bar</div></div></div>',
)
})
}) })

View File

@ -1,4 +1,10 @@
import { isReactive, reactive, shallowReactive } from '../../src/index' import {
effect,
isReactive,
reactive,
readonly,
shallowReactive,
} from '../../src/index'
import { renderList } from '../../src/helpers/renderList' import { renderList } from '../../src/helpers/renderList'
describe('renderList', () => { describe('renderList', () => {
@ -65,4 +71,31 @@ describe('renderList', () => {
const shallowReactiveArray = shallowReactive([{ foo: 1 }]) const shallowReactiveArray = shallowReactive([{ foo: 1 }])
expect(renderList(shallowReactiveArray, isReactive)).toEqual([false]) expect(renderList(shallowReactiveArray, isReactive)).toEqual([false])
}) })
it('should not allow mutation', () => {
const arr = readonly(reactive([{ foo: 1 }]))
expect(
renderList(arr, item => {
;(item as any).foo = 0
return item.foo
}),
).toEqual([1])
expect(
`Set operation on key "foo" failed: target is readonly.`,
).toHaveBeenWarned()
})
it('should trigger effect for deep mutations in readonly reactive arrays', () => {
const arr = reactive([{ foo: 1 }])
const readonlyArr = readonly(arr)
let dummy
effect(() => {
dummy = renderList(readonlyArr, item => item.foo)
})
expect(dummy).toEqual([1])
arr[0].foo = 2
expect(dummy).toEqual([2])
})
}) })

View File

@ -21,6 +21,7 @@ import {
h, h,
nextTick, nextTick,
onMounted, onMounted,
onServerPrefetch,
openBlock, openBlock,
reactive, reactive,
ref, ref,
@ -31,10 +32,13 @@ import {
withCtx, withCtx,
withDirectives, withDirectives,
} from '@vue/runtime-dom' } from '@vue/runtime-dom'
import type { HMRRuntime } from '../src/hmr'
import { type SSRContext, renderToString } from '@vue/server-renderer' import { type SSRContext, renderToString } from '@vue/server-renderer'
import { PatchFlags, normalizeStyle } from '@vue/shared' import { PatchFlags, normalizeStyle } from '@vue/shared'
import { vShowOriginalDisplay } from '../../runtime-dom/src/directives/vShow' import { vShowOriginalDisplay } from '../../runtime-dom/src/directives/vShow'
import { expect } from 'vitest'
declare var __VUE_HMR_RUNTIME__: HMRRuntime
const { createRecord, reload } = __VUE_HMR_RUNTIME__
function mountWithHydration(html: string, render: () => any) { function mountWithHydration(html: string, render: () => any) {
const container = document.createElement('div') const container = document.createElement('div')
@ -518,6 +522,45 @@ describe('SSR hydration', () => {
) )
}) })
test('with data-allow-mismatch component when using onServerPrefetch', async () => {
const Comp = {
template: `
<div>Comp2</div>
`,
}
let foo: any
const App = {
setup() {
const flag = ref(true)
foo = () => {
flag.value = false
}
onServerPrefetch(() => (flag.value = false))
return { flag }
},
components: {
Comp,
},
template: `
<span data-allow-mismatch>
<Comp v-if="flag"></Comp>
</span>
`,
}
// hydrate
const container = document.createElement('div')
container.innerHTML = await renderToString(h(App))
createSSRApp(App).mount(container)
expect(container.innerHTML).toBe(
'<span data-allow-mismatch=""><div>Comp2</div></span>',
)
foo()
await nextTick()
expect(container.innerHTML).toBe(
'<span data-allow-mismatch=""><!--v-if--></span>',
)
})
test('Teleport unmount (full integration)', async () => { test('Teleport unmount (full integration)', async () => {
const Comp1 = { const Comp1 = {
template: ` template: `
@ -1284,6 +1327,84 @@ describe('SSR hydration', () => {
resolve({}) resolve({})
}) })
//#12362
test('nested async wrapper', async () => {
const Toggle = defineAsyncComponent(
() =>
new Promise(r => {
r(
defineComponent({
setup(_, { slots }) {
const show = ref(false)
onMounted(() => {
nextTick(() => {
show.value = true
})
})
return () =>
withDirectives(
h('div', null, [renderSlot(slots, 'default')]),
[[vShow, show.value]],
)
},
}) as any,
)
}),
)
const Wrapper = defineAsyncComponent(() => {
return new Promise(r => {
r(
defineComponent({
render(this: any) {
return renderSlot(this.$slots, 'default')
},
}) as any,
)
})
})
const count = ref(0)
const fn = vi.fn()
const Child = {
setup() {
onMounted(() => {
fn()
count.value++
})
return () => h('div', count.value)
},
}
const App = {
render() {
return h(Toggle, null, {
default: () =>
h(Wrapper, null, {
default: () =>
h(Wrapper, null, {
default: () => h(Child),
}),
}),
})
},
}
const root = document.createElement('div')
root.innerHTML = await renderToString(h(App))
expect(root.innerHTML).toMatchInlineSnapshot(
`"<div style="display:none;"><!--[--><!--[--><!--[--><div>0</div><!--]--><!--]--><!--]--></div>"`,
)
createSSRApp(App).mount(root)
await nextTick()
await nextTick()
expect(root.innerHTML).toMatchInlineSnapshot(
`"<div style=""><!--[--><!--[--><!--[--><div>1</div><!--]--><!--]--><!--]--></div>"`,
)
expect(fn).toBeCalledTimes(1)
})
test('unmount async wrapper before load (fragment)', async () => { test('unmount async wrapper before load (fragment)', async () => {
let resolve: any let resolve: any
const AsyncComp = defineAsyncComponent( const AsyncComp = defineAsyncComponent(
@ -1533,6 +1654,29 @@ describe('SSR hydration', () => {
expect(`mismatch`).not.toHaveBeenWarned() expect(`mismatch`).not.toHaveBeenWarned()
}) })
test('transition appear work with pre-existing class', () => {
const { vnode, container } = mountWithHydration(
`<template><div class="foo">foo</div></template>`,
() =>
h(
Transition,
{ appear: true },
{
default: () => h('div', { class: 'foo' }, 'foo'),
},
),
)
expect(container.firstChild).toMatchInlineSnapshot(`
<div
class="foo v-enter-from v-enter-active"
>
foo
</div>
`)
expect(vnode.el).toBe(container.firstChild)
expect(`mismatch`).not.toHaveBeenWarned()
})
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(
@ -1725,6 +1869,60 @@ describe('SSR hydration', () => {
} }
}) })
test('hmr reload child wrapped in KeepAlive', async () => {
const id = 'child-reload'
const Child = {
__hmrId: id,
template: `<div>foo</div>`,
}
createRecord(id, Child)
const appId = 'test-app-id'
const App = {
__hmrId: appId,
components: { Child },
template: `
<div>
<KeepAlive>
<Child />
</KeepAlive>
</div>
`,
}
const root = document.createElement('div')
root.innerHTML = await renderToString(h(App))
createSSRApp(App).mount(root)
expect(root.innerHTML).toBe('<div><div>foo</div></div>')
reload(id, {
__hmrId: id,
template: `<div>bar</div>`,
})
await nextTick()
expect(root.innerHTML).toBe('<div><div>bar</div></div>')
})
test('hmr root reload', async () => {
const appId = 'test-app-id'
const App = {
__hmrId: appId,
template: `<div>foo</div>`,
}
const root = document.createElement('div')
root.innerHTML = await renderToString(h(App))
createSSRApp(App).mount(root)
expect(root.innerHTML).toBe('<div>foo</div>')
reload(appId, {
__hmrId: appId,
template: `<div>bar</div>`,
})
await nextTick()
expect(root.innerHTML).toBe('<div>bar</div>')
})
describe('mismatch handling', () => { describe('mismatch handling', () => {
test('text node', () => { test('text node', () => {
const { container } = mountWithHydration(`foo`, () => 'bar') const { container } = mountWithHydration(`foo`, () => 'bar')

View File

@ -17,6 +17,7 @@ import {
serializeInner as inner, serializeInner as inner,
nextTick, nextTick,
nodeOps, nodeOps,
onBeforeMount,
onBeforeUnmount, onBeforeUnmount,
onUnmounted, onUnmounted,
openBlock, openBlock,
@ -860,6 +861,114 @@ describe('renderer: optimized mode', () => {
expect(inner(root)).toBe('<div><div>true</div></div>') expect(inner(root)).toBe('<div><div>true</div></div>')
}) })
// #13305
test('patch Suspense nested in list nodes in optimized mode', async () => {
const deps: Promise<any>[] = []
const Item = {
props: {
someId: { type: Number, required: true },
},
async setup(props: any) {
const p = new Promise(resolve => setTimeout(resolve, 1))
deps.push(p)
await p
return () => (
openBlock(),
createElementBlock('li', null, [
createElementVNode(
'p',
null,
String(props.someId),
PatchFlags.TEXT,
),
])
)
},
}
const list = ref([1, 2, 3])
const App = {
setup() {
return () => (
openBlock(),
createElementBlock(
Fragment,
null,
[
createElementVNode(
'p',
null,
JSON.stringify(list.value),
PatchFlags.TEXT,
),
createElementVNode('ol', null, [
(openBlock(),
createBlock(SuspenseImpl, null, {
fallback: withCtx(() => [
createElementVNode('li', null, 'Loading…'),
]),
default: withCtx(() => [
(openBlock(true),
createElementBlock(
Fragment,
null,
renderList(list.value, id => {
return (
openBlock(),
createBlock(
Item,
{
key: id,
'some-id': id,
},
null,
PatchFlags.PROPS,
['some-id'],
)
)
}),
PatchFlags.KEYED_FRAGMENT,
)),
]),
_: 1 /* STABLE */,
})),
]),
],
PatchFlags.STABLE_FRAGMENT,
)
)
},
}
const app = createApp(App)
app.mount(root)
expect(inner(root)).toBe(`<p>[1,2,3]</p>` + `<ol><li>Loading…</li></ol>`)
await Promise.all(deps)
await nextTick()
expect(inner(root)).toBe(
`<p>[1,2,3]</p>` +
`<ol>` +
`<li><p>1</p></li>` +
`<li><p>2</p></li>` +
`<li><p>3</p></li>` +
`</ol>`,
)
list.value = [3, 1, 2]
await nextTick()
expect(inner(root)).toBe(
`<p>[3,1,2]</p>` +
`<ol>` +
`<li><p>3</p></li>` +
`<li><p>1</p></li>` +
`<li><p>2</p></li>` +
`</ol>`,
)
})
// #4183 // #4183
test('should not take unmount children fast path /w Suspense', async () => { test('should not take unmount children fast path /w Suspense', async () => {
const show = ref(true) const show = ref(true)
@ -1199,7 +1308,7 @@ describe('renderer: optimized mode', () => {
createBlock('div', null, [ createBlock('div', null, [
createVNode('div', null, [ createVNode('div', null, [
cache[0] || cache[0] ||
(setBlockTracking(-1), (setBlockTracking(-1, true),
((cache[0] = createVNode('div', null, [ ((cache[0] = createVNode('div', null, [
createVNode(Child), createVNode(Child),
])).cacheIndex = 0), ])).cacheIndex = 0),
@ -1233,4 +1342,64 @@ describe('renderer: optimized mode', () => {
expect(inner(root)).toBe('<!--v-if-->') expect(inner(root)).toBe('<!--v-if-->')
expect(spyUnmounted).toHaveBeenCalledTimes(2) expect(spyUnmounted).toHaveBeenCalledTimes(2)
}) })
// #12371
test('unmount children when the user calls a compiled slot', async () => {
const beforeMountSpy = vi.fn()
const beforeUnmountSpy = vi.fn()
const Child = {
setup() {
onBeforeMount(beforeMountSpy)
onBeforeUnmount(beforeUnmountSpy)
return () => 'child'
},
}
const Wrapper = {
setup(_: any, { slots }: SetupContext) {
return () => (
openBlock(),
createElementBlock('section', null, [
(openBlock(),
createElementBlock('div', { key: 1 }, [
createTextVNode(slots.header!() ? 'foo' : 'bar', 1 /* TEXT */),
renderSlot(slots, 'content'),
])),
])
)
},
}
const show = ref(false)
const app = createApp({
render() {
return show.value
? (openBlock(),
createBlock(Wrapper, null, {
header: withCtx(() => [createVNode({})]),
content: withCtx(() => [createVNode(Child)]),
_: 1,
}))
: createCommentVNode('v-if', true)
},
})
app.mount(root)
expect(inner(root)).toMatchInlineSnapshot(`"<!--v-if-->"`)
expect(beforeMountSpy).toHaveBeenCalledTimes(0)
expect(beforeUnmountSpy).toHaveBeenCalledTimes(0)
show.value = true
await nextTick()
expect(inner(root)).toMatchInlineSnapshot(
`"<section><div>foochild</div></section>"`,
)
expect(beforeMountSpy).toHaveBeenCalledTimes(1)
show.value = false
await nextTick()
expect(inner(root)).toBe('<!--v-if-->')
expect(beforeUnmountSpy).toHaveBeenCalledTimes(1)
})
}) })

View File

@ -1,4 +1,6 @@
import { import {
KeepAlive,
defineAsyncComponent,
defineComponent, defineComponent,
h, h,
nextTick, nextTick,
@ -538,4 +540,68 @@ describe('api: template refs', () => {
'<div><div>[object Object],[object Object]</div><ul><li>2</li><li>3</li></ul></div>', '<div><div>[object Object],[object Object]</div><ul><li>2</li><li>3</li></ul></div>',
) )
}) })
test('with async component which nested in KeepAlive', async () => {
const AsyncComp = defineAsyncComponent(
() =>
new Promise(resolve =>
setTimeout(() =>
resolve(
defineComponent({
setup(_, { expose }) {
expose({
name: 'AsyncComp',
})
return () => h('div')
},
}) as any,
),
),
),
)
const Comp = defineComponent({
setup(_, { expose }) {
expose({
name: 'Comp',
})
return () => h('div')
},
})
const toggle = ref(false)
const instanceRef = ref<any>(null)
const App = {
render: () => {
return h(KeepAlive, () =>
toggle.value
? h(AsyncComp, { ref: instanceRef })
: h(Comp, { ref: instanceRef }),
)
},
}
const root = nodeOps.createElement('div')
render(h(App), root)
expect(instanceRef.value.name).toBe('Comp')
// switch to async component
toggle.value = true
await nextTick()
expect(instanceRef.value).toBe(null)
await new Promise(r => setTimeout(r))
expect(instanceRef.value.name).toBe('AsyncComp')
// switch back to normal component
toggle.value = false
await nextTick()
expect(instanceRef.value.name).toBe('Comp')
// switch to async component again
toggle.value = true
await nextTick()
expect(instanceRef.value.name).toBe('AsyncComp')
})
}) })

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