chore: Merge branch 'vapor' into edison/feat/vaporAsyncComponent
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details

This commit is contained in:
daiwei 2025-06-20 14:09:36 +08:00
commit 637b3cb9b3
133 changed files with 4064 additions and 2248 deletions

View File

@ -290,27 +290,39 @@ This is made possible via several configurations:
```mermaid ```mermaid
flowchart LR flowchart LR
vue["vue"]
compiler-sfc["@vue/compiler-sfc"] compiler-sfc["@vue/compiler-sfc"]
compiler-dom["@vue/compiler-dom"] compiler-dom["@vue/compiler-dom"]
compiler-vapor["@vue/compiler-vapor"]
compiler-core["@vue/compiler-core"] compiler-core["@vue/compiler-core"]
vue["vue"]
runtime-dom["@vue/runtime-dom"] runtime-dom["@vue/runtime-dom"]
runtime-vapor["@vue/runtime-vapor"]
runtime-core["@vue/runtime-core"] runtime-core["@vue/runtime-core"]
reactivity["@vue/reactivity"] reactivity["@vue/reactivity"]
subgraph "Runtime Packages" subgraph "Runtime Packages"
runtime-dom --> runtime-core runtime-dom --> runtime-core
runtime-vapor --> runtime-core
runtime-core --> reactivity runtime-core --> reactivity
end end
subgraph "Compiler Packages" subgraph "Compiler Packages"
compiler-sfc --> compiler-core compiler-sfc --> compiler-core
compiler-sfc --> compiler-dom compiler-sfc --> compiler-dom
compiler-sfc --> compiler-vapor
compiler-dom --> compiler-core compiler-dom --> compiler-core
compiler-vapor --> compiler-core
end end
vue --> compiler-sfc
vue ---> compiler-dom vue ---> compiler-dom
vue --> runtime-dom vue --> runtime-dom
vue --> compiler-vapor
vue --> runtime-vapor
%% Highlight class
classDef highlight stroke:#35eb9a,stroke-width:3px;
class compiler-vapor,runtime-vapor highlight;
``` ```
There are some rules to follow when importing across package boundaries: There are some rules to follow when importing across package boundaries:

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

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@v7 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@v7 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
@ -114,7 +114,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

@ -1 +1 @@
20 22.14.0

View File

@ -1,3 +1,41 @@
## [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) ## [3.5.13](https://github.com/vuejs/core/compare/v3.5.12...v3.5.13) (2024-11-15)
@ -8,7 +46,7 @@
* **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) * **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)) * **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) * **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)
* **reactiivty:** 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:** 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) * **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 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)) * **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))

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

@ -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.13", "version": "3.5.14",
"packageManager": "pnpm@9.15.4", "packageManager": "pnpm@10.9.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "node scripts/dev.js", "dev": "node scripts/dev.js",
@ -65,63 +65,52 @@
"@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.2", "@rollup/plugin-commonjs": "^28.0.3",
"@rollup/plugin-json": "^6.1.0", "@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-replace": "5.0.4", "@rollup/plugin-replace": "5.0.4",
"@swc/core": "^1.10.8", "@swc/core": "^1.11.24",
"@types/hash-sum": "^1.0.2", "@types/hash-sum": "^1.0.2",
"@types/node": "^22.10.7", "@types/node": "^22.14.1",
"@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": "^3.0.2",
"@vitest/ui": "^3.0.2", "@vitest/ui": "^3.0.2",
"@vitest/coverage-v8": "^3.1.3",
"@vitest/eslint-plugin": "^1.1.44",
"@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.2", "esbuild": "^0.25.4",
"esbuild-plugin-polyfill-node": "^0.3.0", "esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^9.18.0", "eslint": "^9.25.1",
"eslint-plugin-import-x": "^4.6.1", "eslint-plugin-import-x": "^4.11.0",
"@vitest/eslint-plugin": "^1.1.25",
"estree-walker": "catalog:", "estree-walker": "catalog:",
"jsdom": "^26.0.0", "jsdom": "^26.1.0",
"lint-staged": "^15.4.1", "lint-staged": "^15.5.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"magic-string": "^0.30.17", "magic-string": "^0.30.17",
"markdown-table": "^3.0.4", "markdown-table": "^3.0.4",
"marked": "13.0.3", "marked": "13.0.3",
"npm-run-all2": "^7.0.2", "npm-run-all2": "^7.0.2",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"prettier": "^3.4.2", "prettier": "^3.5.3",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"pug": "^3.0.3", "pug": "^3.0.3",
"puppeteer": "~24.1.0", "puppeteer": "~24.8.2",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"rollup": "^4.31.0", "rollup": "^4.40.2",
"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.1",
"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.1", "tslib": "^2.8.1",
"typescript": "~5.6.2", "typescript": "~5.6.2",
"typescript-eslint": "^8.20.0", "typescript-eslint": "^8.31.1",
"vite": "catalog:", "vite": "catalog:",
"vitest": "^3.0.2" "vitest": "^3.1.3"
},
"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

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

@ -4,5 +4,5 @@
"isolatedDeclarations": false, "isolatedDeclarations": false,
"allowJs": true "allowJs": true
}, },
"include": ["./**/*", "../packages/*/src"] "include": ["./**/*", "../../packages/*/src"]
} }

View File

@ -182,8 +182,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

@ -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.2.1", "@vitejs/plugin-vue": "^5.2.4",
"vite": "^6.0.7" "vite": "^6.3.5"
} }
} }

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

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

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

@ -17,6 +17,7 @@ export {
createTransformContext, createTransformContext,
traverseNode, traverseNode,
createStructuralDirectiveTransform, createStructuralDirectiveTransform,
getSelfName,
type NodeTransform, type NodeTransform,
type StructuralDirectiveTransform, type StructuralDirectiveTransform,
type DirectiveTransform, type DirectiveTransform,

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

@ -123,6 +123,11 @@ export interface TransformContext
filters?: Set<string> filters?: Set<string>
} }
export function getSelfName(filename: string): string | null {
const nameMatch = filename.replace(/\?.*$/, '').match(/([^/\\]+)\.\w+$/)
return nameMatch ? capitalize(camelize(nameMatch[1])) : null
}
export function createTransformContext( export function createTransformContext(
root: RootNode, root: RootNode,
{ {
@ -150,11 +155,10 @@ export function createTransformContext(
compatConfig, compatConfig,
}: TransformOptions, }: TransformOptions,
): TransformContext { ): TransformContext {
const nameMatch = filename.replace(/\?.*$/, '').match(/([^/\\]+)\.\w+$/)
const context: TransformContext = { const context: TransformContext = {
// options // options
filename, filename,
selfName: nameMatch && capitalize(camelize(nameMatch[1])), selfName: getSelfName(filename),
prefixIdentifiers, prefixIdentifiers,
hoistStatic, hoistStatic,
hmr, hmr,

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

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

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

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

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

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

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compiler-sfc", "name": "@vue/compiler-sfc",
"version": "3.5.13", "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",
@ -50,7 +50,7 @@
"@vue/shared": "workspace:*", "@vue/shared": "workspace:*",
"estree-walker": "catalog:", "estree-walker": "catalog:",
"magic-string": "catalog:", "magic-string": "catalog:",
"postcss": "^8.5.1", "postcss": "^8.5.3",
"source-map-js": "catalog:" "source-map-js": "catalog:"
}, },
"devDependencies": { "devDependencies": {
@ -61,8 +61,8 @@
"merge-source-map": "^1.1.0", "merge-source-map": "^1.1.0",
"minimatch": "~10.0.1", "minimatch": "~10.0.1",
"postcss-modules": "^6.0.1", "postcss-modules": "^6.0.1",
"postcss-selector-parser": "^7.0.0", "postcss-selector-parser": "^7.1.0",
"pug": "^3.0.3", "pug": "^3.0.3",
"sass": "^1.83.4" "sass": "^1.86.3"
} }
} }

View File

@ -1120,6 +1120,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,
) )
) { ) {

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

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

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

@ -149,7 +149,7 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
`; `;
exports[`compile > directives > v-pre > should not affect siblings after it 1`] = ` exports[`compile > directives > v-pre > should not affect siblings after it 1`] = `
"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, child as _child, toDisplayString as _toDisplayString, setText as _setText, setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; "import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, child as _child, setProp as _setProp, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("<div :id=\\"foo\\"><Comp></Comp>{{ bar }}</div>") const t0 = _template("<div :id=\\"foo\\"><Comp></Comp>{{ bar }}</div>")
const t1 = _template("<div> </div>") const t1 = _template("<div> </div>")
@ -157,12 +157,12 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
const _component_Comp = _resolveComponent("Comp") const _component_Comp = _resolveComponent("Comp")
const n0 = t0() const n0 = t0()
const n3 = t1() const n3 = t1()
const n2 = _child(n3)
_setInsertionState(n3, 0) _setInsertionState(n3, 0)
const n1 = _createComponentWithFallback(_component_Comp) const n1 = _createComponentWithFallback(_component_Comp)
const n2 = _child(n3)
_renderEffect(() => { _renderEffect(() => {
_setText(n2, _toDisplayString(_ctx.bar))
_setProp(n3, "id", _ctx.foo) _setProp(n3, "id", _ctx.foo)
_setText(n2, _toDisplayString(_ctx.bar))
}) })
return [n0, n3] return [n0, n3]
}" }"
@ -180,7 +180,7 @@ export function render(_ctx) {
`; `;
exports[`compile > dynamic root nodes and interpolation 1`] = ` exports[`compile > dynamic root nodes and interpolation 1`] = `
"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, setProp as _setProp, renderEffect as _renderEffect, delegateEvents as _delegateEvents, template as _template } from 'vue'; "import { child as _child, setProp as _setProp, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, delegateEvents as _delegateEvents, template as _template } from 'vue';
const t0 = _template("<button> </button>", true) const t0 = _template("<button> </button>", true)
_delegateEvents("click") _delegateEvents("click")
@ -190,13 +190,47 @@ export function render(_ctx) {
n0.$evtclick = e => _ctx.handleClick(e) n0.$evtclick = e => _ctx.handleClick(e)
_renderEffect(() => { _renderEffect(() => {
const _count = _ctx.count const _count = _ctx.count
_setText(x0, _toDisplayString(_count) + "foo" + _toDisplayString(_count) + "foo" + _toDisplayString(_count))
_setProp(n0, "id", _count) _setProp(n0, "id", _count)
_setText(x0, _toDisplayString(_count) + "foo" + _toDisplayString(_count) + "foo" + _toDisplayString(_count))
}) })
return n0 return n0
}" }"
`; `;
exports[`compile > execution order > basic 1`] = `
"import { child as _child, setProp as _setProp, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("<div> </div>", true)
export function render(_ctx) {
const n0 = t0()
const x0 = _child(n0)
_renderEffect(() => {
_setProp(n0, "id", _ctx.foo)
_setText(x0, _toDisplayString(_ctx.bar))
})
return n0
}"
`;
exports[`compile > execution order > with v-once 1`] = `
"import { child as _child, next as _next, nthChild as _nthChild, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("<div><span> </span> <br> </div>", true)
export function render(_ctx) {
const n3 = t0()
const n0 = _child(n3)
const n1 = _next(n0)
const n2 = _nthChild(n3, 3)
const x0 = _child(n0)
_setText(x0, _toDisplayString(_ctx.foo))
_renderEffect(() => {
_setText(n1, " " + _toDisplayString(_ctx.bar))
_setText(n2, " " + _toDisplayString(_ctx.baz))
})
return n3
}"
`;
exports[`compile > expression parsing > interpolation 1`] = ` exports[`compile > expression parsing > interpolation 1`] = `
" "
const n0 = t0() const n0 = t0()
@ -230,6 +264,30 @@ export function render(_ctx) {
}" }"
`; `;
exports[`compile > setInsertionState > next, child and nthChild should be above the setInsertionState 1`] = `
"import { resolveComponent as _resolveComponent, child as _child, next as _next, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, nthChild as _nthChild, createIf as _createIf, setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("<div></div>")
const t1 = _template("<div><div></div><!><div></div><!><div><button></button></div></div>", true)
export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp")
const n6 = t1()
const n5 = _next(_child(n6))
const n7 = _nthChild(n6, 3)
const p0 = _next(n7)
const n4 = _child(p0)
_setInsertionState(n6, n5)
const n0 = _createComponentWithFallback(_component_Comp)
_setInsertionState(n6, n7)
const n1 = _createIf(() => (true), () => {
const n3 = t0()
return n3
})
_renderEffect(() => _setProp(n4, "disabled", _ctx.foo))
return n6
}"
`;
exports[`compile > static + dynamic root 1`] = ` exports[`compile > static + dynamic root 1`] = `
"import { toDisplayString as _toDisplayString, setText as _setText, template as _template } from 'vue'; "import { toDisplayString as _toDisplayString, setText as _setText, template as _template } from 'vue';
const t0 = _template(" ") const t0 = _template(" ")

View File

@ -220,4 +220,46 @@ describe('compile', () => {
expect(code).matchSnapshot() expect(code).matchSnapshot()
}) })
}) })
describe('setInsertionState', () => {
test('next, child and nthChild should be above the setInsertionState', () => {
const code = compile(`
<div>
<div />
<Comp />
<div />
<div v-if="true" />
<div>
<button :disabled="foo" />
</div>
</div>
`)
expect(code).toMatchSnapshot()
})
})
describe('execution order', () => {
test('basic', () => {
const code = compile(`<div :id="foo">{{ bar }}</div>`)
expect(code).matchSnapshot()
expect(code).contains(
`_setProp(n0, "id", _ctx.foo)
_setText(x0, _toDisplayString(_ctx.bar))`,
)
})
test('with v-once', () => {
const code = compile(
`<div>
<span v-once>{{ foo }}</span>
{{ bar }}<br>
{{ baz }}
</div>`,
)
expect(code).matchSnapshot()
expect(code).contains(
`_setText(n1, " " + _toDisplayString(_ctx.bar))
_setText(n2, " " + _toDisplayString(_ctx.baz))`,
)
})
})
}) })

View File

@ -32,3 +32,24 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
return n0 return n0
}" }"
`; `;
exports[`compiler: expression > update expression 1`] = `
"import { child as _child, setProp as _setProp, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("<div> </div>", true)
export function render(_ctx) {
const n1 = t0()
const n0 = _child(n1)
const x1 = _child(n1)
_renderEffect(() => {
const _String = String
const _foo = _ctx.foo
_setProp(n1, "id", _String(_foo.id++))
_setProp(n1, "foo", _foo)
_setProp(n1, "bar", _ctx.bar++)
_setText(n0, _toDisplayString(_String(_foo.id++)) + " " + _toDisplayString(_foo) + " " + _toDisplayString(_ctx.bar))
_setText(x1, _toDisplayString(_String(_foo.id++)) + " " + _toDisplayString(_foo) + " " + _toDisplayString(_ctx.bar))
})
return n1
}"
`;

View File

@ -67,7 +67,6 @@ export function render(_ctx) {
const x2 = _child(n2) const x2 = _child(n2)
_renderEffect(() => { _renderEffect(() => {
const _msg = _ctx.msg const _msg = _ctx.msg
_setText(x0, _toDisplayString(_msg)) _setText(x0, _toDisplayString(_msg))
_setText(x1, _toDisplayString(_msg)) _setText(x1, _toDisplayString(_msg))
_setText(x2, _toDisplayString(_msg)) _setText(x2, _toDisplayString(_msg))

View File

@ -77,6 +77,16 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
}" }"
`; `;
exports[`compiler: element transform > component > resolve implicitly self-referencing component 1`] = `
"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
export function render(_ctx) {
const _component_Example__self = _resolveComponent("Example", true)
const n0 = _createComponentWithFallback(_component_Example__self, null, null, true)
return n0
}"
`;
exports[`compiler: element transform > component > resolve namespaced component from props bindings (inline) 1`] = ` exports[`compiler: element transform > component > resolve namespaced component from props bindings (inline) 1`] = `
" "
const n0 = _createComponent(Foo.Example, null, null, true) const n0 = _createComponent(Foo.Example, null, null, true)
@ -178,6 +188,18 @@ export function render(_ctx) {
}" }"
`; `;
exports[`compiler: element transform > component > v-for on component should not mark as single root 1`] = `
"import { createComponent as _createComponent, createFor as _createFor } from 'vue';
export function render(_ctx, $props, $emit, $attrs, $slots) {
const n0 = _createFor(() => (_ctx.items), (_for_item0) => {
const n2 = _createComponent(_ctx.Comp)
return n2
}, (item) => (item), 2)
return n0
}"
`;
exports[`compiler: element transform > component > v-on expression is a function call 1`] = ` exports[`compiler: element transform > component > v-on expression is a function call 1`] = `
"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; "import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';

View File

@ -13,6 +13,26 @@ export function render(_ctx) {
}" }"
`; `;
exports[`compiler: template ref transform > function ref 1`] = `
"import { createTemplateRefSetter as _createTemplateRefSetter, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("<div></div>", true)
export function render(_ctx) {
const _setTemplateRef = _createTemplateRefSetter()
const n0 = t0()
let r0
_renderEffect(() => {
const _foo = _ctx.foo
r0 = _setTemplateRef(n0, bar => {
_foo.value = bar
;({ baz: _ctx.baz } = bar)
console.log(_foo.value, _ctx.baz)
}, r0)
})
return n0
}"
`;
exports[`compiler: template ref transform > ref + v-for 1`] = ` exports[`compiler: template ref transform > ref + v-for 1`] = `
"import { createTemplateRefSetter as _createTemplateRefSetter, createFor as _createFor, template as _template } from 'vue'; "import { createTemplateRefSetter as _createTemplateRefSetter, createFor as _createFor, template as _template } from 'vue';
const t0 = _template("<div></div>", true) const t0 = _template("<div></div>", true)

View File

@ -55,7 +55,6 @@ export function render(_ctx) {
const _foo = _ctx.foo const _foo = _ctx.foo
const _bar = _ctx.bar const _bar = _ctx.bar
const _foo_bar_baz = _foo[_bar(_ctx.baz)] const _foo_bar_baz = _foo[_bar(_ctx.baz)]
_setProp(n0, "id", _foo_bar_baz) _setProp(n0, "id", _foo_bar_baz)
_setProp(n1, "id", _foo_bar_baz) _setProp(n1, "id", _foo_bar_baz)
_setProp(n2, "id", _bar() + _foo) _setProp(n2, "id", _bar() + _foo)
@ -75,6 +74,17 @@ export function render(_ctx) {
}" }"
`; `;
exports[`cache multiple access > not cache variable in function expression 1`] = `
"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("<div></div>", true)
export function render(_ctx) {
const n0 = t0()
_renderEffect(() => _setDynamicProps(n0, [{ foo: bar => _ctx.foo = bar }], true))
return n0
}"
`;
exports[`cache multiple access > not cache variable only used in property shorthand 1`] = ` exports[`cache multiple access > not cache variable only used in property shorthand 1`] = `
"import { setStyle as _setStyle, renderEffect as _renderEffect, template as _template } from 'vue'; "import { setStyle as _setStyle, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("<div></div>", true) const t0 = _template("<div></div>", true)
@ -96,7 +106,6 @@ export function render(_ctx) {
_renderEffect(() => { _renderEffect(() => {
const _obj = _ctx.obj const _obj = _ctx.obj
const _obj_foo_baz_obj_bar = _obj['foo']['baz'] + _obj.bar const _obj_foo_baz_obj_bar = _obj['foo']['baz'] + _obj.bar
_setProp(n0, "id", _obj_foo_baz_obj_bar) _setProp(n0, "id", _obj_foo_baz_obj_bar)
_setProp(n1, "id", _obj_foo_baz_obj_bar) _setProp(n1, "id", _obj_foo_baz_obj_bar)
}) })
@ -115,7 +124,6 @@ export function render(_ctx) {
_renderEffect(() => { _renderEffect(() => {
const _foo = _ctx.foo const _foo = _ctx.foo
const _foo_bar = _foo + _ctx.bar const _foo_bar = _foo + _ctx.bar
_setProp(n0, "id", _foo_bar) _setProp(n0, "id", _foo_bar)
_setProp(n1, "id", _foo_bar) _setProp(n1, "id", _foo_bar)
_setProp(n2, "id", _foo + _foo_bar) _setProp(n2, "id", _foo + _foo_bar)
@ -133,7 +141,6 @@ export function render(_ctx) {
const n1 = t0() const n1 = t0()
_renderEffect(() => { _renderEffect(() => {
const _foo_bar = _ctx.foo + _ctx.bar const _foo_bar = _ctx.foo + _ctx.bar
_setProp(n0, "id", _foo_bar) _setProp(n0, "id", _foo_bar)
_setProp(n1, "id", _foo_bar) _setProp(n1, "id", _foo_bar)
}) })
@ -166,7 +173,6 @@ export function render(_ctx) {
const n1 = t0() const n1 = t0()
_renderEffect(() => { _renderEffect(() => {
const _foo = _ctx.foo const _foo = _ctx.foo
_setClass(n0, _foo) _setClass(n0, _foo)
_setClass(n1, _foo) _setClass(n1, _foo)
}) })
@ -487,15 +493,13 @@ export function render(_ctx) {
_setAttr(n0, "form", _ctx.form) _setAttr(n0, "form", _ctx.form)
_setAttr(n1, "list", _ctx.list) _setAttr(n1, "list", _ctx.list)
_setAttr(n2, "type", _ctx.type) _setAttr(n2, "type", _ctx.type)
_setAttr(n3, "width", _width) _setAttr(n3, "width", _width)
_setAttr(n4, "width", _width)
_setAttr(n5, "width", _width)
_setAttr(n6, "width", _width)
_setAttr(n3, "height", _height) _setAttr(n3, "height", _height)
_setAttr(n4, "width", _width)
_setAttr(n4, "height", _height) _setAttr(n4, "height", _height)
_setAttr(n5, "width", _width)
_setAttr(n5, "height", _height) _setAttr(n5, "height", _height)
_setAttr(n6, "width", _width)
_setAttr(n6, "height", _height) _setAttr(n6, "height", _height)
}) })
return [n0, n1, n2, n3, n4, n5, n6] return [n0, n1, n2, n3, n4, n5, n6]

View File

@ -115,6 +115,21 @@ export function render(_ctx) {
}" }"
`; `;
exports[`compiler: v-for > object value, key and index 1`] = `
"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
const t0 = _template("<div> </div>", true)
export function render(_ctx) {
const n0 = _createFor(() => (_ctx.list), (_for_item0, _for_key0, _for_index0) => {
const n2 = t0()
const x2 = _child(n2)
_renderEffect(() => _setText(x2, _toDisplayString(_for_item0.value + _for_key0.value + _for_index0.value)))
return n2
}, (value, key, index) => (key))
return n0
}"
`;
exports[`compiler: v-for > v-for aliases w/ complex expressions 1`] = ` exports[`compiler: v-for > v-for aliases w/ complex expressions 1`] = `
"import { getDefaultValue as _getDefaultValue, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue'; "import { getDefaultValue as _getDefaultValue, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
const t0 = _template("<div> </div>", true) const t0 = _template("<div> </div>", true)

View File

@ -134,3 +134,29 @@ export function render(_ctx) {
return n0 return n0
}" }"
`; `;
exports[`compiler: v-if > v-if + v-if / v-else[-if] 1`] = `
"import { setInsertionState as _setInsertionState, createIf as _createIf, template as _template } from 'vue';
const t0 = _template("<span>foo</span>")
const t1 = _template("<span>bar</span>")
const t2 = _template("<span>baz</span>")
const t3 = _template("<div></div>", true)
export function render(_ctx) {
const n8 = t3()
_setInsertionState(n8)
const n0 = _createIf(() => (_ctx.foo), () => {
const n2 = t0()
return n2
})
_setInsertionState(n8)
const n3 = _createIf(() => (_ctx.bar), () => {
const n5 = t1()
return n5
}, () => {
const n7 = t2()
return n7
})
return n8
}"
`;

View File

@ -123,6 +123,18 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
}" }"
`; `;
exports[`v-on > expression with type 1`] = `
"import { delegateEvents as _delegateEvents, template as _template } from 'vue';
const t0 = _template("<div></div>", true)
_delegateEvents("click")
export function render(_ctx, $props, $emit, $attrs, $slots) {
const n0 = t0()
n0.$evtclick = e => _ctx.handleClick(e)
return n0
}"
`;
exports[`v-on > function expression w/ prefixIdentifiers: true 1`] = ` exports[`v-on > function expression w/ prefixIdentifiers: true 1`] = `
"import { delegateEvents as _delegateEvents, template as _template } from 'vue'; "import { delegateEvents as _delegateEvents, template as _template } from 'vue';
const t0 = _template("<div></div>", true) const t0 = _template("<div></div>", true)

View File

@ -252,3 +252,90 @@ export function render(_ctx) {
return n1 return n1
}" }"
`; `;
exports[`compiler: transform slot > slot + v-if / v-else[-if] should not cause error 1`] = `
"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createSlot as _createSlot, createComponentWithFallback as _createComponentWithFallback, createIf as _createIf, template as _template } from 'vue';
const t0 = _template("<div></div>", true)
export function render(_ctx) {
const _component_Foo = _resolveComponent("Foo")
const _component_Bar = _resolveComponent("Bar")
const n6 = t0()
_setInsertionState(n6)
const n0 = _createSlot("foo", null)
_setInsertionState(n6)
const n1 = _createIf(() => (true), () => {
const n3 = _createComponentWithFallback(_component_Foo)
return n3
}, () => {
const n5 = _createComponentWithFallback(_component_Bar)
return n5
})
return n6
}"
`;
exports[`compiler: transform slot > with whitespace: 'preserve' > implicit default slot 1`] = `
"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
const t0 = _template(" Header ")
const t1 = _template(" ")
const t2 = _template("<p></p>")
export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp")
const n4 = _createComponentWithFallback(_component_Comp, null, {
"header": () => {
const n0 = t0()
return n0
},
"default": () => {
const n2 = t1()
const n3 = t2()
return [n2, n3]
}
}, true)
return n4
}"
`;
exports[`compiler: transform slot > with whitespace: 'preserve' > named default slot + implicit whitespace content 1`] = `
"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
const t0 = _template(" Header ")
const t1 = _template(" Default ")
export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp")
const n5 = _createComponentWithFallback(_component_Comp, null, {
"header": () => {
const n0 = t0()
return n0
},
"default": () => {
const n3 = t1()
return n3
}
}, true)
return n5
}"
`;
exports[`compiler: transform slot > with whitespace: 'preserve' > should not generate whitespace only default slot 1`] = `
"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
const t0 = _template(" Header ")
const t1 = _template(" Footer ")
export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp")
const n5 = _createComponentWithFallback(_component_Comp, null, {
"header": () => {
const n0 = t0()
return n0
},
"footer": () => {
const n3 = t1()
return n3
}
}, true)
return n5
}"
`;

View File

@ -1,9 +1,15 @@
import { BindingTypes } from '@vue/compiler-dom' import { BindingTypes } from '@vue/compiler-dom'
import { transformChildren, transformText } from '../../src' import {
transformChildren,
transformElement,
transformText,
transformVBind,
} from '../../src'
import { makeCompile } from './_utils' import { makeCompile } from './_utils'
const compileWithExpression = makeCompile({ const compileWithExpression = makeCompile({
nodeTransforms: [transformChildren, transformText], nodeTransforms: [transformElement, transformChildren, transformText],
directiveTransforms: { bind: transformVBind },
}) })
describe('compiler: expression', () => { describe('compiler: expression', () => {
@ -31,4 +37,14 @@ describe('compiler: expression', () => {
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
expect(code).contains(`$props['bar']`) expect(code).contains(`$props['bar']`)
}) })
test('update expression', () => {
const { code } = compileWithExpression(`
<div :id="String(foo.id++)" :foo="foo" :bar="bar++">
{{ String(foo.id++) }} {{ foo }} {{ bar }}
</div>
`)
expect(code).toMatchSnapshot()
expect(code).contains(`_String(_foo.id++)`)
})
}) })

View File

@ -69,8 +69,8 @@ describe('compiler: children transform', () => {
</div>`, </div>`,
) )
// ensure the insertion anchor is generated before the insertion statement // ensure the insertion anchor is generated before the insertion statement
expect(code).toMatch(`const n3 = _next(_child(n4)) expect(code).toMatch(`const n3 = _next(_child(n4))`)
_setInsertionState(n4, n3)`) expect(code).toMatch(`_setInsertionState(n4, n3)`)
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
}) })

View File

@ -6,16 +6,22 @@ import {
transformElement, transformElement,
transformText, transformText,
transformVBind, transformVBind,
transformVFor,
transformVOn, transformVOn,
} from '../../src' } from '../../src'
import { import {
type BindingMetadata, type BindingMetadata,
BindingTypes, BindingTypes,
NodeTypes, NodeTypes,
} from '@vue/compiler-core' } from '@vue/compiler-dom'
const compileWithElementTransform = makeCompile({ const compileWithElementTransform = makeCompile({
nodeTransforms: [transformElement, transformChildren, transformText], nodeTransforms: [
transformVFor,
transformElement,
transformChildren,
transformText,
],
directiveTransforms: { directiveTransforms: {
bind: transformVBind, bind: transformVBind,
on: transformVOn, on: transformVOn,
@ -39,11 +45,12 @@ describe('compiler: element transform', () => {
}) })
}) })
test.todo('resolve implicitly self-referencing component', () => { test('resolve implicitly self-referencing component', () => {
const { code, helpers } = compileWithElementTransform(`<Example/>`, { const { code, helpers } = compileWithElementTransform(`<Example/>`, {
filename: `/foo/bar/Example.vue?vue&type=template`, filename: `/foo/bar/Example.vue?vue&type=template`,
}) })
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
expect(code).toContain('_resolveComponent("Example", true)')
expect(helpers).toContain('resolveComponent') expect(helpers).toContain('resolveComponent')
}) })
@ -169,6 +176,17 @@ describe('compiler: element transform', () => {
expect(code).contains('_createComponent(_ctx.Comp)') expect(code).contains('_createComponent(_ctx.Comp)')
}) })
test('v-for on component should not mark as single root', () => {
const { code } = compileWithElementTransform(
`<Comp v-for="item in items" :key="item"/>`,
{
bindingMetadata: { Comp: BindingTypes.SETUP_CONST },
},
)
expect(code).toMatchSnapshot()
expect(code).contains('_createComponent(_ctx.Comp)')
})
test('static props', () => { test('static props', () => {
const { code, ir } = compileWithElementTransform( const { code, ir } = compileWithElementTransform(
`<Foo id="foo" class="bar" />`, `<Foo id="foo" class="bar" />`,

View File

@ -1,4 +1,4 @@
import { ErrorCodes, NodeTypes } from '@vue/compiler-core' import { ErrorCodes, NodeTypes } from '@vue/compiler-dom'
import { import {
IRNodeTypes, IRNodeTypes,
transformChildren, transformChildren,

View File

@ -81,6 +81,47 @@ describe('compiler: template ref transform', () => {
expect(code).contains('_setTemplateRef(n0, _ctx.foo, r0)') expect(code).contains('_setTemplateRef(n0, _ctx.foo, r0)')
}) })
test('function ref', () => {
const { ir, code } = compileWithTransformRef(
`<div :ref="bar => {
foo.value = bar
;({ baz } = bar)
console.log(foo.value, baz)
}" />`,
)
expect(ir.block.dynamic.children[0]).toMatchObject({
id: 0,
flags: DynamicFlag.REFERENCED,
})
expect(ir.template).toEqual(['<div></div>'])
expect(ir.block.operation).toMatchObject([
{
type: IRNodeTypes.DECLARE_OLD_REF,
id: 0,
},
])
expect(ir.block.effect).toMatchObject([
{
operations: [
{
type: IRNodeTypes.SET_TEMPLATE_REF,
element: 0,
value: {
isStatic: false,
},
},
],
},
])
expect(code).toMatchSnapshot()
expect(code).contains('const _setTemplateRef = _createTemplateRefSetter()')
expect(code).contains(`_setTemplateRef(n0, bar => {
_foo.value = bar
;({ baz: _ctx.baz } = bar)
console.log(_foo.value, _ctx.baz)
}, r0)`)
})
test('ref + v-if', () => { test('ref + v-if', () => {
const { ir, code } = compileWithTransformRef( const { ir, code } = compileWithTransformRef(
`<div ref="foo" v-if="true" />`, `<div ref="foo" v-if="true" />`,

View File

@ -809,4 +809,12 @@ describe('cache multiple access', () => {
expect(code).matchSnapshot() expect(code).matchSnapshot()
expect(code).not.contains('const _bar = _ctx.bar') expect(code).not.contains('const _bar = _ctx.bar')
}) })
test('not cache variable in function expression', () => {
const { code } = compileWithVBind(`
<div v-bind="{ foo: bar => foo = bar }"></div>
`)
expect(code).matchSnapshot()
expect(code).not.contains('const _bar = _ctx.bar')
})
}) })

View File

@ -119,6 +119,32 @@ describe('compiler: v-for', () => {
}) })
}) })
test('object value, key and index', () => {
const { code, ir } = compileWithVFor(
`<div v-for="(value, key, index) in list" :key="key">{{ value + key + index }}</div>`,
)
expect(code).matchSnapshot()
expect(ir.block.dynamic.children[0].operation).toMatchObject({
type: IRNodeTypes.FOR,
source: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'list',
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'value',
},
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'key',
},
index: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'index',
},
})
})
test('object de-structured value', () => { test('object de-structured value', () => {
const { code, ir } = compileWithVFor( const { code, ir } = compileWithVFor(
'<span v-for="({ id, value }) in items" :key="id">{{ id }}{{ value }}</span>', '<span v-for="({ id, value }) in items" :key="id">{{ id }}{{ value }}</span>',

View File

@ -10,7 +10,7 @@ import {
transformVOnce, transformVOnce,
transformVText, transformVText,
} from '../../src' } from '../../src'
import { NodeTypes } from '@vue/compiler-core' import { NodeTypes } from '@vue/compiler-dom'
const compileWithVIf = makeCompile({ const compileWithVIf = makeCompile({
nodeTransforms: [ nodeTransforms: [
@ -215,6 +215,17 @@ describe('compiler: v-if', () => {
}) })
}) })
test('v-if + v-if / v-else[-if]', () => {
const { code } = compileWithVIf(
`<div>
<span v-if="foo">foo</span>
<span v-if="bar">bar</span>
<span v-else>baz</span>
</div>`,
)
expect(code).toMatchSnapshot()
})
test('comment between branches', () => { test('comment between branches', () => {
const { code, ir } = compileWithVIf(` const { code, ir } = compileWithVIf(`
<div v-if="ok"/> <div v-if="ok"/>

View File

@ -682,4 +682,17 @@ describe('v-on', () => {
'_delegate(n0, "click", _withModifiers(e => _ctx.test(e), ["stop"]))', '_delegate(n0, "click", _withModifiers(e => _ctx.test(e), ["stop"]))',
) )
}) })
test('expression with type', () => {
const { code } = compileWithVOn(
`<div @click="(<number>handleClick as any)"></div>`,
{
bindingMetadata: {
handleClick: BindingTypes.SETUP_CONST,
},
},
)
expect(code).matchSnapshot()
expect(code).include('n0.$evtclick = e => _ctx.handleClick(e)')
})
}) })

View File

@ -1,4 +1,4 @@
import { ErrorCodes, NodeTypes } from '@vue/compiler-core' import { ErrorCodes, NodeTypes } from '@vue/compiler-dom'
import { import {
IRNodeTypes, IRNodeTypes,
IRSlotType, IRSlotType,
@ -371,6 +371,17 @@ describe('compiler: transform slot', () => {
}) })
}) })
test('slot + v-if / v-else[-if] should not cause error', () => {
const { code } = compileWithSlots(
`<div>
<slot name="foo"></slot>
<Foo v-if="true"></Foo>
<Bar v-else />
</div>`,
)
expect(code).toMatchSnapshot()
})
test('quote slot name', () => { test('quote slot name', () => {
const { code } = compileWithSlots( const { code } = compileWithSlots(
`<Comp><template #nav-bar-title-before></template></Comp>`, `<Comp><template #nav-bar-title-before></template></Comp>`,
@ -498,4 +509,60 @@ describe('compiler: transform slot', () => {
}) })
}) })
}) })
describe(`with whitespace: 'preserve'`, () => {
test('named default slot + implicit whitespace content', () => {
const source = `
<Comp>
<template #header> Header </template>
<template #default> Default </template>
</Comp>
`
const { code } = compileWithSlots(source, {
whitespace: 'preserve',
})
expect(
`Extraneous children found when component already has explicitly named default slot.`,
).not.toHaveBeenWarned()
expect(code).toMatchSnapshot()
})
test('implicit default slot', () => {
const source = `
<Comp>
<template #header> Header </template>
<p/>
</Comp>
`
const { code } = compileWithSlots(source, {
whitespace: 'preserve',
})
expect(
`Extraneous children found when component already has explicitly named default slot.`,
).not.toHaveBeenWarned()
expect(code).toMatchSnapshot()
})
test('should not generate whitespace only default slot', () => {
const source = `
<Comp>
<template #header> Header </template>
<template #footer> Footer </template>
</Comp>
`
const { code, ir } = compileWithSlots(source, {
whitespace: 'preserve',
})
const slots = (ir.block.dynamic.children[0].operation as any).slots[0]
.slots
// should be: header, footer (no default)
expect(Object.keys(slots).length).toBe(2)
expect(!!slots['default']).toBe(false)
expect(code).toMatchSnapshot()
})
})
}) })

View File

@ -81,8 +81,8 @@ export function getBaseTransformPreset(): TransformPreset {
transformVFor, transformVFor,
transformSlotOutlet, transformSlotOutlet,
transformTemplateRef, transformTemplateRef,
transformText,
transformElement, transformElement,
transformText,
transformVSlot, transformVSlot,
transformComment, transformComment,
transformChildren, transformChildren,

View File

@ -44,7 +44,21 @@ export function genBlockContent(
const resetBlock = context.enterBlock(block) const resetBlock = context.enterBlock(block)
if (root) { if (root) {
genResolveAssets('component', 'resolveComponent') for (let name of context.ir.component) {
const id = toValidAssetId(name, 'component')
const maybeSelfReference = name.endsWith('__self')
if (maybeSelfReference) name = name.slice(0, -6)
push(
NEWLINE,
`const ${id} = `,
...genCall(
context.helper('resolveComponent'),
JSON.stringify(name),
// pass additional `maybeSelfReference` flag
maybeSelfReference ? 'true' : undefined,
),
)
}
genResolveAssets('directive', 'resolveDirective') genResolveAssets('directive', 'resolveDirective')
} }
@ -52,7 +66,7 @@ export function genBlockContent(
push(...genSelf(child, context)) push(...genSelf(child, context))
} }
for (const child of dynamic.children) { for (const child of dynamic.children) {
push(...genChildren(child, context, `n${child.id!}`)) push(...genChildren(child, context, push, `n${child.id!}`))
} }
push(...genOperations(operation, context)) push(...genOperations(operation, context))

View File

@ -34,7 +34,7 @@ import {
isMemberExpression, isMemberExpression,
toValidAssetId, toValidAssetId,
walkIdentifiers, walkIdentifiers,
} from '@vue/compiler-core' } from '@vue/compiler-dom'
import { genEventHandler } from './event' import { genEventHandler } from './event'
import { genDirectiveModifiers, genDirectivesForElement } from './directive' import { genDirectiveModifiers, genDirectivesForElement } from './directive'
import { genBlock } from './block' import { genBlock } from './block'

View File

@ -10,6 +10,7 @@ import {
NewlineType, NewlineType,
type SimpleExpressionNode, type SimpleExpressionNode,
type SourceLocation, type SourceLocation,
TS_NODE_TYPES,
advancePositionWithClone, advancePositionWithClone,
createSimpleExpression, createSimpleExpression,
isInDestructureAssignment, isInDestructureAssignment,
@ -20,7 +21,6 @@ import type { Identifier, Node } from '@babel/types'
import type { CodegenContext } from '../generate' import type { CodegenContext } from '../generate'
import { isConstantExpression } from '../utils' import { isConstantExpression } from '../utils'
import { type CodeFragment, NEWLINE, buildCodeFragment } from './utils' import { type CodeFragment, NEWLINE, buildCodeFragment } from './utils'
import { walk } from 'estree-walker'
import { type ParserOptions, parseExpression } from '@babel/parser' import { type ParserOptions, parseExpression } from '@babel/parser'
export function genExpression( export function genExpression(
@ -64,6 +64,7 @@ export function genExpression(
let hasMemberExpression = false let hasMemberExpression = false
if (ids.length) { if (ids.length) {
const [frag, push] = buildCodeFragment() const [frag, push] = buildCodeFragment()
const isTSNode = ast && TS_NODE_TYPES.includes(ast.type)
ids ids
.sort((a, b) => a.start! - b.start!) .sort((a, b) => a.start! - b.start!)
.forEach((id, i) => { .forEach((id, i) => {
@ -72,8 +73,10 @@ export function genExpression(
const end = id.end! - 1 const end = id.end! - 1
const last = ids[i - 1] const last = ids[i - 1]
const leadingText = content.slice(last ? last.end! - 1 : 0, start) if (!(isTSNode && i === 0)) {
if (leadingText.length) push([leadingText, NewlineType.Unknown]) const leadingText = content.slice(last ? last.end! - 1 : 0, start)
if (leadingText.length) push([leadingText, NewlineType.Unknown])
}
const source = content.slice(start, end) const source = content.slice(start, end)
const parentStack = parentStackMap.get(id)! const parentStack = parentStackMap.get(id)!
@ -100,7 +103,7 @@ export function genExpression(
), ),
) )
if (i === ids.length - 1 && end < content.length) { if (i === ids.length - 1 && end < content.length && !isTSNode) {
push([content.slice(end), NewlineType.Unknown]) push([content.slice(end), NewlineType.Unknown])
} }
}) })
@ -245,8 +248,13 @@ export function processExpressions(
expressions: SimpleExpressionNode[], expressions: SimpleExpressionNode[],
): DeclarationResult { ): DeclarationResult {
// analyze variables // analyze variables
const { seenVariable, variableToExpMap, expToVariableMap, seenIdentifier } = const {
analyzeExpressions(expressions) seenVariable,
variableToExpMap,
expToVariableMap,
seenIdentifier,
updatedVariable,
} = analyzeExpressions(expressions)
// process repeated identifiers and member expressions // process repeated identifiers and member expressions
// e.g., `foo[baz]` will be transformed into `foo_baz` // e.g., `foo[baz]` will be transformed into `foo_baz`
@ -256,6 +264,7 @@ export function processExpressions(
variableToExpMap, variableToExpMap,
expToVariableMap, expToVariableMap,
seenIdentifier, seenIdentifier,
updatedVariable,
) )
// process duplicate expressions after identifier and member expression handling. // process duplicate expressions after identifier and member expression handling.
@ -264,6 +273,8 @@ export function processExpressions(
context, context,
expressions, expressions,
varDeclarations, varDeclarations,
updatedVariable,
expToVariableMap,
) )
return genDeclarations([...varDeclarations, ...expDeclarations], context) return genDeclarations([...varDeclarations, ...expDeclarations], context)
@ -274,11 +285,13 @@ function analyzeExpressions(expressions: SimpleExpressionNode[]) {
const variableToExpMap = new Map<string, Set<SimpleExpressionNode>>() const variableToExpMap = new Map<string, Set<SimpleExpressionNode>>()
const expToVariableMap = new Map<SimpleExpressionNode, string[]>() const expToVariableMap = new Map<SimpleExpressionNode, string[]>()
const seenIdentifier = new Set<string>() const seenIdentifier = new Set<string>()
const updatedVariable = new Set<string>()
const registerVariable = ( const registerVariable = (
name: string, name: string,
exp: SimpleExpressionNode, exp: SimpleExpressionNode,
isIdentifier: boolean, isIdentifier: boolean,
parentStack: Node[] = [],
) => { ) => {
if (isIdentifier) seenIdentifier.add(name) if (isIdentifier) seenIdentifier.add(name)
seenVariable[name] = (seenVariable[name] || 0) + 1 seenVariable[name] = (seenVariable[name] || 0) + 1
@ -287,6 +300,13 @@ function analyzeExpressions(expressions: SimpleExpressionNode[]) {
(variableToExpMap.get(name) || new Set()).add(exp), (variableToExpMap.get(name) || new Set()).add(exp),
) )
expToVariableMap.set(exp, (expToVariableMap.get(exp) || []).concat(name)) expToVariableMap.set(exp, (expToVariableMap.get(exp) || []).concat(name))
if (
parentStack.some(
p => p.type === 'UpdateExpression' || p.type === 'AssignmentExpression',
)
) {
updatedVariable.add(name)
}
} }
for (const exp of expressions) { for (const exp of expressions) {
@ -295,37 +315,25 @@ function analyzeExpressions(expressions: SimpleExpressionNode[]) {
continue continue
} }
walk(exp.ast, { walkIdentifiers(exp.ast, (currentNode, parent, parentStack) => {
enter(currentNode: Node, parent: Node | null) { if (parent && isMemberExpression(parent)) {
if (currentNode.type === 'MemberExpression') { const memberExp = extractMemberExpression(parent, name => {
const memberExp = extractMemberExpression( registerVariable(name, exp, true)
currentNode, })
(name: string) => { registerVariable(memberExp, exp, false, parentStack)
registerVariable(name, exp, true) } else if (!parentStack.some(isMemberExpression)) {
}, registerVariable(currentNode.name, exp, true, parentStack)
) }
registerVariable(memberExp, exp, false)
return this.skip()
}
// skip shorthand or non-computed property keys
if (
parent &&
parent.type === 'ObjectProperty' &&
parent.key === currentNode &&
(parent.shorthand || !parent.computed)
) {
return this.skip()
}
if (currentNode.type === 'Identifier') {
registerVariable(currentNode.name, exp, true)
}
},
}) })
} }
return { seenVariable, seenIdentifier, variableToExpMap, expToVariableMap } return {
seenVariable,
seenIdentifier,
variableToExpMap,
expToVariableMap,
updatedVariable,
}
} }
function processRepeatedVariables( function processRepeatedVariables(
@ -334,9 +342,11 @@ function processRepeatedVariables(
variableToExpMap: Map<string, Set<SimpleExpressionNode>>, variableToExpMap: Map<string, Set<SimpleExpressionNode>>,
expToVariableMap: Map<SimpleExpressionNode, string[]>, expToVariableMap: Map<SimpleExpressionNode, string[]>,
seenIdentifier: Set<string>, seenIdentifier: Set<string>,
updatedVariable: Set<string>,
): DeclarationValue[] { ): DeclarationValue[] {
const declarations: DeclarationValue[] = [] const declarations: DeclarationValue[] = []
for (const [name, exps] of variableToExpMap) { for (const [name, exps] of variableToExpMap) {
if (updatedVariable.has(name)) continue
if (seenVariable[name] > 1 && exps.size > 0) { if (seenVariable[name] > 1 && exps.size > 0) {
const isIdentifier = seenIdentifier.has(name) const isIdentifier = seenIdentifier.has(name)
const varName = isIdentifier ? name : genVarName(name) const varName = isIdentifier ? name : genVarName(name)
@ -428,12 +438,19 @@ function processRepeatedExpressions(
context: CodegenContext, context: CodegenContext,
expressions: SimpleExpressionNode[], expressions: SimpleExpressionNode[],
varDeclarations: DeclarationValue[], varDeclarations: DeclarationValue[],
updatedVariable: Set<string>,
expToVariableMap: Map<SimpleExpressionNode, string[]>,
): DeclarationValue[] { ): DeclarationValue[] {
const declarations: DeclarationValue[] = [] const declarations: DeclarationValue[] = []
const seenExp = expressions.reduce( const seenExp = expressions.reduce(
(acc, exp) => { (acc, exp) => {
const variables = expToVariableMap.get(exp)
// only handle expressions that are not identifiers // only handle expressions that are not identifiers
if (exp.ast && exp.ast.type !== 'Identifier') { if (
exp.ast &&
exp.ast.type !== 'Identifier' &&
!(variables && variables.some(v => updatedVariable.has(v)))
) {
acc[exp.content] = (acc[exp.content] || 0) + 1 acc[exp.content] = (acc[exp.content] || 0) + 1
} }
return acc return acc
@ -580,3 +597,9 @@ function extractMemberExpression(
return '' return ''
} }
} }
const isMemberExpression = (node: Node) => {
return (
node.type === 'MemberExpression' || node.type === 'OptionalMemberExpression'
)
}

View File

@ -44,7 +44,7 @@ export function genOperationWithInsertionState(
): CodeFragment[] { ): CodeFragment[] {
const [frag, push] = buildCodeFragment() const [frag, push] = buildCodeFragment()
if (isBlockOperation(oper) && oper.parent) { if (isBlockOperation(oper) && oper.parent) {
push(...genInsertionstate(oper, context)) push(...genInsertionState(oper, context))
} }
push(...genOperation(oper, context)) push(...genOperation(oper, context))
return frag return frag
@ -99,10 +99,8 @@ export function genEffects(
effects: IREffect[], effects: IREffect[],
context: CodegenContext, context: CodegenContext,
): CodeFragment[] { ): CodeFragment[] {
const { const { helper } = context
helper, const expressions = effects.flatMap(effect => effect.expressions)
block: { expressions },
} = context
const [frag, push, unshift] = buildCodeFragment() const [frag, push, unshift] = buildCodeFragment()
let operationsCount = 0 let operationsCount = 0
const { ids, frag: declarationFrags } = processExpressions( const { ids, frag: declarationFrags } = processExpressions(
@ -152,7 +150,7 @@ export function genEffect(
return frag return frag
} }
function genInsertionstate( function genInsertionState(
operation: InsertionStateTypes, operation: InsertionStateTypes,
context: CodegenContext, context: CodegenContext,
): CodeFragment[] { ): CodeFragment[] {

View File

@ -2,7 +2,7 @@ import {
NewlineType, NewlineType,
type SimpleExpressionNode, type SimpleExpressionNode,
isSimpleIdentifier, isSimpleIdentifier,
} from '@vue/compiler-core' } from '@vue/compiler-dom'
import type { CodegenContext } from '../generate' import type { CodegenContext } from '../generate'
import { import {
IRDynamicPropsKind, IRDynamicPropsKind,

View File

@ -41,6 +41,7 @@ export function genSelf(
export function genChildren( export function genChildren(
dynamic: IRDynamicInfo, dynamic: IRDynamicInfo,
context: CodegenContext, context: CodegenContext,
pushBlock: (...items: CodeFragment[]) => number,
from: string = `n${dynamic.id}`, from: string = `n${dynamic.id}`,
): CodeFragment[] { ): CodeFragment[] {
const { helper } = context const { helper } = context
@ -72,17 +73,17 @@ export function genChildren(
// p for "placeholder" variables that are meant for possible reuse by // p for "placeholder" variables that are meant for possible reuse by
// other access paths // other access paths
const variable = id === undefined ? `p${context.block.tempId++}` : `n${id}` const variable = id === undefined ? `p${context.block.tempId++}` : `n${id}`
push(NEWLINE, `const ${variable} = `) pushBlock(NEWLINE, `const ${variable} = `)
if (prev) { if (prev) {
if (elementIndex - prev[1] === 1) { if (elementIndex - prev[1] === 1) {
push(...genCall(helper('next'), prev[0])) pushBlock(...genCall(helper('next'), prev[0]))
} else { } else {
push(...genCall(helper('nthChild'), from, String(elementIndex))) pushBlock(...genCall(helper('nthChild'), from, String(elementIndex)))
} }
} else { } else {
if (elementIndex === 0) { if (elementIndex === 0) {
push(...genCall(helper('child'), from)) pushBlock(...genCall(helper('child'), from))
} else { } else {
// check if there's a node that we can reuse from // check if there's a node that we can reuse from
let init = genCall(helper('child'), from) let init = genCall(helper('child'), from)
@ -91,7 +92,7 @@ export function genChildren(
} else if (elementIndex > 1) { } else if (elementIndex > 1) {
init = genCall(helper('nthChild'), from, String(elementIndex)) init = genCall(helper('nthChild'), from, String(elementIndex))
} }
push(...init) pushBlock(...init)
} }
} }
@ -109,7 +110,7 @@ export function genChildren(
if (childrenToGen.length) { if (childrenToGen.length) {
for (const [child, from] of childrenToGen) { for (const [child, from] of childrenToGen) {
push(...genChildren(child, context, from)) push(...genChildren(child, context, pushBlock, from))
} }
} }

View File

@ -52,7 +52,6 @@ export interface BlockIRNode extends BaseIRNode {
tempId: number tempId: number
effect: IREffect[] effect: IREffect[]
operation: OperationNode[] operation: OperationNode[]
expressions: SimpleExpressionNode[]
returns: number[] returns: number[]
} }

View File

@ -11,6 +11,7 @@ import {
type TemplateChildNode, type TemplateChildNode,
defaultOnError, defaultOnError,
defaultOnWarn, defaultOnWarn,
getSelfName,
isVSlot, isVSlot,
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { EMPTY_OBJ, NOOP, extend, isArray, isString } from '@vue/shared' import { EMPTY_OBJ, NOOP, extend, isArray, isString } from '@vue/shared'
@ -61,6 +62,7 @@ export type StructuralDirectiveTransform = (
export type TransformOptions = HackOptions<BaseTransformOptions> export type TransformOptions = HackOptions<BaseTransformOptions>
export class TransformContext<T extends AllNode = AllNode> { export class TransformContext<T extends AllNode = AllNode> {
selfName: string | null = null
parent: TransformContext<RootNode | ElementNode> | null = null parent: TransformContext<RootNode | ElementNode> | null = null
root: TransformContext<RootNode> root: TransformContext<RootNode>
index: number = 0 index: number = 0
@ -92,6 +94,7 @@ export class TransformContext<T extends AllNode = AllNode> {
) { ) {
this.options = extend({}, defaultOptions, options) this.options = extend({}, defaultOptions, options)
this.root = this as TransformContext<RootNode> this.root = this as TransformContext<RootNode>
if (options.filename) this.selfName = getSelfName(options.filename)
} }
enterBlock(ir: BlockIRNode, isVFor: boolean = false): () => void { enterBlock(ir: BlockIRNode, isVFor: boolean = false): () => void {
@ -137,8 +140,10 @@ export class TransformContext<T extends AllNode = AllNode> {
registerEffect( registerEffect(
expressions: SimpleExpressionNode[], expressions: SimpleExpressionNode[],
...operations: OperationNode[] operation: OperationNode | OperationNode[],
getIndex = (): number => this.block.effect.length,
): void { ): void {
const operations = [operation].flat()
expressions = expressions.filter(exp => !isConstantExpression(exp)) expressions = expressions.filter(exp => !isConstantExpression(exp))
if ( if (
this.inVOnce || this.inVOnce ||
@ -150,26 +155,10 @@ export class TransformContext<T extends AllNode = AllNode> {
return this.registerOperation(...operations) return this.registerOperation(...operations)
} }
this.block.expressions.push(...expressions) this.block.effect.splice(getIndex(), 0, {
const existing = this.block.effect.find(e => expressions,
isSameExpression(e.expressions, expressions), operations,
) })
if (existing) {
existing.operations.push(...operations)
} else {
this.block.effect.push({
expressions,
operations,
})
}
function isSameExpression(
a: SimpleExpressionNode[],
b: SimpleExpressionNode[],
) {
if (a.length !== b.length) return false
return a.every((exp, i) => exp.content === b[i].content)
}
} }
registerOperation(...node: OperationNode[]): void { registerOperation(...node: OperationNode[]): void {

View File

@ -44,6 +44,8 @@ export const isReservedProp: (key: string) => boolean = /*#__PURE__*/ makeMap(
) )
export const transformElement: NodeTransform = (node, context) => { export const transformElement: NodeTransform = (node, context) => {
let effectIndex = context.block.effect.length
const getEffectIndex = () => effectIndex++
return function postTransformElement() { return function postTransformElement() {
;({ node } = context) ;({ node } = context)
if ( if (
@ -62,6 +64,7 @@ export const transformElement: NodeTransform = (node, context) => {
context as TransformContext<ElementNode>, context as TransformContext<ElementNode>,
isComponent, isComponent,
isDynamicComponent, isDynamicComponent,
getEffectIndex,
) )
let { parent } = context let { parent } = context
@ -78,13 +81,23 @@ export const transformElement: NodeTransform = (node, context) => {
parent.node.children.filter(child => child.type !== NodeTypes.COMMENT) parent.node.children.filter(child => child.type !== NodeTypes.COMMENT)
.length === 1 .length === 1
;(isComponent ? transformComponentElement : transformNativeElement)( if (isComponent) {
node as any, transformComponentElement(
propsResult, node as ComponentNode,
singleRoot, propsResult,
context as TransformContext<ElementNode>, singleRoot,
isDynamicComponent, context,
) isDynamicComponent,
)
} else {
transformNativeElement(
node as PlainElementNode,
propsResult,
singleRoot,
context,
getEffectIndex,
)
}
} }
} }
@ -119,6 +132,13 @@ function transformComponentElement(
} }
if (asset) { if (asset) {
// self referencing component (inferred from filename)
if (context.selfName && capitalize(camelize(tag)) === context.selfName) {
// generators/block.ts has special check for __self postfix when generating
// component imports, which will pass additional `maybeSelfReference` flag
// to `resolveComponent`.
tag += `__self`
}
context.component.add(tag) context.component.add(tag)
} }
} }
@ -130,7 +150,7 @@ function transformComponentElement(
tag, tag,
props: propsResult[0] ? propsResult[1] : [propsResult[1]], props: propsResult[0] ? propsResult[1] : [propsResult[1]],
asset, asset,
root: singleRoot, root: singleRoot && !context.inVFor,
slots: [...context.slots], slots: [...context.slots],
once: context.inVOnce, once: context.inVOnce,
dynamic: dynamicComponent, dynamic: dynamicComponent,
@ -176,7 +196,8 @@ function transformNativeElement(
node: PlainElementNode, node: PlainElementNode,
propsResult: PropsResult, propsResult: PropsResult,
singleRoot: boolean, singleRoot: boolean,
context: TransformContext<ElementNode>, context: TransformContext,
getEffectIndex: () => number,
) { ) {
const { tag } = node const { tag } = node
const { scopeId } = context.options const { scopeId } = context.options
@ -189,12 +210,16 @@ function transformNativeElement(
const dynamicProps: string[] = [] const dynamicProps: string[] = []
if (propsResult[0] /* dynamic props */) { if (propsResult[0] /* dynamic props */) {
const [, dynamicArgs, expressions] = propsResult const [, dynamicArgs, expressions] = propsResult
context.registerEffect(expressions, { context.registerEffect(
type: IRNodeTypes.SET_DYNAMIC_PROPS, expressions,
element: context.reference(), {
props: dynamicArgs, type: IRNodeTypes.SET_DYNAMIC_PROPS,
root: singleRoot, element: context.reference(),
}) props: dynamicArgs,
root: singleRoot,
},
getEffectIndex,
)
} else { } else {
for (const prop of propsResult[1]) { for (const prop of propsResult[1]) {
const { key, values } = prop const { key, values } = prop
@ -203,13 +228,17 @@ function transformNativeElement(
if (values[0].content) template += `="${values[0].content}"` if (values[0].content) template += `="${values[0].content}"`
} else { } else {
dynamicProps.push(key.content) dynamicProps.push(key.content)
context.registerEffect(values, { context.registerEffect(
type: IRNodeTypes.SET_PROP, values,
element: context.reference(), {
prop, type: IRNodeTypes.SET_PROP,
root: singleRoot, element: context.reference(),
tag, prop,
}) root: singleRoot,
tag,
},
getEffectIndex,
)
} }
} }
} }
@ -246,6 +275,7 @@ export function buildProps(
context: TransformContext<ElementNode>, context: TransformContext<ElementNode>,
isComponent: boolean, isComponent: boolean,
isDynamicComponent?: boolean, isDynamicComponent?: boolean,
getEffectIndex?: () => number,
): PropsResult { ): PropsResult {
const props = node.props as (VaporDirectiveNode | AttributeNode)[] const props = node.props as (VaporDirectiveNode | AttributeNode)[]
if (props.length === 0) return [false, []] if (props.length === 0) return [false, []]
@ -292,12 +322,12 @@ export function buildProps(
} else { } else {
context.registerEffect( context.registerEffect(
[prop.exp], [prop.exp],
{ {
type: IRNodeTypes.SET_DYNAMIC_EVENTS, type: IRNodeTypes.SET_DYNAMIC_EVENTS,
element: context.reference(), element: context.reference(),
event: prop.exp, event: prop.exp,
}, },
getEffectIndex,
) )
} }
} else { } else {

View File

@ -9,7 +9,7 @@ import {
createSimpleExpression, createSimpleExpression,
isStaticArgOf, isStaticArgOf,
isStaticExp, isStaticExp,
} from '@vue/compiler-core' } from '@vue/compiler-dom'
import type { NodeTransform, TransformContext } from '../transform' import type { NodeTransform, TransformContext } from '../transform'
import { import {
type BlockIRNode, type BlockIRNode,

View File

@ -23,6 +23,13 @@ const seen = new WeakMap<
WeakSet<TemplateChildNode | RootNode> WeakSet<TemplateChildNode | RootNode>
>() >()
export function markNonTemplate(
node: TemplateChildNode,
context: TransformContext,
): void {
seen.get(context.root)!.add(node)
}
export const transformText: NodeTransform = (node, context) => { export const transformText: NodeTransform = (node, context) => {
if (!seen.has(context.root)) seen.set(context.root, new WeakSet()) if (!seen.has(context.root)) seen.set(context.root, new WeakSet())
if (seen.get(context.root)!.has(node)) { if (seen.get(context.root)!.has(node)) {
@ -68,7 +75,7 @@ export const transformText: NodeTransform = (node, context) => {
prev.type === NodeTypes.TEXT prev.type === NodeTypes.TEXT
) { ) {
// mark leading text node for skipping // mark leading text node for skipping
seen.get(context.root)!.add(prev) markNonTemplate(prev, context)
} }
} }
} }
@ -143,7 +150,7 @@ function processTextContainer(
} }
function createTextLikeExpression(node: TextLike, context: TransformContext) { function createTextLikeExpression(node: TextLike, context: TransformContext) {
seen.get(context.root)!.add(node) markNonTemplate(node, context)
if (node.type === NodeTypes.TEXT) { if (node.type === NodeTypes.TEXT) {
return createSimpleExpression(node.content, true, node.loc) return createSimpleExpression(node.content, true, node.loc)
} else { } else {

View File

@ -29,7 +29,6 @@ export const newBlock = (node: BlockIRNode['node']): BlockIRNode => ({
effect: [], effect: [],
operation: [], operation: [],
returns: [], returns: [],
expressions: [],
tempId: 0, tempId: 0,
}) })

View File

@ -65,7 +65,13 @@ export function processIf(
if (siblings) { if (siblings) {
let i = siblings.length let i = siblings.length
while (i--) { while (i--) {
if (siblings[i].operation) lastIfNode = siblings[i].operation if (
siblings[i].operation &&
siblings[i].operation!.type === IRNodeTypes.IF
) {
lastIfNode = siblings[i].operation
break
}
} }
} }

View File

@ -8,7 +8,7 @@ import {
createCompilerError, createCompilerError,
isTemplateNode, isTemplateNode,
isVSlot, isVSlot,
} from '@vue/compiler-core' } from '@vue/compiler-dom'
import type { NodeTransform, TransformContext } from '../transform' import type { NodeTransform, TransformContext } from '../transform'
import { newBlock } from './utils' import { newBlock } from './utils'
import { import {
@ -24,6 +24,7 @@ import {
type VaporDirectiveNode, type VaporDirectiveNode,
} from '../ir' } from '../ir'
import { findDir, resolveExpression } from '../utils' import { findDir, resolveExpression } from '../utils'
import { markNonTemplate } from './transformText'
export const transformVSlot: NodeTransform = (node, context) => { export const transformVSlot: NodeTransform = (node, context) => {
if (node.type !== NodeTypes.ELEMENT) return if (node.type !== NodeTypes.ELEMENT) return
@ -66,11 +67,21 @@ function transformComponentSlot(
) { ) {
const { children } = node const { children } = node
const arg = dir && dir.arg const arg = dir && dir.arg
const nonSlotTemplateChildren = children.filter(
n => // whitespace: 'preserve'
isNonWhitespaceContent(node) && const emptyTextNodes: TemplateChildNode[] = []
!(n.type === NodeTypes.ELEMENT && n.props.some(isVSlot)), const nonSlotTemplateChildren = children.filter(n => {
) if (isNonWhitespaceContent(n)) {
return !(n.type === NodeTypes.ELEMENT && n.props.some(isVSlot))
} else {
emptyTextNodes.push(n)
}
})
if (!nonSlotTemplateChildren.length) {
emptyTextNodes.forEach(n => {
markNonTemplate(n, context)
})
}
const [block, onExit] = createSlotBlock(node, dir, context) const [block, onExit] = createSlotBlock(node, dir, context)

View File

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

@ -20,7 +20,7 @@ describe.skipIf(!global.gc)('reactivity/gc', () => {
} }
// #9233 // #9233
it.todo('should release computed cache', async () => { it('should release computed cache', async () => {
const src = ref<{} | undefined>({}) const src = ref<{} | undefined>({})
// @ts-expect-error ES2021 API // @ts-expect-error ES2021 API
const srcRef = new WeakRef(src.value!) const srcRef = new WeakRef(src.value!)
@ -35,7 +35,7 @@ describe.skipIf(!global.gc)('reactivity/gc', () => {
expect(srcRef.deref()).toBeUndefined() expect(srcRef.deref()).toBeUndefined()
}) })
it.todo('should release reactive property dep', async () => { it('should release reactive property dep', async () => {
const src = reactive({ foo: 1 }) const src = reactive({ foo: 1 })
let c: ComputedRef | undefined = computed(() => src.foo) let c: ComputedRef | undefined = computed(() => src.foo)

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

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

@ -87,7 +87,10 @@ export class ComputedRefImpl<T = any> implements Dependency, Subscriber {
get dep(): Dependency { get dep(): Dependency {
return this return this
} }
// for backwards compat /**
* @internal
* for backwards compat
*/
get _dirty(): boolean { get _dirty(): boolean {
const flags = this.flags const flags = this.flags
if ( if (
@ -99,6 +102,10 @@ export class ComputedRefImpl<T = any> implements Dependency, Subscriber {
} }
return false return false
} }
/**
* @internal
* for backwards compat
*/
set _dirty(v: boolean) { set _dirty(v: boolean) {
if (v) { if (v) {
this.flags |= SubscriberFlags.Dirty this.flags |= SubscriberFlags.Dirty
@ -112,12 +119,6 @@ export class ComputedRefImpl<T = any> implements Dependency, Subscriber {
// dev only // dev only
onTrigger?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void
/**
* Dev only
* @internal
*/
_warnRecursive?: boolean
constructor( constructor(
public fn: ComputedGetter<T>, public fn: ComputedGetter<T>,
private readonly setter: ComputedSetter<T> | undefined, private readonly setter: ComputedSetter<T> | undefined,

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

@ -68,7 +68,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
@ -238,7 +238,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.
* *
@ -348,7 +348,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

@ -1,5 +1,5 @@
/* eslint-disable */ /* eslint-disable */
// Ported from https://github.com/stackblitz/alien-signals/blob/v1.0.4/src/system.ts // Ported from https://github.com/stackblitz/alien-signals/blob/v1.0.13/src/system.ts
import type { ComputedRefImpl as Computed } from './computed.js' import type { ComputedRefImpl as Computed } from './computed.js'
import type { ReactiveEffect as Effect } from './effect.js' import type { ReactiveEffect as Effect } from './effect.js'
@ -32,9 +32,16 @@ export const enum SubscriberFlags {
Propagated = Dirty | PendingComputed, Propagated = Dirty | PendingComputed,
} }
interface OneWayLink<T> {
target: T
linked: OneWayLink<T> | undefined
}
const notifyBuffer: (Effect | undefined)[] = []
let batchDepth = 0 let batchDepth = 0
let queuedEffects: Effect | undefined let notifyIndex = 0
let queuedEffectsTail: Effect | undefined let notifyBufferLength = 0
export function startBatch(): void { export function startBatch(): void {
++batchDepth ++batchDepth
@ -67,80 +74,81 @@ export function link(dep: Dependency, sub: Subscriber): Link | undefined {
return linkNewDep(dep, sub, nextDep, currentDep) return linkNewDep(dep, sub, nextDep, currentDep)
} }
export function propagate(link: Link): void { export function propagate(current: Link): void {
let next = current.nextSub
let branchs: OneWayLink<Link | undefined> | undefined
let branchDepth = 0
let targetFlag = SubscriberFlags.Dirty let targetFlag = SubscriberFlags.Dirty
let subs = link
let stack = 0
top: do { top: do {
const sub = link.sub const sub = current.sub
const subFlags = sub.flags const subFlags = sub.flags
let shouldNotify = false
if ( if (
(!( !(
subFlags & subFlags &
(SubscriberFlags.Tracking | (SubscriberFlags.Tracking |
SubscriberFlags.Recursed | SubscriberFlags.Recursed |
SubscriberFlags.Propagated) SubscriberFlags.Propagated)
) && )
((sub.flags = subFlags | targetFlag), true)) ||
(subFlags & SubscriberFlags.Recursed &&
!(subFlags & SubscriberFlags.Tracking) &&
((sub.flags = (subFlags & ~SubscriberFlags.Recursed) | targetFlag),
true)) ||
(!(subFlags & SubscriberFlags.Propagated) &&
isValidLink(link, sub) &&
((sub.flags = subFlags | SubscriberFlags.Recursed | targetFlag),
(sub as Dependency).subs !== undefined))
) { ) {
sub.flags = subFlags | targetFlag
shouldNotify = true
} else if (
subFlags & SubscriberFlags.Recursed &&
!(subFlags & SubscriberFlags.Tracking)
) {
sub.flags = (subFlags & ~SubscriberFlags.Recursed) | targetFlag
shouldNotify = true
} else if (
!(subFlags & SubscriberFlags.Propagated) &&
isValidLink(current, sub)
) {
sub.flags = subFlags | SubscriberFlags.Recursed | targetFlag
shouldNotify = (sub as Dependency).subs !== undefined
}
if (shouldNotify) {
const subSubs = (sub as Dependency).subs const subSubs = (sub as Dependency).subs
if (subSubs !== undefined) { if (subSubs !== undefined) {
current = subSubs
if (subSubs.nextSub !== undefined) { if (subSubs.nextSub !== undefined) {
subSubs.prevSub = subs branchs = { target: next, linked: branchs }
link = subs = subSubs ++branchDepth
targetFlag = SubscriberFlags.PendingComputed next = current.nextSub
++stack
} else {
link = subSubs
targetFlag = SubscriberFlags.PendingComputed
} }
targetFlag = SubscriberFlags.PendingComputed
continue continue
} }
if (subFlags & SubscriberFlags.Effect) { if (subFlags & SubscriberFlags.Effect) {
if (queuedEffectsTail !== undefined) { notifyBuffer[notifyBufferLength++] = sub as Effect
queuedEffectsTail.depsTail!.nextDep = sub.deps
} else {
queuedEffects = sub as Effect
}
queuedEffectsTail = sub as Effect
} }
} else if (!(subFlags & (SubscriberFlags.Tracking | targetFlag))) { } else if (!(subFlags & (SubscriberFlags.Tracking | targetFlag))) {
sub.flags = subFlags | targetFlag sub.flags = subFlags | targetFlag
} else if ( } else if (
!(subFlags & targetFlag) && !(subFlags & targetFlag) &&
subFlags & SubscriberFlags.Propagated && subFlags & SubscriberFlags.Propagated &&
isValidLink(link, sub) isValidLink(current, sub)
) { ) {
sub.flags = subFlags | targetFlag sub.flags = subFlags | targetFlag
} }
if ((link = subs.nextSub!) !== undefined) { if ((current = next!) !== undefined) {
subs = link next = current.nextSub
targetFlag = stack targetFlag = branchDepth
? SubscriberFlags.PendingComputed ? SubscriberFlags.PendingComputed
: SubscriberFlags.Dirty : SubscriberFlags.Dirty
continue continue
} }
while (stack) { while (branchDepth--) {
--stack current = branchs!.target!
const dep = subs.dep branchs = branchs!.linked
const depSubs = dep.subs! if (current !== undefined) {
subs = depSubs.prevSub! next = current.nextSub
depSubs.prevSub = undefined targetFlag = branchDepth
if ((link = subs.nextSub!) !== undefined) {
subs = link
targetFlag = stack
? SubscriberFlags.PendingComputed ? SubscriberFlags.PendingComputed
: SubscriberFlags.Dirty : SubscriberFlags.Dirty
continue top continue top
@ -194,35 +202,26 @@ export function processComputedUpdate(
computed: Computed, computed: Computed,
flags: SubscriberFlags, flags: SubscriberFlags,
): void { ): void {
if ( if (flags & SubscriberFlags.Dirty || checkDirty(computed.deps!)) {
flags & SubscriberFlags.Dirty ||
(checkDirty(computed.deps!)
? true
: ((computed.flags = flags & ~SubscriberFlags.PendingComputed), false))
) {
if (computed.update()) { if (computed.update()) {
const subs = computed.subs const subs = computed.subs
if (subs !== undefined) { if (subs !== undefined) {
shallowPropagate(subs) shallowPropagate(subs)
} }
} }
} else {
computed.flags = flags & ~SubscriberFlags.PendingComputed
} }
} }
export function processEffectNotifications(): void { export function processEffectNotifications(): void {
while (queuedEffects !== undefined) { while (notifyIndex < notifyBufferLength) {
const effect = queuedEffects const effect = notifyBuffer[notifyIndex]!
const depsTail = effect.depsTail! notifyBuffer[notifyIndex++] = undefined
const queuedNext = depsTail.nextDep
if (queuedNext !== undefined) {
depsTail.nextDep = undefined
queuedEffects = queuedNext.sub as Effect
} else {
queuedEffects = undefined
queuedEffectsTail = undefined
}
effect.notify() effect.notify()
} }
notifyIndex = 0
notifyBufferLength = 0
} }
function linkNewDep( function linkNewDep(
@ -259,15 +258,18 @@ function linkNewDep(
return newLink return newLink
} }
function checkDirty(link: Link): boolean { function checkDirty(current: Link): boolean {
let stack = 0 let prevLinks: OneWayLink<Link> | undefined
let checkDepth = 0
let dirty: boolean let dirty: boolean
top: do { top: do {
dirty = false dirty = false
const dep = link.dep const dep = current.dep
if ('flags' in dep) { if (current.sub.flags & SubscriberFlags.Dirty) {
dirty = true
} else if ('flags' in dep) {
const depFlags = dep.flags const depFlags = dep.flags
if ( if (
(depFlags & (SubscriberFlags.Computed | SubscriberFlags.Dirty)) === (depFlags & (SubscriberFlags.Computed | SubscriberFlags.Dirty)) ===
@ -285,58 +287,49 @@ function checkDirty(link: Link): boolean {
(SubscriberFlags.Computed | SubscriberFlags.PendingComputed)) === (SubscriberFlags.Computed | SubscriberFlags.PendingComputed)) ===
(SubscriberFlags.Computed | SubscriberFlags.PendingComputed) (SubscriberFlags.Computed | SubscriberFlags.PendingComputed)
) { ) {
const depSubs = dep.subs! if (current.nextSub !== undefined || current.prevSub !== undefined) {
if (depSubs.nextSub !== undefined) { prevLinks = { target: current, linked: prevLinks }
depSubs.prevSub = link
} }
link = dep.deps! current = dep.deps!
++stack ++checkDepth
continue continue
} }
} }
if (!dirty && link.nextDep !== undefined) { if (!dirty && current.nextDep !== undefined) {
link = link.nextDep current = current.nextDep
continue continue
} }
if (stack) { while (checkDepth) {
let sub = link.sub as Computed --checkDepth
do { const sub = current.sub as Computed
--stack const firstSub = sub.subs!
const subSubs = sub.subs! if (dirty) {
if (sub.update()) {
if (dirty) { if (firstSub.nextSub !== undefined) {
if (sub.update()) { current = prevLinks!.target
if ((link = subSubs.prevSub!) !== undefined) { prevLinks = prevLinks!.linked
subSubs.prevSub = undefined shallowPropagate(firstSub)
shallowPropagate(subSubs) } else {
sub = link.sub as Computed current = firstSub
} else {
sub = subSubs.sub as Computed
}
continue
} }
} else { continue
sub.flags &= ~SubscriberFlags.PendingComputed
} }
} else {
if ((link = subSubs.prevSub!) !== undefined) { sub.flags &= ~SubscriberFlags.PendingComputed
subSubs.prevSub = undefined }
if (link.nextDep !== undefined) { if (firstSub.nextSub !== undefined) {
link = link.nextDep current = prevLinks!.target
continue top prevLinks = prevLinks!.linked
} } else {
sub = link.sub as Computed current = firstSub
} else { }
if ((link = subSubs.nextDep!) !== undefined) { if (current.nextDep !== undefined) {
continue top current = current.nextDep
} continue top
sub = subSubs.sub as Computed }
} dirty = false
dirty = false
} while (stack)
} }
return dirty return dirty

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,28 @@ import {
markRaw, markRaw,
nextTick, nextTick,
nodeOps, nodeOps,
onMounted,
h as originalH, h as originalH,
ref, ref,
render, render,
serialize,
serializeInner, serializeInner,
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', () => {
@ -243,6 +257,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')
@ -269,6 +316,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')
@ -741,4 +816,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

@ -32,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')
@ -1843,6 +1846,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

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

View File

@ -1,17 +1,10 @@
import { type ComputedRefImpl, computed as _computed } from '@vue/reactivity' import { computed as _computed } from '@vue/reactivity'
import { getCurrentInstance, isInSSRComponentSetup } from './component' import { isInSSRComponentSetup } from './component'
export const computed: typeof _computed = ( export const computed: typeof _computed = (
getterOrOptions: any, getterOrOptions: any,
debugOptions?: any, debugOptions?: any,
) => { ) => {
// @ts-expect-error // @ts-expect-error
const c = _computed(getterOrOptions, debugOptions, isInSSRComponentSetup) return _computed(getterOrOptions, debugOptions, isInSSRComponentSetup) as any
if (__DEV__) {
const i = getCurrentInstance()
if (i && i.appContext.config.warnRecursiveComputed) {
;(c as unknown as ComputedRefImpl<any>)._warnRecursive = true
}
}
return c as any
} }

View File

@ -40,9 +40,9 @@ export interface App<HostElement = any> {
use<Options extends unknown[]>( use<Options extends unknown[]>(
plugin: Plugin<Options>, plugin: Plugin<Options>,
...options: Options ...options: NoInfer<Options>
): this ): this
use<Options>(plugin: Plugin<Options>, options: Options): this use<Options>(plugin: Plugin<Options>, options: NoInfer<Options>): this
mixin(mixin: ComponentOptions): this mixin(mixin: ComponentOptions): this
component(name: string): Component | undefined component(name: string): Component | undefined
@ -140,12 +140,6 @@ export interface GenericAppConfig {
trace: string, trace: string,
) => void ) => void
/**
* TODO document for 3.5
* Enable warnings for computed getters that recursively trigger itself.
*/
warnRecursiveComputed?: boolean
/** /**
* Whether to throw unhandled errors in production. * Whether to throw unhandled errors in production.
* Default is `false` to avoid crashing on any error (and only logs it) * Default is `false` to avoid crashing on any error (and only logs it)
@ -266,9 +260,11 @@ export type ObjectPlugin<Options = any[]> = {
export type FunctionPlugin<Options = any[]> = PluginInstallFunction<Options> & export type FunctionPlugin<Options = any[]> = PluginInstallFunction<Options> &
Partial<ObjectPlugin<Options>> Partial<ObjectPlugin<Options>>
export type Plugin<Options = any[]> = export type Plugin<
| FunctionPlugin<Options> Options = any[],
| ObjectPlugin<Options> // TODO: in next major Options extends unknown[] and remove P
P extends unknown[] = Options extends unknown[] ? Options : [Options],
> = FunctionPlugin<P> | ObjectPlugin<P>
export function createAppContext(): AppContext { export function createAppContext(): AppContext {
return { return {

View File

@ -837,7 +837,7 @@ export function setupComponent(
vi(instance) vi(instance)
} else { } else {
initProps(instance, props, isStateful, isSSR) initProps(instance, props, isStateful, isSSR)
initSlots(instance, children, optimized) initSlots(instance, children, optimized || isSSR)
} }
const setupResult = isStateful const setupResult = isStateful

View File

@ -17,7 +17,11 @@ import {
} from '@vue/shared' } from '@vue/shared'
import { warn } from './warning' import { warn } from './warning'
import { isKeepAlive } from './components/KeepAlive' import { isKeepAlive } from './components/KeepAlive'
import { type ContextualRenderFn, withCtx } from './componentRenderContext' import {
type ContextualRenderFn,
currentRenderingInstance,
withCtx,
} from './componentRenderContext'
import { isHmrUpdating } from './hmr' import { isHmrUpdating } from './hmr'
import { DeprecationTypes, isCompatEnabled } from './compat/compatConfig' import { DeprecationTypes, isCompatEnabled } from './compat/compatConfig'
import { TriggerOpTypes, trigger } from '@vue/reactivity' import { TriggerOpTypes, trigger } from '@vue/reactivity'
@ -75,6 +79,11 @@ export type RawSlots = {
* @internal * @internal
*/ */
_?: SlotFlags _?: SlotFlags
/**
* cache indexes for slot content
* @internal
*/
__?: number[]
} }
const isInternalKey = (key: string) => key[0] === '_' || key === '$stable' const isInternalKey = (key: string) => key[0] === '_' || key === '$stable'
@ -98,7 +107,8 @@ const normalizeSlot = (
__DEV__ && __DEV__ &&
currentInstance && currentInstance &&
!currentInstance.vapor && !currentInstance.vapor &&
(!ctx || ctx.root === (currentInstance as ComponentInternalInstance).root) !(ctx === null && currentRenderingInstance) &&
!(ctx && ctx.root !== currentInstance.root)
) { ) {
warn( warn(
`Slot "${key}" invoked outside of the render function: ` + `Slot "${key}" invoked outside of the render function: ` +
@ -171,7 +181,7 @@ const assignSlots = (
// when rendering the optimized slots by manually written render function, // when rendering the optimized slots by manually written render function,
// do not copy the `slots._` compiler flag so that `renderSlot` creates // do not copy the `slots._` compiler flag so that `renderSlot` creates
// slot Fragment with BAIL patchFlag to force full updates // slot Fragment with BAIL patchFlag to force full updates
if (optimized || key !== '_') { if (optimized || !isInternalKey(key)) {
slots[key] = children[key] slots[key] = children[key]
} }
} }

View File

@ -501,9 +501,8 @@ function getInnerChild(vnode: VNode): VNode | undefined {
return vnode return vnode
} }
// #7121 ensure get the child component subtree in case // #7121,#12465 get the component subtree if it's been mounted
// it's been replaced during HMR if (vnode.component) {
if (__DEV__ && vnode.component) {
return vnode.component.subTree return vnode.component.subTree
} }

View File

@ -201,6 +201,11 @@ const KeepAliveImpl: ComponentOptions = {
// Update components tree // Update components tree
devtoolsComponentAdded(instance) devtoolsComponentAdded(instance)
} }
// for e2e test
if (__DEV__ && __BROWSER__) {
;(instance as any).__keepAliveStorageContainer = storageContainer
}
} }
function unmount(vnode: VNode) { function unmount(vnode: VNode) {

View File

@ -220,7 +220,8 @@ export const TeleportImpl = {
// even in block tree mode we need to make sure all root-level nodes // even in block tree mode we need to make sure all root-level nodes
// in the teleport inherit previous DOM references so that they can // in the teleport inherit previous DOM references so that they can
// be moved in future patches. // be moved in future patches.
traverseStaticChildren(n1, n2, true) // in dev mode, deep traversal is necessary for HMR
traverseStaticChildren(n1, n2, !__DEV__)
} else if (!optimized) { } else if (!optimized) {
patchChildren( patchChildren(
n1, n1,

View File

@ -4,6 +4,8 @@ import {
isReadonly, isReadonly,
isRef, isRef,
isShallow, isShallow,
pauseTracking,
resetTracking,
toRaw, toRaw,
} from '@vue/reactivity' } from '@vue/reactivity'
import { EMPTY_OBJ, extend, isArray, isFunction, isObject } from '@vue/shared' import { EMPTY_OBJ, extend, isArray, isFunction, isObject } from '@vue/shared'
@ -34,13 +36,16 @@ export function initCustomFormatter(): void {
if (obj.__isVue) { if (obj.__isVue) {
return ['div', vueStyle, `VueInstance`] return ['div', vueStyle, `VueInstance`]
} else if (isRef(obj)) { } else if (isRef(obj)) {
// avoid tracking during debugger accessing
pauseTracking()
const value = obj.value
resetTracking()
return [ return [
'div', 'div',
{}, {},
['span', vueStyle, genRefFlag(obj)], ['span', vueStyle, genRefFlag(obj)],
'<', '<',
// avoid debugger accessing value affecting behavior formatValue(value),
formatValue('_value' in obj ? obj._value : obj),
`>`, `>`,
] ]
} else if (isReactive(obj)) { } else if (isReactive(obj)) {

View File

@ -111,7 +111,9 @@ export type Directive<
| ObjectDirective<HostElement, Value, Modifiers, Arg> | ObjectDirective<HostElement, Value, Modifiers, Arg>
| FunctionDirective<HostElement, Value, Modifiers, Arg> | FunctionDirective<HostElement, Value, Modifiers, Arg>
export type DirectiveModifiers<K extends string = string> = Record<K, boolean> export type DirectiveModifiers<K extends string = string> = Partial<
Record<K, boolean>
>
export function validateDirectiveName(name: string): void { export function validateDirectiveName(name: string): void {
if (isBuiltInDirective(name)) { if (isBuiltInDirective(name)) {

View File

@ -1,9 +1,11 @@
import type { VNode, VNodeChild } from '../vnode' import type { VNode, VNodeChild } from '../vnode'
import { import {
isReactive, isReactive,
isReadonly,
isShallow, isShallow,
shallowReadArray, shallowReadArray,
toReactive, toReactive,
toReadonly,
} from '@vue/reactivity' } from '@vue/reactivity'
import { isArray, isObject, isString } from '@vue/shared' import { isArray, isObject, isString } from '@vue/shared'
import { warn } from '../warning' import { warn } from '../warning'
@ -69,14 +71,20 @@ export function renderList(
if (sourceIsArray || isString(source)) { if (sourceIsArray || isString(source)) {
const sourceIsReactiveArray = sourceIsArray && isReactive(source) const sourceIsReactiveArray = sourceIsArray && isReactive(source)
let needsWrap = false let needsWrap = false
let isReadonlySource = false
if (sourceIsReactiveArray) { if (sourceIsReactiveArray) {
needsWrap = !isShallow(source) needsWrap = !isShallow(source)
isReadonlySource = isReadonly(source)
source = shallowReadArray(source) source = shallowReadArray(source)
} }
ret = new Array(source.length) ret = new Array(source.length)
for (let i = 0, l = source.length; i < l; i++) { for (let i = 0, l = source.length; i < l; i++) {
ret[i] = renderItem( ret[i] = renderItem(
needsWrap ? toReactive(source[i]) : source[i], needsWrap
? isReadonlySource
? toReadonly(toReactive(source[i]))
: toReactive(source[i])
: source[i],
i, i,
undefined, undefined,
cached && cached[i], cached && cached[i],

View File

@ -5,6 +5,8 @@ import { EMPTY_OBJ } from '@vue/shared'
export const knownTemplateRefs: WeakSet<ShallowRef> = new WeakSet() export const knownTemplateRefs: WeakSet<ShallowRef> = new WeakSet()
export type TemplateRef<T = unknown> = Readonly<ShallowRef<T | null>>
export function useTemplateRef<T = unknown, Keys extends string = string>( export function useTemplateRef<T = unknown, Keys extends string = string>(
key: Keys, key: Keys,
): Readonly<ShallowRef<T | null>> { ): Readonly<ShallowRef<T | null>> {

View File

@ -64,7 +64,7 @@ export { defineComponent } from './apiDefineComponent'
export { defineAsyncComponent } from './apiAsyncComponent' export { defineAsyncComponent } from './apiAsyncComponent'
export { useAttrs, useSlots } from './apiSetupHelpers' export { useAttrs, useSlots } from './apiSetupHelpers'
export { useModel } from './helpers/useModel' export { useModel } from './helpers/useModel'
export { useTemplateRef } from './helpers/useTemplateRef' export { useTemplateRef, type TemplateRef } from './helpers/useTemplateRef'
export { useId } from './helpers/useId' export { useId } from './helpers/useId'
export { export {
hydrateOnIdle, hydrateOnIdle,
@ -569,3 +569,7 @@ export {
* @internal * @internal
*/ */
export { markAsyncBoundary } from './helpers/useId' export { markAsyncBoundary } from './helpers/useId'
/**
* @internal
*/
export { createInternalObject } from './internalObject'

View File

@ -1248,12 +1248,12 @@ function baseCreateRenderer(
} }
} }
// avoid hydration for hmr updating
if (__DEV__ && isHmrUpdating) initialVNode.el = null
// setup() is async. This component relies on async logic to be resolved // setup() is async. This component relies on async logic to be resolved
// before proceeding // before proceeding
if (__FEATURE_SUSPENSE__ && instance.asyncDep) { if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
// avoid hydration for hmr updating
if (__DEV__ && isHmrUpdating) initialVNode.el = null
parentSuspense && parentSuspense &&
parentSuspense.registerDep(instance, setupRenderEffect, optimized) parentSuspense.registerDep(instance, setupRenderEffect, optimized)
@ -2120,7 +2120,13 @@ function baseCreateRenderer(
queuePostRenderEffect(() => transition!.enter(el!), parentSuspense) queuePostRenderEffect(() => transition!.enter(el!), parentSuspense)
} else { } else {
const { leave, delayLeave, afterLeave } = transition! const { leave, delayLeave, afterLeave } = transition!
const remove = () => hostInsert(el!, container, anchor) const remove = () => {
if (vnode.ctx!.isUnmounted) {
hostRemove(el!)
} else {
hostInsert(el!, container, anchor)
}
}
const performLeave = () => { const performLeave = () => {
leave(el!, () => { leave(el!, () => {
remove() remove()
@ -2163,7 +2169,9 @@ function baseCreateRenderer(
// unset ref // unset ref
if (ref != null) { if (ref != null) {
pauseTracking()
setRef(ref, null, parentSuspense, vnode, true) setRef(ref, null, parentSuspense, vnode, true)
resetTracking()
} }
// #6593 should clean memo cache when unmount // #6593 should clean memo cache when unmount
@ -2337,7 +2345,17 @@ function baseCreateRenderer(
unregisterHMR(instance) unregisterHMR(instance)
} }
const { bum, scope, job, subTree, um, m, a } = instance const {
bum,
scope,
job,
subTree,
um,
m,
a,
parent,
slots: { __: slotCacheKeys },
} = instance
invalidateMount(m) invalidateMount(m)
invalidateMount(a) invalidateMount(a)
@ -2346,6 +2364,13 @@ function baseCreateRenderer(
invokeArrayFns(bum) invokeArrayFns(bum)
} }
// remove slots content from parent renderCache
if (parent && isArray(slotCacheKeys)) {
slotCacheKeys.forEach(v => {
;(parent as ComponentInternalInstance).renderCache[v] = undefined
})
}
if ( if (
__COMPAT__ && __COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance) isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
@ -2496,9 +2521,12 @@ function baseCreateRenderer(
// HMR root reload // HMR root reload
if (__DEV__) { if (__DEV__) {
app._context.reload = () => { app._context.reload = () => {
const cloned = cloneVNode(vnode)
// avoid hydration for hmr updating
cloned.el = null
// casting to ElementNamespace because TS doesn't guarantee type narrowing // casting to ElementNamespace because TS doesn't guarantee type narrowing
// over function boundaries // over function boundaries
render(cloneVNode(vnode), container, namespace as ElementNamespace) render(cloned, container, namespace as ElementNamespace)
} }
} }
@ -2604,11 +2632,15 @@ export function traverseStaticChildren(
if (c2.type === Text) { if (c2.type === Text) {
c2.el = c1.el c2.el = c1.el
} }
// also inherit for comment nodes, but not placeholders (e.g. v-if which // #2324 also inherit for comment nodes, but not placeholders (e.g. v-if which
// would have received .el during block patch) // would have received .el during block patch)
if (__DEV__ && c2.type === Comment && !c2.el) { if (c2.type === Comment && !c2.el) {
c2.el = c1.el c2.el = c1.el
} }
if (__DEV__) {
c2.el && (c2.el.__vnode = c2)
}
} }
} }
} }

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