Merge branch 'main' into feat/reactiveity-transform-shorthands

This commit is contained in:
Anthony Fu 2022-07-05 13:53:29 +08:00 committed by GitHub
commit fbbaa0cc3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
108 changed files with 2231 additions and 669 deletions

View File

@ -6,6 +6,7 @@ module.exports = {
parserOptions: { parserOptions: {
sourceType: 'module' sourceType: 'module'
}, },
plugins: ["jest"],
rules: { rules: {
'no-debugger': 'error', 'no-debugger': 'error',
'no-unused-vars': [ 'no-unused-vars': [
@ -32,7 +33,9 @@ module.exports = {
files: ['**/__tests__/**', 'test-dts/**'], files: ['**/__tests__/**', 'test-dts/**'],
rules: { rules: {
'no-restricted-globals': 'off', 'no-restricted-globals': 'off',
'no-restricted-syntax': 'off' 'no-restricted-syntax': 'off',
'jest/no-disabled-tests': 'error',
'jest/no-focused-tests': 'error'
} }
}, },
// shared, may be used in any env // shared, may be used in any env

View File

@ -17,6 +17,12 @@ body:
Also try to search for your issue - it may have already been answered or even fixed in the development branch. Also try to search for your issue - it may have already been answered or even fixed in the development branch.
However, if you find that an old, closed issue still persists in the latest version, However, if you find that an old, closed issue still persists in the latest version,
you should open a new issue using the form below instead of commenting on the old issue. you should open a new issue using the form below instead of commenting on the old issue.
- type: input
id: version
attributes:
label: Vue version
validations:
required: true
- type: input - type: input
id: reproduction-link id: reproduction-link
attributes: attributes:

View File

@ -19,6 +19,8 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before
- Checkout a topic branch from a base branch, e.g. `main`, and merge back against that branch. - Checkout a topic branch from a base branch, e.g. `main`, and merge back against that branch.
- [Make sure to tick the "Allow edits from maintainers" box](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork). This allows us to directly make minor edits / refactors and saves a lot of time.
- If adding a new feature: - If adding a new feature:
- Add accompanying test case. - Add accompanying test case.

View File

@ -62,3 +62,9 @@ updates:
- dependency-name: node-notifier - dependency-name: node-notifier
versions: versions:
- 8.0.1 - 8.0.1
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: monthly
open-pull-requests-limit: 10
versioning-strategy: lockfile-only

View File

@ -13,9 +13,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2.0.1 uses: pnpm/action-setup@v2
with:
version: 7.0.1
- name: Set node version to 16 - name: Set node version to 16
uses: actions/setup-node@v2 uses: actions/setup-node@v2
@ -34,9 +32,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2.0.1 uses: pnpm/action-setup@v2
with:
version: 7.0.1
- name: Set node version to 16 - name: Set node version to 16
uses: actions/setup-node@v2 uses: actions/setup-node@v2
@ -55,9 +51,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2.0.1 uses: pnpm/action-setup@v2
with:
version: 7.0.1
- name: Set node version to 16 - name: Set node version to 16
uses: actions/setup-node@v2 uses: actions/setup-node@v2
@ -81,9 +75,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2.0.1 uses: pnpm/action-setup@v2
with:
version: 7.0.1
- name: Set node version to 16 - name: Set node version to 16
uses: actions/setup-node@v2 uses: actions/setup-node@v2

View File

@ -1,3 +1,138 @@
## [3.2.37](https://github.com/vuejs/core/compare/v3.2.36...v3.2.37) (2022-06-06)
### Bug Fixes
* **compiler-sfc:** improve css v-bind parsing ([e60244b](https://github.com/vuejs/core/commit/e60244bcdf0b386de1560ff7c205ae0870bab355)), closes [#6022](https://github.com/vuejs/core/issues/6022)
* **runtime-core:** hydrate Static vnode ([#6015](https://github.com/vuejs/core/issues/6015)) ([11e17a1](https://github.com/vuejs/core/commit/11e17a1a29cf3d0b37628241d63ff3e8d8525f95)), closes [#6008](https://github.com/vuejs/core/issues/6008)
* **sfc:** avoid auto name inference leading to unwanted recursion ([9734b31](https://github.com/vuejs/core/commit/9734b31c312244a2b5c5cf83c75d7b34076a0c4b)), closes [#5965](https://github.com/vuejs/core/issues/5965) [#6027](https://github.com/vuejs/core/issues/6027) [#6029](https://github.com/vuejs/core/issues/6029)
* **ssr:** ensure app can be unmounted when created with createSSRApp() ([#5992](https://github.com/vuejs/core/issues/5992)) ([d4d3319](https://github.com/vuejs/core/commit/d4d3319c1be16dc9a046b2c5521096debc205f25)), closes [#5990](https://github.com/vuejs/core/issues/5990)
* **ssr:** hydration for transition wrapper components with empty slot content ([#5995](https://github.com/vuejs/core/issues/5995)) ([eb22a62](https://github.com/vuejs/core/commit/eb22a62798d845a8756b0a73b68afdd874feda59)), closes [#5991](https://github.com/vuejs/core/issues/5991)
## [3.2.36](https://github.com/vuejs/core/compare/v3.2.35...v3.2.36) (2022-05-23)
### Bug Fixes
* **compat:** fix app-level asset registration affecting other local apps ([#5979](https://github.com/vuejs/core/issues/5979)) ([7fb5732](https://github.com/vuejs/core/commit/7fb57327b9d0e4d9eb675149f167d915fb0d59fa))
* **compat:** fix globalProperties pollution in v3 mode ([2f07e34](https://github.com/vuejs/core/commit/2f07e3460bf51bc1b083f3d03b3d192e042d2d75)), closes [#5699](https://github.com/vuejs/core/issues/5699)
* **compiler-core:** fix svg with directives being incorrectly hoisted ([#5919](https://github.com/vuejs/core/issues/5919)) ([7fbc933](https://github.com/vuejs/core/commit/7fbc933f4d80c0259ee24872ba790681cf3cbe76)), closes [#5289](https://github.com/vuejs/core/issues/5289)
* **sfc/types:** allow use default factory for primitive types in `withDefaults` ([#5939](https://github.com/vuejs/core/issues/5939)) ([b546282](https://github.com/vuejs/core/commit/b5462822d6c0a43866deef2b3437bbe3bbfb3625)), closes [#5938](https://github.com/vuejs/core/issues/5938)
* **transition:** fix cancel leave regression ([#5974](https://github.com/vuejs/core/issues/5974)) ([dddbd96](https://github.com/vuejs/core/commit/dddbd96dfe69292cee401f72d2703e8fb3708a14)), closes [#5973](https://github.com/vuejs/core/issues/5973)
### Performance Improvements
* improve the performance of getNow ([#5944](https://github.com/vuejs/core/issues/5944)) ([3bdc41d](https://github.com/vuejs/core/commit/3bdc41dff305422cb5334a64353c314bce1202a4))
## [3.2.35](https://github.com/vuejs/core/compare/v3.2.34...v3.2.35) (2022-05-20)
### Bug Fixes
* **compiler-sfc:** fix usage detection for types in v-for/v-slot expressions ([583b625](https://github.com/vuejs/core/commit/583b6259870211c32efee0bb4a60b342799d80f7)), closes [#5959](https://github.com/vuejs/core/issues/5959)
* **types:** fix typescript error when spreading `$props`([#5968](https://github.com/vuejs/core/issues/5968)) ([0c7fd13](https://github.com/vuejs/core/commit/0c7fd13ea628a2f1b72c6f4150c0dba32da4468e))
* **types:** restore DefineComponent argument order ([8071ef4](https://github.com/vuejs/core/commit/8071ef47b5adcd5fcd9d0d2ea2cefff5c34ce095)), closes [#5416](https://github.com/vuejs/core/issues/5416) [#3796](https://github.com/vuejs/core/issues/3796) [#5967](https://github.com/vuejs/core/issues/5967)
## [3.2.34](https://github.com/vuejs/core/compare/v3.2.34-beta.1...v3.2.34) (2022-05-19)
### Bug Fixes
* **compiler-core:** should generate HYDRATE_EVENTS flag on dynamic component that resolves to element ([415091b](https://github.com/vuejs/core/commit/415091b0ee2de66e622145028f00523f2032ce77)), closes [#5870](https://github.com/vuejs/core/issues/5870)
* **compiler-sfc:** support `export { default } from '...'` ([#5937](https://github.com/vuejs/core/issues/5937)) ([73e6523](https://github.com/vuejs/core/commit/73e6523134a013f9e369f53f213a214497ac7c40)), closes [#5935](https://github.com/vuejs/core/issues/5935)
* **compiler-sfc:** type-only defineProps does not recognize Promise (fix [#5941](https://github.com/vuejs/core/issues/5941)) ([#5943](https://github.com/vuejs/core/issues/5943)) ([991d623](https://github.com/vuejs/core/commit/991d62322fa67d50b7ae8b0460f294d6b39f9711))
* **compiler-ssr:** fix component event handlers inheritance in ssr ([f811dc2](https://github.com/vuejs/core/commit/f811dc2b60ba7efdbb9b1ab330dcbc18c1cc9a75)), closes [#5664](https://github.com/vuejs/core/issues/5664)
* **compiler-ssr:** fix wrong attrs fallthrough on non-single-root v-if branches ([516bc54](https://github.com/vuejs/core/commit/516bc548fce295f6d564c7fb371c8067ead7cd71)), closes [#5140](https://github.com/vuejs/core/issues/5140)
* **compiler-ssr:** only inject fallthrough attrs for root transition/keep-alive ([c65b805](https://github.com/vuejs/core/commit/c65b805ef1f9b164fb8aaa7bc679a91248b5891a))
* **keep-alive:** fix keep-alive rendering when placed in vnode branch ([0841b9b](https://github.com/vuejs/core/commit/0841b9b5243acdaf191099b25e9a145b30189dea)), closes [#4817](https://github.com/vuejs/core/issues/4817)
* **runtime-core:** adjust force diff of dev root fragments ([cdda49b](https://github.com/vuejs/core/commit/cdda49bbfb1939c9cf812d624992ea7bdae74c78)), closes [#5946](https://github.com/vuejs/core/issues/5946)
* **ssr/teleport:** support nested teleports in ssr ([595263c](https://github.com/vuejs/core/commit/595263c0e9f5728c3650c6526dbed27cda9ba114)), closes [#5242](https://github.com/vuejs/core/issues/5242)
* **ssr:** fix hydration error on falsy v-if inside transition/keep-alive ([ee4186e](https://github.com/vuejs/core/commit/ee4186ef9ebbc45827b208f6f5b648dbf4337d1d)), closes [#5352](https://github.com/vuejs/core/issues/5352)
* **ssr:** fix hydration error when teleport is used as component root ([b60cff0](https://github.com/vuejs/core/commit/b60cff052c880b2965d06007f0ec4d0349ab47c0)), closes [#4293](https://github.com/vuejs/core/issues/4293)
* **ssr:** fix hydration error when transition contains comment children ([3705b3b](https://github.com/vuejs/core/commit/3705b3b46aa8f3e929014f564f8afa4a663e6375)), closes [#5351](https://github.com/vuejs/core/issues/5351)
* **ssr:** fix hydration for slot with empty text node ([939209c](https://github.com/vuejs/core/commit/939209c6b554aed6634d9cf2ca10a2aa46ba7673)), closes [#5728](https://github.com/vuejs/core/issues/5728)
* **ssr:** fix hydration mismatch caused by multi-line comments inside slot ([e1bc268](https://github.com/vuejs/core/commit/e1bc2681ef64aed7975ad38950a478ae53c1abad)), closes [#5355](https://github.com/vuejs/core/issues/5355)
* **ssr:** inherit scope id on functional component during ssr ([847d7f7](https://github.com/vuejs/core/commit/847d7f782bb6074c6b31378e07d94cb41ad30bd2)), closes [#5817](https://github.com/vuejs/core/issues/5817)
* **ssr:** render fallthrough attributes for transition-group with tag ([aed10c5](https://github.com/vuejs/core/commit/aed10c507279900f8afc4861dc01ca4f2b95acb8)), closes [#5141](https://github.com/vuejs/core/issues/5141)
* **ssr:** support client-compiled v-model with dynamic type during ssr ([#5787](https://github.com/vuejs/core/issues/5787)) ([c03459b](https://github.com/vuejs/core/commit/c03459b9b6d3c18450235bc4074a603677996320)), closes [#5786](https://github.com/vuejs/core/issues/5786)
* **types:** export ComponentProvideOptions ([#5947](https://github.com/vuejs/core/issues/5947)) ([3e2850f](https://github.com/vuejs/core/commit/3e2850fa6c628284b4a1ab5deba3b11f1d2f66b6))
* **types:** fix `defineComponent` inference to `Component` ([#5949](https://github.com/vuejs/core/issues/5949)) ([7c8f457](https://github.com/vuejs/core/commit/7c8f4578e9e7178e326cf8e343f7a8b4143ba63b))
## [3.2.34-beta.1](https://github.com/vuejs/core/compare/v3.2.33...v3.2.34-beta.1) (2022-05-17)
### Bug Fixes
* **compiler-core:** normalize v-bind:style with array literal value ([0f00cf4](https://github.com/vuejs/core/commit/0f00cf43cf5eaeeee7f12d523a5f4936f7dc0a84)), closes [#5106](https://github.com/vuejs/core/issues/5106)
* **compiler-core:** template v-if should never be treated as dev root fragment ([51f3d38](https://github.com/vuejs/core/commit/51f3d386de7f5fcec6eb4c1c223ba824be036648)), closes [#5189](https://github.com/vuejs/core/issues/5189)
* **compiler-dom:** properly stringify v-html/v-text with constant value ([6283b2e](https://github.com/vuejs/core/commit/6283b2ec413b78fe88775d249d3598cdce977b7a)), closes [#5439](https://github.com/vuejs/core/issues/5439) [#5445](https://github.com/vuejs/core/issues/5445)
* **compiler-sfc:** `<script>` after `<script setup>` the script content not end with `\\n` ([3b7b107](https://github.com/vuejs/core/commit/3b7b107120c6dba70b068312afd594c3575ea9eb))
* **compiler-sfc:** add test for [#5808](https://github.com/vuejs/core/issues/5808) ([a0290fe](https://github.com/vuejs/core/commit/a0290fe781a0ab2e90239d615d18fdb7ee37cdfe))
* **compiler-sfc:** async transformer doesn't correctly detect need for semicolon in block [#5808](https://github.com/vuejs/core/issues/5808) ([6c3b681](https://github.com/vuejs/core/commit/6c3b681d235bc70293827c535572f90be1ab6c68))
* **compiler-sfc:** automatically infer component name from filename when using script setup ([#4997](https://github.com/vuejs/core/issues/4997)) ([1693924](https://github.com/vuejs/core/commit/16939241b0f64cefea254b024a9b5a25caea93d9)), closes [#4993](https://github.com/vuejs/core/issues/4993)
* **compiler-sfc:** defineProps return binding or rest binding should be considered reactive ([4101441](https://github.com/vuejs/core/commit/410144149fbaaecad2b2d36a9cfe965ab7b2b6e6))
* **compiler-sfc:** ensure consistent behavior of export default render with script setup ([b7025d2](https://github.com/vuejs/core/commit/b7025d24f1c33023d020d60292319740852d1810)), closes [#4980](https://github.com/vuejs/core/issues/4980)
* **compiler-sfc:** fix defineEmits() scope reference check error message ([#5404](https://github.com/vuejs/core/issues/5404)) ([f2c48f5](https://github.com/vuejs/core/commit/f2c48f535250d01133a5e49c0bf2c26a46b6d935))
* **compiler-sfc:** fix object default values for reactive props destructure ([7dfe146](https://github.com/vuejs/core/commit/7dfe146096487a98ba1733598c44407bc89a1b9f))
* **compiler-sfc:** fix skipped srcset transform when using base option ([41d255b](https://github.com/vuejs/core/commit/41d255ba5ddd40f1a1dd2dd6add01f38e20d6325)), closes [#4835](https://github.com/vuejs/core/issues/4835) [#4819](https://github.com/vuejs/core/issues/4819) [#4834](https://github.com/vuejs/core/issues/4834) [#4835](https://github.com/vuejs/core/issues/4835)
* **compiler-sfc:** fix template usage check false positives on types ([ccf9256](https://github.com/vuejs/core/commit/ccf92564d339cdee3947aecfba2e861c88864883)), closes [#5414](https://github.com/vuejs/core/issues/5414)
* **compiler-sfc:** fix treeshaking of namespace import when used in template ([8a123ac](https://github.com/vuejs/core/commit/8a123ac34fd30fc36ac9e0c252dd345d6d7c7f50)), closes [#5209](https://github.com/vuejs/core/issues/5209)
* **compiler-sfc:** remove the jsx from the babelParserPlugins when not match the case of adding jsx ([#5846](https://github.com/vuejs/core/issues/5846)) ([7d7a241](https://github.com/vuejs/core/commit/7d7a2410e58d3ae59ca3fcf619f332699209fc96)), closes [#5845](https://github.com/vuejs/core/issues/5845)
* **keep-alive:** fix unmounting late-included components ([da49c86](https://github.com/vuejs/core/commit/da49c863a21fb9d9de4a1b816dcc4faff8046fdb)), closes [#3648](https://github.com/vuejs/core/issues/3648) [#3650](https://github.com/vuejs/core/issues/3650)
* **keep-alive:** invoke initial activated hook for async components ([20ed16f](https://github.com/vuejs/core/commit/20ed16f68c632a271d4e8cb09087c7ed4e936637)), closes [#5459](https://github.com/vuejs/core/issues/5459) [#5095](https://github.com/vuejs/core/issues/5095) [#5651](https://github.com/vuejs/core/issues/5651)
* **reactivity-transform:** fix props access codegen for non-identifier prop names ([#5436](https://github.com/vuejs/core/issues/5436)) ([242914d](https://github.com/vuejs/core/commit/242914d938fccad1c09a52a7ed09f15ac509cf6b)), closes [#5425](https://github.com/vuejs/core/issues/5425)
* **reactivity:** ensure computed is invalidated before other effects ([82bdf86](https://github.com/vuejs/core/commit/82bdf8625475e81c44f0d4db4061b882d2fe7612)), closes [#5720](https://github.com/vuejs/core/issues/5720)
* **reactivity:** ios10.x compatibility ([#4900](https://github.com/vuejs/core/issues/4900)) ([b87dc06](https://github.com/vuejs/core/commit/b87dc061930e62c78b4cc9a79f385e5880cdf8ae))
* **runtime-core:** clone root vnode before inheriting directives ([d36ca4d](https://github.com/vuejs/core/commit/d36ca4d80e297056f6c78cd7e3bc2004e0b58660))
* **runtime-core:** ensure consistent behavior between dev/prod on invalid v-for range ([67099fe](https://github.com/vuejs/core/commit/67099fe20299a51f9974f0e2f9ef19ca05efe92b)), closes [#5867](https://github.com/vuejs/core/issues/5867)
* **runtime-core:** ensure consistent identity of $forceUpdate and $nextTick instance methods ([d52907f](https://github.com/vuejs/core/commit/d52907f4ebc9bcfd7b3141fbebecd4c5b19aa80f)), closes [#5556](https://github.com/vuejs/core/issues/5556)
* **runtime-core:** ensure raw slot function is only normalized once ([#5358](https://github.com/vuejs/core/issues/5358)) ([e4dffe9](https://github.com/vuejs/core/commit/e4dffe900a7475c9b4c22c06283b5635e5c2de37)), closes [#5343](https://github.com/vuejs/core/issues/5343)
* **runtime-core:** fix activated hook when using async component with KeepAlive ([#5459](https://github.com/vuejs/core/issues/5459)) ([f1d1cdb](https://github.com/vuejs/core/commit/f1d1cdbb699e27ac6a0d241209f7e8f8d9ebf2c7)), closes [#5095](https://github.com/vuejs/core/issues/5095) [#5651](https://github.com/vuejs/core/issues/5651)
* **runtime-core:** fix directive inheritance on dev root fragment ([2bab639](https://github.com/vuejs/core/commit/2bab63968355995a4026cf5cd1ccad7c79e8d89c)), closes [#5523](https://github.com/vuejs/core/issues/5523)
* **runtime-core:** fix missed updates when passing vnode to <component :is> ([ba17792](https://github.com/vuejs/core/commit/ba17792b7248552ea01a1ca6eca7d49b47d827f0)), closes [#4903](https://github.com/vuejs/core/issues/4903)
* **runtime-core:** handle NaN identity check in v-memo ([#5852](https://github.com/vuejs/core/issues/5852)) ([a388129](https://github.com/vuejs/core/commit/a3881299e98de6217f05bbd42ac509e8cc8be3d9)), closes [#5853](https://github.com/vuejs/core/issues/5853)
* **runtime-core:** transition hooks can be arrays of functions ([#5177](https://github.com/vuejs/core/issues/5177)) ([fec12d7](https://github.com/vuejs/core/commit/fec12d7dccc573c016ee2d8e1fa2b67f134c2786))
* **runtime-dom:** "el._assign is not a function" in compat mode ([#4121](https://github.com/vuejs/core/issues/4121)) ([e58277f](https://github.com/vuejs/core/commit/e58277f6eaeaec84cf05b34126bec01b619a1b90))
* **sfc-playground:** default selected app ([#5370](https://github.com/vuejs/core/issues/5370)) ([04fff05](https://github.com/vuejs/core/commit/04fff05f0043adb753928e806f9d8fb604701a38))
* **shared:** improve isDate check ([#5803](https://github.com/vuejs/core/issues/5803)) ([eef1447](https://github.com/vuejs/core/commit/eef14471b228e8f3d3c921a0451d268f81a0a74d))
* **shared:** missed Symbol judge in looseEqual ([#3553](https://github.com/vuejs/core/issues/3553)) ([0aeb4bc](https://github.com/vuejs/core/commit/0aeb4bc9bf68c0006b496142bb5aeb3f06689b7c))
* **ssr/sfc-css-vars:** fix v-bind css vars codegen for SSR ([efea4a8](https://github.com/vuejs/core/commit/efea4a8b5784f4660836b124d16dc29f7fea41e4)), closes [#5443](https://github.com/vuejs/core/issues/5443) [#5444](https://github.com/vuejs/core/issues/5444)
* **ssr:** don't warn for missing teleport target if disabled ([#5135](https://github.com/vuejs/core/issues/5135)) ([da10dd7](https://github.com/vuejs/core/commit/da10dd7de91123666152d058f3c18da3e9e7f22a))
* **ssr:** fix hydration error for slot outlet inside transition ([9309b04](https://github.com/vuejs/core/commit/9309b044bd4f9d0a34e0d702ed4690a529443a41)), closes [#3989](https://github.com/vuejs/core/issues/3989)
* **ssr:** fix ssr render output for fragment in slots ([70c2d5b](https://github.com/vuejs/core/commit/70c2d5bbc066585febd12536a6ab39c4384b277b)), closes [#5859](https://github.com/vuejs/core/issues/5859)
* **ssr:** implement empty read() on node stream ([c355c4b](https://github.com/vuejs/core/commit/c355c4b78451708b04e17a7c50680dee507f0c40)), closes [#3846](https://github.com/vuejs/core/issues/3846) [#3867](https://github.com/vuejs/core/issues/3867)
* **ssr:** render teleport inside async component ([#5187](https://github.com/vuejs/core/issues/5187)) ([4d7803e](https://github.com/vuejs/core/commit/4d7803ed28fb67d45a83d3500f5407754e65bf64))
* **ssr:** resolve teleports for stream render APIs ([77fef97](https://github.com/vuejs/core/commit/77fef9734496abe1e842d8e4086c497656d1fab2))
* **ssr:** should de-optimize on vnode with PatchFlags.BAIL ([#4818](https://github.com/vuejs/core/issues/4818)) ([cd659fc](https://github.com/vuejs/core/commit/cd659fc86f74741987bacc351f0d94ef3b80a1ca)), closes [#4679](https://github.com/vuejs/core/issues/4679) [#5771](https://github.com/vuejs/core/issues/5771)
* **ssr:** should not hoist transformed asset urls in ssr compile ([57bb37b](https://github.com/vuejs/core/commit/57bb37bd64cc6b2aa3099d91585441db6d43e2a2)), closes [#3874](https://github.com/vuejs/core/issues/3874)
* **transition/v-show:** ensure transition is in persisted mode when used with v-show ([425310e](https://github.com/vuejs/core/commit/425310e8b614ad91660dd93d4e7905fbe572ef31)), closes [#4845](https://github.com/vuejs/core/issues/4845) [#4852](https://github.com/vuejs/core/issues/4852)
* **transition:** handle edge case of cancel leave before next frame ([59cf295](https://github.com/vuejs/core/commit/59cf2958e7bae5f1e13953373fcdbb4ad988be6c)), closes [#4462](https://github.com/vuejs/core/issues/4462)
* **types:** add `Set<any>` to checkbox binding type for v-model ([#5713](https://github.com/vuejs/core/issues/5713)) ([e5a9089](https://github.com/vuejs/core/commit/e5a90893a6a25a40464cc9d5484093ec531e8b78))
* **types:** allow css variables in style binding ([#5542](https://github.com/vuejs/core/issues/5542)) ([9def7aa](https://github.com/vuejs/core/commit/9def7aa50827b12fdfbef28e5db6f373d5cba279))
* **types:** allow indeterminate for checkbox ([#3473](https://github.com/vuejs/core/issues/3473)) ([d4fcfdd](https://github.com/vuejs/core/commit/d4fcfddec6170cc1528a285fc8e0b3f3c5590311))
* **types:** keep the original type when unwrapping `markRaw` ([#3791](https://github.com/vuejs/core/issues/3791)) ([32e53bf](https://github.com/vuejs/core/commit/32e53bfd478af895dd090ea6c8766fa043e179e4))
* **types:** preserve and expose original options on defineComponent return type ([#5416](https://github.com/vuejs/core/issues/5416)) ([98b821d](https://github.com/vuejs/core/commit/98b821d94a0a0fb4d7701809da6bec331a47e6e5)), closes [#3796](https://github.com/vuejs/core/issues/3796)
* **v-model:** exclude range from lazy guard logic ([8c51c65](https://github.com/vuejs/core/commit/8c51c6514f99b4c70183f4c1a0eaabd482f88d5b)), closes [#5875](https://github.com/vuejs/core/issues/5875)
* **v-model:** fix case where .trim and .number modifiers are used together ([#5842](https://github.com/vuejs/core/issues/5842)) ([71066b5](https://github.com/vuejs/core/commit/71066b5afed7d3707b8ec9a6313dbdbd1adad45e)), closes [#5839](https://github.com/vuejs/core/issues/5839)
* **watch:** fix flush: pre watchers triggered synchronously in setup ([74d2a76](https://github.com/vuejs/core/commit/74d2a76af6e830af5abb8aac8484dc1b3e90a510)), closes [#5721](https://github.com/vuejs/core/issues/5721)
* **watch:** fix watching multiple sources containing shallowRef ([#5381](https://github.com/vuejs/core/issues/5381)) ([220f255](https://github.com/vuejs/core/commit/220f255fe94d5f1d2ccf3af3a469e9328c916167)), closes [#5371](https://github.com/vuejs/core/issues/5371)
### Features
* **types:** avoid props JSDocs loss by `default` option ([#5871](https://github.com/vuejs/core/issues/5871)) ([c901dca](https://github.com/vuejs/core/commit/c901dca5add2c32554403e5896247fdb8aa7cf7d))
## [3.2.33](https://github.com/vuejs/core/compare/v3.2.32...v3.2.33) (2022-04-14) ## [3.2.33](https://github.com/vuejs/core/compare/v3.2.32...v3.2.33) (2022-04-14)

View File

@ -1,13 +1,14 @@
{ {
"private": true, "private": true,
"version": "3.2.33", "version": "3.2.37",
"packageManager": "pnpm@7.1.0",
"scripts": { "scripts": {
"dev": "node scripts/dev.js", "dev": "node scripts/dev.js",
"build": "node scripts/build.js", "build": "node scripts/build.js",
"size": "run-s size-global size-baseline", "size": "run-s size-global size-baseline",
"size-global": "node scripts/build.js vue runtime-dom -f global -p", "size-global": "node scripts/build.js vue runtime-dom -f global -p",
"size-baseline": "node scripts/build.js runtime-dom runtime-core reactivity shared -f esm-bundler && cd packages/size-check && vite build && node brotli", "size-baseline": "node scripts/build.js runtime-dom runtime-core reactivity shared -f esm-bundler && cd packages/size-check && vite build && node brotli",
"lint": "eslint --ext .ts packages/*/src/**.ts", "lint": "eslint --ext .ts packages/*/{src,__tests__}/**.ts",
"format": "prettier --write --parser typescript \"packages/**/*.ts?(x)\"", "format": "prettier --write --parser typescript \"packages/**/*.ts?(x)\"",
"test": "run-s \"test-unit {@}\" \"test-e2e {@}\"", "test": "run-s \"test-unit {@}\" \"test-e2e {@}\"",
"test-unit": "jest --filter ./scripts/filter-unit.js", "test-unit": "jest --filter ./scripts/filter-unit.js",
@ -17,13 +18,14 @@
"test-coverage": "node scripts/build.js vue -f global -d && jest --runInBand --coverage --bail", "test-coverage": "node scripts/build.js vue -f global -d && jest --runInBand --coverage --bail",
"release": "node scripts/release.js", "release": "node scripts/release.js",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"dev-esm": "node scripts/dev.js -if esm-bundler-runtime",
"dev-compiler": "run-p \"dev template-explorer\" serve", "dev-compiler": "run-p \"dev template-explorer\" serve",
"dev-sfc": "run-p \"dev compiler-sfc -f esm-browser\" \"dev vue -if esm-bundler-runtime \" serve-sfc-playground", "dev-sfc": "run-p \"dev compiler-sfc -f esm-browser\" \"dev vue -if esm-bundler-runtime\" \"dev server-renderer -if esm-bundler\" serve-sfc-playground",
"serve-sfc-playground": "vite packages/sfc-playground --host", "serve-sfc-playground": "vite packages/sfc-playground --host",
"serve": "serve", "serve": "serve",
"open": "open http://localhost:5000/packages/template-explorer/local.html", "open": "open http://localhost:5000/packages/template-explorer/local.html",
"preinstall": "node ./scripts/preinstall.js", "preinstall": "node ./scripts/preinstall.js",
"prebuild-sfc-playground": "node scripts/build.js compiler reactivity-transform shared -af cjs && node scripts/build.js runtime reactivity shared -af esm-bundler && node scripts/build.js vue -f esm-bundler-runtime && node scripts/build.js vue -f esm-browser-runtime && node scripts/build.js compiler-sfc -f esm-browser", "prebuild-sfc-playground": "node scripts/build.js compiler reactivity-transform shared -af cjs && node scripts/build.js runtime reactivity shared -af esm-bundler && node scripts/build.js vue -f esm-bundler-runtime && node scripts/build.js vue -f esm-browser-runtime && node scripts/build.js compiler-sfc server-renderer -f esm-browser",
"build-sfc-playground": "cd packages/sfc-playground && npm run build" "build-sfc-playground": "cd packages/sfc-playground && npm run build"
}, },
"types": "test-dts/index.d.ts", "types": "test-dts/index.d.ts",
@ -44,7 +46,7 @@
] ]
}, },
"engines": { "engines": {
"node": ">=16.5.0" "node": ">=16.11.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/types": "^7.12.0", "@babel/types": "^7.12.0",
@ -69,6 +71,7 @@
"enquirer": "^2.3.2", "enquirer": "^2.3.2",
"esbuild": "^0.14.35", "esbuild": "^0.14.35",
"eslint": "^7.7.0", "eslint": "^7.7.0",
"eslint-plugin-jest": "26.1.5",
"execa": "^4.0.2", "execa": "^4.0.2",
"fs-extra": "^9.0.1", "fs-extra": "^9.0.1",
"jest": "^27.1.0", "jest": "^27.1.0",

View File

@ -211,6 +211,30 @@ return function render(_ctx, _cache) {
}" }"
`; `;
exports[`compiler: hoistStatic transform prefixIdentifiers should NOT hoist SVG with directives 1`] = `
"const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode(\\"path\\", { d: \\"M2,3H5.5L12\\" }, null, -1 /* HOISTED */)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) {
with (_ctx) {
const { createElementVNode: _createElementVNode, resolveDirective: _resolveDirective, openBlock: _openBlock, createElementBlock: _createElementBlock, withDirectives: _withDirectives } = _Vue
const _directive_foo = _resolveDirective(\\"foo\\")
return (_openBlock(), _createElementBlock(\\"div\\", null, [
_withDirectives((_openBlock(), _createElementBlock(\\"svg\\", null, _hoisted_2)), [
[_directive_foo]
])
]))
}
}"
`;
exports[`compiler: hoistStatic transform prefixIdentifiers should NOT hoist elements with cached handlers + other bindings 1`] = ` exports[`compiler: hoistStatic transform prefixIdentifiers should NOT hoist elements with cached handlers + other bindings 1`] = `
"import { normalizeClass as _normalizeClass, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from \\"vue\\" "import { normalizeClass as _normalizeClass, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from \\"vue\\"

View File

@ -616,5 +616,11 @@ describe('compiler: hoistStatic transform', () => {
expect(root.hoists.length).toBe(0) expect(root.hoists.length).toBe(0)
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('should NOT hoist SVG with directives', () => {
const root = transformWithHoist(`<div><svg v-foo><path d="M2,3H5.5L12"/></svg></div>`)
expect(root.hoists.length).toBe(2)
expect(generate(root).code).toMatchSnapshot()
})
}) })
}) })

View File

@ -1053,6 +1053,21 @@ describe('compiler: element transform', () => {
genFlagText([PatchFlags.PROPS, PatchFlags.HYDRATE_EVENTS]) genFlagText([PatchFlags.PROPS, PatchFlags.HYDRATE_EVENTS])
) )
}) })
// #5870
test('HYDRATE_EVENTS on dynamic component', () => {
const { node } = parseWithElementTransform(
`<component :is="foo" @input="foo" />`,
{
directiveTransforms: {
on: transformOn
}
}
)
expect(node.patchFlag).toBe(
genFlagText([PatchFlags.PROPS, PatchFlags.HYDRATE_EVENTS])
)
})
}) })
describe('dynamic component', () => { describe('dynamic component', () => {

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compiler-core", "name": "@vue/compiler-core",
"version": "3.2.33", "version": "3.2.37",
"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",
@ -32,7 +32,7 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-core#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-core#readme",
"dependencies": { "dependencies": {
"@vue/shared": "3.2.33", "@vue/shared": "3.2.37",
"@babel/parser": "^7.16.4", "@babel/parser": "^7.16.4",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"source-map": "^0.6.1" "source-map": "^0.6.1"

View File

@ -68,7 +68,7 @@ export interface DirectiveTransformResult {
ssrTagParts?: TemplateLiteral['elements'] ssrTagParts?: TemplateLiteral['elements']
} }
// A structural directive transform is a technically a NodeTransform; // A structural directive transform is technically also a NodeTransform;
// Only v-if and v-for fall into this category. // Only v-if and v-for fall into this category.
export type StructuralDirectiveTransform = ( export type StructuralDirectiveTransform = (
node: ElementNode, node: ElementNode,

View File

@ -230,6 +230,15 @@ export function getConstantType(
// static then they don't need to be blocks since there will be no // static then they don't need to be blocks since there will be no
// nested updates. // nested updates.
if (codegenNode.isBlock) { if (codegenNode.isBlock) {
// except set custom directives.
for (let i = 0; i < node.props.length; i++) {
const p = node.props[i]
if (p.type === NodeTypes.DIRECTIVE) {
constantCache.set(node, ConstantTypes.NOT_CONSTANT)
return ConstantTypes.NOT_CONSTANT
}
}
context.removeHelper(OPEN_BLOCK) context.removeHelper(OPEN_BLOCK)
context.removeHelper( context.removeHelper(
getVNodeBlockHelper(context.inSSR, codegenNode.isComponent) getVNodeBlockHelper(context.inSSR, codegenNode.isComponent)

View File

@ -121,7 +121,13 @@ export const transformElement: NodeTransform = (node, context) => {
// props // props
if (props.length > 0) { if (props.length > 0) {
const propsBuildResult = buildProps(node, context) const propsBuildResult = buildProps(
node,
context,
undefined,
isComponent,
isDynamicComponent
)
vnodeProps = propsBuildResult.props vnodeProps = propsBuildResult.props
patchFlag = propsBuildResult.patchFlag patchFlag = propsBuildResult.patchFlag
dynamicPropNames = propsBuildResult.dynamicPropNames dynamicPropNames = propsBuildResult.dynamicPropNames
@ -380,6 +386,8 @@ export function buildProps(
node: ElementNode, node: ElementNode,
context: TransformContext, context: TransformContext,
props: ElementNode['props'] = node.props, props: ElementNode['props'] = node.props,
isComponent: boolean,
isDynamicComponent: boolean,
ssr = false ssr = false
): { ): {
props: PropsExpression | undefined props: PropsExpression | undefined
@ -389,7 +397,6 @@ export function buildProps(
shouldUseBlock: boolean shouldUseBlock: boolean
} { } {
const { tag, loc: elementLoc, children } = node const { tag, loc: elementLoc, children } = node
const isComponent = node.tagType === ElementTypes.COMPONENT
let properties: ObjectExpression['properties'] = [] let properties: ObjectExpression['properties'] = []
const mergeArgs: PropsExpression[] = [] const mergeArgs: PropsExpression[] = []
const runtimeDirectives: DirectiveNode[] = [] const runtimeDirectives: DirectiveNode[] = []
@ -411,8 +418,8 @@ export function buildProps(
const name = key.content const name = key.content
const isEventHandler = isOn(name) const isEventHandler = isOn(name)
if ( if (
!isComponent &&
isEventHandler && isEventHandler &&
(!isComponent || isDynamicComponent) &&
// omit the flag for click handlers because hydration gives click // omit the flag for click handlers because hydration gives click
// dedicated fast path. // dedicated fast path.
name.toLowerCase() !== 'onclick' && name.toLowerCase() !== 'onclick' &&

View File

@ -87,7 +87,13 @@ export function processSlotOutlet(
} }
if (nonNameProps.length > 0) { if (nonNameProps.length > 0) {
const { props, directives } = buildProps(node, context, nonNameProps) const { props, directives } = buildProps(
node,
context,
nonNameProps,
false,
false
)
slotProps = props slotProps = props
if (directives.length) { if (directives.length) {

View File

@ -5,7 +5,8 @@ import {
createCallExpression, createCallExpression,
CallExpression, CallExpression,
ElementTypes, ElementTypes,
ConstantTypes ConstantTypes,
createCompoundExpression
} from '../ast' } from '../ast'
import { isText } from '../utils' import { isText } from '../utils'
import { CREATE_TEXT } from '../runtimeHelpers' import { CREATE_TEXT } from '../runtimeHelpers'
@ -36,11 +37,10 @@ export const transformText: NodeTransform = (node, context) => {
const next = children[j] const next = children[j]
if (isText(next)) { if (isText(next)) {
if (!currentContainer) { if (!currentContainer) {
currentContainer = children[i] = { currentContainer = children[i] = createCompoundExpression(
type: NodeTypes.COMPOUND_EXPRESSION, [child],
loc: child.loc, child.loc
children: [child] )
}
} }
// merge adjacent text node into current // merge adjacent text node into current
currentContainer.children.push(` + `, next) currentContainer.children.push(` + `, next)

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compiler-dom", "name": "@vue/compiler-dom",
"version": "3.2.33", "version": "3.2.37",
"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",
@ -37,7 +37,7 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-dom#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-dom#readme",
"dependencies": { "dependencies": {
"@vue/shared": "3.2.33", "@vue/shared": "3.2.37",
"@vue/compiler-core": "3.2.33" "@vue/compiler-core": "3.2.37"
} }
} }

View File

@ -2,7 +2,7 @@
exports[`SFC analyze <script> bindings auto name inference basic 1`] = ` exports[`SFC analyze <script> bindings auto name inference basic 1`] = `
"export default { "export default {
name: 'FooBar', __name: 'FooBar',
setup(__props, { expose }) { setup(__props, { expose }) {
expose(); expose();
const a = 1 const a = 1
@ -722,7 +722,7 @@ return { props, a, emit }
exports[`SFC compile <script setup> dev mode import usage check TS annotations 1`] = ` exports[`SFC compile <script setup> dev mode import usage check TS annotations 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
import { Foo, Bar, Baz } from './x' import { Foo, Bar, Baz, Qux, Fred } from './x'
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
setup(__props, { expose }) { setup(__props, { expose }) {
@ -1039,8 +1039,7 @@ return (_ctx, _cache) => {
`; `;
exports[`SFC compile <script setup> inlineTemplate mode ssr codegen 1`] = ` exports[`SFC compile <script setup> inlineTemplate mode ssr codegen 1`] = `
"import { useCssVars as _useCssVars, unref as _unref } from 'vue' "import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from \\"vue/server-renderer\\"
import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from \\"vue/server-renderer\\"
import { ref } from 'vue' import { ref } from 'vue'
@ -1048,15 +1047,11 @@ export default {
__ssrInlineRender: true, __ssrInlineRender: true,
setup(__props) { setup(__props) {
_useCssVars(_ctx => ({
\\"xxxxxxxx-count\\": (count.value)
}))
const count = ref(0) const count = ref(0)
return (_ctx, _push, _parent, _attrs) => { return (_ctx, _push, _parent, _attrs) => {
const _cssVars = { style: { const _cssVars = { style: {
\\"xxxxxxxx-count\\": (count.value) \\"--xxxxxxxx-count\\": (count.value)
}} }}
_push(\`<!--[--><div\${ _push(\`<!--[--><div\${
_ssrRenderAttrs(_cssVars) _ssrRenderAttrs(_cssVars)

View File

@ -1,5 +1,5 @@
import { BindingTypes } from '@vue/compiler-core' import { BindingTypes } from '@vue/compiler-core'
import { compileSFCScript as compile, assertCode } from './utils' import { compileSFCScript as compile, assertCode, mockId } from './utils'
describe('SFC compile <script setup>', () => { describe('SFC compile <script setup>', () => {
test('should expose top level declarations', () => { test('should expose top level declarations', () => {
@ -446,7 +446,7 @@ defineExpose({ foo: 123 })
test('TS annotations', () => { test('TS annotations', () => {
const { content } = compile(` const { content } = compile(`
<script setup lang="ts"> <script setup lang="ts">
import { Foo, Bar, Baz } from './x' import { Foo, Bar, Baz, Qux, Fred } from './x'
const a = 1 const a = 1
function b() {} function b() {}
</script> </script>
@ -454,11 +454,26 @@ defineExpose({ foo: 123 })
{{ a as Foo }} {{ a as Foo }}
{{ b<Bar>() }} {{ b<Bar>() }}
{{ Baz }} {{ Baz }}
<Comp v-slot="{ data }: Qux">{{ data }}</Comp>
<div v-for="{ z = x as Qux } in list as Fred"/>
</template> </template>
`) `)
expect(content).toMatch(`return { a, b, Baz }`) expect(content).toMatch(`return { a, b, Baz }`)
assertCode(content) assertCode(content)
}) })
// vuejs/vue#12591
test('v-on inline statement', () => {
// should not error
compile(`
<script setup lang="ts">
import { foo } from './foo'
</script>
<template>
<div @click="$emit('update:a');"></div>
</tempalte>
`)
})
}) })
describe('inlineTemplate mode', () => { describe('inlineTemplate mode', () => {
@ -726,6 +741,8 @@ defineExpose({ foo: 123 })
expect(content).toMatch(`\n __ssrInlineRender: true,\n`) expect(content).toMatch(`\n __ssrInlineRender: true,\n`)
expect(content).toMatch(`return (_ctx, _push`) expect(content).toMatch(`return (_ctx, _push`)
expect(content).toMatch(`ssrInterpolate`) expect(content).toMatch(`ssrInterpolate`)
expect(content).not.toMatch(`useCssVars`)
expect(content).toMatch(`"--${mockId}-count": (count.value)`)
assertCode(content) assertCode(content)
}) })
}) })
@ -1646,7 +1663,7 @@ describe('SFC analyze <script> bindings', () => {
} }
) )
expect(content).toMatch(`export default { expect(content).toMatch(`export default {
name: 'FooBar'`) __name: 'FooBar'`)
assertCode(content) assertCode(content)
}) })

View File

@ -1,4 +1,4 @@
import { compileStyle } from '../src' import { compileStyle, parse } from '../src'
import { mockId, compileSFCScript, assertCode } from './utils' import { mockId, compileSFCScript, assertCode } from './utils'
describe('CSS vars injection', () => { describe('CSS vars injection', () => {
@ -231,5 +231,21 @@ describe('CSS vars injection', () => {
})`) })`)
assertCode(content) assertCode(content)
}) })
// #6022
test('should be able to parse incomplete expressions', () => {
const {
descriptor: { cssVars }
} = parse(
`<script setup>let xxx = 1</script>
<style scoped>
label {
font-weight: v-bind("count.toString(");
font-weight: v-bind(xxx);
}
</style>`
)
expect(cssVars).toMatchObject([`count.toString(`, `xxx`])
})
}) })
}) })

View File

@ -25,6 +25,17 @@ describe('compiler sfc: rewriteDefault', () => {
export { a as b, a as c} export { a as b, a as c}
const script = a" const script = a"
`) `)
expect(
rewriteDefault(
`const a = 1 \n export { a as b, a as default , a as c}`,
'script'
)
).toMatchInlineSnapshot(`
"const a = 1
export { a as b, a as c}
const script = a"
`)
}) })
test('w/ comments', async () => { test('w/ comments', async () => {
@ -63,6 +74,81 @@ describe('compiler sfc: rewriteDefault', () => {
// export { myFunction as default } // export { myFunction as default }
const script = a" const script = a"
`) `)
expect(
rewriteDefault(
`const a = 1 \n export {\n a as b,\n a as default ,\n a as c}\n` +
`// export { myFunction as default }`,
'script'
)
).toMatchInlineSnapshot(`
"const a = 1
export {
a as b,
a as c}
// export { myFunction as default }
const script = a"
`)
})
test(`export { default } from '...'`, async () => {
expect(
rewriteDefault(`export { default, foo } from './index.js'`, 'script')
).toMatchInlineSnapshot(`
"import { default as __VUE_DEFAULT__ } from './index.js'
export { foo } from './index.js'
const script = __VUE_DEFAULT__"
`)
expect(
rewriteDefault(`export { default , foo } from './index.js'`, 'script')
).toMatchInlineSnapshot(`
"import { default as __VUE_DEFAULT__ } from './index.js'
export { foo } from './index.js'
const script = __VUE_DEFAULT__"
`)
expect(
rewriteDefault(`export { foo, default } from './index.js'`, 'script')
).toMatchInlineSnapshot(`
"import { default as __VUE_DEFAULT__ } from './index.js'
export { foo, } from './index.js'
const script = __VUE_DEFAULT__"
`)
expect(
rewriteDefault(
`export { foo as default, bar } from './index.js'`,
'script'
)
).toMatchInlineSnapshot(`
"import { foo } from './index.js'
export { bar } from './index.js'
const script = foo"
`)
expect(
rewriteDefault(
`export { foo as default , bar } from './index.js'`,
'script'
)
).toMatchInlineSnapshot(`
"import { foo } from './index.js'
export { bar } from './index.js'
const script = foo"
`)
expect(
rewriteDefault(
`export { bar, foo as default } from './index.js'`,
'script'
)
).toMatchInlineSnapshot(`
"import { foo } from './index.js'
export { bar, } from './index.js'
const script = foo"
`)
}) })
test('export default class', async () => { test('export default class', async () => {

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compiler-sfc", "name": "@vue/compiler-sfc",
"version": "3.2.33", "version": "3.2.37",
"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",
@ -33,11 +33,11 @@
"homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-sfc#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-sfc#readme",
"dependencies": { "dependencies": {
"@babel/parser": "^7.16.4", "@babel/parser": "^7.16.4",
"@vue/compiler-core": "3.2.33", "@vue/compiler-core": "3.2.37",
"@vue/compiler-dom": "3.2.33", "@vue/compiler-dom": "3.2.37",
"@vue/compiler-ssr": "3.2.33", "@vue/compiler-ssr": "3.2.37",
"@vue/reactivity-transform": "3.2.33", "@vue/reactivity-transform": "3.2.37",
"@vue/shared": "3.2.33", "@vue/shared": "3.2.37",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"magic-string": "^0.25.7", "magic-string": "^0.25.7",
"source-map": "^0.6.1", "source-map": "^0.6.1",

View File

@ -74,7 +74,7 @@ const WITH_DEFAULTS = 'withDefaults'
const DEFAULT_VAR = `__default__` const DEFAULT_VAR = `__default__`
const isBuiltInDir = makeMap( const isBuiltInDir = makeMap(
`once,memo,if,else,else-if,slot,text,html,on,bind,model,show,cloak,is` `once,memo,if,for,else,else-if,slot,text,html,on,bind,model,show,cloak,is`
) )
export interface SFCScriptCompileOptions { export interface SFCScriptCompileOptions {
@ -201,7 +201,12 @@ export function compileScript(
) )
} }
if (options.babelParserPlugins) plugins.push(...options.babelParserPlugins) if (options.babelParserPlugins) plugins.push(...options.babelParserPlugins)
if (isTS) plugins.push('typescript', 'decorators-legacy') if (isTS) {
plugins.push('typescript')
if (!plugins.includes('decorators')) {
plugins.push('decorators-legacy')
}
}
if (!scriptSetup) { if (!scriptSetup) {
if (!script) { if (!script) {
@ -1332,7 +1337,11 @@ export function compileScript(
} }
// 8. inject `useCssVars` calls // 8. inject `useCssVars` calls
if (cssVars.length) { if (
cssVars.length &&
// no need to do this when targeting SSR
!(options.inlineTemplate && options.templateOptions?.ssr)
) {
helperImports.add(CSS_VARS_HELPER) helperImports.add(CSS_VARS_HELPER)
helperImports.add('unref') helperImports.add('unref')
s.prependRight( s.prependRight(
@ -1486,7 +1495,7 @@ export function compileScript(
if (!hasDefaultExportName && filename && filename !== DEFAULT_FILENAME) { if (!hasDefaultExportName && filename && filename !== DEFAULT_FILENAME) {
const match = filename.match(/([^/\\]+)\.\w+$/) const match = filename.match(/([^/\\]+)\.\w+$/)
if (match) { if (match) {
runtimeOptions += `\n name: '${match[1]}',` runtimeOptions += `\n __name: '${match[1]}',`
} }
} }
if (hasInlinedSsrRenderFn) { if (hasInlinedSsrRenderFn) {
@ -1833,6 +1842,7 @@ function inferRuntimeType(
case 'WeakSet': case 'WeakSet':
case 'WeakMap': case 'WeakMap':
case 'Date': case 'Date':
case 'Promise':
return [node.typeName.name] return [node.typeName.name]
case 'Record': case 'Record':
case 'Partial': case 'Partial':
@ -2125,7 +2135,8 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
} }
if (prop.exp) { if (prop.exp) {
code += `,${processExp( code += `,${processExp(
(prop.exp as SimpleExpressionNode).content (prop.exp as SimpleExpressionNode).content,
prop.name
)}` )}`
} }
} }
@ -2144,8 +2155,21 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
return code return code
} }
function processExp(exp: string) { const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
if (/ as \w|<.*>/.test(exp)) {
function processExp(exp: string, dir?: string): string {
if (/ as\s+\w|<.*>|:/.test(exp)) {
if (dir === 'slot') {
exp = `(${exp})=>{}`
} else if (dir === 'on') {
exp = `()=>{${exp}}`
} else if (dir === 'for') {
const inMatch = exp.match(forAliasRE)
if (inMatch) {
const [, LHS, RHS] = inMatch
return processExp(`(${LHS})=>{}`) + processExp(RHS)
}
}
let ret = '' let ret = ''
// has potential type cast or generic arguments that uses types // has potential type cast or generic arguments that uses types
const ast = parseExpression(exp, { plugins: ['typescript'] }) const ast = parseExpression(exp, { plugins: ['typescript'] })

View File

@ -202,7 +202,7 @@ function doCompileTemplate({
cacheHandlers: true, cacheHandlers: true,
ssrCssVars: ssrCssVars:
ssr && ssrCssVars && ssrCssVars.length ssr && ssrCssVars && ssrCssVars.length
? genCssVarsFromList(ssrCssVars, shortId, isProd) ? genCssVarsFromList(ssrCssVars, shortId, isProd, true)
: '', : '',
scopeId: scoped ? longId : undefined, scopeId: scoped ? longId : undefined,
slotted, slotted,

View File

@ -12,16 +12,17 @@ import { PluginCreator } from 'postcss'
import hash from 'hash-sum' import hash from 'hash-sum'
export const CSS_VARS_HELPER = `useCssVars` export const CSS_VARS_HELPER = `useCssVars`
// match v-bind() with max 2-levels of nested parens.
const cssVarRE = /v-bind\s*\(((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*)\)/g
export function genCssVarsFromList( export function genCssVarsFromList(
vars: string[], vars: string[],
id: string, id: string,
isProd: boolean isProd: boolean,
isSSR = false
): string { ): string {
return `{\n ${vars return `{\n ${vars
.map(key => `"${genVarName(id, key, isProd)}": (${key})`) .map(
key => `"${isSSR ? `--` : ``}${genVarName(id, key, isProd)}": (${key})`
)
.join(',\n ')}\n}` .join(',\n ')}\n}`
} }
@ -44,22 +45,71 @@ function normalizeExpression(exp: string) {
return exp return exp
} }
const vBindRE = /v-bind\s*\(/g
export function parseCssVars(sfc: SFCDescriptor): string[] { export function parseCssVars(sfc: SFCDescriptor): string[] {
const vars: string[] = [] const vars: string[] = []
sfc.styles.forEach(style => { sfc.styles.forEach(style => {
let match let match
// ignore v-bind() in comments /* ... */ // ignore v-bind() in comments /* ... */
const content = style.content.replace(/\/\*([\s\S]*?)\*\//g, '') const content = style.content.replace(/\/\*([\s\S]*?)\*\//g, '')
while ((match = cssVarRE.exec(content))) { while ((match = vBindRE.exec(content))) {
const variable = normalizeExpression(match[1]) const start = match.index + match[0].length
const end = lexBinding(content, start)
if (end !== null) {
const variable = normalizeExpression(content.slice(start, end))
if (!vars.includes(variable)) { if (!vars.includes(variable)) {
vars.push(variable) vars.push(variable)
} }
} }
}
}) })
return vars return vars
} }
const enum LexerState {
inParens,
inSingleQuoteString,
inDoubleQuoteString
}
function lexBinding(content: string, start: number): number | null {
let state: LexerState = LexerState.inParens
let parenDepth = 0
for (let i = start; i < content.length; i++) {
const char = content.charAt(i)
switch (state) {
case LexerState.inParens:
if (char === `'`) {
state = LexerState.inSingleQuoteString
} else if (char === `"`) {
state = LexerState.inDoubleQuoteString
} else if (char === `(`) {
parenDepth++
} else if (char === `)`) {
if (parenDepth > 0) {
parenDepth--
} else {
return i
}
}
break
case LexerState.inSingleQuoteString:
if (char === `'`) {
state = LexerState.inParens
}
break
case LexerState.inDoubleQuoteString:
if (char === `"`) {
state = LexerState.inParens
}
break
}
}
return null
}
// for compileStyle // for compileStyle
export interface CssVarsPluginOptions { export interface CssVarsPluginOptions {
id: string id: string
@ -72,10 +122,24 @@ export const cssVarsPlugin: PluginCreator<CssVarsPluginOptions> = opts => {
postcssPlugin: 'vue-sfc-vars', postcssPlugin: 'vue-sfc-vars',
Declaration(decl) { Declaration(decl) {
// rewrite CSS variables // rewrite CSS variables
if (cssVarRE.test(decl.value)) { const value = decl.value
decl.value = decl.value.replace(cssVarRE, (_, $1) => { if (vBindRE.test(value)) {
return `var(--${genVarName(id, normalizeExpression($1), isProd)})` vBindRE.lastIndex = 0
}) let transformed = ''
let lastIndex = 0
let match
while ((match = vBindRE.exec(value))) {
const start = match.index + match[0].length
const end = lexBinding(value, start)
if (end !== null) {
const variable = normalizeExpression(value.slice(start, end))
transformed +=
value.slice(lastIndex, match.index) +
`var(--${genVarName(id, variable, isProd)})`
lastIndex = end + 1
}
}
decl.value = transformed + value.slice(lastIndex)
} }
} }
} }

View File

@ -2,7 +2,7 @@ import { parse, ParserPlugin } from '@babel/parser'
import MagicString from 'magic-string' import MagicString from 'magic-string'
const defaultExportRE = /((?:^|\n|;)\s*)export(\s*)default/ const defaultExportRE = /((?:^|\n|;)\s*)export(\s*)default/
const namedDefaultExportRE = /((?:^|\n|;)\s*)export(.+)as(\s*)default/s const namedDefaultExportRE = /((?:^|\n|;)\s*)export(.+)(?:as)?(\s*)default/s
const exportDefaultClassRE = const exportDefaultClassRE =
/((?:^|\n|;)\s*)export\s+default\s+class\s+([\w$]+)/ /((?:^|\n|;)\s*)export\s+default\s+class\s+([\w$]+)/
@ -45,21 +45,39 @@ export function rewriteDefault(
s.overwrite(node.start!, node.declaration.start!, `const ${as} = `) s.overwrite(node.start!, node.declaration.start!, `const ${as} = `)
} }
if (node.type === 'ExportNamedDeclaration') { if (node.type === 'ExportNamedDeclaration') {
node.specifiers.forEach(specifier => { for (const specifier of node.specifiers) {
if ( if (
specifier.type === 'ExportSpecifier' && specifier.type === 'ExportSpecifier' &&
specifier.exported.type === 'Identifier' && specifier.exported.type === 'Identifier' &&
specifier.exported.name === 'default' specifier.exported.name === 'default'
) { ) {
const end = specifier.end! if (node.source) {
s.overwrite( if (specifier.local.name === 'default') {
specifier.start!, const end = specifierEnd(input, specifier.local.end!, node.end)
input.charAt(end) === ',' ? end + 1 : end, s.prepend(
`` `import { default as __VUE_DEFAULT__ } from '${node.source.value}'\n`
) )
s.overwrite(specifier.start!, end, ``)
s.append(`\nconst ${as} = __VUE_DEFAULT__`)
continue
} else {
const end = specifierEnd(input, specifier.exported.end!, node.end)
s.prepend(
`import { ${input.slice(
specifier.local.start!,
specifier.local.end!
)} } from '${node.source.value}'\n`
)
s.overwrite(specifier.start!, end, ``)
s.append(`\nconst ${as} = ${specifier.local.name}`)
continue
}
}
const end = specifierEnd(input, specifier.end!, node.end)
s.overwrite(specifier.start!, end, ``)
s.append(`\nconst ${as} = ${specifier.local.name}`) s.append(`\nconst ${as} = ${specifier.local.name}`)
} }
}) }
} }
}) })
return s.toString() return s.toString()
@ -68,3 +86,21 @@ export function rewriteDefault(
export function hasDefaultExport(input: string): boolean { export function hasDefaultExport(input: string): boolean {
return defaultExportRE.test(input) || namedDefaultExportRE.test(input) return defaultExportRE.test(input) || namedDefaultExportRE.test(input)
} }
function specifierEnd(input: string, end: number, nodeEnd: number | null) {
// export { default , foo } ...
let hasCommas = false
let oldEnd = end
while (end < nodeEnd!) {
if (/\s/.test(input.charAt(end))) {
end++
} else if (input.charAt(end) === ',') {
end++
hasCommas = true
break
} else if (input.charAt(end) === '}') {
break
}
}
return hasCommas ? end : oldEnd
}

View File

@ -17,6 +17,20 @@ describe('ssr: components', () => {
`) `)
}) })
// event listeners should still be passed
test('event listeners', () => {
expect(compile(`<foo @click="bar" />`).code).toMatchInlineSnapshot(`
"const { resolveComponent: _resolveComponent, mergeProps: _mergeProps } = require(\\"vue\\")
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
const _component_foo = _resolveComponent(\\"foo\\")
_push(_ssrRenderComponent(_component_foo, _mergeProps({ onClick: _ctx.bar }, _attrs), null, _parent))
}"
`)
})
test('dynamic component', () => { test('dynamic component', () => {
expect(compile(`<component is="foo" prop="b" />`).code) expect(compile(`<component is="foo" prop="b" />`).code)
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
@ -266,7 +280,8 @@ describe('ssr: components', () => {
`) `)
}) })
test('built-in fallthroughs', () => { describe('built-in fallthroughs', () => {
test('transition', () => {
expect(compile(`<transition><div/></transition>`).code) expect(compile(`<transition><div/></transition>`).code)
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
"const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\") "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
@ -275,7 +290,9 @@ describe('ssr: components', () => {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
}" }"
`) `)
})
test('keep-alive', () => {
expect(compile(`<keep-alive><foo/></keep-alive>`).code) expect(compile(`<keep-alive><foo/></keep-alive>`).code)
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
"const { resolveComponent: _resolveComponent } = require(\\"vue\\") "const { resolveComponent: _resolveComponent } = require(\\"vue\\")
@ -289,89 +306,42 @@ describe('ssr: components', () => {
`) `)
}) })
// transition-group should flatten and concat its children fragments into // #5352
// a single one test('should push marker string if is slot root', () => {
describe('transition-group', () => {
test('basic', () => {
expect( expect(
compile( compile(`<foo><transition><div v-if="false"/></transition></foo>`)
`<transition-group><div v-for="i in list"/></transition-group>` .code
).code
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
"const { ssrRenderList: _ssrRenderList } = require(\\"vue/server-renderer\\") "const { resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode, Transition: _Transition, createVNode: _createVNode } = require(\\"vue\\")
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<!--[-->\`) const _component_foo = _resolveComponent(\\"foo\\")
_ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`)
})
_push(\`<!--]-->\`)
}"
`)
})
test('with static tag', () => { _push(_ssrRenderComponent(_component_foo, _attrs, {
expect( default: _withCtx((_, _push, _parent, _scopeId) => {
compile( if (_push) {
`<transition-group tag="ul"><div v-for="i in list"/></transition-group>` _push(\`\`)
).code if (false) {
).toMatchInlineSnapshot(` _push(\`<div\${_scopeId}></div>\`)
"const { ssrRenderList: _ssrRenderList } = require(\\"vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<ul>\`)
_ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`)
})
_push(\`</ul>\`)
}"
`)
})
test('with dynamic tag', () => {
expect(
compile(
`<transition-group :tag="someTag"><div v-for="i in list"/></transition-group>`
).code
).toMatchInlineSnapshot(`
"const { ssrRenderList: _ssrRenderList } = require(\\"vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<\${_ctx.someTag}>\`)
_ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`)
})
_push(\`</\${_ctx.someTag}>\`)
}"
`)
})
test('with multi fragments children', () => {
expect(
compile(
`<transition-group>
<div v-for="i in 10"/>
<div v-for="i in 10"/>
<template v-if="ok"><div>ok</div></template>
</transition-group>`
).code
).toMatchInlineSnapshot(`
"const { ssrRenderList: _ssrRenderList } = require(\\"vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<!--[-->\`)
_ssrRenderList(10, (i) => {
_push(\`<div></div>\`)
})
_ssrRenderList(10, (i) => {
_push(\`<div></div>\`)
})
if (_ctx.ok) {
_push(\`<div>ok</div>\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
_push(\`<!--]-->\`) } else {
return [
_createVNode(_Transition, null, {
default: _withCtx(() => [
false
? (_openBlock(), _createBlock(\\"div\\", { key: 0 }))
: _createCommentVNode(\\"v-if\\", true)
]),
_: 1 /* STABLE */
})
]
}
}),
_: 1 /* STABLE */
}, _parent))
}" }"
`) `)
}) })

View File

@ -0,0 +1,60 @@
import { compile } from '../src'
describe('ssr: attrs fallthrough', () => {
test('basic', () => {
expect(compile(`<div/>`).code).toMatchInlineSnapshot(`
"const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
}"
`)
})
test('with comments', () => {
expect(compile(`<!--!--><div/>`).code).toMatchInlineSnapshot(`
"const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<!--[--><!--!--><div\${_ssrRenderAttrs(_attrs)}></div><!--]-->\`)
}"
`)
})
// #5140
test('should not inject to non-single-root if branches', () => {
expect(compile(`<div v-if="true"/><div/>`).code).toMatchInlineSnapshot(`
"
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<!--[-->\`)
if (true) {
_push(\`<div></div>\`)
} else {
_push(\`<!---->\`)
}
_push(\`<div></div><!--]-->\`)
}"
`)
})
test('fallthrough component content (root with coomments)', () => {
expect(compile(`<!--root--><transition><div/></transition>`).code)
.toMatchInlineSnapshot(`
"const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<!--[--><!--root--><div\${_ssrRenderAttrs(_attrs)}></div><!--]-->\`)
}"
`)
})
test('should not inject to fallthrough component content if not root', () => {
expect(compile(`<div/><transition><div/></transition>`).code)
.toMatchInlineSnapshot(`
"
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<!--[--><div></div><div></div><!--]-->\`)
}"
`)
})
})

View File

@ -123,7 +123,7 @@ describe('ssr: <slot>', () => {
"const { ssrRenderSlotInner: _ssrRenderSlotInner } = require(\\"vue/server-renderer\\") "const { ssrRenderSlotInner: _ssrRenderSlotInner } = require(\\"vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
_ssrRenderSlotInner(_ctx.$slots, \\"default\\", {}, null, _push, _parent) _ssrRenderSlotInner(_ctx.$slots, \\"default\\", {}, null, _push, _parent, null, true)
}" }"
`) `)
}) })

View File

@ -0,0 +1,111 @@
import { compile } from '../src'
// transition-group should flatten and concat its children fragments into
// a single one
describe('transition-group', () => {
test('basic', () => {
expect(
compile(`<transition-group><div v-for="i in list"/></transition-group>`)
.code
).toMatchInlineSnapshot(`
"const { ssrRenderList: _ssrRenderList } = require(\\"vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<!--[-->\`)
_ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`)
})
_push(\`<!--]-->\`)
}"
`)
})
test('with static tag', () => {
expect(
compile(
`<transition-group tag="ul"><div v-for="i in list"/></transition-group>`
).code
).toMatchInlineSnapshot(`
"const { ssrRenderAttrs: _ssrRenderAttrs, ssrRenderList: _ssrRenderList } = require(\\"vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<ul\${_ssrRenderAttrs(_attrs)}>\`)
_ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`)
})
_push(\`</ul>\`)
}"
`)
})
test('with dynamic tag', () => {
expect(
compile(
`<transition-group :tag="someTag"><div v-for="i in list"/></transition-group>`
).code
).toMatchInlineSnapshot(`
"const { ssrRenderAttrs: _ssrRenderAttrs, ssrRenderList: _ssrRenderList } = require(\\"vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<\${
_ctx.someTag
}\${
_ssrRenderAttrs(_attrs)
}>\`)
_ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`)
})
_push(\`</\${_ctx.someTag}>\`)
}"
`)
})
test('with multi fragments children', () => {
expect(
compile(
`<transition-group>
<div v-for="i in 10"/>
<div v-for="i in 10"/>
<template v-if="ok"><div>ok</div></template>
</transition-group>`
).code
).toMatchInlineSnapshot(`
"const { ssrRenderList: _ssrRenderList } = require(\\"vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<!--[-->\`)
_ssrRenderList(10, (i) => {
_push(\`<div></div>\`)
})
_ssrRenderList(10, (i) => {
_push(\`<div></div>\`)
})
if (_ctx.ok) {
_push(\`<div>ok</div>\`)
} else {
_push(\`<!---->\`)
}
_push(\`<!--]-->\`)
}"
`)
})
test('attribute fallthrough', () => {
expect(
compile(
`<transition-group tag="ul" class="red" id="ok">
</transition-group>`
).code
).toMatchInlineSnapshot(`
"const { mergeProps: _mergeProps } = require(\\"vue\\")
const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<ul\${_ssrRenderAttrs(_mergeProps({
class: \\"red\\",
id: \\"ok\\"
}, _attrs))}></ul>\`)
}"
`)
})
})

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compiler-ssr", "name": "@vue/compiler-ssr",
"version": "3.2.33", "version": "3.2.37",
"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",
@ -28,7 +28,7 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-ssr#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-ssr#readme",
"dependencies": { "dependencies": {
"@vue/shared": "3.2.33", "@vue/shared": "3.2.37",
"@vue/compiler-dom": "3.2.33" "@vue/compiler-dom": "3.2.37"
} }
} }

View File

@ -10,7 +10,8 @@ import {
trackSlotScopes, trackSlotScopes,
noopDirectiveTransform, noopDirectiveTransform,
transformBind, transformBind,
transformStyle transformStyle,
transformOn
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { ssrCodegenTransform } from './ssrCodegenTransform' import { ssrCodegenTransform } from './ssrCodegenTransform'
import { ssrTransformElement } from './transforms/ssrTransformElement' import { ssrTransformElement } from './transforms/ssrTransformElement'
@ -70,11 +71,12 @@ export function compile(
directiveTransforms: { directiveTransforms: {
// reusing core v-bind // reusing core v-bind
bind: transformBind, bind: transformBind,
on: transformOn,
// model and show has dedicated SSR handling // model and show has dedicated SSR handling
model: ssrTransformModel, model: ssrTransformModel,
show: ssrTransformShow, show: ssrTransformShow,
// the following are ignored during SSR // the following are ignored during SSR
on: noopDirectiveTransform, // on: noopDirectiveTransform,
cloak: noopDirectiveTransform, cloak: noopDirectiveTransform,
once: noopDirectiveTransform, once: noopDirectiveTransform,
memo: noopDirectiveTransform, memo: noopDirectiveTransform,

View File

@ -51,7 +51,7 @@ export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
const isFragment = const isFragment =
ast.children.length > 1 && ast.children.some(c => !isText(c)) ast.children.length > 1 && ast.children.some(c => !isText(c))
processChildren(ast.children, context, isFragment) processChildren(ast, context, isFragment)
ast.codegenNode = createBlockStatement(context.body) ast.codegenNode = createBlockStatement(context.body)
// Finalize helpers. // Finalize helpers.
@ -125,8 +125,12 @@ function createChildContext(
) )
} }
interface Container {
children: TemplateChildNode[]
}
export function processChildren( export function processChildren(
children: TemplateChildNode[], parent: Container,
context: SSRTransformContext, context: SSRTransformContext,
asFragment = false, asFragment = false,
disableNestedFragments = false disableNestedFragments = false
@ -134,6 +138,7 @@ export function processChildren(
if (asFragment) { if (asFragment) {
context.pushStringPart(`<!--[-->`) context.pushStringPart(`<!--[-->`)
} }
const { children } = parent
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
const child = children[i] const child = children[i]
switch (child.type) { switch (child.type) {
@ -143,7 +148,7 @@ export function processChildren(
ssrProcessElement(child, context) ssrProcessElement(child, context)
break break
case ElementTypes.COMPONENT: case ElementTypes.COMPONENT:
ssrProcessComponent(child, context) ssrProcessComponent(child, context, parent)
break break
case ElementTypes.SLOT: case ElementTypes.SLOT:
ssrProcessSlotOutlet(child, context) ssrProcessSlotOutlet(child, context)
@ -208,12 +213,12 @@ export function processChildren(
} }
export function processChildrenAsStatement( export function processChildrenAsStatement(
children: TemplateChildNode[], parent: Container,
parentContext: SSRTransformContext, parentContext: SSRTransformContext,
asFragment = false, asFragment = false,
withSlotScopeId = parentContext.withSlotScopeId withSlotScopeId = parentContext.withSlotScopeId
): BlockStatement { ): BlockStatement {
const childContext = createChildContext(parentContext, withSlotScopeId) const childContext = createChildContext(parentContext, withSlotScopeId)
processChildren(children, childContext, asFragment) processChildren(parent, childContext, asFragment)
return createBlockStatement(childContext.body) return createBlockStatement(childContext.body)
} }

View File

@ -11,8 +11,11 @@ import {
isBuiltInType isBuiltInType
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
const filterChild = (node: ParentNode) =>
node.children.filter(n => n.type !== NodeTypes.COMMENT)
const hasSingleChild = (node: ParentNode): boolean => const hasSingleChild = (node: ParentNode): boolean =>
node.children.filter(n => n.type !== NodeTypes.COMMENT).length === 1 filterChild(node).length === 1
export const ssrInjectFallthroughAttrs: NodeTransform = (node, context) => { export const ssrInjectFallthroughAttrs: NodeTransform = (node, context) => {
// _attrs is provided as a function argument. // _attrs is provided as a function argument.
@ -28,11 +31,14 @@ export const ssrInjectFallthroughAttrs: NodeTransform = (node, context) => {
(isBuiltInType(node.tag, 'Transition') || (isBuiltInType(node.tag, 'Transition') ||
isBuiltInType(node.tag, 'KeepAlive')) isBuiltInType(node.tag, 'KeepAlive'))
) { ) {
const rootChildren = filterChild(context.root)
if (rootChildren.length === 1 && rootChildren[0] === node) {
if (hasSingleChild(node)) { if (hasSingleChild(node)) {
injectFallthroughAttrs(node.children[0]) injectFallthroughAttrs(node.children[0])
} }
return return
} }
}
const parent = context.parent const parent = context.parent
if (!parent || parent.type !== NodeTypes.ROOT) { if (!parent || parent.type !== NodeTypes.ROOT) {
@ -40,6 +46,25 @@ export const ssrInjectFallthroughAttrs: NodeTransform = (node, context) => {
} }
if (node.type === NodeTypes.IF_BRANCH && hasSingleChild(node)) { if (node.type === NodeTypes.IF_BRANCH && hasSingleChild(node)) {
// detect cases where the parent v-if is not the only root level node
let hasEncounteredIf = false
for (const c of filterChild(parent)) {
if (
c.type === NodeTypes.IF ||
(c.type === NodeTypes.ELEMENT && findDir(c, 'if'))
) {
// multiple root v-if
if (hasEncounteredIf) return
hasEncounteredIf = true
} else if (
// node before v-if
!hasEncounteredIf ||
// non else nodes
!(c.type === NodeTypes.ELEMENT && findDir(c, /else/, true))
) {
return
}
}
injectFallthroughAttrs(node.children[0]) injectFallthroughAttrs(node.children[0])
} else if (hasSingleChild(parent)) { } else if (hasSingleChild(parent)) {
injectFallthroughAttrs(node) injectFallthroughAttrs(node)

View File

@ -34,7 +34,9 @@ import {
TRANSITION_GROUP, TRANSITION_GROUP,
CREATE_VNODE, CREATE_VNODE,
CallExpression, CallExpression,
JSChildNode JSChildNode,
RESOLVE_DYNAMIC_COMPONENT,
TRANSITION
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { SSR_RENDER_COMPONENT, SSR_RENDER_VNODE } from '../runtimeHelpers' import { SSR_RENDER_COMPONENT, SSR_RENDER_VNODE } from '../runtimeHelpers'
import { import {
@ -47,7 +49,10 @@ import {
ssrProcessSuspense, ssrProcessSuspense,
ssrTransformSuspense ssrTransformSuspense
} from './ssrTransformSuspense' } from './ssrTransformSuspense'
import { ssrProcessTransitionGroup } from './ssrTransformTransitionGroup' import {
ssrProcessTransitionGroup,
ssrTransformTransitionGroup
} from './ssrTransformTransitionGroup'
import { isSymbol, isObject, isArray } from '@vue/shared' import { isSymbol, isObject, isArray } from '@vue/shared'
import { buildSSRProps } from './ssrTransformElement' import { buildSSRProps } from './ssrTransformElement'
@ -57,7 +62,10 @@ import { buildSSRProps } from './ssrTransformElement'
// pass and complete them in the 2nd pass. // pass and complete them in the 2nd pass.
const wipMap = new WeakMap<ComponentNode, WIPSlotEntry[]>() const wipMap = new WeakMap<ComponentNode, WIPSlotEntry[]>()
const WIP_SLOT = Symbol()
interface WIPSlotEntry { interface WIPSlotEntry {
type: typeof WIP_SLOT
fn: FunctionExpression fn: FunctionExpression
children: TemplateChildNode[] children: TemplateChildNode[]
vnodeBranch: ReturnStatement vnodeBranch: ReturnStatement
@ -83,13 +91,18 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
} }
const component = resolveComponentType(node, context, true /* ssr */) const component = resolveComponentType(node, context, true /* ssr */)
const isDynamicComponent =
isObject(component) && component.callee === RESOLVE_DYNAMIC_COMPONENT
componentTypeMap.set(node, component) componentTypeMap.set(node, component)
if (isSymbol(component)) { if (isSymbol(component)) {
if (component === SUSPENSE) { if (component === SUSPENSE) {
return ssrTransformSuspense(node, context) return ssrTransformSuspense(node, context)
} }
return // built-in component: fallthrough if (component === TRANSITION_GROUP) {
return ssrTransformTransitionGroup(node, context)
}
return // other built-in components: fallthrough
} }
// Build the fallback vnode-based branch for the component's slots. // Build the fallback vnode-based branch for the component's slots.
@ -116,7 +129,13 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
if (node.props.length) { if (node.props.length) {
// note we are not passing ssr: true here because for components, v-on // note we are not passing ssr: true here because for components, v-on
// handlers should still be passed // handlers should still be passed
const { props, directives } = buildProps(node, context) const { props, directives } = buildProps(
node,
context,
undefined,
true,
isDynamicComponent
)
if (props || directives.length) { if (props || directives.length) {
propsExp = buildSSRProps(props, directives, context) propsExp = buildSSRProps(props, directives, context)
} }
@ -134,6 +153,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
loc loc
) )
wipEntries.push({ wipEntries.push({
type: WIP_SLOT,
fn, fn,
children, children,
// also collect the corresponding vnode branch built earlier // also collect the corresponding vnode branch built earlier
@ -173,7 +193,8 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
export function ssrProcessComponent( export function ssrProcessComponent(
node: ComponentNode, node: ComponentNode,
context: SSRTransformContext context: SSRTransformContext,
parent: { children: TemplateChildNode[] }
) { ) {
const component = componentTypeMap.get(node)! const component = componentTypeMap.get(node)!
if (!node.ssrCodegenNode) { if (!node.ssrCodegenNode) {
@ -187,13 +208,23 @@ export function ssrProcessComponent(
} else { } else {
// real fall-through: Transition / KeepAlive // real fall-through: Transition / KeepAlive
// just render its children. // just render its children.
processChildren(node.children, context) // #5352: if is at root level of a slot, push an empty string.
// this does not affect the final output, but avoids all-comment slot
// content of being treated as empty by ssrRenderSlot().
if ((parent as WIPSlotEntry).type === WIP_SLOT) {
context.pushStringPart(``)
}
// #5351: filter out comment children inside transition
if (component === TRANSITION) {
node.children = node.children.filter(c => c.type !== NodeTypes.COMMENT)
}
processChildren(node, context)
} }
} else { } else {
// finish up slot function expressions from the 1st pass. // finish up slot function expressions from the 1st pass.
const wipEntries = wipMap.get(node) || [] const wipEntries = wipMap.get(node) || []
for (let i = 0; i < wipEntries.length; i++) { for (let i = 0; i < wipEntries.length; i++) {
const { fn, children, vnodeBranch } = wipEntries[i] const { fn, vnodeBranch } = wipEntries[i]
// For each slot, we generate two branches: one SSR-optimized branch and // For each slot, we generate two branches: one SSR-optimized branch and
// one normal vnode-based branch. The branches are taken based on the // one normal vnode-based branch. The branches are taken based on the
// presence of the 2nd `_push` argument (which is only present if the slot // presence of the 2nd `_push` argument (which is only present if the slot
@ -201,7 +232,7 @@ export function ssrProcessComponent(
fn.body = createIfStatement( fn.body = createIfStatement(
createSimpleExpression(`_push`, false), createSimpleExpression(`_push`, false),
processChildrenAsStatement( processChildrenAsStatement(
children, wipEntries[i],
context, context,
false, false,
true /* withSlotScopeId */ true /* withSlotScopeId */

View File

@ -89,6 +89,8 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
node, node,
context, context,
node.props, node.props,
false /* isComponent */,
false /* isDynamicComponent */,
true /* ssr */ true /* ssr */
) )
if (props || directives.length) { if (props || directives.length) {
@ -194,7 +196,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
if (!needMergeProps) { if (!needMergeProps) {
node.children = [createInterpolation(prop.exp, prop.loc)] node.children = [createInterpolation(prop.exp, prop.loc)]
} }
} else if (!needMergeProps) { } else if (!needMergeProps && prop.name !== 'on') {
// Directive transforms. // Directive transforms.
const directiveTransform = context.directiveTransforms[prop.name] const directiveTransform = context.directiveTransforms[prop.name]
if (directiveTransform) { if (directiveTransform) {
@ -426,7 +428,7 @@ export function ssrProcessElement(
if (rawChildren) { if (rawChildren) {
context.pushStringPart(rawChildren) context.pushStringPart(rawChildren)
} else if (node.children.length) { } else if (node.children.length) {
processChildren(node.children, context) processChildren(node, context)
} }
if (!isVoidTag(node.tag)) { if (!isVoidTag(node.tag)) {

View File

@ -50,6 +50,10 @@ export const ssrTransformSlotOutlet: NodeTransform = (node, context) => {
parent.children.filter(c => c.type === NodeTypes.ELEMENT).length === 1 parent.children.filter(c => c.type === NodeTypes.ELEMENT).length === 1
) { ) {
method = SSR_RENDER_SLOT_INNER method = SSR_RENDER_SLOT_INNER
if (!(context.scopeId && context.slotted !== false)) {
args.push('null')
}
args.push('true')
} }
node.ssrCodegenNode = createCallExpression(context.helper(method), args) node.ssrCodegenNode = createCallExpression(context.helper(method), args)
@ -65,7 +69,7 @@ export function ssrProcessSlotOutlet(
// has fallback content // has fallback content
if (node.children.length) { if (node.children.length) {
const fallbackRenderFn = createFunctionExpression([]) const fallbackRenderFn = createFunctionExpression([])
fallbackRenderFn.body = processChildrenAsStatement(node.children, context) fallbackRenderFn.body = processChildrenAsStatement(node, context)
// _renderSlot(slots, name, props, fallback, ...) // _renderSlot(slots, name, props, fallback, ...)
renderCall.arguments[3] = fallbackRenderFn renderCall.arguments[3] = fallbackRenderFn
} }

View File

@ -66,8 +66,8 @@ export function ssrProcessSuspense(
} }
const { slotsExp, wipSlots } = wipEntry const { slotsExp, wipSlots } = wipEntry
for (let i = 0; i < wipSlots.length; i++) { for (let i = 0; i < wipSlots.length; i++) {
const { fn, children } = wipSlots[i] const slot = wipSlots[i]
fn.body = processChildrenAsStatement(children, context) slot.fn.body = processChildrenAsStatement(slot, context)
} }
// _push(ssrRenderSuspense(slots)) // _push(ssrRenderSuspense(slots))
context.pushStatement( context.pushStatement(

View File

@ -58,7 +58,7 @@ export function ssrProcessTeleport(
false, // isSlot false, // isSlot
node.loc node.loc
) )
contentRenderFn.body = processChildrenAsStatement(node.children, context) contentRenderFn.body = processChildrenAsStatement(node, context)
context.pushStatement( context.pushStatement(
createCallExpression(context.helper(SSR_RENDER_TELEPORT), [ createCallExpression(context.helper(SSR_RENDER_TELEPORT), [
`_push`, `_push`,

View File

@ -1,20 +1,75 @@
import { ComponentNode, findProp, NodeTypes } from '@vue/compiler-dom' import {
AttributeNode,
buildProps,
ComponentNode,
createCallExpression,
DirectiveNode,
findProp,
JSChildNode,
NodeTypes,
TransformContext
} from '@vue/compiler-dom'
import { SSR_RENDER_ATTRS } from '../runtimeHelpers'
import { processChildren, SSRTransformContext } from '../ssrCodegenTransform' import { processChildren, SSRTransformContext } from '../ssrCodegenTransform'
import { buildSSRProps } from './ssrTransformElement'
const wipMap = new WeakMap<ComponentNode, WIPEntry>()
interface WIPEntry {
tag: AttributeNode | DirectiveNode
propsExp: string | JSChildNode | null
}
// phase 1: build props
export function ssrTransformTransitionGroup(
node: ComponentNode,
context: TransformContext
) {
return () => {
const tag = findProp(node, 'tag')
if (tag) {
const otherProps = node.props.filter(p => p !== tag)
const { props, directives } = buildProps(
node,
context,
otherProps,
true, /* isComponent */
false, /* isDynamicComponent */
true /* ssr (skip event listeners) */
)
let propsExp = null
if (props || directives.length) {
propsExp = createCallExpression(context.helper(SSR_RENDER_ATTRS), [
buildSSRProps(props, directives, context)
])
}
wipMap.set(node, {
tag,
propsExp
})
}
}
}
// phase 2: process children
export function ssrProcessTransitionGroup( export function ssrProcessTransitionGroup(
node: ComponentNode, node: ComponentNode,
context: SSRTransformContext context: SSRTransformContext
) { ) {
const tag = findProp(node, 'tag') const entry = wipMap.get(node)
if (tag) { if (entry) {
const { tag, propsExp } = entry
if (tag.type === NodeTypes.DIRECTIVE) { if (tag.type === NodeTypes.DIRECTIVE) {
// dynamic :tag // dynamic :tag
context.pushStringPart(`<`) context.pushStringPart(`<`)
context.pushStringPart(tag.exp!) context.pushStringPart(tag.exp!)
if (propsExp) {
context.pushStringPart(propsExp)
}
context.pushStringPart(`>`) context.pushStringPart(`>`)
processChildren( processChildren(
node.children, node,
context, context,
false, false,
/** /**
@ -30,12 +85,16 @@ export function ssrProcessTransitionGroup(
context.pushStringPart(`>`) context.pushStringPart(`>`)
} else { } else {
// static tag // static tag
context.pushStringPart(`<${tag.value!.content}>`) context.pushStringPart(`<${tag.value!.content}`)
processChildren(node.children, context, false, true) if (propsExp) {
context.pushStringPart(propsExp)
}
context.pushStringPart(`>`)
processChildren(node, context, false, true)
context.pushStringPart(`</${tag.value!.content}>`) context.pushStringPart(`</${tag.value!.content}>`)
} }
} else { } else {
// fragment // fragment
processChildren(node.children, context, true, true) processChildren(node, context, true, true)
} }
} }

View File

@ -33,7 +33,7 @@ export function ssrProcessFor(
createForLoopParams(node.parseResult) createForLoopParams(node.parseResult)
) )
renderLoop.body = processChildrenAsStatement( renderLoop.body = processChildrenAsStatement(
node.children, node,
context, context,
needFragmentWrapper needFragmentWrapper
) )

View File

@ -72,5 +72,5 @@ function processIfBranch(
(children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) && (children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) &&
// optimize away nested fragments when the only child is a ForNode // optimize away nested fragments when the only child is a ForNode
!(children.length === 1 && children[0].type === NodeTypes.FOR) !(children.length === 1 && children[0].type === NodeTypes.FOR)
return processChildrenAsStatement(children, context, needFragmentWrapper) return processChildrenAsStatement(branch, context, needFragmentWrapper)
} }

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018-present, Yuxi (Evan) You
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/reactivity-transform", "name": "@vue/reactivity-transform",
"version": "3.2.33", "version": "3.2.37",
"description": "@vue/reactivity-transform", "description": "@vue/reactivity-transform",
"main": "dist/reactivity-transform.cjs.js", "main": "dist/reactivity-transform.cjs.js",
"files": [ "files": [
@ -29,8 +29,8 @@
"homepage": "https://github.com/vuejs/core/tree/dev/packages/reactivity-transform#readme", "homepage": "https://github.com/vuejs/core/tree/dev/packages/reactivity-transform#readme",
"dependencies": { "dependencies": {
"@babel/parser": "^7.16.4", "@babel/parser": "^7.16.4",
"@vue/compiler-core": "3.2.33", "@vue/compiler-core": "3.2.37",
"@vue/shared": "3.2.33", "@vue/shared": "3.2.37",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"magic-string": "^0.25.7" "magic-string": "^0.25.7"
}, },

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/reactivity", "name": "@vue/reactivity",
"version": "3.2.33", "version": "3.2.37",
"description": "@vue/reactivity", "description": "@vue/reactivity",
"main": "index.js", "main": "index.js",
"module": "dist/reactivity.esm-bundler.js", "module": "dist/reactivity.esm-bundler.js",
@ -36,6 +36,6 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/main/packages/reactivity#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/reactivity#readme",
"dependencies": { "dependencies": {
"@vue/shared": "3.2.33" "@vue/shared": "3.2.37"
} }
} }

View File

@ -231,6 +231,7 @@ describe('api: lifecycle hooks', () => {
} }
const Mid = { const Mid = {
props: ['count'],
setup(props: any) { setup(props: any) {
onBeforeMount(() => calls.push('mid onBeforeMount')) onBeforeMount(() => calls.push('mid onBeforeMount'))
onMounted(() => calls.push('mid onMounted')) onMounted(() => calls.push('mid onMounted'))
@ -243,6 +244,7 @@ describe('api: lifecycle hooks', () => {
} }
const Child = { const Child = {
props: ['count'],
setup(props: any) { setup(props: any) {
onBeforeMount(() => calls.push('child onBeforeMount')) onBeforeMount(() => calls.push('child onBeforeMount'))
onMounted(() => calls.push('child onMounted')) onMounted(() => calls.push('child onMounted'))

View File

@ -75,39 +75,6 @@ describe('api: setup context', () => {
expect(dummy).toBe(1) expect(dummy).toBe(1)
}) })
it('setup props should resolve the correct types from props object', async () => {
const count = ref(0)
let dummy
const Parent = {
render: () => h(Child, { count: count.value })
}
const Child = defineComponent({
props: {
count: Number
},
setup(props) {
watchEffect(() => {
dummy = props.count
})
return () => h('div', props.count)
}
})
const root = nodeOps.createElement('div')
render(h(Parent), root)
expect(serializeInner(root)).toMatch(`<div>0</div>`)
expect(dummy).toBe(0)
// props should be reactive
count.value++
await nextTick()
expect(serializeInner(root)).toMatch(`<div>1</div>`)
expect(dummy).toBe(1)
})
it('context.attrs', async () => { it('context.attrs', async () => {
const toggle = ref(true) const toggle = ref(true)

View File

@ -13,7 +13,8 @@ import {
createTextVNode, createTextVNode,
createVNode, createVNode,
withDirectives, withDirectives,
vModelCheckbox vModelCheckbox,
renderSlot
} from '@vue/runtime-dom' } from '@vue/runtime-dom'
import { renderToString, SSRContext } from '@vue/server-renderer' import { renderToString, SSRContext } from '@vue/server-renderer'
import { PatchFlags } from '../../shared/src' import { PatchFlags } from '../../shared/src'
@ -96,6 +97,28 @@ describe('SSR hydration', () => {
expect(s.children).toBe(staticContent) expect(s.children).toBe(staticContent)
}) })
// #6008
test('static (with text node as starting node)', () => {
const html = ` A <span>foo</span> B`
const { vnode, container } = mountWithHydration(html, () =>
createStaticVNode(` A <span>foo</span> B`, 3)
)
expect(vnode.el).toBe(container.firstChild)
expect(vnode.anchor).toBe(container.lastChild)
expect(`Hydration node mismatch`).not.toHaveBeenWarned()
})
test('static with content adoption', () => {
const html = ` A <span>foo</span> B`
const { vnode, container } = mountWithHydration(html, () =>
createStaticVNode(``, 3)
)
expect(vnode.el).toBe(container.firstChild)
expect(vnode.anchor).toBe(container.lastChild)
expect(vnode.children).toBe(html)
expect(`Hydration node mismatch`).not.toHaveBeenWarned()
})
test('element with text children', async () => { test('element with text children', async () => {
const msg = ref('foo') const msg = ref('foo')
const { vnode, container } = mountWithHydration( const { vnode, container } = mountWithHydration(
@ -202,7 +225,7 @@ describe('SSR hydration', () => {
const fn = jest.fn() const fn = jest.fn()
const teleportContainer = document.createElement('div') const teleportContainer = document.createElement('div')
teleportContainer.id = 'teleport' teleportContainer.id = 'teleport'
teleportContainer.innerHTML = `<span>foo</span><span class="foo"></span><!---->` teleportContainer.innerHTML = `<span>foo</span><span class="foo"></span><!--teleport anchor-->`
document.body.appendChild(teleportContainer) document.body.appendChild(teleportContainer)
const { vnode, container } = mountWithHydration( const { vnode, container } = mountWithHydration(
@ -233,7 +256,7 @@ describe('SSR hydration', () => {
msg.value = 'bar' msg.value = 'bar'
await nextTick() await nextTick()
expect(teleportContainer.innerHTML).toBe( expect(teleportContainer.innerHTML).toBe(
`<span>bar</span><span class="bar"></span><!---->` `<span>bar</span><span class="bar"></span><!--teleport anchor-->`
) )
}) })
@ -263,7 +286,7 @@ describe('SSR hydration', () => {
const teleportHtml = ctx.teleports!['#teleport2'] const teleportHtml = ctx.teleports!['#teleport2']
expect(teleportHtml).toMatchInlineSnapshot( expect(teleportHtml).toMatchInlineSnapshot(
`"<span>foo</span><span class=\\"foo\\"></span><!----><span>foo2</span><span class=\\"foo2\\"></span><!---->"` `"<span>foo</span><span class=\\"foo\\"></span><!--teleport anchor--><span>foo2</span><span class=\\"foo2\\"></span><!--teleport anchor-->"`
) )
teleportContainer.innerHTML = teleportHtml teleportContainer.innerHTML = teleportHtml
@ -300,7 +323,7 @@ describe('SSR hydration', () => {
msg.value = 'bar' msg.value = 'bar'
await nextTick() await nextTick()
expect(teleportContainer.innerHTML).toMatchInlineSnapshot( expect(teleportContainer.innerHTML).toMatchInlineSnapshot(
`"<span>bar</span><span class=\\"bar\\"></span><!----><span>bar2</span><span class=\\"bar2\\"></span><!---->"` `"<span>bar</span><span class=\\"bar\\"></span><!--teleport anchor--><span>bar2</span><span class=\\"bar2\\"></span><!--teleport anchor-->"`
) )
}) })
@ -327,7 +350,7 @@ describe('SSR hydration', () => {
) )
const teleportHtml = ctx.teleports!['#teleport3'] const teleportHtml = ctx.teleports!['#teleport3']
expect(teleportHtml).toMatchInlineSnapshot(`"<!---->"`) expect(teleportHtml).toMatchInlineSnapshot(`"<!--teleport anchor-->"`)
teleportContainer.innerHTML = teleportHtml teleportContainer.innerHTML = teleportHtml
document.body.appendChild(teleportContainer) document.body.appendChild(teleportContainer)
@ -366,6 +389,67 @@ describe('SSR hydration', () => {
) )
}) })
test('Teleport (as component root)', () => {
const teleportContainer = document.createElement('div')
teleportContainer.id = 'teleport4'
teleportContainer.innerHTML = `hello<!--teleport anchor-->`
document.body.appendChild(teleportContainer)
const wrapper = {
render() {
return h(Teleport, { to: '#teleport4' }, ['hello'])
}
}
const { vnode, container } = mountWithHydration(
'<div><!--teleport start--><!--teleport end--><div></div></div>',
() => h('div', [h(wrapper), h('div')])
)
expect(vnode.el).toBe(container.firstChild)
// component el
const wrapperVNode = (vnode as any).children[0]
const tpStart = container.firstChild?.firstChild
const tpEnd = tpStart?.nextSibling
expect(wrapperVNode.el).toBe(tpStart)
expect(wrapperVNode.component.subTree.el).toBe(tpStart)
expect(wrapperVNode.component.subTree.anchor).toBe(tpEnd)
// next node hydrate properly
const nextVNode = (vnode as any).children[1]
expect(nextVNode.el).toBe(container.firstChild?.lastChild)
})
test('Teleport (nested)', () => {
const teleportContainer = document.createElement('div')
teleportContainer.id = 'teleport5'
teleportContainer.innerHTML = `<div><!--teleport start--><!--teleport end--></div><!--teleport anchor--><div>child</div><!--teleport anchor-->`
document.body.appendChild(teleportContainer)
const { vnode, container } = mountWithHydration(
'<!--teleport start--><!--teleport end-->',
() =>
h(Teleport, { to: '#teleport5' }, [
h('div', [h(Teleport, { to: '#teleport5' }, [h('div', 'child')])])
])
)
expect(vnode.el).toBe(container.firstChild)
expect(vnode.anchor).toBe(container.lastChild)
const childDivVNode = (vnode as any).children[0]
const div = teleportContainer.firstChild
expect(childDivVNode.el).toBe(div)
expect(vnode.targetAnchor).toBe(div?.nextSibling)
const childTeleportVNode = childDivVNode.children[0]
expect(childTeleportVNode.el).toBe(div?.firstChild)
expect(childTeleportVNode.anchor).toBe(div?.lastChild)
expect(childTeleportVNode.targetAnchor).toBe(teleportContainer.lastChild)
expect(childTeleportVNode.children[0].el).toBe(
teleportContainer.lastChild?.previousSibling
)
})
// compile SSR + client render fn from the same template & hydrate // compile SSR + client render fn from the same template & hydrate
test('full compiler integration', async () => { test('full compiler integration', async () => {
const mounted: string[] = [] const mounted: string[] = []
@ -851,6 +935,53 @@ describe('SSR hydration', () => {
expect((container.firstChild!.firstChild as any)._value).toBe(true) expect((container.firstChild!.firstChild as any)._value).toBe(true)
}) })
// #5728
test('empty text node in slot', () => {
const Comp = {
render(this: any) {
return renderSlot(this.$slots, 'default', {}, () => [
createTextVNode('')
])
}
}
const { container, vnode } = mountWithHydration('<!--[--><!--]-->', () =>
h(Comp)
)
expect(container.childNodes.length).toBe(3)
const text = container.childNodes[1]
expect(text.nodeType).toBe(3)
expect(vnode.el).toBe(container.childNodes[0])
// component => slot fragment => text node
expect((vnode as any).component?.subTree.children[0].el).toBe(text)
})
test('app.unmount()', async () => {
const container = document.createElement('DIV')
container.innerHTML = '<button></button>'
const App = defineComponent({
setup(_, { expose }) {
const count = ref(0)
expose({ count })
return () =>
h('button', {
onClick: () => count.value++
})
}
})
const app = createSSRApp(App)
const vm = app.mount(container)
await nextTick()
expect((container as any)._vnode).toBeDefined()
// @ts-expect-error - expose()'d properties are not available on vm type
expect(vm.count).toBe(0)
app.unmount()
expect((container as any)._vnode).toBe(null)
})
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.2.33", "version": "3.2.37",
"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",
@ -32,7 +32,7 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-core#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-core#readme",
"dependencies": { "dependencies": {
"@vue/shared": "3.2.33", "@vue/shared": "3.2.37",
"@vue/reactivity": "3.2.33" "@vue/reactivity": "3.2.37"
} }
} }

View File

@ -111,7 +111,7 @@ export function defineAsyncComponent<
) )
} }
return defineComponent<{}>({ return defineComponent({
name: 'AsyncComponentWrapper', name: 'AsyncComponentWrapper',
__asyncLoader: load, __asyncLoader: load,

View File

@ -6,8 +6,7 @@ import {
ComponentOptionsWithObjectProps, ComponentOptionsWithObjectProps,
ComponentOptionsMixin, ComponentOptionsMixin,
RenderFunction, RenderFunction,
ComponentOptionsBase, ComponentOptionsBase
ComponentProvideOptions
} from './componentOptions' } from './componentOptions'
import { import {
SetupContext, SetupContext,
@ -41,8 +40,6 @@ export type DefineComponent<
Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = {}, E extends EmitsOptions = {},
EE extends string = string, EE extends string = string,
Provide extends ComponentProvideOptions = ComponentProvideOptions,
RawOptions extends {} = {},
PP = PublicProps, PP = PublicProps,
Props = Readonly< Props = Readonly<
PropsOrPropOptions extends ComponentPropsOptions PropsOrPropOptions extends ComponentPropsOptions
@ -51,8 +48,7 @@ export type DefineComponent<
> & > &
({} extends E ? {} : EmitsToProps<E>), ({} extends E ? {} : EmitsToProps<E>),
Defaults = ExtractDefaultPropTypes<PropsOrPropOptions> Defaults = ExtractDefaultPropTypes<PropsOrPropOptions>
> = RawOptions & > = ComponentPublicInstanceConstructor<
ComponentPublicInstanceConstructor<
CreateComponentPublicInstance< CreateComponentPublicInstance<
Props, Props,
RawBindings, RawBindings,
@ -78,8 +74,7 @@ export type DefineComponent<
Extends, Extends,
E, E,
EE, EE,
Defaults, Defaults
Provide
> & > &
PP PP
@ -109,12 +104,9 @@ export function defineComponent<
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions, E extends EmitsOptions = EmitsOptions,
EE extends string = string, EE extends string = string
Provide extends ComponentProvideOptions = ComponentProvideOptions,
Options extends {} = {}
>( >(
options: Options & options: ComponentOptionsWithoutProps<
ComponentOptionsWithoutProps<
Props, Props,
RawBindings, RawBindings,
D, D,
@ -123,22 +115,9 @@ export function defineComponent<
Mixin, Mixin,
Extends, Extends,
E, E,
EE, EE
Provide
>
): DefineComponent<
Props,
RawBindings,
D,
C,
M,
Mixin,
Extends,
E,
EE,
Provide,
Options
> >
): DefineComponent<Props, RawBindings, D, C, M, Mixin, Extends, E, EE>
// overload 3: object format with array props declaration // overload 3: object format with array props declaration
// props inferred as { [key in PropNames]?: any } // props inferred as { [key in PropNames]?: any }
@ -152,12 +131,9 @@ export function defineComponent<
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = Record<string, any>, E extends EmitsOptions = Record<string, any>,
EE extends string = string, EE extends string = string
Provide extends ComponentProvideOptions = ComponentProvideOptions,
Options extends {} = {}
>( >(
options: Options & options: ComponentOptionsWithArrayProps<
ComponentOptionsWithArrayProps<
PropNames, PropNames,
RawBindings, RawBindings,
D, D,
@ -166,8 +142,7 @@ export function defineComponent<
Mixin, Mixin,
Extends, Extends,
E, E,
EE, EE
Provide
> >
): DefineComponent< ): DefineComponent<
Readonly<{ [key in PropNames]?: any }>, Readonly<{ [key in PropNames]?: any }>,
@ -178,9 +153,7 @@ export function defineComponent<
Mixin, Mixin,
Extends, Extends,
E, E,
EE, EE
Provide,
Options
> >
// overload 4: object format with object props declaration // overload 4: object format with object props declaration
@ -196,12 +169,9 @@ export function defineComponent<
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = Record<string, any>, E extends EmitsOptions = Record<string, any>,
EE extends string = string, EE extends string = string
Provide extends ComponentProvideOptions = ComponentProvideOptions,
Options extends {} = {}
>( >(
options: Options & options: ComponentOptionsWithObjectProps<
ComponentOptionsWithObjectProps<
PropsOptions, PropsOptions,
RawBindings, RawBindings,
D, D,
@ -210,22 +180,9 @@ export function defineComponent<
Mixin, Mixin,
Extends, Extends,
E, E,
EE, EE
Provide
>
): DefineComponent<
PropsOptions,
RawBindings,
D,
C,
M,
Mixin,
Extends,
E,
EE,
Provide,
Options
> >
): DefineComponent<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>
// implementation, close to no-op // implementation, close to no-op
export function defineComponent(options: unknown) { export function defineComponent(options: unknown) {

View File

@ -139,7 +139,7 @@ type InferDefault<P, T> = T extends
| boolean | boolean
| symbol | symbol
| Function | Function
? T ? T | ((props: P) => T)
: (props: P) => T : (props: P) => T
type PropsWithDefaults<Base, Defaults> = Base & { type PropsWithDefaults<Base, Defaults> = Base & {

View File

@ -381,9 +381,10 @@ function installLegacyAPIs(app: App) {
function applySingletonAppMutations(app: App) { function applySingletonAppMutations(app: App) {
// copy over asset registries and deopt flag // copy over asset registries and deopt flag
;['mixins', 'components', 'directives', 'filters', 'deopt'].forEach(key => { app._context.mixins = [...singletonApp._context.mixins]
;['components', 'directives', 'filters'].forEach(key => {
// @ts-ignore // @ts-ignore
app._context[key] = singletonApp._context[key] app._context[key] = Object.create(singletonApp._context[key])
}) })
// copy over global config mutations // copy over global config mutations
@ -398,7 +399,7 @@ function applySingletonAppMutations(app: App) {
} }
const val = singletonApp.config[key as keyof AppConfig] const val = singletonApp.config[key as keyof AppConfig]
// @ts-ignore // @ts-ignore
app.config[key] = val app.config[key] = isObject(val) ? Object.create(val) : val
// compat for runtime ignoredElements -> isCustomElement // compat for runtime ignoredElements -> isCustomElement
if ( if (

View File

@ -106,6 +106,10 @@ export interface ComponentInternalOptions {
* This one should be exposed so that devtools can make use of it * This one should be exposed so that devtools can make use of it
*/ */
__file?: string __file?: string
/**
* name inferred from filename
*/
__name?: string
} }
export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}> export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}>
@ -949,11 +953,12 @@ const classify = (str: string): string =>
str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '') str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '')
export function getComponentName( export function getComponentName(
Component: ConcreteComponent Component: ConcreteComponent,
): string | undefined { includeInferred = true
): string | false | undefined {
return isFunction(Component) return isFunction(Component)
? Component.displayName || Component.name ? Component.displayName || Component.name
: Component.name : Component.name || (includeInferred && Component.__name)
} }
/* istanbul ignore next */ /* istanbul ignore next */

View File

@ -58,7 +58,8 @@ import { EmitsOptions, EmitsToProps } from './componentEmits'
import { Directive } from './directives' import { Directive } from './directives'
import { import {
CreateComponentPublicInstance, CreateComponentPublicInstance,
ComponentPublicInstance ComponentPublicInstance,
isReservedPrefix
} from './componentPublicInstance' } from './componentPublicInstance'
import { warn } from './warning' import { warn } from './warning'
import { VNodeChild } from './vnode' import { VNodeChild } from './vnode'
@ -117,9 +118,8 @@ export interface ComponentOptionsBase<
Extends extends ComponentOptionsMixin, Extends extends ComponentOptionsMixin,
E extends EmitsOptions, E extends EmitsOptions,
EE extends string = string, EE extends string = string,
Defaults = {}, Defaults = {}
Provide extends ComponentProvideOptions = ComponentProvideOptions > extends LegacyOptions<Props, D, C, M, Mixin, Extends>,
> extends LegacyOptions<Props, D, C, M, Mixin, Extends, Provide>,
ComponentInternalOptions, ComponentInternalOptions,
ComponentCustomOptions { ComponentCustomOptions {
setup?: ( setup?: (
@ -225,7 +225,6 @@ export type ComponentOptionsWithoutProps<
Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions, E extends EmitsOptions = EmitsOptions,
EE extends string = string, EE extends string = string,
Provide extends ComponentProvideOptions = ComponentProvideOptions,
PE = Props & EmitsToProps<E> PE = Props & EmitsToProps<E>
> = ComponentOptionsBase< > = ComponentOptionsBase<
PE, PE,
@ -237,8 +236,7 @@ export type ComponentOptionsWithoutProps<
Extends, Extends,
E, E,
EE, EE,
{}, {}
Provide
> & { > & {
props?: undefined props?: undefined
} & ThisType< } & ThisType<
@ -255,7 +253,6 @@ export type ComponentOptionsWithArrayProps<
Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions, E extends EmitsOptions = EmitsOptions,
EE extends string = string, EE extends string = string,
Provide extends ComponentProvideOptions = ComponentProvideOptions,
Props = Readonly<{ [key in PropNames]?: any }> & EmitsToProps<E> Props = Readonly<{ [key in PropNames]?: any }> & EmitsToProps<E>
> = ComponentOptionsBase< > = ComponentOptionsBase<
Props, Props,
@ -267,8 +264,7 @@ export type ComponentOptionsWithArrayProps<
Extends, Extends,
E, E,
EE, EE,
{}, {}
Provide
> & { > & {
props: PropNames[] props: PropNames[]
} & ThisType< } & ThisType<
@ -294,7 +290,6 @@ export type ComponentOptionsWithObjectProps<
Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions, E extends EmitsOptions = EmitsOptions,
EE extends string = string, EE extends string = string,
Provide extends ComponentProvideOptions = ComponentProvideOptions,
Props = Readonly<ExtractPropTypes<PropsOptions>> & EmitsToProps<E>, Props = Readonly<ExtractPropTypes<PropsOptions>> & EmitsToProps<E>,
Defaults = ExtractDefaultPropTypes<PropsOptions> Defaults = ExtractDefaultPropTypes<PropsOptions>
> = ComponentOptionsBase< > = ComponentOptionsBase<
@ -307,8 +302,7 @@ export type ComponentOptionsWithObjectProps<
Extends, Extends,
E, E,
EE, EE,
Defaults, Defaults
Provide
> & { > & {
props: PropsOptions & ThisType<void> props: PropsOptions & ThisType<void>
} & ThisType< } & ThisType<
@ -408,8 +402,7 @@ interface LegacyOptions<
C extends ComputedOptions, C extends ComputedOptions,
M extends MethodOptions, M extends MethodOptions,
Mixin extends ComponentOptionsMixin, Mixin extends ComponentOptionsMixin,
Extends extends ComponentOptionsMixin, Extends extends ComponentOptionsMixin
Provide extends ComponentProvideOptions = ComponentProvideOptions
> { > {
compatConfig?: CompatConfig compatConfig?: CompatConfig
@ -443,7 +436,7 @@ interface LegacyOptions<
computed?: C computed?: C
methods?: M methods?: M
watch?: ComponentWatchOptions watch?: ComponentWatchOptions
provide?: Provide provide?: ComponentProvideOptions
inject?: ComponentInjectOptions inject?: ComponentInjectOptions
// assets // assets
@ -681,7 +674,7 @@ export function applyOptions(instance: ComponentInternalInstance) {
for (const key in data) { for (const key in data) {
checkDuplicateProperties!(OptionTypes.DATA, key) checkDuplicateProperties!(OptionTypes.DATA, key)
// expose data on ctx during dev // expose data on ctx during dev
if (key[0] !== '$' && key[0] !== '_') { if (!isReservedPrefix(key[0])) {
Object.defineProperty(ctx, key, { Object.defineProperty(ctx, key, {
configurable: true, configurable: true,
enumerable: true, enumerable: true,

View File

@ -34,8 +34,7 @@ import {
OptionTypesKeys, OptionTypesKeys,
resolveMergedOptions, resolveMergedOptions,
shouldCacheAccess, shouldCacheAccess,
MergedComponentOptionsOverride, MergedComponentOptionsOverride
ComponentProvideOptions
} from './componentOptions' } from './componentOptions'
import { EmitsOptions, EmitFn } from './componentEmits' import { EmitsOptions, EmitFn } from './componentEmits'
import { Slots } from './componentSlots' import { Slots } from './componentSlots'
@ -151,8 +150,7 @@ export type CreateComponentPublicInstance<
PublicM extends MethodOptions = UnwrapMixinsType<PublicMixin, 'M'> & PublicM extends MethodOptions = UnwrapMixinsType<PublicMixin, 'M'> &
EnsureNonVoid<M>, EnsureNonVoid<M>,
PublicDefaults = UnwrapMixinsType<PublicMixin, 'Defaults'> & PublicDefaults = UnwrapMixinsType<PublicMixin, 'Defaults'> &
EnsureNonVoid<Defaults>, EnsureNonVoid<Defaults>
Provide extends ComponentProvideOptions = ComponentProvideOptions
> = ComponentPublicInstance< > = ComponentPublicInstance<
PublicP, PublicP,
PublicB, PublicB,
@ -163,19 +161,7 @@ export type CreateComponentPublicInstance<
PublicProps, PublicProps,
PublicDefaults, PublicDefaults,
MakeDefaultsOptional, MakeDefaultsOptional,
ComponentOptionsBase< ComponentOptionsBase<P, B, D, C, M, Mixin, Extends, E, string, Defaults>
P,
B,
D,
C,
M,
Mixin,
Extends,
E,
string,
Defaults,
Provide
>
> >
// public properties exposed on the proxy, which is used as the render context // public properties exposed on the proxy, which is used as the render context
@ -274,6 +260,8 @@ export interface ComponentRenderContext {
_: ComponentInternalInstance _: ComponentInternalInstance
} }
export const isReservedPrefix = (key: string) => key === '_' || key === '$'
export const PublicInstanceProxyHandlers: ProxyHandler<any> = { export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
get({ _: instance }: ComponentRenderContext, key: string) { get({ _: instance }: ComponentRenderContext, key: string) {
const { ctx, setupState, data, props, accessCache, type, appContext } = const { ctx, setupState, data, props, accessCache, type, appContext } =
@ -385,11 +373,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
// to infinite warning loop // to infinite warning loop
key.indexOf('__v') !== 0) key.indexOf('__v') !== 0)
) { ) {
if ( if (data !== EMPTY_OBJ && isReservedPrefix(key[0]) && hasOwn(data, key)) {
data !== EMPTY_OBJ &&
(key[0] === '$' || key[0] === '_') &&
hasOwn(data, key)
) {
warn( warn(
`Property ${JSON.stringify( `Property ${JSON.stringify(
key key
@ -571,7 +555,7 @@ export function exposeSetupStateOnRenderContext(
const { ctx, setupState } = instance const { ctx, setupState } = instance
Object.keys(toRaw(setupState)).forEach(key => { Object.keys(toRaw(setupState)).forEach(key => {
if (!setupState.__isScriptSetup) { if (!setupState.__isScriptSetup) {
if (key[0] === '$' || key[0] === '_') { if (isReservedPrefix(key[0])) {
warn( warn(
`setup() return property ${JSON.stringify( `setup() return property ${JSON.stringify(
key key

View File

@ -38,6 +38,8 @@ export function markAttrsAccessed() {
accessedAttrs = true accessedAttrs = true
} }
type SetRootFn = ((root: VNode) => void) | undefined
export function renderComponentRoot( export function renderComponentRoot(
instance: ComponentInternalInstance instance: ComponentInternalInstance
): VNode { ): VNode {
@ -121,7 +123,7 @@ export function renderComponentRoot(
// in dev mode, comments are preserved, and it's possible for a template // in dev mode, comments are preserved, and it's possible for a template
// to have comments along side the root element which makes it a fragment // to have comments along side the root element which makes it a fragment
let root = result let root = result
let setRoot: ((root: VNode) => void) | undefined = undefined let setRoot: SetRootFn = undefined
if ( if (
__DEV__ && __DEV__ &&
result.patchFlag > 0 && result.patchFlag > 0 &&
@ -246,9 +248,7 @@ export function renderComponentRoot(
* template into a fragment root, but we need to locate the single element * template into a fragment root, but we need to locate the single element
* root for attrs and scope id processing. * root for attrs and scope id processing.
*/ */
const getChildRoot = ( const getChildRoot = (vnode: VNode): [VNode, SetRootFn] => {
vnode: VNode
): [VNode, ((root: VNode) => void) | undefined] => {
const rawChildren = vnode.children as VNodeArrayChildren const rawChildren = vnode.children as VNodeArrayChildren
const dynamicChildren = vnode.dynamicChildren const dynamicChildren = vnode.dynamicChildren
const childRoot = filterSingleRoot(rawChildren) const childRoot = filterSingleRoot(rawChildren)
@ -257,7 +257,7 @@ const getChildRoot = (
} }
const index = rawChildren.indexOf(childRoot) const index = rawChildren.indexOf(childRoot)
const dynamicIndex = dynamicChildren ? dynamicChildren.indexOf(childRoot) : -1 const dynamicIndex = dynamicChildren ? dynamicChildren.indexOf(childRoot) : -1
const setRoot = (updatedRoot: VNode) => { const setRoot: SetRootFn = (updatedRoot: VNode) => {
rawChildren[index] = updatedRoot rawChildren[index] = updatedRoot
if (dynamicChildren) { if (dynamicChildren) {
if (dynamicIndex > -1) { if (dynamicIndex > -1) {

View File

@ -96,8 +96,11 @@ const KeepAliveImpl: ComponentOptions = {
// if the internal renderer is not registered, it indicates that this is server-side rendering, // if the internal renderer is not registered, it indicates that this is server-side rendering,
// for KeepAlive, we just need to render its children // for KeepAlive, we just need to render its children
if (!sharedContext.renderer) { if (__SSR__ && !sharedContext.renderer) {
return slots.default return () => {
const children = slots.default && slots.default()
return children && children.length === 1 ? children[0] : children
}
} }
const cache: Cache = new Map() const cache: Cache = new Map()

View File

@ -353,7 +353,26 @@ function hydrateTeleport(
vnode.targetAnchor = targetNode vnode.targetAnchor = targetNode
} else { } else {
vnode.anchor = nextSibling(node) vnode.anchor = nextSibling(node)
vnode.targetAnchor = hydrateChildren(
// lookahead until we find the target anchor
// we cannot rely on return value of hydrateChildren() because there
// could be nested teleports
let targetAnchor = targetNode
while (targetAnchor) {
targetAnchor = nextSibling(targetAnchor)
if (
targetAnchor &&
targetAnchor.nodeType === 8 &&
(targetAnchor as Comment).data === 'teleport anchor'
) {
vnode.targetAnchor = targetAnchor
;(target as TeleportTargetElement)._lpa =
vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
break
}
}
hydrateChildren(
targetNode, targetNode,
vnode, vnode,
target, target,
@ -363,8 +382,6 @@ function hydrateTeleport(
optimized optimized
) )
} }
;(target as TeleportTargetElement)._lpa =
vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
} }
} }
return vnode.anchor && nextSibling(vnode.anchor as Node) return vnode.anchor && nextSibling(vnode.anchor as Node)

View File

@ -86,7 +86,10 @@ function resolveAsset(
// explicit self name has highest priority // explicit self name has highest priority
if (type === COMPONENTS) { if (type === COMPONENTS) {
const selfName = getComponentName(Component) const selfName = getComponentName(
Component,
false /* do not include inferred name to avoid breaking existing code */
)
if ( if (
selfName && selfName &&
(selfName === name || (selfName === name ||

View File

@ -27,7 +27,7 @@ import { isAsyncWrapper } from './apiAsyncComponent'
export type RootHydrateFunction = ( export type RootHydrateFunction = (
vnode: VNode<Node, Element>, vnode: VNode<Node, Element>,
container: Element | ShadowRoot container: (Element | ShadowRoot) & { _vnode?: VNode }
) => void ) => void
const enum DOMNodeTypes { const enum DOMNodeTypes {
@ -55,7 +55,15 @@ export function createHydrationFunctions(
const { const {
mt: mountComponent, mt: mountComponent,
p: patch, p: patch,
o: { patchProp, nextSibling, parentNode, remove, insert, createComment } o: {
patchProp,
createText,
nextSibling,
parentNode,
remove,
insert,
createComment
}
} = rendererInternals } = rendererInternals
const hydrate: RootHydrateFunction = (vnode, container) => { const hydrate: RootHydrateFunction = (vnode, container) => {
@ -67,11 +75,13 @@ export function createHydrationFunctions(
) )
patch(null, vnode, container) patch(null, vnode, container)
flushPostFlushCbs() flushPostFlushCbs()
container._vnode = vnode
return return
} }
hasMismatch = false hasMismatch = false
hydrateNode(container.firstChild!, vnode, null, null, null) hydrateNode(container.firstChild!, vnode, null, null, null)
flushPostFlushCbs() flushPostFlushCbs()
container._vnode = vnode
if (hasMismatch && !__TEST__) { if (hasMismatch && !__TEST__) {
// this error should show up in production // this error should show up in production
console.error(`Hydration completed but contains mismatches.`) console.error(`Hydration completed but contains mismatches.`)
@ -110,7 +120,14 @@ export function createHydrationFunctions(
switch (type) { switch (type) {
case Text: case Text:
if (domType !== DOMNodeTypes.TEXT) { if (domType !== DOMNodeTypes.TEXT) {
// #5728 empty text node inside a slot can cause hydration failure
// because the server rendered HTML won't contain a text node
if (vnode.children === '') {
insert((vnode.el = createText('')), parentNode(node)!, node)
nextNode = node
} else {
nextNode = onMismatch() nextNode = onMismatch()
}
} else { } else {
if ((node as Text).data !== vnode.children) { if ((node as Text).data !== vnode.children) {
hasMismatch = true hasMismatch = true
@ -133,7 +150,7 @@ export function createHydrationFunctions(
} }
break break
case Static: case Static:
if (domType !== DOMNodeTypes.ELEMENT) { if (domType !== DOMNodeTypes.ELEMENT && domType !== DOMNodeTypes.TEXT) {
nextNode = onMismatch() nextNode = onMismatch()
} else { } else {
// determine anchor, adopt content // determine anchor, adopt content
@ -143,7 +160,10 @@ export function createHydrationFunctions(
const needToAdoptContent = !(vnode.children as string).length const needToAdoptContent = !(vnode.children as string).length
for (let i = 0; i < vnode.staticCount!; i++) { for (let i = 0; i < vnode.staticCount!; i++) {
if (needToAdoptContent) if (needToAdoptContent)
vnode.children += (nextNode as Element).outerHTML vnode.children +=
nextNode.nodeType === DOMNodeTypes.ELEMENT
? (nextNode as Element).outerHTML
: (nextNode as Text).data
if (i === vnode.staticCount! - 1) { if (i === vnode.staticCount! - 1) {
vnode.anchor = nextNode vnode.anchor = nextNode
} }
@ -207,6 +227,15 @@ export function createHydrationFunctions(
? locateClosingAsyncAnchor(node) ? locateClosingAsyncAnchor(node)
: nextSibling(node) : nextSibling(node)
// #4293 teleport as component root
if (
nextNode &&
isComment(nextNode) &&
nextNode.data === 'teleport end'
) {
nextNode = nextSibling(nextNode)
}
// #3787 // #3787
// if component is async, it may get moved / unmounted before its // if component is async, it may get moved / unmounted before its
// inner component is loaded, so we need to give it a placeholder // inner component is loaded, so we need to give it a placeholder

View File

@ -86,7 +86,7 @@ export { h } from './h'
// Advanced render function utilities // Advanced render function utilities
export { createVNode, cloneVNode, mergeProps, isVNode } from './vnode' export { createVNode, cloneVNode, mergeProps, isVNode } from './vnode'
// VNode types // VNode types
export { Fragment, Text, Comment, Static } from './vnode' export { Fragment, Text, Comment, Static, VNodeRef } from './vnode'
// Built-in components // Built-in components
export { Teleport, TeleportProps } from './components/Teleport' export { Teleport, TeleportProps } from './components/Teleport'
export { Suspense, SuspenseProps } from './components/Suspense' export { Suspense, SuspenseProps } from './components/Suspense'
@ -217,6 +217,7 @@ export {
ComponentOptionsWithArrayProps, ComponentOptionsWithArrayProps,
ComponentCustomOptions, ComponentCustomOptions,
ComponentOptionsBase, ComponentOptionsBase,
ComponentProvideOptions,
RenderFunction, RenderFunction,
MethodOptions, MethodOptions,
ComputedOptions, ComputedOptions,
@ -324,7 +325,7 @@ const _ssrUtils = {
} }
/** /**
* SSR utils for \@vue/server-renderer. Only exposed in cjs builds. * SSR utils for \@vue/server-renderer. Only exposed in ssr-possible builds.
* @internal * @internal
*/ */
export const ssrUtils = (__SSR__ ? _ssrUtils : null) as typeof _ssrUtils export const ssrUtils = (__SSR__ ? _ssrUtils : null) as typeof _ssrUtils

View File

@ -1064,8 +1064,12 @@ function baseCreateRenderer(
let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2 let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2
if (__DEV__ && isHmrUpdating) { if (
// HMR updated, force full diff __DEV__ &&
// #5523 dev root fragment may inherit directives
(isHmrUpdating || patchFlag & PatchFlags.DEV_ROOT_FRAGMENT)
) {
// HMR updated / Dev root fragment (w/ comments), force full diff
patchFlag = 0 patchFlag = 0
optimized = false optimized = false
dynamicChildren = null dynamicChildren = null
@ -1098,8 +1102,6 @@ function baseCreateRenderer(
if ( if (
patchFlag > 0 && patchFlag > 0 &&
patchFlag & PatchFlags.STABLE_FRAGMENT && patchFlag & PatchFlags.STABLE_FRAGMENT &&
// #5523 dev root fragment may inherit directives so always force update
!(__DEV__ && patchFlag & PatchFlags.DEV_ROOT_FRAGMENT) &&
dynamicChildren && dynamicChildren &&
// #2715 the previous fragment could've been a BAILed one as a result // #2715 the previous fragment could've been a BAILed one as a result
// of renderSlot() with no valid children // of renderSlot() with no valid children

View File

@ -107,7 +107,7 @@ export function setRef(
if (hasOwn(setupState, ref)) { if (hasOwn(setupState, ref)) {
setupState[ref] = value setupState[ref] = value
} }
} else if (isRef(ref)) { } else if (_isRef) {
ref.value = value ref.value = value
if (rawRef.k) refs[rawRef.k] = value if (rawRef.k) refs[rawRef.k] = value
} else if (__DEV__) { } else if (__DEV__) {

View File

@ -43,6 +43,7 @@ import { convertLegacyComponent } from './compat/component'
import { convertLegacyVModelProps } from './compat/componentVModel' import { convertLegacyVModelProps } from './compat/componentVModel'
import { defineLegacyVNodeProperties } from './compat/renderFn' import { defineLegacyVNodeProperties } from './compat/renderFn'
import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling' import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling'
import { ComponentPublicInstance } from './componentPublicInstance'
export const Fragment = Symbol(__DEV__ ? 'Fragment' : undefined) as any as { export const Fragment = Symbol(__DEV__ ? 'Fragment' : undefined) as any as {
__isFragment: true __isFragment: true
@ -68,7 +69,10 @@ export type VNodeTypes =
export type VNodeRef = export type VNodeRef =
| string | string
| Ref | Ref
| ((ref: object | null, refs: Record<string, any>) => void) | ((
ref: Element | ComponentPublicInstance | null,
refs: Record<string, any>
) => void)
export type VNodeNormalizedRefAtom = { export type VNodeNormalizedRefAtom = {
i: ComponentInternalInstance i: ComponentInternalInstance

View File

@ -291,6 +291,94 @@ describe('vModel', () => {
expect(data.lazy).toEqual('foo') expect(data.lazy).toEqual('foo')
}) })
it('should work with range', async () => {
const component = defineComponent({
data() {
return { value: 25 }
},
render() {
return [
withVModel(
h('input', {
type: 'range',
min: 1,
max: 100,
class: 'foo',
'onUpdate:modelValue': setValue.bind(this)
}),
this.value,
{
number: true
}
),
withVModel(
h('input', {
type: 'range',
min: 1,
max: 100,
class: 'bar',
'onUpdate:modelValue': setValue.bind(this)
}),
this.value,
{
lazy: true
}
)
]
}
})
render(h(component), root)
const foo = root.querySelector('.foo')
const bar = root.querySelector('.bar')
const data = root._vnode.component.data
foo.value = 20
triggerEvent('input', foo)
await nextTick()
expect(data.value).toEqual(20)
foo.value = 200
triggerEvent('input', foo)
await nextTick()
expect(data.value).toEqual(100)
foo.value = -1
triggerEvent('input', foo)
await nextTick()
expect(data.value).toEqual(1)
bar.value = 30
triggerEvent('change', bar)
await nextTick()
expect(data.value).toEqual('30')
bar.value = 200
triggerEvent('change', bar)
await nextTick()
expect(data.value).toEqual('100')
bar.value = -1
triggerEvent('change', bar)
await nextTick()
expect(data.value).toEqual('1')
data.value = 60
await nextTick()
expect(foo.value).toEqual('60')
expect(bar.value).toEqual('60')
data.value = -1
await nextTick()
expect(foo.value).toEqual('1')
expect(bar.value).toEqual('1')
data.value = 200
await nextTick()
expect(foo.value).toEqual('100')
expect(bar.value).toEqual('100')
})
it('should work with checkbox', async () => { it('should work with checkbox', async () => {
const component = defineComponent({ const component = defineComponent({
data() { data() {

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/runtime-dom", "name": "@vue/runtime-dom",
"version": "3.2.33", "version": "3.2.37",
"description": "@vue/runtime-dom", "description": "@vue/runtime-dom",
"main": "index.js", "main": "index.js",
"module": "dist/runtime-dom.esm-bundler.js", "module": "dist/runtime-dom.esm-bundler.js",
@ -35,8 +35,8 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-dom#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-dom#readme",
"dependencies": { "dependencies": {
"@vue/shared": "3.2.33", "@vue/shared": "3.2.37",
"@vue/runtime-core": "3.2.33", "@vue/runtime-core": "3.2.37",
"csstype": "^2.6.8" "csstype": "^2.6.8"
} }
} }

View File

@ -174,9 +174,11 @@ export function resolveTransitionProps(
done && done() done && done()
} }
let isLeaving = false const finishLeave = (
const finishLeave = (el: Element, done?: () => void) => { el: Element & { _isLeaving?: boolean },
isLeaving = false done?: () => void
) => {
el._isLeaving = false
removeTransitionClass(el, leaveFromClass) removeTransitionClass(el, leaveFromClass)
removeTransitionClass(el, leaveToClass) removeTransitionClass(el, leaveToClass)
removeTransitionClass(el, leaveActiveClass) removeTransitionClass(el, leaveActiveClass)
@ -223,8 +225,8 @@ export function resolveTransitionProps(
}, },
onEnter: makeEnterHook(false), onEnter: makeEnterHook(false),
onAppear: makeEnterHook(true), onAppear: makeEnterHook(true),
onLeave(el, done) { onLeave(el: Element & { _isLeaving?: boolean }, done) {
isLeaving = true el._isLeaving = true
const resolve = () => finishLeave(el, done) const resolve = () => finishLeave(el, done)
addTransitionClass(el, leaveFromClass) addTransitionClass(el, leaveFromClass)
if (__COMPAT__ && legacyClassEnabled) { if (__COMPAT__ && legacyClassEnabled) {
@ -234,7 +236,7 @@ export function resolveTransitionProps(
forceReflow() forceReflow()
addTransitionClass(el, leaveActiveClass) addTransitionClass(el, leaveActiveClass)
nextFrame(() => { nextFrame(() => {
if (!isLeaving) { if (!el._isLeaving) {
// cancelled // cancelled
return return
} }

View File

@ -269,6 +269,24 @@ export const vModelDynamic: ObjectDirective<
} }
} }
function resolveDynamicModel(tagName: string, type: string | undefined) {
switch (tagName) {
case 'SELECT':
return vModelSelect
case 'TEXTAREA':
return vModelText
default:
switch (type) {
case 'checkbox':
return vModelCheckbox
case 'radio':
return vModelRadio
default:
return vModelText
}
}
}
function callModelHook( function callModelHook(
el: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement, el: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement,
binding: DirectiveBinding, binding: DirectiveBinding,
@ -276,26 +294,10 @@ function callModelHook(
prevVNode: VNode | null, prevVNode: VNode | null,
hook: keyof ObjectDirective hook: keyof ObjectDirective
) { ) {
let modelToUse: ObjectDirective const modelToUse = resolveDynamicModel(
switch (el.tagName) { el.tagName,
case 'SELECT': vnode.props && vnode.props.type
modelToUse = vModelSelect )
break
case 'TEXTAREA':
modelToUse = vModelText
break
default:
switch (vnode.props && vnode.props.type) {
case 'checkbox':
modelToUse = vModelCheckbox
break
case 'radio':
modelToUse = vModelRadio
break
default:
modelToUse = vModelText
}
}
const fn = modelToUse[hook] as DirectiveHook const fn = modelToUse[hook] as DirectiveHook
fn && fn(el, binding, vnode, prevVNode) fn && fn(el, binding, vnode, prevVNode)
} }
@ -324,4 +326,18 @@ export function initVModelForSSR() {
return { checked: true } return { checked: true }
} }
} }
vModelDynamic.getSSRProps = (binding, vnode) => {
if (typeof vnode.type !== 'string') {
return
}
const modelToUse = resolveDynamicModel(
// resolveDynamicModel expects an uppercase tag name, but vnode.type is lowercase
vnode.type.toUpperCase(),
vnode.props && vnode.props.type
)
if (modelToUse.getSSRProps) {
return modelToUse.getSSRProps(binding, vnode)
}
}
} }

View File

@ -25,7 +25,7 @@ const [_getNow, skipTimestampCheck] = /*#__PURE__*/ (() => {
// if the low-res timestamp which is bigger than the event timestamp // if the low-res timestamp which is bigger than the event timestamp
// (which is evaluated AFTER) it means the event is using a hi-res timestamp, // (which is evaluated AFTER) it means the event is using a hi-res timestamp,
// and we need to use the hi-res version for event listeners as well. // and we need to use the hi-res version for event listeners as well.
_getNow = () => performance.now() _getNow = performance.now.bind(performance)
} }
// #3485: Firefox <= 53 has incorrect Event.timeStamp implementation // #3485: Firefox <= 53 has incorrect Event.timeStamp implementation
// and does not fire microtasks in between event propagation, so safe to exclude. // and does not fire microtasks in between event propagation, so safe to exclude.

View File

@ -40,6 +40,7 @@ export interface CSSProperties
* For examples and more information, visit: * For examples and more information, visit:
* https://github.com/frenic/csstype#what-should-i-do-when-i-get-type-errors * https://github.com/frenic/csstype#what-should-i-do-when-i-get-type-errors
*/ */
[v: `--${string}`]: string | number | undefined
} }
type Booleanish = boolean | 'true' | 'false' type Booleanish = boolean | 'true' | 'false'
@ -457,7 +458,7 @@ export interface InputHTMLAttributes extends HTMLAttributes {
autocomplete?: string autocomplete?: string
autofocus?: Booleanish autofocus?: Booleanish
capture?: boolean | 'user' | 'environment' // https://www.w3.org/tr/html-media-capture/#the-capture-attribute capture?: boolean | 'user' | 'environment' // https://www.w3.org/tr/html-media-capture/#the-capture-attribute
checked?: Booleanish | any[] // for IDE v-model multi-checkbox support checked?: Booleanish | any[] | Set<any> // for IDE v-model multi-checkbox support
crossorigin?: string crossorigin?: string
disabled?: Booleanish disabled?: Booleanish
form?: string form?: string
@ -1309,10 +1310,7 @@ import * as RuntimeCore from '@vue/runtime-core'
type ReservedProps = { type ReservedProps = {
key?: string | number | symbol key?: string | number | symbol
ref?: ref?: RuntimeCore.VNodeRef
| string
| RuntimeCore.Ref
| ((ref: Element | RuntimeCore.ComponentPublicInstance | null) => void)
ref_for?: boolean ref_for?: boolean
ref_key?: string ref_key?: string
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/runtime-test", "name": "@vue/runtime-test",
"version": "3.2.33", "version": "3.2.37",
"description": "@vue/runtime-test", "description": "@vue/runtime-test",
"private": true, "private": true,
"main": "index.js", "main": "index.js",
@ -25,7 +25,7 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-test#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-test#readme",
"dependencies": { "dependencies": {
"@vue/shared": "3.2.33", "@vue/shared": "3.2.37",
"@vue/runtime-core": "3.2.33" "@vue/runtime-core": "3.2.37"
} }
} }

View File

@ -676,7 +676,7 @@ function testRender(type: string, render: typeof renderToString) {
render: () => h('p', 'hello') render: () => h('p', 'hello')
} }
expect(await render(h(KeepAlive, () => h(MyComp)))).toBe( expect(await render(h(KeepAlive, () => h(MyComp)))).toBe(
`<!--[--><p>hello</p><!--]-->` `<p>hello</p>`
) )
}) })

View File

@ -11,6 +11,7 @@ import {
vModelText, vModelText,
vModelRadio, vModelRadio,
vModelCheckbox, vModelCheckbox,
vModelDynamic,
resolveDirective resolveDirective
} from 'vue' } from 'vue'
import { ssrGetDirectiveProps, ssrRenderAttrs } from '../src' import { ssrGetDirectiveProps, ssrRenderAttrs } from '../src'
@ -376,6 +377,100 @@ describe('ssr: directives', () => {
}) })
}) })
describe('vnode v-model dynamic', () => {
test('text', async () => {
expect(
await renderToString(
createApp({
render() {
return withDirectives(h('input'), [[vModelDynamic, 'hello']])
}
})
)
).toBe(`<input value="hello">`)
})
test('radio', async () => {
expect(
await renderToString(
createApp({
render() {
return withDirectives(
h('input', { type: 'radio', value: 'hello' }),
[[vModelDynamic, 'hello']]
)
}
})
)
).toBe(`<input type="radio" value="hello" checked>`)
expect(
await renderToString(
createApp({
render() {
return withDirectives(
h('input', { type: 'radio', value: 'hello' }),
[[vModelDynamic, 'foo']]
)
}
})
)
).toBe(`<input type="radio" value="hello">`)
})
test('checkbox', async () => {
expect(
await renderToString(
createApp({
render() {
return withDirectives(h('input', { type: 'checkbox' }), [
[vModelDynamic, true]
])
}
})
)
).toBe(`<input type="checkbox" checked>`)
expect(
await renderToString(
createApp({
render() {
return withDirectives(h('input', { type: 'checkbox' }), [
[vModelDynamic, false]
])
}
})
)
).toBe(`<input type="checkbox">`)
expect(
await renderToString(
createApp({
render() {
return withDirectives(
h('input', { type: 'checkbox', value: 'foo' }),
[[vModelDynamic, ['foo']]]
)
}
})
)
).toBe(`<input type="checkbox" value="foo" checked>`)
expect(
await renderToString(
createApp({
render() {
return withDirectives(
h('input', { type: 'checkbox', value: 'foo' }),
[[vModelDynamic, []]]
)
}
})
)
).toBe(`<input type="checkbox" value="foo">`)
})
})
test('custom directive w/ getSSRProps (vdom)', async () => { test('custom directive w/ getSSRProps (vdom)', async () => {
expect( expect(
await renderToString( await renderToString(

View File

@ -0,0 +1,144 @@
/**
* @jest-environment node
*/
import { createApp } from 'vue'
import { renderToString } from '../src/renderToString'
const components = {
one: {
template: `<div><slot/></div>`
}
}
describe('ssr: slot', () => {
test('text slot', async () => {
expect(
await renderToString(
createApp({
components,
template: `<one>hello</one>`
})
)
).toBe(`<div><!--[-->hello<!--]--></div>`)
})
test('element slot', async () => {
expect(
await renderToString(
createApp({
components,
template: `<one><div>hi</div></one>`
})
)
).toBe(`<div><!--[--><div>hi</div><!--]--></div>`)
})
test('empty slot', async () => {
expect(
await renderToString(
createApp({
components: {
one: {
template: `<div><slot/></div>`
}
},
template: `<one><template v-if="false"/></one>`
})
)
).toBe(`<div><!--[--><!--]--></div>`)
})
test('empty slot (manual comments)', async () => {
expect(
await renderToString(
createApp({
components: {
one: {
template: `<div><slot/></div>`
}
},
template: `<one><!--hello--></one>`
})
)
).toBe(`<div><!--[--><!--]--></div>`)
})
test('empty slot (multi-line comments)', async () => {
expect(
await renderToString(
createApp({
components: {
one: {
template: `<div><slot/></div>`
}
},
template: `<one><!--he\nllo--></one>`
})
)
).toBe(`<div><!--[--><!--]--></div>`)
})
test('multiple elements', async () => {
expect(
await renderToString(
createApp({
components,
template: `<one><div>one</div><div>two</div></one>`
})
)
).toBe(`<div><!--[--><div>one</div><div>two</div><!--]--></div>`)
})
test('fragment slot (template v-if)', async () => {
expect(
await renderToString(
createApp({
components,
template: `<one><template v-if="true">hello</template></one>`
})
)
).toBe(`<div><!--[--><!--[-->hello<!--]--><!--]--></div>`)
})
test('fragment slot (template v-if + multiple elements)', async () => {
expect(
await renderToString(
createApp({
components,
template: `<one><template v-if="true"><div>one</div><div>two</div></template></one>`
})
)
).toBe(
`<div><!--[--><!--[--><div>one</div><div>two</div><!--]--><!--]--></div>`
)
})
test('transition slot', async () => {
expect(
await renderToString(
createApp({
components: {
one: {
template: `<transition><slot/></transition>`
}
},
template: `<one><div v-if="false">foo</div></one>`
})
)
).toBe(`<!---->`)
expect(
await renderToString(
createApp({
components: {
one: {
template: `<transition><slot/></transition>`
}
},
template: `<one><div v-if="true">foo</div></one>`
})
)
).toBe(`<div>foo</div>`)
})
})

View File

@ -4,6 +4,7 @@
import { createApp, h, Teleport } from 'vue' import { createApp, h, Teleport } from 'vue'
import { renderToString } from '../src/renderToString' import { renderToString } from '../src/renderToString'
import { renderToSimpleStream } from '../src/renderToStream'
import { SSRContext } from '../src/render' import { SSRContext } from '../src/render'
import { ssrRenderTeleport } from '../src/helpers/ssrRenderTeleport' import { ssrRenderTeleport } from '../src/helpers/ssrRenderTeleport'
@ -30,7 +31,9 @@ describe('ssrRenderTeleport', () => {
ctx ctx
) )
expect(html).toBe('<!--teleport start--><!--teleport end-->') expect(html).toBe('<!--teleport start--><!--teleport end-->')
expect(ctx.teleports!['#target']).toBe(`<div>content</div><!---->`) expect(ctx.teleports!['#target']).toBe(
`<div>content</div><!--teleport anchor-->`
)
}) })
test('teleport rendering (compiled + disabled)', async () => { test('teleport rendering (compiled + disabled)', async () => {
@ -57,7 +60,7 @@ describe('ssrRenderTeleport', () => {
expect(html).toBe( expect(html).toBe(
'<!--teleport start--><div>content</div><!--teleport end-->' '<!--teleport start--><div>content</div><!--teleport end-->'
) )
expect(ctx.teleports!['#target']).toBe(`<!---->`) expect(ctx.teleports!['#target']).toBe(`<!--teleport anchor-->`)
}) })
test('teleport rendering (vnode)', async () => { test('teleport rendering (vnode)', async () => {
@ -73,7 +76,9 @@ describe('ssrRenderTeleport', () => {
ctx ctx
) )
expect(html).toBe('<!--teleport start--><!--teleport end-->') expect(html).toBe('<!--teleport start--><!--teleport end-->')
expect(ctx.teleports!['#target']).toBe('<span>hello</span><!---->') expect(ctx.teleports!['#target']).toBe(
'<span>hello</span><!--teleport anchor-->'
)
}) })
test('teleport rendering (vnode + disabled)', async () => { test('teleport rendering (vnode + disabled)', async () => {
@ -92,7 +97,7 @@ describe('ssrRenderTeleport', () => {
expect(html).toBe( expect(html).toBe(
'<!--teleport start--><span>hello</span><!--teleport end-->' '<!--teleport start--><span>hello</span><!--teleport end-->'
) )
expect(ctx.teleports!['#target']).toBe(`<!---->`) expect(ctx.teleports!['#target']).toBe(`<!--teleport anchor-->`)
}) })
test('multiple teleports with same target', async () => { test('multiple teleports with same target', async () => {
@ -114,7 +119,7 @@ describe('ssrRenderTeleport', () => {
'<div><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--></div>' '<div><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--></div>'
) )
expect(ctx.teleports!['#target']).toBe( expect(ctx.teleports!['#target']).toBe(
'<span>hello</span><!---->world<!---->' '<span>hello</span><!--teleport anchor-->world<!--teleport anchor-->'
) )
}) })
@ -132,6 +137,43 @@ describe('ssrRenderTeleport', () => {
ctx ctx
) )
expect(html).toBe('<!--teleport start--><!--teleport end-->') expect(html).toBe('<!--teleport start--><!--teleport end-->')
expect(ctx.teleports!['#target']).toBe(`<div>content</div><!---->`) expect(ctx.teleports!['#target']).toBe(
`<div>content</div><!--teleport anchor-->`
)
})
test('teleport inside async component (stream)', async () => {
const ctx: SSRContext = {}
const asyncComponent = {
template: '<teleport to="#target"><div>content</div></teleport>',
async setup() {}
}
let html = ''
let resolve: any
const p = new Promise(r => (resolve = r))
renderToSimpleStream(
h({
template: '<async-component />',
components: { asyncComponent }
}),
ctx,
{
push(chunk) {
if (chunk === null) {
resolve()
} else {
html += chunk
}
},
destroy(err) {
throw err
}
}
)
await p
expect(html).toBe('<!--teleport start--><!--teleport end-->')
expect(ctx.teleports!['#target']).toBe(
`<div>content</div><!--teleport anchor-->`
)
}) })
}) })

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/server-renderer", "name": "@vue/server-renderer",
"version": "3.2.33", "version": "3.2.37",
"description": "@vue/server-renderer", "description": "@vue/server-renderer",
"main": "index.js", "main": "index.js",
"module": "dist/server-renderer.esm-bundler.js", "module": "dist/server-renderer.esm-bundler.js",
@ -13,6 +13,7 @@
"name": "VueServerRenderer", "name": "VueServerRenderer",
"formats": [ "formats": [
"esm-bundler", "esm-bundler",
"esm-browser",
"cjs" "cjs"
] ]
}, },
@ -31,10 +32,10 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/main/packages/server-renderer#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/server-renderer#readme",
"peerDependencies": { "peerDependencies": {
"vue": "3.2.33" "vue": "3.2.37"
}, },
"dependencies": { "dependencies": {
"@vue/shared": "3.2.33", "@vue/shared": "3.2.37",
"@vue/compiler-ssr": "3.2.33" "@vue/compiler-ssr": "3.2.37"
} }
} }

View File

@ -40,7 +40,8 @@ export function ssrRenderSlotInner(
fallbackRenderFn: (() => void) | null, fallbackRenderFn: (() => void) | null,
push: PushFn, push: PushFn,
parentComponent: ComponentInternalInstance, parentComponent: ComponentInternalInstance,
slotScopeId?: string slotScopeId?: string,
transition?: boolean
) { ) {
const slotFn = slots[slotName] const slotFn = slots[slotName]
if (slotFn) { if (slotFn) {
@ -61,12 +62,16 @@ export function ssrRenderSlotInner(
// ssr slot. // ssr slot.
// check if the slot renders all comments, in which case use the fallback // check if the slot renders all comments, in which case use the fallback
let isEmptySlot = true let isEmptySlot = true
if (transition) {
isEmptySlot = false
} else {
for (let i = 0; i < slotBuffer.length; i++) { for (let i = 0; i < slotBuffer.length; i++) {
if (!isComment(slotBuffer[i])) { if (!isComment(slotBuffer[i])) {
isEmptySlot = false isEmptySlot = false
break break
} }
} }
}
if (isEmptySlot) { if (isEmptySlot) {
if (fallbackRenderFn) { if (fallbackRenderFn) {
fallbackRenderFn() fallbackRenderFn()
@ -82,7 +87,11 @@ export function ssrRenderSlotInner(
} }
} }
const commentRE = /^<!--.*-->$/ const commentTestRE = /^<!--.*-->$/s
const commentRE = /<!--[^]*?-->/gm
function isComment(item: SSRBufferItem) { function isComment(item: SSRBufferItem) {
return typeof item === 'string' && commentRE.test(item) if (typeof item !== 'string' || !commentTestRE.test(item)) return false
// if item is '<!---->' or '<!--[-->' or '<!--]-->', return true directly
if (item.length <= 8) return true
return !item.replace(commentRE, '').trim()
} }

View File

@ -10,28 +10,28 @@ export function ssrRenderTeleport(
) { ) {
parentPush('<!--teleport start-->') parentPush('<!--teleport start-->')
let teleportContent: SSRBufferItem
if (disabled) {
contentRenderFn(parentPush)
teleportContent = `<!---->`
} else {
const { getBuffer, push } = createBuffer()
contentRenderFn(push)
push(`<!---->`) // teleport end anchor
teleportContent = getBuffer()
}
const context = parentComponent.appContext.provides[ const context = parentComponent.appContext.provides[
ssrContextKey as any ssrContextKey as any
] as SSRContext ] as SSRContext
const teleportBuffers = const teleportBuffers =
context.__teleportBuffers || (context.__teleportBuffers = {}) context.__teleportBuffers || (context.__teleportBuffers = {})
if (teleportBuffers[target]) { const targetBuffer = teleportBuffers[target] || (teleportBuffers[target] = [])
teleportBuffers[target].push(teleportContent) // record current index of the target buffer to handle nested teleports
// since the parent needs to be rendered before the child
const bufferIndex = targetBuffer.length
let teleportContent: SSRBufferItem
if (disabled) {
contentRenderFn(parentPush)
teleportContent = `<!--teleport anchor-->`
} else { } else {
teleportBuffers[target] = [teleportContent] const { getBuffer, push } = createBuffer()
contentRenderFn(push)
push(`<!--teleport anchor-->`)
teleportContent = getBuffer()
} }
targetBuffer.splice(bufferIndex, 0, teleportContent)
parentPush('<!--teleport end-->') parentPush('<!--teleport end-->')
} }

View File

@ -4,6 +4,7 @@ import {
ComponentInternalInstance, ComponentInternalInstance,
DirectiveBinding, DirectiveBinding,
Fragment, Fragment,
FunctionalComponent,
mergeProps, mergeProps,
ssrUtils, ssrUtils,
Static, Static,
@ -112,12 +113,17 @@ function renderComponentSubTree(
const comp = instance.type as Component const comp = instance.type as Component
const { getBuffer, push } = createBuffer() const { getBuffer, push } = createBuffer()
if (isFunction(comp)) { if (isFunction(comp)) {
renderVNode( let root = renderComponentRoot(instance)
push, // #5817 scope ID attrs not falling through if functional component doesn't
(instance.subTree = renderComponentRoot(instance)), // have props
instance, if (!(comp as FunctionalComponent).props) {
slotScopeId for (const key in instance.attrs) {
) if (key.startsWith(`data-v-`)) {
;(root.props || (root.props = {}))[key] = ``
}
}
}
renderVNode(push, (instance.subTree = root), instance, slotScopeId)
} else { } else {
if ( if (
(!instance.render || instance.render === NOOP) && (!instance.render || instance.render === NOOP) &&

View File

@ -9,6 +9,7 @@ import {
import { isString, isPromise } from '@vue/shared' import { isString, isPromise } from '@vue/shared'
import { renderComponentVNode, SSRBuffer, SSRContext } from './render' import { renderComponentVNode, SSRBuffer, SSRContext } from './render'
import { Readable, Writable } from 'stream' import { Readable, Writable } from 'stream'
import { resolveTeleports } from './renderToString'
const { isVNode } = ssrUtils const { isVNode } = ssrUtils
@ -74,6 +75,7 @@ export function renderToSimpleStream<T extends SimpleReadable>(
Promise.resolve(renderComponentVNode(vnode)) Promise.resolve(renderComponentVNode(vnode))
.then(buffer => unrollBuffer(buffer, stream)) .then(buffer => unrollBuffer(buffer, stream))
.then(() => resolveTeleports(context))
.then(() => stream.push(null)) .then(() => stream.push(null))
.catch(error => { .catch(error => {
stream.destroy(error) stream.destroy(error)

View File

@ -70,7 +70,7 @@ export async function renderToString(
return result return result
} }
async function resolveTeleports(context: SSRContext) { export async function resolveTeleports(context: SSRContext) {
if (context.__teleportBuffers) { if (context.__teleportBuffers) {
context.teleports = context.teleports || {} context.teleports = context.teleports || {}
for (const key in context.__teleportBuffers) { for (const key in context.__teleportBuffers) {

View File

@ -9,10 +9,10 @@
<script> <script>
// process shim for old versions of @vue/compiler-sfc dependency // process shim for old versions of @vue/compiler-sfc dependency
window.process = { env: {} } window.process = { env: {} }
const saved = localStorage.getItem('vue-sfc-playground-prefer-dark') const savedPreferDark = localStorage.getItem('vue-sfc-playground-prefer-dark')
if ( if (
saved !== 'false' || savedPreferDark === 'true' ||
window.matchMedia('(prefers-color-scheme: dark)').matches (!savedPreferDark && window.matchMedia('(prefers-color-scheme: dark)').matches)
) { ) {
document.documentElement.classList.add('dark') document.documentElement.classList.add('dark')
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/sfc-playground", "name": "@vue/sfc-playground",
"version": "3.2.33", "version": "3.2.37",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@ -12,8 +12,8 @@
"vite": "^2.9.8" "vite": "^2.9.8"
}, },
"dependencies": { "dependencies": {
"vue": "3.2.33", "vue": "3.2.37",
"@vue/repl": "^1.0.0", "@vue/repl": "^1.3.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jszip": "^3.6.0" "jszip": "^3.6.0"
} }

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import Header from './Header.vue' import Header from './Header.vue'
import { Repl, ReplStore } from '@vue/repl' import { Repl, ReplStore } from '@vue/repl'
import { watchEffect } from 'vue' import { ref, watchEffect } from 'vue'
const setVH = () => { const setVH = () => {
document.documentElement.style.setProperty('--vh', window.innerHeight + `px`) document.documentElement.style.setProperty('--vh', window.innerHeight + `px`)
@ -9,29 +9,70 @@ const setVH = () => {
window.addEventListener('resize', setVH) window.addEventListener('resize', setVH)
setVH() setVH()
const useDevMode = ref(false)
const useSSRMode = ref(false)
let hash = location.hash.slice(1)
if (hash.startsWith('__DEV__')) {
hash = hash.slice(7)
useDevMode.value = true
}
if (hash.startsWith('__SSR__')) {
hash = hash.slice(7)
useSSRMode.value = true
}
const store = new ReplStore({ const store = new ReplStore({
serializedState: location.hash.slice(1), serializedState: hash,
defaultVueRuntimeURL: import.meta.env.PROD defaultVueRuntimeURL: import.meta.env.PROD
? `${location.origin}/vue.runtime.esm-browser.js` ? `${location.origin}/vue.runtime.esm-browser.js`
: `${location.origin}/src/vue-dev-proxy` : `${location.origin}/src/vue-dev-proxy`,
defaultVueServerRendererURL: import.meta.env.PROD
? `${location.origin}/server-renderer.esm-browser.js`
: `${location.origin}/src/vue-server-renderer-dev-proxy`
}) })
// enable experimental features // enable experimental features
const sfcOptions = { const sfcOptions = {
script: { script: {
inlineTemplate: !useDevMode.value,
reactivityTransform: true reactivityTransform: true
} }
} }
// persist state // persist state
watchEffect(() => history.replaceState({}, '', store.serialize())) watchEffect(() => {
const newHash = store
.serialize()
.replace(/^#/, useSSRMode.value ? `#__SSR__` : `#`)
.replace(/^#/, useDevMode.value ? `#__DEV__` : `#`)
history.replaceState({}, '', newHash)
})
function toggleDevMode() {
const dev = (useDevMode.value = !useDevMode.value)
sfcOptions.script.inlineTemplate = !dev
store.setFiles(store.getFiles())
}
function toggleSSR() {
useSSRMode.value = !useSSRMode.value
store.setFiles(store.getFiles())
}
</script> </script>
<template> <template>
<Header :store="store" /> <Header
:store="store"
:dev="useDevMode"
:ssr="useSSRMode"
@toggle-dev="toggleDevMode"
@toggle-ssr="toggleSSR"
/>
<Repl <Repl
@keydown.ctrl.s.prevent @keydown.ctrl.s.prevent
@keydown.meta.s.prevent @keydown.meta.s.prevent
:ssr="useSSRMode"
:store="store" :store="store"
:showCompileOutput="true" :showCompileOutput="true"
:autoResize="true" :autoResize="true"
@ -41,6 +82,10 @@ watchEffect(() => history.replaceState({}, '', store.serialize()))
</template> </template>
<style> <style>
.dark {
color-scheme: dark;
}
body { body {
font-size: 13px; font-size: 13px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,

View File

@ -8,7 +8,8 @@ import Download from './icons/Download.vue'
import GitHub from './icons/GitHub.vue' import GitHub from './icons/GitHub.vue'
// @ts-ignore // @ts-ignore
const { store } = defineProps(['store']) const props = defineProps(['store', 'dev', 'ssr'])
const { store } = props
const currentCommit = __COMMIT__ const currentCommit = __COMMIT__
const activeVersion = ref(`@${currentCommit}`) const activeVersion = ref(`@${currentCommit}`)
@ -53,6 +54,11 @@ onMounted(async () => {
window.addEventListener('click', () => { window.addEventListener('click', () => {
expanded.value = false expanded.value = false
}) })
window.addEventListener('blur', () => {
if (document.activeElement?.tagName === 'IFRAME') {
expanded.value = false
}
});
}) })
async function fetchVersions(): Promise<string[]> { async function fetchVersions(): Promise<string[]> {
@ -93,7 +99,8 @@ async function fetchVersions(): Promise<string[]> {
<div class="links"> <div class="links">
<div class="version" @click.stop> <div class="version" @click.stop>
<span class="active-version" @click="toggle"> <span class="active-version" @click="toggle">
Version: {{ activeVersion }} Version
<span class="number">{{ activeVersion }}</span>
</span> </span>
<ul class="versions" :class="{ expanded }"> <ul class="versions" :class="{ expanded }">
<li v-if="!publishedVersions"><a>loading versions...</a></li> <li v-if="!publishedVersions"><a>loading versions...</a></li>
@ -112,6 +119,22 @@ async function fetchVersions(): Promise<string[]> {
</li> </li>
</ul> </ul>
</div> </div>
<button
title="Toggle development production mode"
class="toggle-dev"
:class="{ dev }"
@click="$emit('toggle-dev')"
>
<span>{{ dev ? 'DEV' : 'PROD' }}</span>
</button>
<button
title="Toggle server rendering mode"
class="toggle-ssr"
:class="{ enabled: ssr }"
@click="$emit('toggle-ssr')"
>
<span>{{ ssr ? 'SSR ON' : 'SSR OFF' }}</span>
</button>
<button title="Toggle dark mode" class="toggle-dark" @click="toggleDark"> <button title="Toggle dark mode" class="toggle-dark" @click="toggleDark">
<Sun class="light" /> <Sun class="light" />
<Moon class="dark" /> <Moon class="dark" />
@ -126,10 +149,7 @@ async function fetchVersions(): Promise<string[]> {
> >
<Download /> <Download />
</button> </button>
<button <button title="View on GitHub" class="github">
title="View on GitHub"
class="github"
>
<a <a
href="https://github.com/vuejs/core/tree/main/packages/sfc-playground" href="https://github.com/vuejs/core/tree/main/packages/sfc-playground"
target="_blank" target="_blank"
@ -146,6 +166,11 @@ nav {
--bg: #fff; --bg: #fff;
--bg-light: #fff; --bg-light: #fff;
--border: #ddd; --border: #ddd;
--btn: #666;
--highlight: #333;
--green: #3ca877;
--purple: #904cbc;
--btn-bg: #eee;
color: var(--base); color: var(--base);
height: var(--nav-height); height: var(--nav-height);
@ -164,25 +189,22 @@ nav {
--bg: #1a1a1a; --bg: #1a1a1a;
--bg-light: #242424; --bg-light: #242424;
--border: #383838; --border: #383838;
--highlight: #fff;
--btn-bg: #333;
box-shadow: none; box-shadow: none;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
h1 { h1 {
margin: 0;
line-height: var(--nav-height);
font-weight: 500; font-weight: 500;
display: inline-block; display: inline-flex;
vertical-align: middle; place-items: center;
} }
h1 img { h1 img {
height: 24px; height: 24px;
vertical-align: middle;
margin-right: 10px; margin-right: 10px;
position: relative;
top: -2px;
} }
@media (max-width: 560px) { @media (max-width: 560px) {
@ -202,7 +224,6 @@ h1 img {
} }
.version { .version {
display: inline-block;
margin-right: 12px; margin-right: 12px;
position: relative; position: relative;
} }
@ -210,28 +231,53 @@ h1 img {
.active-version { .active-version {
cursor: pointer; cursor: pointer;
position: relative; position: relative;
display: inline-block; display: inline-flex;
vertical-align: middle; place-items: center;
line-height: var(--nav-height);
padding-right: 15px;
} }
.active-version:after { .active-version .number {
color: var(--green);
margin-left: 4px;
}
.active-version::after {
content: ''; content: '';
width: 0; width: 0;
height: 0; height: 0;
border-left: 4px solid transparent; border-left: 4px solid transparent;
border-right: 4px solid transparent; border-right: 4px solid transparent;
border-top: 6px solid #aaa; border-top: 6px solid #aaa;
position: absolute; margin-left: 8px;
right: 0; }
top: 22px;
.toggle-dev span,
.toggle-ssr span {
font-size: 12px;
border-radius: 4px;
padding: 4px 6px;
}
.toggle-dev span {
background: var(--purple);
color: #fff;
}
.toggle-dev.dev span {
background: var(--green);
}
.toggle-ssr span {
background-color: var(--btn-bg);
}
.toggle-ssr.enabled span {
color: #fff;
background-color: var(--green);
} }
.toggle-dark svg { .toggle-dark svg {
width: 18px; width: 18px;
height: 18px; height: 18px;
fill: #666;
} }
.toggle-dark .dark, .toggle-dark .dark,
@ -243,12 +289,22 @@ h1 img {
display: inline-block; display: inline-block;
} }
.version:hover .active-version:after { .links button,
border-top-color: var(--base); .links button a {
color: var(--btn);
} }
.dark .version:hover .active-version:after { .links button:hover,
border-top-color: #fff; .links button:hover a {
color: var(--highlight);
}
.version:hover .active-version::after {
border-top-color: var(--btn);
}
.dark .version:hover .active-version::after {
border-top-color: var(--highlight);
} }
.versions { .versions {
@ -276,16 +332,19 @@ h1 img {
} }
.versions a:hover { .versions a:hover {
color: #3ca877; color: var(--green);
} }
.versions.expanded { .versions.expanded {
display: block; display: block;
} }
.share, .links > * {
.download, display: flex;
.github { align-items: center;
margin: 0 2px; }
.links > * + * {
margin-left: 4px;
} }
</style> </style>

View File

@ -1,6 +1,6 @@
<template> <template>
<svg width="1.7em" height="1.7em" viewBox="0 0 24 24"> <svg width="1.7em" height="1.7em" viewBox="0 0 24 24" fill="currentColor">
<g fill="#666"> <g>
<rect x="4" y="18" width="16" height="2" rx="1" ry="1" /> <rect x="4" y="18" width="16" height="2" rx="1" ry="1" />
<rect <rect
x="3" x="3"

View File

@ -1,7 +1,6 @@
<template> <template>
<svg width="1.7em" height="1.7em" viewBox="0 0 24 24"> <svg width="1.7em" height="1.7em" viewBox="0 0 24 24" fill="currentColor">
<path <path
fill="#666" d="M10.9,2.1c-4.6,0.5-8.3,4.2-8.8,8.7c-0.5,4.7,2.2,8.9,6.3,10.5C8.7,21.4,9,21.2,9,20.8v-1.6c0,0-0.4,0.1-0.9,0.1 c-1.4,0-2-1.2-2.1-1.9c-0.1-0.4-0.3-0.7-0.6-1C5.1,16.3,5,16.3,5,16.2C5,16,5.3,16,5.4,16c0.6,0,1.1,0.7,1.3,1c0.5,0.8,1.1,1,1.4,1 c0.4,0,0.7-0.1,0.9-0.2c0.1-0.7,0.4-1.4,1-1.8c-2.3-0.5-4-1.8-4-4c0-1.1,0.5-2.2,1.2-3C7.1,8.8,7,8.3,7,7.6C7,7.2,7,6.6,7.3,6 c0,0,1.4,0,2.8,1.3C10.6,7.1,11.3,7,12,7s1.4,0.1,2,0.3C15.3,6,16.8,6,16.8,6C17,6.6,17,7.2,17,7.6c0,0.8-0.1,1.2-0.2,1.4 c0.7,0.8,1.2,1.8,1.2,3c0,2.2-1.7,3.5-4,4c0.6,0.5,1,1.4,1,2.3v2.6c0,0.3,0.3,0.6,0.7,0.5c3.7-1.5,6.3-5.1,6.3-9.3 C22,6.1,16.9,1.4,10.9,2.1z"/>
d="M10.9,2.1c-4.6,0.5-8.3,4.2-8.8,8.7c-0.5,4.7,2.2,8.9,6.3,10.5C8.7,21.4,9,21.2,9,20.8v-1.6c0,0-0.4,0.1-0.9,0.1 c-1.4,0-2-1.2-2.1-1.9c-0.1-0.4-0.3-0.7-0.6-1C5.1,16.3,5,16.3,5,16.2C5,16,5.3,16,5.4,16c0.6,0,1.1,0.7,1.3,1c0.5,0.8,1.1,1,1.4,1 c0.4,0,0.7-0.1,0.9-0.2c0.1-0.7,0.4-1.4,1-1.8c-2.3-0.5-4-1.8-4-4c0-1.1,0.5-2.2,1.2-3C7.1,8.8,7,8.3,7,7.6C7,7.2,7,6.6,7.3,6 c0,0,1.4,0,2.8,1.3C10.6,7.1,11.3,7,12,7s1.4,0.1,2,0.3C15.3,6,16.8,6,16.8,6C17,6.6,17,7.2,17,7.6c0,0.8-0.1,1.2-0.2,1.4 c0.7,0.8,1.2,1.8,1.2,3c0,2.2-1.7,3.5-4,4c0.6,0.5,1,1.4,1,2.3v2.6c0,0.3,0.3,0.6,0.7,0.5c3.7-1.5,6.3-5.1,6.3-9.3 C22,6.1,16.9,1.4,10.9,2.1z"></path>
</svg> </svg>
</template> </template>

View File

@ -1,5 +1,5 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12.1,22c-0.3,0-0.6,0-0.9,0c-5.5-0.5-9.5-5.4-9-10.9c0.4-4.8,4.2-8.6,9-9c0.4,0,0.8,0.2,1,0.5c0.2,0.3,0.2,0.8-0.1,1.1c-2,2.7-1.4,6.4,1.3,8.4c2.1,1.6,5,1.6,7.1,0c0.3-0.2,0.7-0.3,1.1-0.1c0.3,0.2,0.5,0.6,0.5,1c-0.2,2.7-1.5,5.1-3.6,6.8C16.6,21.2,14.4,22,12.1,22zM9.3,4.4c-2.9,1-5,3.6-5.2,6.8c-0.4,4.4,2.8,8.3,7.2,8.7c2.1,0.2,4.2-0.4,5.8-1.8c1.1-0.9,1.9-2.1,2.4-3.4c-2.5,0.9-5.3,0.5-7.5-1.1C9.2,11.4,8.1,7.7,9.3,4.4z" /> <path fill="currentColor" d="M12.1,22c-0.3,0-0.6,0-0.9,0c-5.5-0.5-9.5-5.4-9-10.9c0.4-4.8,4.2-8.6,9-9c0.4,0,0.8,0.2,1,0.5c0.2,0.3,0.2,0.8-0.1,1.1c-2,2.7-1.4,6.4,1.3,8.4c2.1,1.6,5,1.6,7.1,0c0.3-0.2,0.7-0.3,1.1-0.1c0.3,0.2,0.5,0.6,0.5,1c-0.2,2.7-1.5,5.1-3.6,6.8C16.6,21.2,14.4,22,12.1,22zM9.3,4.4c-2.9,1-5,3.6-5.2,6.8c-0.4,4.4,2.8,8.3,7.2,8.7c2.1,0.2,4.2-0.4,5.8-1.8c1.1-0.9,1.9-2.1,2.4-3.4c-2.5,0.9-5.3,0.5-7.5-1.1C9.2,11.4,8.1,7.7,9.3,4.4z" />
</svg> </svg>
</template> </template>

View File

@ -2,7 +2,7 @@
<svg width="1.4em" height="1.4em" viewBox="0 0 24 24"> <svg width="1.4em" height="1.4em" viewBox="0 0 24 24">
<g <g
fill="none" fill="none"
stroke="#666" stroke="currentColor"
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"

View File

@ -1,13 +1,13 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12,18c-3.3,0-6-2.7-6-6s2.7-6,6-6s6,2.7,6,6S15.3,18,12,18zM12,8c-2.2,0-4,1.8-4,4c0,2.2,1.8,4,4,4c2.2,0,4-1.8,4-4C16,9.8,14.2,8,12,8z" /> <path fill="currentColor" d="M12,18c-3.3,0-6-2.7-6-6s2.7-6,6-6s6,2.7,6,6S15.3,18,12,18zM12,8c-2.2,0-4,1.8-4,4c0,2.2,1.8,4,4,4c2.2,0,4-1.8,4-4C16,9.8,14.2,8,12,8z" />
<path d="M12,4c-0.6,0-1-0.4-1-1V1c0-0.6,0.4-1,1-1s1,0.4,1,1v2C13,3.6,12.6,4,12,4z" /> <path fill="currentColor" d="M12,4c-0.6,0-1-0.4-1-1V1c0-0.6,0.4-1,1-1s1,0.4,1,1v2C13,3.6,12.6,4,12,4z" />
<path d="M12,24c-0.6,0-1-0.4-1-1v-2c0-0.6,0.4-1,1-1s1,0.4,1,1v2C13,23.6,12.6,24,12,24z" /> <path fill="currentColor" d="M12,24c-0.6,0-1-0.4-1-1v-2c0-0.6,0.4-1,1-1s1,0.4,1,1v2C13,23.6,12.6,24,12,24z" />
<path d="M5.6,6.6c-0.3,0-0.5-0.1-0.7-0.3L3.5,4.9c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l1.4,1.4c0.4,0.4,0.4,1,0,1.4C6.2,6.5,5.9,6.6,5.6,6.6z" /> <path fill="currentColor" d="M5.6,6.6c-0.3,0-0.5-0.1-0.7-0.3L3.5,4.9c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l1.4,1.4c0.4,0.4,0.4,1,0,1.4C6.2,6.5,5.9,6.6,5.6,6.6z" />
<path d="M19.8,20.8c-0.3,0-0.5-0.1-0.7-0.3l-1.4-1.4c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l1.4,1.4c0.4,0.4,0.4,1,0,1.4C20.3,20.7,20,20.8,19.8,20.8z" /> <path fill="currentColor" d="M19.8,20.8c-0.3,0-0.5-0.1-0.7-0.3l-1.4-1.4c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l1.4,1.4c0.4,0.4,0.4,1,0,1.4C20.3,20.7,20,20.8,19.8,20.8z" />
<path d="M3,13H1c-0.6,0-1-0.4-1-1s0.4-1,1-1h2c0.6,0,1,0.4,1,1S3.6,13,3,13z" /> <path fill="currentColor" d="M3,13H1c-0.6,0-1-0.4-1-1s0.4-1,1-1h2c0.6,0,1,0.4,1,1S3.6,13,3,13z" />
<path d="M23,13h-2c-0.6,0-1-0.4-1-1s0.4-1,1-1h2c0.6,0,1,0.4,1,1S23.6,13,23,13z" /> <path fill="currentColor" d="M23,13h-2c-0.6,0-1-0.4-1-1s0.4-1,1-1h2c0.6,0,1,0.4,1,1S23.6,13,23,13z" />
<path d="M4.2,20.8c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l1.4-1.4c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-1.4,1.4C4.7,20.7,4.5,20.8,4.2,20.8z" /> <path fill="currentColor" d="M4.2,20.8c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l1.4-1.4c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-1.4,1.4C4.7,20.7,4.5,20.8,4.2,20.8z" />
<path d="M18.4,6.6c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l1.4-1.4c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-1.4,1.4C18.9,6.5,18.6,6.6,18.4,6.6z" /> <path fill="currentColor" d="M18.4,6.6c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l1.4-1.4c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-1.4,1.4C18.9,6.5,18.6,6.6,18.4,6.6z" />
</svg> </svg>
</template> </template>

View File

@ -0,0 +1,2 @@
// serve vue/server-renderer to the iframe sandbox during dev.
export * from 'vue/server-renderer'

View File

@ -21,21 +21,24 @@ function copyVuePlugin(): Plugin {
return { return {
name: 'copy-vue', name: 'copy-vue',
generateBundle() { generateBundle() {
const filePath = path.resolve( const copyFile = (file: string) => {
__dirname, const filePath = path.resolve(__dirname, file)
'../vue/dist/vue.runtime.esm-browser.js' const basename = path.basename(file)
)
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
throw new Error( throw new Error(
`vue.runtime.esm-browser.js not built. ` + `${basename} not built. ` +
`Run "nr build vue -f esm-browser" first.` `Run "nr build vue -f esm-browser" first.`
) )
} }
this.emitFile({ this.emitFile({
type: 'asset', type: 'asset',
fileName: 'vue.runtime.esm-browser.js', fileName: basename,
source: fs.readFileSync(filePath, 'utf-8') source: fs.readFileSync(filePath, 'utf-8')
}) })
} }
copyFile(`../vue/dist/vue.runtime.esm-browser.js`)
copyFile(`../server-renderer/dist/server-renderer.esm-browser.js`)
}
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/shared", "name": "@vue/shared",
"version": "3.2.33", "version": "3.2.37",
"description": "internal utils shared across @vue packages", "description": "internal utils shared across @vue packages",
"main": "index.js", "main": "index.js",
"module": "dist/shared.esm-bundler.js", "module": "dist/shared.esm-bundler.js",

View File

@ -52,7 +52,7 @@ export const isMap = (val: unknown): val is Map<any, any> =>
export const isSet = (val: unknown): val is Set<any> => export const isSet = (val: unknown): val is Set<any> =>
toTypeString(val) === '[object Set]' toTypeString(val) === '[object Set]'
export const isDate = (val: unknown): val is Date => val instanceof Date export const isDate = (val: unknown): val is Date => toTypeString(val) === '[object Date]'
export const isFunction = (val: unknown): val is Function => export const isFunction = (val: unknown): val is Function =>
typeof val === 'function' typeof val === 'function'
export const isString = (val: unknown): val is string => typeof val === 'string' export const isString = (val: unknown): val is string => typeof val === 'string'

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/size-check", "name": "@vue/size-check",
"version": "3.2.33", "version": "3.2.37",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "vite build" "build": "vite build"

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/template-explorer", "name": "@vue/template-explorer",
"version": "3.2.33", "version": "3.2.37",
"private": true, "private": true,
"buildOptions": { "buildOptions": {
"formats": [ "formats": [

View File

@ -1,6 +1,6 @@
import Vue from '@vue/compat' import Vue from '@vue/compat'
import { effect, isReactive } from '@vue/reactivity' import { effect, isReactive } from '@vue/reactivity'
import { nextTick } from '@vue/runtime-core' import { h, nextTick } from '@vue/runtime-core'
import { import {
DeprecationTypes, DeprecationTypes,
deprecationData, deprecationData,
@ -448,3 +448,57 @@ test('global asset registration should affect apps created via createApp', () =>
expect(vm.$el.textContent).toBe('foo') expect(vm.$el.textContent).toBe('foo')
delete singletonApp._context.components.foo delete singletonApp._context.components.foo
}) })
test('post-facto global asset registration should affect apps created via createApp', () => {
const app = createApp({
template: '<foo/>'
})
Vue.component('foo', { template: 'foo' })
const vm = app.mount(document.createElement('div')) as any
expect(vm.$el.textContent).toBe('foo')
delete singletonApp._context.components.foo
})
test('local asset registration should not affect other local apps', () => {
const app1 = createApp({})
const app2 = createApp({})
app1.component('foo', {})
app2.component('foo', {})
expect(
`Component "foo" has already been registered in target app`
).not.toHaveBeenWarned()
})
test('local app-level mixin registration should not affect other local apps', () => {
const app1 = createApp({ render: () => h('div') })
const app2 = createApp({})
const mixin = { created: jest.fn() }
app1.mixin(mixin)
app2.mixin(mixin)
expect(`Mixin has already been applied`).not.toHaveBeenWarned()
app1.mount(document.createElement('div'))
expect(mixin.created).toHaveBeenCalledTimes(1)
})
// #5699
test('local app config should not affect other local apps in v3 mode', () => {
Vue.configureCompat({ MODE: 3 })
const app1 = createApp({
render: () => h('div'),
provide() {
return {
test: 123
}
}
})
app1.config.globalProperties.test = () => {}
app1.mount(document.createElement('div'))
const app2 = createApp({})
expect(app2.config.globalProperties.test).toBe(undefined)
})

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compat", "name": "@vue/compat",
"version": "3.2.33", "version": "3.2.37",
"description": "Vue 3 compatibility build for Vue 2", "description": "Vue 3 compatibility build for Vue 2",
"main": "index.js", "main": "index.js",
"module": "dist/vue.runtime.esm-bundler.js", "module": "dist/vue.runtime.esm-bundler.js",
@ -38,6 +38,6 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/main/packages/vue-compat#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/vue-compat#readme",
"peerDependencies": { "peerDependencies": {
"vue": "3.2.33" "vue": "3.2.37"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "vue", "name": "vue",
"version": "3.2.33", "version": "3.2.37",
"description": "The progressive JavaScript framework for building modern web UI.", "description": "The progressive JavaScript framework for building modern web UI.",
"main": "index.js", "main": "index.js",
"module": "dist/vue.runtime.esm-bundler.js", "module": "dist/vue.runtime.esm-bundler.js",
@ -66,10 +66,10 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/main/packages/vue#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/vue#readme",
"dependencies": { "dependencies": {
"@vue/shared": "3.2.33", "@vue/shared": "3.2.37",
"@vue/compiler-dom": "3.2.33", "@vue/compiler-dom": "3.2.37",
"@vue/runtime-dom": "3.2.33", "@vue/runtime-dom": "3.2.37",
"@vue/compiler-sfc": "3.2.33", "@vue/compiler-sfc": "3.2.37",
"@vue/server-renderer": "3.2.33" "@vue/server-renderer": "3.2.37"
} }
} }

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