diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 9816f7f5257..95bc65f357b 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -31,4 +31,4 @@ jobs: - name: Run prettier run: pnpm run format - - uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef + - uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b84bd85cb7..2a0b96332b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,54 @@ +## [3.5.18](https://github.com/vuejs/core/compare/v3.5.17...v3.5.18) (2025-07-23) + + +### Bug Fixes + +* **compiler-core:** avoid cached text vnodes retaining detached DOM nodes ([#13662](https://github.com/vuejs/core/issues/13662)) ([00695a5](https://github.com/vuejs/core/commit/00695a5b41b2d032deaeada83831ff83aa6bfd4e)), closes [#13661](https://github.com/vuejs/core/issues/13661) +* **compiler-core:** avoid self updates of `v-pre` ([#12556](https://github.com/vuejs/core/issues/12556)) ([21b685a](https://github.com/vuejs/core/commit/21b685ad9d9d0e6060fc7d07b719bf35f2d9ae1f)) +* **compiler-core:** identifiers in function parameters should not be inferred as references ([#13548](https://github.com/vuejs/core/issues/13548)) ([9b02923](https://github.com/vuejs/core/commit/9b029239edf88558465b941e1e4c085f92b1ebff)) +* **compiler-core:** recognize empty string as non-identifier ([#12553](https://github.com/vuejs/core/issues/12553)) ([ce93339](https://github.com/vuejs/core/commit/ce933390ad1c72bed258f7ad959a78f0e8acdf57)) +* **compiler-core:** transform empty `v-bind` dynamic argument content correctly ([#12554](https://github.com/vuejs/core/issues/12554)) ([d3af67e](https://github.com/vuejs/core/commit/d3af67e878790892f9d34cfea15d13625aabe733)) +* **compiler-sfc:** transform empty srcset w/ includeAbsolute: true ([#13639](https://github.com/vuejs/core/issues/13639)) ([d8e40ef](https://github.com/vuejs/core/commit/d8e40ef7e1c20ee86b294e7cf78e2de60d12830e)), closes [vitejs/vite-plugin-vue#631](https://github.com/vitejs/vite-plugin-vue/issues/631) +* **css-vars:** nullish v-bind in style should not lead to unexpected inheritance ([#12461](https://github.com/vuejs/core/issues/12461)) ([c85f1b5](https://github.com/vuejs/core/commit/c85f1b5a132eb8ec25f71b250e25e65a5c20964f)), closes [#12434](https://github.com/vuejs/core/issues/12434) [#12439](https://github.com/vuejs/core/issues/12439) [#7474](https://github.com/vuejs/core/issues/7474) [#7475](https://github.com/vuejs/core/issues/7475) +* **custom-element:** ensure exposed methods are accessible from custom elements by making them enumerable ([#13634](https://github.com/vuejs/core/issues/13634)) ([90573b0](https://github.com/vuejs/core/commit/90573b06bf6fb6c14c6bbff6c4e34e0ab108953a)), closes [#13632](https://github.com/vuejs/core/issues/13632) +* **hydration:** prevent lazy hydration for updated components ([#13511](https://github.com/vuejs/core/issues/13511)) ([a9269c6](https://github.com/vuejs/core/commit/a9269c642bf944560bc29adb5dae471c11cd9ee8)), closes [#13510](https://github.com/vuejs/core/issues/13510) +* **runtime-core:** ensure correct anchor el for unresolved async components ([#13560](https://github.com/vuejs/core/issues/13560)) ([7f29943](https://github.com/vuejs/core/commit/7f2994393dcdb82cacbf62e02b5ba5565f32588b)), closes [#13559](https://github.com/vuejs/core/issues/13559) +* **slots:** refine internal key checking to support slot names starting with an underscore ([#13612](https://github.com/vuejs/core/issues/13612)) ([c5f7db1](https://github.com/vuejs/core/commit/c5f7db11542bb2246363aef78c88a8e6cef0ee93)), closes [#13611](https://github.com/vuejs/core/issues/13611) +* **ssr:** ensure empty slots render as a comment node in Transition ([#13396](https://github.com/vuejs/core/issues/13396)) ([8cfc10a](https://github.com/vuejs/core/commit/8cfc10a80b9cbf5d801ab149e49b8506d192e7e1)), closes [#13394](https://github.com/vuejs/core/issues/13394) + + + +## [3.5.17](https://github.com/vuejs/core/compare/v3.5.16...v3.5.17) (2025-06-18) + + +### Bug Fixes + +* **compat:** allow v-model built in modifiers on component ([#12654](https://github.com/vuejs/core/issues/12654)) ([cb14b86](https://github.com/vuejs/core/commit/cb14b860f150c4a83bcd52cd26096b7a5aa3a2bf)), closes [#12652](https://github.com/vuejs/core/issues/12652) +* **compile-sfc:** handle mapped types work with omit and pick ([#12648](https://github.com/vuejs/core/issues/12648)) ([4eb46e4](https://github.com/vuejs/core/commit/4eb46e443f1878199755cb73d481d318a9714392)), closes [#12647](https://github.com/vuejs/core/issues/12647) +* **compiler-core:** do not increase newlines in `InEntity` state ([#13362](https://github.com/vuejs/core/issues/13362)) ([f05a8d6](https://github.com/vuejs/core/commit/f05a8d613bd873b811cfdb9979ccac8382dba322)) +* **compiler-core:** ignore whitespace when matching adjacent v-if ([#12321](https://github.com/vuejs/core/issues/12321)) ([10ebcef](https://github.com/vuejs/core/commit/10ebcef8c870dbc042b0ea49b1424b2e8f692145)), closes [#9173](https://github.com/vuejs/core/issues/9173) +* **compiler-core:** prevent comments from blocking static node hoisting ([#13345](https://github.com/vuejs/core/issues/13345)) ([55dad62](https://github.com/vuejs/core/commit/55dad625acd9e9ddd5a933d5e323ecfdec1a612f)), closes [#13344](https://github.com/vuejs/core/issues/13344) +* **compiler-sfc:** improved type resolution for function type aliases ([#13452](https://github.com/vuejs/core/issues/13452)) ([f3479aa](https://github.com/vuejs/core/commit/f3479aac9625f4459e650d1c0a70e73863147903)), closes [#13444](https://github.com/vuejs/core/issues/13444) +* **custom-element:** ensure configureApp is applied to async component ([#12607](https://github.com/vuejs/core/issues/12607)) ([5ba1afb](https://github.com/vuejs/core/commit/5ba1afba09c3ea56c1c17484f5d8aeae210ce52a)), closes [#12448](https://github.com/vuejs/core/issues/12448) +* **custom-element:** prevent injecting child styles if shadowRoot is false ([#12769](https://github.com/vuejs/core/issues/12769)) ([73055d8](https://github.com/vuejs/core/commit/73055d8d9578d485e3fe846726b50666e1aa56f5)), closes [#12630](https://github.com/vuejs/core/issues/12630) +* **reactivity:** add `__v_skip` flag to `Dep` to prevent reactive conversion ([#12804](https://github.com/vuejs/core/issues/12804)) ([e8d8f5f](https://github.com/vuejs/core/commit/e8d8f5f604e821acc46b4200d5b06979c05af1c2)), closes [#12803](https://github.com/vuejs/core/issues/12803) +* **runtime-core:** unset old ref during patching when new ref is absent ([#12900](https://github.com/vuejs/core/issues/12900)) ([47ddf98](https://github.com/vuejs/core/commit/47ddf986021dff8de68b0da72787e53a6c19de4c)), closes [#12898](https://github.com/vuejs/core/issues/12898) +* **slots:** make cache indexes marker non-enumerable ([#13469](https://github.com/vuejs/core/issues/13469)) ([919c447](https://github.com/vuejs/core/commit/919c44744bba1f0c661c87d2059c3b429611aa7e)), closes [#13468](https://github.com/vuejs/core/issues/13468) +* **ssr:** handle initial selected state for select with v-model + v-for/v-if option ([#13487](https://github.com/vuejs/core/issues/13487)) ([1552095](https://github.com/vuejs/core/commit/15520954f9f1c7f834175938a50dba5d4be0e6c4)), closes [#13486](https://github.com/vuejs/core/issues/13486) +* **types:** typo of `vOnce` and `vSlot` ([#13343](https://github.com/vuejs/core/issues/13343)) ([762fae4](https://github.com/vuejs/core/commit/762fae4b57ad60602e5c84465a3bff562785b314)) + + + +## [3.5.16](https://github.com/vuejs/core/compare/v3.5.15...v3.5.16) (2025-05-29) + + +### Reverts + +* Revert "fix(compiler-sfc): add scoping tag to trailing universal selector" (#13406) ([19f23b1](https://github.com/vuejs/core/commit/19f23b180bb679e38db95d6a10a420abeedc8e1c)), closes [#13406](https://github.com/vuejs/core/issues/13406) +* Revert "fix(compiler-sfc): add error handling for defineModel() without variable" (#13390) ([42f879f](https://github.com/vuejs/core/commit/42f879fcab48e0e1011967a771b4ad9e8838d760)), closes [#13390](https://github.com/vuejs/core/issues/13390) + + + ## [3.5.15](https://github.com/vuejs/core/compare/v3.5.14...v3.5.15) (2025-05-26) diff --git a/changelogs/CHANGELOG-3.0.md b/changelogs/CHANGELOG-3.0.md index 16483767fe4..c19ef26890b 100644 --- a/changelogs/CHANGELOG-3.0.md +++ b/changelogs/CHANGELOG-3.0.md @@ -56,13 +56,13 @@ - **hydration:** handle camel-case tag name when performing match assertion ([#3247](https://github.com/vuejs/core/issues/3247)) ([9036f88](https://github.com/vuejs/core/commit/9036f88d8304a3455265f1ecd86ec8f4a5ea4715)), closes [#3243](https://github.com/vuejs/core/issues/3243) - **KeepAlive:** adapt keepalive for ssr ([#3259](https://github.com/vuejs/core/issues/3259)) ([e8e9b00](https://github.com/vuejs/core/commit/e8e9b00f81ed42434afd92f84101e7a14d70a23c)), closes [#3255](https://github.com/vuejs/core/issues/3255) - **reactivity:** ensure computed can be wrapped by readonly ([41e02f0](https://github.com/vuejs/core/commit/41e02f0fac069c93c94438741517e713f3c94215)), closes [#3376](https://github.com/vuejs/core/issues/3376) -- **reactivity:** ensure that shallow and normal proxies are tracked seperately (close [#2843](https://github.com/vuejs/core/issues/2843)) ([#2851](https://github.com/vuejs/core/issues/2851)) ([22cc4a7](https://github.com/vuejs/core/commit/22cc4a76592cfe336e75e2fa0c05232ae1f0f149)) +- **reactivity:** ensure that shallow and normal proxies are tracked separately (close [#2843](https://github.com/vuejs/core/issues/2843)) ([#2851](https://github.com/vuejs/core/issues/2851)) ([22cc4a7](https://github.com/vuejs/core/commit/22cc4a76592cfe336e75e2fa0c05232ae1f0f149)) - **reactivity:** fix shallow readonly behavior for collections ([#3003](https://github.com/vuejs/core/issues/3003)) ([68de9f4](https://github.com/vuejs/core/commit/68de9f408a2e61a5726a4a0d03b026cba451c5bd)), closes [#3007](https://github.com/vuejs/core/issues/3007) - **rumtime-core:** custom dom props should be cloned when cloning a hoisted DOM ([#3080](https://github.com/vuejs/core/issues/3080)) ([5dbe834](https://github.com/vuejs/core/commit/5dbe8348581dacd7a3594a9b0055ce350ce8e5bf)), closes [#3072](https://github.com/vuejs/core/issues/3072) - **runtime-core:** cache props default values to avoid unnecessary watcher trigger ([#3474](https://github.com/vuejs/core/issues/3474)) ([44166b4](https://github.com/vuejs/core/commit/44166b43d9be1062f79612880f71284049bcab0b)), closes [#3471](https://github.com/vuejs/core/issues/3471) - **runtime-core:** ensure only skip unflushed job ([#3406](https://github.com/vuejs/core/issues/3406)) ([bf34e33](https://github.com/vuejs/core/commit/bf34e33c909da89681b9c5004cdf04ab198ec5a7)) - **runtime-core:** fix async component ref handling ([#3191](https://github.com/vuejs/core/issues/3191)) ([7562e72](https://github.com/vuejs/core/commit/7562e72c2b58a5646bd4fbd9adea11eb884fe140)), closes [#3188](https://github.com/vuejs/core/issues/3188) -- **runtime-core:** fix erraneous emits warnings w/ mixins ([60d777d](https://github.com/vuejs/core/commit/60d777d228414515cc32526ad72a53ef070501be)), closes [#2651](https://github.com/vuejs/core/issues/2651) +- **runtime-core:** fix erroneous emits warnings w/ mixins ([60d777d](https://github.com/vuejs/core/commit/60d777d228414515cc32526ad72a53ef070501be)), closes [#2651](https://github.com/vuejs/core/issues/2651) - **runtime-core:** fix warning for absent props ([#3363](https://github.com/vuejs/core/issues/3363)) ([86ceef4](https://github.com/vuejs/core/commit/86ceef43523bfbbb0a24731d3802ca6849cbefd6)), closes [#3362](https://github.com/vuejs/core/issues/3362) - **runtime-core:** handle error in async setup ([#2881](https://github.com/vuejs/core/issues/2881)) ([d668d48](https://github.com/vuejs/core/commit/d668d48e9e5211a49ee53361ea5b4d67ba16e0a3)) - **runtime-core:** handle error in async watchEffect ([#3129](https://github.com/vuejs/core/issues/3129)) ([eb1fae6](https://github.com/vuejs/core/commit/eb1fae63f926435fb0eef890663d24e09d4c79e1)) @@ -202,7 +202,7 @@ may cause build issues in projects still using TS 3.x. - **script-setup:** ensure useContext() return valid context ([73cdb9d](https://github.com/vuejs/core/commit/73cdb9d4208f887fe08349657122e39175d7166c)) - **slots:** dynamically named slots should be keyed by name ([2ab8c41](https://github.com/vuejs/core/commit/2ab8c41a1a43952fb229587a9da48d9a1214ab9e)), closes [#2535](https://github.com/vuejs/core/issues/2535) - **slots:** should render fallback content when slot content contains no valid nodes ([#2485](https://github.com/vuejs/core/issues/2485)) ([ce4915d](https://github.com/vuejs/core/commit/ce4915d8bed12f4cdb5fa8ca39bda98d0d3aabb7)), closes [#2347](https://github.com/vuejs/core/issues/2347) [#2461](https://github.com/vuejs/core/issues/2461) -- **suspense:** fix nested async child toggle inside already resovled suspense ([cf7f1db](https://github.com/vuejs/core/commit/cf7f1dbc9be8d50ad220e3630c38f5a9a217d693)), closes [#2215](https://github.com/vuejs/core/issues/2215) +- **suspense:** fix nested async child toggle inside already resolved suspense ([cf7f1db](https://github.com/vuejs/core/commit/cf7f1dbc9be8d50ad220e3630c38f5a9a217d693)), closes [#2215](https://github.com/vuejs/core/issues/2215) - **teleport:** Teleport into SVG elements ([#2648](https://github.com/vuejs/core/issues/2648)) ([cd92836](https://github.com/vuejs/core/commit/cd928362232747a51d1fd4790bb20adcdd59d187)), closes [#2652](https://github.com/vuejs/core/issues/2652) - **transition:** avoid invoking stale transition end callbacks ([eaf8a67](https://github.com/vuejs/core/commit/eaf8a67c7219e1b79d6abca44a1d7f1b341b58b0)), closes [#2482](https://github.com/vuejs/core/issues/2482) - **transition:** respect rules in \*-leave-from transition class ([#2597](https://github.com/vuejs/core/issues/2597)) ([e2618a6](https://github.com/vuejs/core/commit/e2618a632d4add2819ffb8b575af0da189dc3204)), closes [#2593](https://github.com/vuejs/core/issues/2593) @@ -236,7 +236,7 @@ may cause build issues in projects still using TS 3.x. - **compiler-sfc:** compileScript inline render function mode ([886ed76](https://github.com/vuejs/core/commit/886ed7681dd203c07ff3b504538328f43e14d9b0)) - **compiler-sfc:** new script setup implementation ([556560f](https://github.com/vuejs/core/commit/556560fae31d9e406cfae656089657b6332686c1)) -- **compiler-sfc:** new SFC css varaible injection implementation ([41bb7fa](https://github.com/vuejs/core/commit/41bb7fa330e78c4a354a2e67742bd13bee2f4293)) +- **compiler-sfc:** new SFC css variable injection implementation ([41bb7fa](https://github.com/vuejs/core/commit/41bb7fa330e78c4a354a2e67742bd13bee2f4293)) - **compiler-sfc:** support kebab-case components in ` diff --git a/packages/compiler-core/__tests__/parse.spec.ts b/packages/compiler-core/__tests__/parse.spec.ts index 4e5a9616511..cdc2b09fd48 100644 --- a/packages/compiler-core/__tests__/parse.spec.ts +++ b/packages/compiler-core/__tests__/parse.spec.ts @@ -2271,6 +2271,11 @@ describe('compiler: parse', () => { expect(span.loc.start.offset).toBe(0) expect(span.loc.end.offset).toBe(27) }) + + test('correct loc when a line in attribute value ends with &', () => { + const [span] = baseParse(``).children + expect(span.loc.end.line).toBe(2) + }) }) describe('decodeEntities option', () => { diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap index 375a0c8674a..91a82db5bba 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap @@ -8,7 +8,7 @@ return function render(_ctx, _cache) { const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ - _createElementVNode("div", { key: "foo" }, null, -1 /* HOISTED */) + _createElementVNode("div", { key: "foo" }, null, -1 /* CACHED */) ]))) } }" @@ -25,11 +25,11 @@ return function render(_ctx, _cache) { _createElementVNode("p", null, [ _createElementVNode("span"), _createElementVNode("span") - ], -1 /* HOISTED */), + ], -1 /* CACHED */), _createElementVNode("p", null, [ _createElementVNode("span"), _createElementVNode("span") - ], -1 /* HOISTED */) + ], -1 /* CACHED */) ]))) } }" @@ -45,7 +45,7 @@ return function render(_ctx, _cache) { return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ _createElementVNode("div", null, [ _createCommentVNode("comment") - ], -1 /* HOISTED */) + ], -1 /* CACHED */) ]))) } }" @@ -59,9 +59,9 @@ return function render(_ctx, _cache) { const { createElementVNode: _createElementVNode, createTextVNode: _createTextVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ - _createElementVNode("span", null, null, -1 /* HOISTED */), - _createTextVNode("foo"), - _createElementVNode("div", null, null, -1 /* HOISTED */) + _createElementVNode("span", null, null, -1 /* CACHED */), + _createTextVNode("foo", -1 /* CACHED */), + _createElementVNode("div", null, null, -1 /* CACHED */) ]))) } }" @@ -75,7 +75,7 @@ return function render(_ctx, _cache) { const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ - _createElementVNode("span", { class: "inline" }, "hello", -1 /* HOISTED */) + _createElementVNode("span", { class: "inline" }, "hello", -1 /* CACHED */) ]))) } }" @@ -148,7 +148,7 @@ return function render(_ctx, _cache) { const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ - _createElementVNode("span", null, "foo " + _toDisplayString(1) + " " + _toDisplayString(true), -1 /* HOISTED */) + _createElementVNode("span", null, "foo " + _toDisplayString(1) + " " + _toDisplayString(true), -1 /* CACHED */) ]))) } }" @@ -162,7 +162,7 @@ return function render(_ctx, _cache) { const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ - _createElementVNode("span", { foo: 0 }, _toDisplayString(1), -1 /* HOISTED */) + _createElementVNode("span", { foo: 0 }, _toDisplayString(1), -1 /* CACHED */) ]))) } }" @@ -178,7 +178,7 @@ return function render(_ctx, _cache) { return (_openBlock(), _createElementBlock("div", null, [ (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(1, (i) => { return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [ - _createElementVNode("span", { class: "hi" }, null, -1 /* HOISTED */) + _createElementVNode("span", { class: "hi" }, null, -1 /* CACHED */) ]))])) }), 256 /* UNKEYED_FRAGMENT */)) ])) @@ -216,7 +216,7 @@ return function render(_ctx, _cache) { return (_openBlock(), _createElementBlock("div", null, [ _withDirectives((_openBlock(), _createElementBlock("svg", null, _cache[0] || (_cache[0] = [ - _createElementVNode("path", { d: "M2,3H5.5L12" }, null, -1 /* HOISTED */) + _createElementVNode("path", { d: "M2,3H5.5L12" }, null, -1 /* CACHED */) ]))), [ [_directive_foo] ]) @@ -402,7 +402,7 @@ return function render(_ctx, _cache) { return (_openBlock(), _createElementBlock("div", null, [ ok ? (_openBlock(), _createElementBlock("div", _hoisted_1, _cache[0] || (_cache[0] = [ - _createElementVNode("span", null, null, -1 /* HOISTED */) + _createElementVNode("span", null, null, -1 /* CACHED */) ]))) : _createCommentVNode("v-if", true) ])) @@ -410,6 +410,32 @@ return function render(_ctx, _cache) { }" `; +exports[`compiler: cacheStatic transform > should hoist props for root with single element excluding comments 1`] = ` +"const _Vue = Vue +const { createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode } = _Vue + +const _hoisted_1 = { id: "a" } + +return function render(_ctx, _cache) { + with (_ctx) { + const { createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue + + return (_openBlock(), _createElementBlock(_Fragment, null, [ + _createCommentVNode("comment"), + _createElementVNode("div", _hoisted_1, _cache[0] || (_cache[0] = [ + _createElementVNode("div", { id: "b" }, [ + _createElementVNode("div", { id: "c" }, [ + _createElementVNode("div", { id: "d" }, [ + _createElementVNode("div", { id: "e" }, "hello") + ]) + ]) + ], -1 /* CACHED */) + ])) + ], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */)) + } +}" +`; + exports[`compiler: cacheStatic transform > should hoist v-for children if static 1`] = ` "const _Vue = Vue const { createElementVNode: _createElementVNode } = _Vue @@ -423,7 +449,7 @@ return function render(_ctx, _cache) { return (_openBlock(), _createElementBlock("div", null, [ (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(list, (i) => { return (_openBlock(), _createElementBlock("div", _hoisted_1, _cache[0] || (_cache[0] = [ - _createElementVNode("span", null, null, -1 /* HOISTED */) + _createElementVNode("span", null, null, -1 /* CACHED */) ]))) }), 256 /* UNKEYED_FRAGMENT */)) ])) diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap index 0d9c0e743de..2cd13bab036 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap @@ -246,6 +246,28 @@ return function render(_ctx, _cache) { }" `; +exports[`compiler: transform component slots > with whitespace: 'preserve' > named slot with v-if + v-else 1`] = ` +"const { resolveComponent: _resolveComponent, withCtx: _withCtx, createSlots: _createSlots, openBlock: _openBlock, createBlock: _createBlock } = Vue + +return function render(_ctx, _cache) { + const _component_Comp = _resolveComponent("Comp") + + return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _: 2 /* DYNAMIC */ }, [ + ok + ? { + name: "one", + fn: _withCtx(() => ["foo"]), + key: "0" + } + : { + name: "two", + fn: _withCtx(() => ["baz"]), + key: "1" + } + ]), 1024 /* DYNAMIC_SLOTS */)) +}" +`; + exports[`compiler: transform component slots > with whitespace: 'preserve' > should not generate whitespace only default slot 1`] = ` "const { resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = Vue diff --git a/packages/compiler-core/__tests__/transforms/cacheStatic.spec.ts b/packages/compiler-core/__tests__/transforms/cacheStatic.spec.ts index 358c0e31c3d..74f6caca328 100644 --- a/packages/compiler-core/__tests__/transforms/cacheStatic.spec.ts +++ b/packages/compiler-core/__tests__/transforms/cacheStatic.spec.ts @@ -543,6 +543,32 @@ describe('compiler: cacheStatic transform', () => { expect(generate(root).code).toMatchSnapshot() }) + test('should hoist props for root with single element excluding comments', () => { + // deeply nested div to trigger stringification condition + const root = transformWithCache( + `
hello
`, + ) + expect(root.cached.length).toBe(1) + expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'a' })]) + + expect((root.codegenNode as VNodeCall).children).toMatchObject([ + { + type: NodeTypes.COMMENT, + content: 'comment', + }, + { + type: NodeTypes.ELEMENT, + codegenNode: { + type: NodeTypes.VNODE_CALL, + tag: `"div"`, + props: { content: `_hoisted_1` }, + children: { type: NodeTypes.JS_CACHE_EXPRESSION }, + }, + }, + ]) + expect(generate(root).code).toMatchSnapshot() + }) + describe('prefixIdentifiers', () => { test('cache nested static tree with static interpolation', () => { const root = transformWithCache( diff --git a/packages/compiler-core/__tests__/transforms/vIf.spec.ts b/packages/compiler-core/__tests__/transforms/vIf.spec.ts index 2c2fedab0d5..73b6e221554 100644 --- a/packages/compiler-core/__tests__/transforms/vIf.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vIf.spec.ts @@ -301,6 +301,25 @@ describe('compiler: v-if', () => { ]) }) + test('error on adjacent v-else', () => { + const onError = vi.fn() + + const { + node: { branches }, + } = parseWithIfTransform( + `
`, + { onError }, + 0, + ) + + expect(onError.mock.calls[0]).toMatchObject([ + { + code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, + loc: branches[branches.length - 1].loc, + }, + ]) + }) + test('error on user key', () => { const onError = vi.fn() // dynamic diff --git a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts index 4766c2ca9d8..e0f44a064fb 100644 --- a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts @@ -988,5 +988,19 @@ describe('compiler: transform component slots', () => { expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot() }) + + test('named slot with v-if + v-else', () => { + const source = ` + + + + + ` + const { root } = parseWithSlots(source, { + whitespace: 'preserve', + }) + + expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot() + }) }) }) diff --git a/packages/compiler-core/__tests__/utils.spec.ts b/packages/compiler-core/__tests__/utils.spec.ts index 2d377a271ac..000b10e11bd 100644 --- a/packages/compiler-core/__tests__/utils.spec.ts +++ b/packages/compiler-core/__tests__/utils.spec.ts @@ -1,4 +1,9 @@ -import type { ExpressionNode, TransformContext } from '../src' +import { babelParse, walkIdentifiers } from '@vue/compiler-sfc' +import { + type ExpressionNode, + type TransformContext, + isReferencedIdentifier, +} from '../src' import { type Position, createSimpleExpression } from '../src/ast' import { advancePositionWithClone, @@ -115,3 +120,18 @@ test('toValidAssetId', () => { '_component_test_2797935797_1', ) }) + +describe('isReferencedIdentifier', () => { + test('identifiers in function parameters should not be inferred as references', () => { + expect.assertions(4) + const ast = babelParse(`(({ title }) => [])`) + walkIdentifiers( + ast.program.body[0], + (node, parent, parentStack, isReference) => { + expect(isReference).toBe(false) + expect(isReferencedIdentifier(node, parent, parentStack)).toBe(false) + }, + true, + ) + }) +}) diff --git a/packages/compiler-core/package.json b/packages/compiler-core/package.json index cd478cdf06a..a59342aeb93 100644 --- a/packages/compiler-core/package.json +++ b/packages/compiler-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-core", - "version": "3.5.15", + "version": "3.5.18", "description": "@vue/compiler-core", "main": "index.js", "module": "dist/compiler-core.esm-bundler.js", diff --git a/packages/compiler-core/src/babelUtils.ts b/packages/compiler-core/src/babelUtils.ts index 52fabeea896..51614612b10 100644 --- a/packages/compiler-core/src/babelUtils.ts +++ b/packages/compiler-core/src/babelUtils.ts @@ -122,7 +122,7 @@ export function isReferencedIdentifier( return false } - if (isReferenced(id, parent)) { + if (isReferenced(id, parent, parentStack[parentStack.length - 2])) { return true } @@ -132,7 +132,8 @@ export function isReferencedIdentifier( case 'AssignmentExpression': case 'AssignmentPattern': return true - case 'ObjectPattern': + case 'ObjectProperty': + return parent.key !== id && isInDestructureAssignment(parent, parentStack) case 'ArrayPattern': return isInDestructureAssignment(parent, parentStack) } diff --git a/packages/compiler-core/src/parser.ts b/packages/compiler-core/src/parser.ts index 7d1b01360c4..a6e25681d75 100644 --- a/packages/compiler-core/src/parser.ts +++ b/packages/compiler-core/src/parser.ts @@ -43,6 +43,7 @@ import { isCoreComponent, isSimpleIdentifier, isStaticArgOf, + isVPre, } from './utils' import { decodeHTML } from 'entities/lib/decode.js' import { @@ -246,7 +247,7 @@ const tokenizer = new Tokenizer(stack, { ondirarg(start, end) { if (start === end) return const arg = getSlice(start, end) - if (inVPre) { + if (inVPre && !isVPre(currentProp!)) { ;(currentProp as AttributeNode).name += arg setLocEnd((currentProp as AttributeNode).nameLoc, end) } else { @@ -262,7 +263,7 @@ const tokenizer = new Tokenizer(stack, { ondirmodifier(start, end) { const mod = getSlice(start, end) - if (inVPre) { + if (inVPre && !isVPre(currentProp!)) { ;(currentProp as AttributeNode).name += '.' + mod setLocEnd((currentProp as AttributeNode).nameLoc, end) } else if ((currentProp as DirectiveNode).name === 'slot') { @@ -647,7 +648,7 @@ function onCloseTag(el: ElementNode, end: number, isImplied = false) { // whitespace management if (!tokenizer.inRCDATA) { - el.children = condenseWhitespace(children, tag) + el.children = condenseWhitespace(children) } if (ns === Namespaces.HTML && currentOptions.isIgnoreNewlineTag(tag)) { @@ -832,10 +833,7 @@ function isUpperCase(c: number) { } const windowsNewlineRE = /\r\n/g -function condenseWhitespace( - nodes: TemplateChildNode[], - tag?: string, -): TemplateChildNode[] { +function condenseWhitespace(nodes: TemplateChildNode[]): TemplateChildNode[] { const shouldCondense = currentOptions.whitespace !== 'preserve' let removedWhitespace = false for (let i = 0; i < nodes.length; i++) { diff --git a/packages/compiler-core/src/tokenizer.ts b/packages/compiler-core/src/tokenizer.ts index 329e8b48181..b8a74790259 100644 --- a/packages/compiler-core/src/tokenizer.ts +++ b/packages/compiler-core/src/tokenizer.ts @@ -929,7 +929,7 @@ export default class Tokenizer { this.buffer = input while (this.index < this.buffer.length) { const c = this.buffer.charCodeAt(this.index) - if (c === CharCodes.NewLine) { + if (c === CharCodes.NewLine && this.state !== State.InEntity) { this.newlines.push(this.index) } switch (this.state) { diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index aeb96cc2b4a..9d8fd842935 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -37,7 +37,7 @@ import { helperNameMap, } from './runtimeHelpers' import { isVSlot } from './utils' -import { cacheStatic, isSingleElementRoot } from './transforms/cacheStatic' +import { cacheStatic, getSingleElementRoot } from './transforms/cacheStatic' import type { CompilerCompatOptions } from './compat/compatConfig' // There are two types of transforms: @@ -356,12 +356,12 @@ function createRootCodegen(root: RootNode, context: TransformContext) { const { helper } = context const { children } = root if (children.length === 1) { - const child = children[0] + const singleElementRootChild = getSingleElementRoot(root) // if the single child is an element, turn it into a block. - if (isSingleElementRoot(root, child) && child.codegenNode) { + if (singleElementRootChild && singleElementRootChild.codegenNode) { // single element root is never hoisted so codegenNode will never be // SimpleExpressionNode - const codegenNode = child.codegenNode + const codegenNode = singleElementRootChild.codegenNode if (codegenNode.type === NodeTypes.VNODE_CALL) { convertToBlock(codegenNode, context) } @@ -370,7 +370,7 @@ function createRootCodegen(root: RootNode, context: TransformContext) { // - single , IfNode, ForNode: already blocks. // - single text node: always patched. // root codegen falls through via genNode() - root.codegenNode = child + root.codegenNode = children[0] } } else if (children.length > 1) { // root has multiple nodes - return a fragment block. diff --git a/packages/compiler-core/src/transforms/cacheStatic.ts b/packages/compiler-core/src/transforms/cacheStatic.ts index e5d67380640..0f112e19cad 100644 --- a/packages/compiler-core/src/transforms/cacheStatic.ts +++ b/packages/compiler-core/src/transforms/cacheStatic.ts @@ -24,7 +24,13 @@ import { getVNodeHelper, } from '../ast' import type { TransformContext } from '../transform' -import { PatchFlags, isArray, isString, isSymbol } from '@vue/shared' +import { + PatchFlagNames, + PatchFlags, + isArray, + isString, + isSymbol, +} from '@vue/shared' import { findDir, isSlotOutlet } from '../utils' import { GUARD_REACTIVE_PROPS, @@ -41,20 +47,19 @@ export function cacheStatic(root: RootNode, context: TransformContext): void { context, // Root node is unfortunately non-hoistable due to potential parent // fallthrough attributes. - isSingleElementRoot(root, root.children[0]), + !!getSingleElementRoot(root), ) } -export function isSingleElementRoot( +export function getSingleElementRoot( root: RootNode, - child: TemplateChildNode, -): child is PlainElementNode | ComponentNode | TemplateNode { - const { children } = root - return ( - children.length === 1 && - child.type === NodeTypes.ELEMENT && - !isSlotOutlet(child) - ) +): PlainElementNode | ComponentNode | TemplateNode | null { + const children = root.children.filter(x => x.type !== NodeTypes.COMMENT) + return children.length === 1 && + children[0].type === NodeTypes.ELEMENT && + !isSlotOutlet(children[0]) + ? children[0] + : null } function walk( @@ -110,6 +115,15 @@ function walk( ? ConstantTypes.NOT_CONSTANT : getConstantType(child, context) if (constantType >= ConstantTypes.CAN_CACHE) { + if ( + child.codegenNode.type === NodeTypes.JS_CALL_EXPRESSION && + child.codegenNode.arguments.length > 0 + ) { + child.codegenNode.arguments.push( + PatchFlags.CACHED + + (__DEV__ ? ` /* ${PatchFlagNames[PatchFlags.CACHED]} */` : ``), + ) + } toCache.push(child) continue } diff --git a/packages/compiler-core/src/transforms/vBind.ts b/packages/compiler-core/src/transforms/vBind.ts index 1e5e371418b..c82706c10c7 100644 --- a/packages/compiler-core/src/transforms/vBind.ts +++ b/packages/compiler-core/src/transforms/vBind.ts @@ -65,7 +65,7 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => { arg.children.unshift(`(`) arg.children.push(`) || ""`) } else if (!arg.isStatic) { - arg.content = `${arg.content} || ""` + arg.content = arg.content ? `${arg.content} || ""` : `""` } // .sync is replaced by v-model:arg diff --git a/packages/compiler-core/src/transforms/vFor.ts b/packages/compiler-core/src/transforms/vFor.ts index 0dca0ba9ab4..a639caf2cae 100644 --- a/packages/compiler-core/src/transforms/vFor.ts +++ b/packages/compiler-core/src/transforms/vFor.ts @@ -263,7 +263,7 @@ export function processFor( dir: DirectiveNode, context: TransformContext, processCodegen?: (forNode: ForNode) => (() => void) | undefined, -) { +): (() => void) | undefined { if (!dir.exp) { context.onError( createCompilerError(ErrorCodes.X_V_FOR_NO_EXPRESSION, dir.loc), diff --git a/packages/compiler-core/src/transforms/vIf.ts b/packages/compiler-core/src/transforms/vIf.ts index 54c505407a3..8bf5c6a32ff 100644 --- a/packages/compiler-core/src/transforms/vIf.ts +++ b/packages/compiler-core/src/transforms/vIf.ts @@ -141,9 +141,9 @@ export function processIf( } if (sibling && sibling.type === NodeTypes.IF) { - // Check if v-else was followed by v-else-if + // Check if v-else was followed by v-else-if or there are two adjacent v-else if ( - dir.name === 'else-if' && + (dir.name === 'else-if' || dir.name === 'else') && sibling.branches[sibling.branches.length - 1].condition === undefined ) { context.onError( diff --git a/packages/compiler-core/src/transforms/vSlot.ts b/packages/compiler-core/src/transforms/vSlot.ts index 28625439a47..43296dcc9b6 100644 --- a/packages/compiler-core/src/transforms/vSlot.ts +++ b/packages/compiler-core/src/transforms/vSlot.ts @@ -222,7 +222,7 @@ export function buildSlots( let prev while (j--) { prev = children[j] - if (prev.type !== NodeTypes.COMMENT) { + if (prev.type !== NodeTypes.COMMENT && isNonWhitespaceContent(prev)) { break } } diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts index b49d70bb2fb..ab851ed6f69 100644 --- a/packages/compiler-core/src/utils.ts +++ b/packages/compiler-core/src/utils.ts @@ -63,7 +63,7 @@ export function isCoreComponent(tag: string): symbol | void { } } -const nonIdentifierRE = /^\d|[^\$\w\xA0-\uFFFF]/ +const nonIdentifierRE = /^$|^\d|[^\$\w\xA0-\uFFFF]/ export const isSimpleIdentifier = (name: string): boolean => !nonIdentifierRE.test(name) @@ -343,6 +343,10 @@ export function isText( return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT } +export function isVPre(p: ElementNode['props'][0]): p is DirectiveNode { + return p.type === NodeTypes.DIRECTIVE && p.name === 'pre' +} + export function isVSlot(p: ElementNode['props'][0]): p is DirectiveNode { return p.type === NodeTypes.DIRECTIVE && p.name === 'slot' } diff --git a/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap b/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap index 2ed15ef5e62..5bc40d3fab5 100644 --- a/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap +++ b/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap @@ -6,7 +6,7 @@ exports[`stringify static html > eligible content (elements > 20) + non-eligible return function render(_ctx, _cache) { return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ _createStaticVNode("", 20), - _createElementVNode("div", { key: "1" }, "1", -1 /* HOISTED */), + _createElementVNode("div", { key: "1" }, "1", -1 /* CACHED */), _createStaticVNode("", 20) ]))) }" @@ -54,7 +54,7 @@ return function render(_ctx, _cache) { _createElementVNode("option", { value: "1" }), _createElementVNode("option", { value: "1" }), _createElementVNode("option", { value: "1" }) - ], -1 /* HOISTED */) + ], -1 /* CACHED */) ]))) }" `; @@ -70,11 +70,27 @@ return function render(_ctx, _cache) { _createElementVNode("option", { value: 1 }), _createElementVNode("option", { value: 1 }), _createElementVNode("option", { value: 1 }) - ], -1 /* HOISTED */) + ], -1 /* CACHED */) ]))) }" `; +exports[`stringify static html > should bail for comments 1`] = ` +"const { createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue + +const _hoisted_1 = { class: "a" } + +return function render(_ctx, _cache) { + return (_openBlock(), _createElementBlock(_Fragment, null, [ + _createCommentVNode(" Comment 1 "), + _createElementVNode("div", _hoisted_1, [ + _createCommentVNode(" Comment 2 "), + _cache[0] || (_cache[0] = _createStaticVNode("", 5)) + ]) + ], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */)) +}" +`; + exports[`stringify static html > should bail on bindings that are cached but not stringifiable 1`] = ` "const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue @@ -87,7 +103,7 @@ return function render(_ctx, _cache) { _createElementVNode("span", { class: "foo" }, "foo"), _createElementVNode("span", { class: "foo" }, "foo"), _createElementVNode("img", { src: _imports_0_ }) - ], -1 /* HOISTED */) + ], -1 /* CACHED */) ]))) }" `; diff --git a/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts index 79e6fc9c6e8..f58e207d6cf 100644 --- a/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts +++ b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts @@ -491,6 +491,16 @@ describe('stringify static html', () => { expect(code).toMatchSnapshot() }) + test('should bail for comments', () => { + const { code } = compileWithStringify( + `
${repeat( + ``, + StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, + )}
`, + ) + expect(code).toMatchSnapshot() + }) + test('should bail for
\`) + }" + `) + + expect( + compileWithWrapper(` + `).code, + ).toMatchInlineSnapshot(` + "const { ssrRenderAttr: _ssrRenderAttr, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`
\`) + }" + `) + + expect( + compileWithWrapper(` + `).code, + ).toMatchInlineSnapshot(` + "const { ssrRenderAttr: _ssrRenderAttr, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`
\`) + }" + `) + + expect( + compileWithWrapper(` + `).code, + ).toMatchInlineSnapshot(` + "const { ssrRenderAttr: _ssrRenderAttr, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) }) test('', () => { diff --git a/packages/compiler-ssr/package.json b/packages/compiler-ssr/package.json index 0e49a83c2c4..d11be67c489 100644 --- a/packages/compiler-ssr/package.json +++ b/packages/compiler-ssr/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-ssr", - "version": "3.5.15", + "version": "3.5.18", "description": "@vue/compiler-ssr", "main": "dist/compiler-ssr.cjs.js", "types": "dist/compiler-ssr.d.ts", diff --git a/packages/compiler-ssr/src/errors.ts b/packages/compiler-ssr/src/errors.ts index e4fd505d282..bea0774215e 100644 --- a/packages/compiler-ssr/src/errors.ts +++ b/packages/compiler-ssr/src/errors.ts @@ -29,7 +29,7 @@ if (__TEST__) { if (SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME < DOMErrorCodes.__EXTEND_POINT__) { throw new Error( `SSRErrorCodes need to be updated to ${ - DOMErrorCodes.__EXTEND_POINT__ + 1 + DOMErrorCodes.__EXTEND_POINT__ } to match extension point from core DOMErrorCodes.`, ) } diff --git a/packages/compiler-ssr/src/transforms/ssrVModel.ts b/packages/compiler-ssr/src/transforms/ssrVModel.ts index 80e3839318b..cbe5b2b42a3 100644 --- a/packages/compiler-ssr/src/transforms/ssrVModel.ts +++ b/packages/compiler-ssr/src/transforms/ssrVModel.ts @@ -39,6 +39,18 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => { } } + const processSelectChildren = (children: TemplateChildNode[]) => { + children.forEach(child => { + if (child.type === NodeTypes.ELEMENT) { + processOption(child as PlainElementNode) + } else if (child.type === NodeTypes.FOR) { + processSelectChildren(child.children) + } else if (child.type === NodeTypes.IF) { + child.branches.forEach(b => processSelectChildren(b.children)) + } + }) + } + function processOption(plainNode: PlainElementNode) { if (plainNode.tag === 'option') { if (plainNode.props.findIndex(p => p.name === 'selected') === -1) { @@ -65,9 +77,7 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => { ) } } else if (plainNode.tag === 'optgroup') { - plainNode.children.forEach(option => - processOption(option as PlainElementNode), - ) + processSelectChildren(plainNode.children) } } @@ -163,18 +173,7 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => { checkDuplicatedValue() node.children = [createInterpolation(model, model.loc)] } else if (node.tag === 'select') { - const processChildren = (children: TemplateChildNode[]) => { - children.forEach(child => { - if (child.type === NodeTypes.ELEMENT) { - processOption(child as PlainElementNode) - } else if (child.type === NodeTypes.FOR) { - processChildren(child.children) - } else if (child.type === NodeTypes.IF) { - child.branches.forEach(b => processChildren(b.children)) - } - }) - } - processChildren(node.children) + processSelectChildren(node.children) } else { context.onError( createDOMCompilerError( diff --git a/packages/reactivity/__tests__/reactive.spec.ts b/packages/reactivity/__tests__/reactive.spec.ts index a3ba6a39c1d..9b17f960b1a 100644 --- a/packages/reactivity/__tests__/reactive.spec.ts +++ b/packages/reactivity/__tests__/reactive.spec.ts @@ -195,8 +195,8 @@ describe('reactivity/reactive', () => { test('toRaw on object using reactive as prototype', () => { const original = { foo: 1 } const observed = reactive(original) - const inherted = Object.create(observed) - expect(toRaw(inherted)).toBe(inherted) + const inherited = Object.create(observed) + expect(toRaw(inherited)).toBe(inherited) }) test('toRaw on user Proxy wrapping reactive', () => { diff --git a/packages/reactivity/__tests__/readonly.spec.ts b/packages/reactivity/__tests__/readonly.spec.ts index 9acd5c6491a..b035779f85a 100644 --- a/packages/reactivity/__tests__/readonly.spec.ts +++ b/packages/reactivity/__tests__/readonly.spec.ts @@ -8,7 +8,9 @@ import { reactive, readonly, ref, + shallowRef, toRaw, + triggerRef, } from '../src' /** @@ -520,3 +522,16 @@ describe('reactivity/readonly', () => { expect(r.value).toBe(ro) }) }) + +test('should be able to trigger with triggerRef', () => { + const r = shallowRef({ a: 1 }) + const ror = readonly(r) + let dummy + effect(() => { + dummy = ror.value.a + }) + r.value.a = 2 + expect(dummy).toBe(1) + triggerRef(ror) + expect(dummy).toBe(2) +}) diff --git a/packages/reactivity/package.json b/packages/reactivity/package.json index 8886fe928d1..a26334ee876 100644 --- a/packages/reactivity/package.json +++ b/packages/reactivity/package.json @@ -1,6 +1,6 @@ { "name": "@vue/reactivity", - "version": "3.5.15", + "version": "3.5.18", "description": "@vue/reactivity", "main": "index.js", "module": "dist/reactivity.esm-bundler.js", diff --git a/packages/reactivity/src/dep.ts b/packages/reactivity/src/dep.ts index 196c2aaf98e..8ad8a389baf 100644 --- a/packages/reactivity/src/dep.ts +++ b/packages/reactivity/src/dep.ts @@ -93,6 +93,12 @@ export class Dep { */ sc: number = 0 + /** + * @internal + */ + readonly __v_skip = true + // TODO isolatedDeclarations ReactiveFlags.SKIP + constructor(public computed?: ComputedRefImpl | undefined) { if (__DEV__) { this.subsHead = undefined diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index 39032a63699..ff06fbea774 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -1595,7 +1595,7 @@ describe('api: watch', () => { num.value++ await nextTick() - // would not be calld when value>1 + // would not be called when value>1 expect(spy1).toHaveBeenCalledTimes(1) expect(spy2).toHaveBeenCalledTimes(1) }) @@ -1874,7 +1874,7 @@ describe('api: watch', () => { expect(foo.value.a).toBe(2) }) - test('watch immediate error in effect scope should be catched by onErrorCaptured', async () => { + test('watch immediate error in effect scope should be caught by onErrorCaptured', async () => { const warn = vi.spyOn(console, 'warn') warn.mockImplementation(() => {}) const ERROR_IN_SCOPE = 'ERROR_IN_SCOPE' diff --git a/packages/runtime-core/__tests__/componentSlots.spec.ts b/packages/runtime-core/__tests__/componentSlots.spec.ts index 2cf50b964bf..765fce33e42 100644 --- a/packages/runtime-core/__tests__/componentSlots.spec.ts +++ b/packages/runtime-core/__tests__/componentSlots.spec.ts @@ -6,6 +6,8 @@ import { nodeOps, ref, render, + serializeInner, + useSlots, } from '@vue/runtime-test' import { createBlock, normalizeVNode } from '../src/vnode' import { createSlots } from '../src/helpers/createSlots' @@ -42,6 +44,29 @@ describe('component: slots', () => { expect(slots).toMatchObject({}) }) + test('initSlots: ensure compiler marker non-enumerable', () => { + const Comp = { + render() { + const slots = useSlots() + // Only user-defined slots should be enumerable + expect(Object.keys(slots)).toEqual(['foo']) + + // Internal compiler markers must still exist but be non-enumerable + expect(slots).toHaveProperty('_') + expect(Object.getOwnPropertyDescriptor(slots, '_')!.enumerable).toBe( + false, + ) + expect(slots).toHaveProperty('__') + expect(Object.getOwnPropertyDescriptor(slots, '__')!.enumerable).toBe( + false, + ) + return h('div') + }, + } + const slots = { foo: () => {}, _: 1, __: [1] } + render(createBlock(Comp, null, slots), nodeOps.createElement('div')) + }) + test('initSlots: should normalize object slots (when value is null, string, array)', () => { const { slots } = renderWithSlots({ _inner: '_inner', @@ -50,6 +75,10 @@ describe('component: slots', () => { footer: ['f1', 'f2'], }) + expect( + '[Vue warn]: Non-function value encountered for slot "_inner". Prefer function slots for better performance.', + ).toHaveBeenWarned() + expect( '[Vue warn]: Non-function value encountered for slot "header". Prefer function slots for better performance.', ).toHaveBeenWarned() @@ -58,8 +87,8 @@ describe('component: slots', () => { '[Vue warn]: Non-function value encountered for slot "footer". Prefer function slots for better performance.', ).toHaveBeenWarned() - expect(slots).not.toHaveProperty('_inner') expect(slots).not.toHaveProperty('foo') + expect(slots._inner()).toMatchObject([normalizeVNode('_inner')]) expect(slots.header()).toMatchObject([normalizeVNode('header')]) expect(slots.footer()).toMatchObject([ normalizeVNode('f1'), @@ -418,4 +447,22 @@ describe('component: slots', () => { 'Slot "default" invoked outside of the render function', ).toHaveBeenWarned() }) + + test('slot name starts with underscore', () => { + const Comp = { + setup(_: any, { slots }: any) { + return () => slots._foo() + }, + } + + const App = { + setup() { + return () => h(Comp, null, { _foo: () => 'foo' }) + }, + } + + const root = nodeOps.createElement('div') + createApp(App).mount(root) + expect(serializeInner(root)).toBe('foo') + }) }) diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts index 65e801de277..563c91a179d 100644 --- a/packages/runtime-core/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts @@ -2230,5 +2230,57 @@ describe('Suspense', () => { fallback: [h('div'), h('div')], }) }) + + // #13559 + test('renders multiple async components in Suspense with v-for and updates on items change', async () => { + const CompAsyncSetup = defineAsyncComponent({ + props: ['item'], + render(ctx: any) { + return h('div', ctx.item.name) + }, + }) + + const items = ref([ + { id: 1, name: '111' }, + { id: 2, name: '222' }, + { id: 3, name: '333' }, + ]) + + const Comp = { + setup() { + return () => + h(Suspense, null, { + default: () => + h( + Fragment, + null, + items.value.map(item => + h(CompAsyncSetup, { item, key: item.id }), + ), + ), + }) + }, + } + + const root = nodeOps.createElement('div') + render(h(Comp), root) + await nextTick() + await Promise.all(deps) + + expect(serializeInner(root)).toBe( + `
111
222
333
`, + ) + + items.value = [ + { id: 4, name: '444' }, + { id: 5, name: '555' }, + { id: 6, name: '666' }, + ] + await nextTick() + await Promise.all(deps) + expect(serializeInner(root)).toBe( + `
444
555
666
`, + ) + }) }) }) diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index 4a9e0fac2b6..6828e61ec56 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -1160,6 +1160,69 @@ describe('SSR hydration', () => { ) }) + // #13510 + test('update async component after parent mount before async component resolve', async () => { + const Comp = { + props: ['toggle'], + render(this: any) { + return h('h1', [ + this.toggle ? 'Async component' : 'Updated async component', + ]) + }, + } + let serverResolve: any + let AsyncComp = defineAsyncComponent( + () => + new Promise(r => { + serverResolve = r + }), + ) + + const toggle = ref(true) + const App = { + setup() { + onMounted(() => { + // change state, after mount and before async component resolve + nextTick(() => (toggle.value = false)) + }) + + return () => { + return h(AsyncComp, { toggle: toggle.value }) + } + }, + } + + // server render + const htmlPromise = renderToString(h(App)) + serverResolve(Comp) + const html = await htmlPromise + expect(html).toMatchInlineSnapshot(`"

Async component

"`) + + // hydration + let clientResolve: any + AsyncComp = defineAsyncComponent( + () => + new Promise(r => { + clientResolve = r + }), + ) + + const container = document.createElement('div') + container.innerHTML = html + createSSRApp(App).mount(container) + + // resolve + clientResolve(Comp) + await new Promise(r => setTimeout(r)) + + // prevent lazy hydration since the component has been patched + expect('Skipping lazy hydration for component').toHaveBeenWarned() + expect(`Hydration node mismatch`).not.toHaveBeenWarned() + expect(container.innerHTML).toMatchInlineSnapshot( + `"

Updated async component

"`, + ) + }) + test('hydrate safely when property used by async setup changed before render', async () => { const toggle = ref(true) @@ -1677,6 +1740,35 @@ describe('SSR hydration', () => { expect(`mismatch`).not.toHaveBeenWarned() }) + // #13394 + test('transition appear work with empty content', async () => { + const show = ref(true) + const { vnode, container } = mountWithHydration( + ``, + function (this: any) { + return h( + Transition, + { appear: true }, + { + default: () => + show.value + ? renderSlot(this.$slots, 'default') + : createTextVNode('foo'), + }, + ) + }, + ) + + // empty slot render as a comment node + expect(container.firstChild!.nodeType).toBe(Node.COMMENT_NODE) + expect(vnode.el).toBe(container.firstChild) + expect(`mismatch`).not.toHaveBeenWarned() + + show.value = false + await nextTick() + expect(container.innerHTML).toBe('foo') + }) + test('transition appear with v-if', () => { const show = false const { vnode, container } = mountWithHydration( diff --git a/packages/runtime-core/__tests__/rendererTemplateRef.spec.ts b/packages/runtime-core/__tests__/rendererTemplateRef.spec.ts index a7ae7a06bfd..7803826e3df 100644 --- a/packages/runtime-core/__tests__/rendererTemplateRef.spec.ts +++ b/packages/runtime-core/__tests__/rendererTemplateRef.spec.ts @@ -179,6 +179,37 @@ describe('api: template refs', () => { expect(el.value).toBe(null) }) + it('unset old ref when new ref is absent', async () => { + const root1 = nodeOps.createElement('div') + const root2 = nodeOps.createElement('div') + const el1 = ref(null) + const el2 = ref(null) + const toggle = ref(true) + + const Comp1 = { + setup() { + return () => (toggle.value ? h('div', { ref: el1 }) : h('div')) + }, + } + + const Comp2 = { + setup() { + return () => h('div', { ref: toggle.value ? el2 : undefined }) + }, + } + + render(h(Comp1), root1) + render(h(Comp2), root2) + + expect(el1.value).toBe(root1.children[0]) + expect(el2.value).toBe(root2.children[0]) + + toggle.value = false + await nextTick() + expect(el1.value).toBe(null) + expect(el2.value).toBe(null) + }) + test('string ref inside slots', async () => { const root = nodeOps.createElement('div') const spy = vi.fn() diff --git a/packages/runtime-core/package.json b/packages/runtime-core/package.json index 94350274334..5c2541c9ca5 100644 --- a/packages/runtime-core/package.json +++ b/packages/runtime-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-core", - "version": "3.5.15", + "version": "3.5.18", "description": "@vue/runtime-core", "main": "index.js", "module": "dist/runtime-core.esm-bundler.js", diff --git a/packages/runtime-core/src/apiAsyncComponent.ts b/packages/runtime-core/src/apiAsyncComponent.ts index cb675f06e43..ab4ab51b66a 100644 --- a/packages/runtime-core/src/apiAsyncComponent.ts +++ b/packages/runtime-core/src/apiAsyncComponent.ts @@ -123,28 +123,30 @@ export function defineAsyncComponent< __asyncHydrate(el, instance, hydrate) { let patched = false + ;(instance.bu || (instance.bu = [])).push(() => (patched = true)) + const performHydrate = () => { + // skip hydration if the component has been patched + if (patched) { + if (__DEV__) { + warn( + `Skipping lazy hydration for component '${getComponentName(resolvedComp!) || resolvedComp!.__file}': ` + + `it was updated before lazy hydration performed.`, + ) + } + return + } + hydrate() + } const doHydrate = hydrateStrategy ? () => { - const performHydrate = () => { - // skip hydration if the component has been patched - if (__DEV__ && patched) { - warn( - `Skipping lazy hydration for component '${getComponentName(resolvedComp!)}': ` + - `it was updated before lazy hydration performed.`, - ) - return - } - hydrate() - } const teardown = hydrateStrategy(performHydrate, cb => forEachElement(el, cb), ) if (teardown) { ;(instance.bum || (instance.bum = [])).push(teardown) } - ;(instance.u || (instance.u = [])).push(() => (patched = true)) } - : hydrate + : performHydrate if (resolvedComp) { doHydrate() } else { diff --git a/packages/runtime-core/src/apiInject.ts b/packages/runtime-core/src/apiInject.ts index 711c5d84de8..d5c97a52b83 100644 --- a/packages/runtime-core/src/apiInject.ts +++ b/packages/runtime-core/src/apiInject.ts @@ -1,6 +1,5 @@ import { isFunction } from '@vue/shared' -import { currentInstance } from './component' -import { currentRenderingInstance } from './componentRenderContext' +import { currentInstance, getCurrentInstance } from './component' import { currentApp } from './apiCreateApp' import { warn } from './warning' @@ -51,7 +50,7 @@ export function inject( ) { // fallback to `currentRenderingInstance` so that this can be called in // a functional component - const instance = currentInstance || currentRenderingInstance + const instance = getCurrentInstance() // also support looking up from app-level provides w/ `app.runWithContext()` if (instance || currentApp) { @@ -90,5 +89,5 @@ export function inject( * user. One example is `useRoute()` in `vue-router`. */ export function hasInjectionContext(): boolean { - return !!(currentInstance || currentRenderingInstance || currentApp) + return !!(getCurrentInstance() || currentApp) } diff --git a/packages/runtime-core/src/apiSetupHelpers.ts b/packages/runtime-core/src/apiSetupHelpers.ts index 2ddaeb509ad..209b3364cec 100644 --- a/packages/runtime-core/src/apiSetupHelpers.ts +++ b/packages/runtime-core/src/apiSetupHelpers.ts @@ -382,17 +382,17 @@ export function withDefaults< } export function useSlots(): SetupContext['slots'] { - return getContext().slots + return getContext('useSlots').slots } export function useAttrs(): SetupContext['attrs'] { - return getContext().attrs + return getContext('useAttrs').attrs } -function getContext(): SetupContext { +function getContext(calledFunctionName: string): SetupContext { const i = getCurrentInstance()! if (__DEV__ && !i) { - warn(`useContext() called without active instance.`) + warn(`${calledFunctionName}() called without active instance.`) } return i.setupContext || (i.setupContext = createSetupContext(i)) } diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index f191c36df12..60552d736d5 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -585,13 +585,13 @@ export interface ComponentInternalInstance { * For updating css vars on contained teleports * @internal */ - ut?: (vars?: Record) => void + ut?: (vars?: Record) => void /** * dev only. For style v-bind hydration mismatch checks * @internal */ - getCssVars?: () => Record + getCssVars?: () => Record /** * v2 compat only, for caching mutated $options diff --git a/packages/runtime-core/src/componentEmits.ts b/packages/runtime-core/src/componentEmits.ts index db52bc88c33..c03bead3a92 100644 --- a/packages/runtime-core/src/componentEmits.ts +++ b/packages/runtime-core/src/componentEmits.ts @@ -151,10 +151,14 @@ export function emit( } let args = rawArgs - const isModelListener = event.startsWith('update:') + const isCompatModelListener = + __COMPAT__ && compatModelEventPrefix + event in props + const isModelListener = isCompatModelListener || event.startsWith('update:') + const modifiers = isCompatModelListener + ? props.modelModifiers + : isModelListener && getModelModifiers(props, event.slice(7)) // for v-model update:xxx events, apply modifiers on args - const modifiers = isModelListener && getModelModifiers(props, event.slice(7)) if (modifiers) { if (modifiers.trim) { args = rawArgs.map(a => (isString(a) ? a.trim() : a)) diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index 5db6a0a1760..25d21477cfc 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -756,6 +756,7 @@ export function applyOptions(instance: ComponentInternalInstance): void { Object.defineProperty(exposed, key, { get: () => publicThis[key], set: val => (publicThis[key] = val), + enumerable: true, }) }) } else if (!instance.exposed) { diff --git a/packages/runtime-core/src/componentSlots.ts b/packages/runtime-core/src/componentSlots.ts index 3812695431e..380728750f8 100644 --- a/packages/runtime-core/src/componentSlots.ts +++ b/packages/runtime-core/src/componentSlots.ts @@ -86,7 +86,8 @@ export type RawSlots = { __?: number[] } -const isInternalKey = (key: string) => key[0] === '_' || key === '$stable' +const isInternalKey = (key: string) => + key === '_' || key === '__' || key === '_ctx' || key === '$stable' const normalizeSlotValue = (value: unknown): VNode[] => isArray(value) @@ -193,6 +194,10 @@ export const initSlots = ( ): void => { const slots = (instance.slots = createInternalObject()) if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) { + const cacheIndexes = (children as RawSlots).__ + // make cache indexes marker non-enumerable + if (cacheIndexes) def(slots, '__', cacheIndexes, true) + const type = (children as RawSlots)._ if (type) { assignSlots(slots, children as Slots, optimized) diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index 6ffdbc68de4..bdebed59602 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -28,6 +28,7 @@ import { isReservedProp, isString, normalizeClass, + normalizeCssVarValue, normalizeStyle, stringifyStyle, } from '@vue/shared' @@ -945,10 +946,8 @@ function resolveCssVars( ) { const cssVars = instance.getCssVars() for (const key in cssVars) { - expectedMap.set( - `--${getEscapedCssVarName(key, false)}`, - String(cssVars[key]), - ) + const value = normalizeCssVarValue(cssVars[key]) + expectedMap.set(`--${getEscapedCssVarName(key, false)}`, value) } } if (vnode === root && instance.parent) { @@ -997,6 +996,6 @@ function isMismatchAllowed( if (allowedType === MismatchTypes.TEXT && list.includes('children')) { return true } - return allowedAttr.split(',').includes(MismatchTypeString[allowedType]) + return list.includes(MismatchTypeString[allowedType]) } } diff --git a/packages/runtime-core/src/profiling.ts b/packages/runtime-core/src/profiling.ts index 1984f5a21f2..4e832a7c4a9 100644 --- a/packages/runtime-core/src/profiling.ts +++ b/packages/runtime-core/src/profiling.ts @@ -28,12 +28,10 @@ export function endMeasure( if (instance.appContext.config.performance && isSupported()) { const startTag = `vue-${type}-${instance.uid}` const endTag = startTag + `:end` + const measureName = `<${formatComponentName(instance, instance.type)}> ${type}` perf.mark(endTag) - perf.measure( - `<${formatComponentName(instance, instance.type)}> ${type}`, - startTag, - endTag, - ) + perf.measure(measureName, startTag, endTag) + perf.clearMeasures(measureName) perf.clearMarks(startTag) perf.clearMarks(endTag) } diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 7b39aa917a2..f046e93ad85 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -86,6 +86,7 @@ import { isAsyncWrapper } from './apiAsyncComponent' import { isCompatEnabled } from './compat/compatConfig' import { DeprecationTypes } from './compat/compatConfig' import type { TransitionHooks } from './components/BaseTransition' +import type { VueElement } from '@vue/runtime-dom' export interface Renderer { render: RootRenderFunction @@ -484,6 +485,8 @@ function baseCreateRenderer( // set ref if (ref != null && parentComponent) { setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2) + } else if (ref == null && n1 && n1.ref != null) { + setRef(n1.ref, null, parentSuspense, n1, true) } } @@ -1223,6 +1226,7 @@ function baseCreateRenderer( if (!initialVNode.el) { const placeholder = (instance.subTree = createVNode(Comment)) processCommentNode(null, placeholder, container!, anchor) + initialVNode.placeholder = placeholder.el } } else { setupRenderEffect( @@ -1348,7 +1352,11 @@ function baseCreateRenderer( } } else { // custom element style injection - if (root.ce) { + if ( + root.ce && + // @ts-expect-error _def is private + (root.ce as VueElement)._def.shadowRoot !== false + ) { root.ce._injectChildStyle(type) } @@ -1972,8 +1980,12 @@ function baseCreateRenderer( for (i = toBePatched - 1; i >= 0; i--) { const nextIndex = s2 + i const nextChild = c2[nextIndex] as VNode + const anchorVNode = c2[nextIndex + 1] as VNode const anchor = - nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor + nextIndex + 1 < l2 + ? // #13559, fallback to el placeholder for unresolved async component + anchorVNode.el || anchorVNode.placeholder + : parentAnchor if (newIndexToOldIndexMap[i] === 0) { // mount new patch( diff --git a/packages/runtime-core/src/rendererTemplateRef.ts b/packages/runtime-core/src/rendererTemplateRef.ts index ca21030dc35..cea9e5b8438 100644 --- a/packages/runtime-core/src/rendererTemplateRef.ts +++ b/packages/runtime-core/src/rendererTemplateRef.ts @@ -2,6 +2,7 @@ import type { SuspenseBoundary } from './components/Suspense' import type { VNode, VNodeNormalizedRef, VNodeNormalizedRefAtom } from './vnode' import { EMPTY_OBJ, + NO, ShapeFlags, hasOwn, isArray, @@ -77,7 +78,7 @@ export function setRef( const rawSetupState = toRaw(setupState) const canSetSetupRef = setupState === EMPTY_OBJ - ? () => false + ? NO : (key: string) => { if (__DEV__) { if (hasOwn(rawSetupState, key) && !isRef(rawSetupState[key])) { diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index a8c5340cd1f..cd1ef948d73 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -196,6 +196,7 @@ export interface VNode< // DOM el: HostNode | null + placeholder: HostNode | null // async component el placeholder anchor: HostNode | null // fragment anchor target: HostElement | null // teleport target targetStart: HostNode | null // teleport target start anchor @@ -711,6 +712,8 @@ export function cloneVNode( suspense: vnode.suspense, ssContent: vnode.ssContent && cloneVNode(vnode.ssContent), ssFallback: vnode.ssFallback && cloneVNode(vnode.ssFallback), + placeholder: vnode.placeholder, + el: vnode.el, anchor: vnode.anchor, ctx: vnode.ctx, diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts index eee2151716e..07ea091486e 100644 --- a/packages/runtime-dom/__tests__/customElement.spec.ts +++ b/packages/runtime-dom/__tests__/customElement.spec.ts @@ -916,6 +916,30 @@ describe('defineCustomElement', () => { assertStyles(el, [`div { color: blue; }`, `div { color: red; }`]) }) + test("child components should not inject styles to root element's shadow root w/ shadowRoot false", async () => { + const Bar = defineComponent({ + styles: [`div { color: green; }`], + render() { + return 'bar' + }, + }) + const Baz = () => h(Bar) + const Foo = defineCustomElement( + { + render() { + return [h(Baz)] + }, + }, + { shadowRoot: false }, + ) + + customElements.define('my-foo-with-shadowroot-false', Foo) + container.innerHTML = `` + const el = container.childNodes[0] as VueElement + const style = el.shadowRoot?.querySelector('style') + expect(style).toBeUndefined() + }) + test('with nonce', () => { const Foo = defineCustomElement( { @@ -1378,6 +1402,34 @@ describe('defineCustomElement', () => { }) describe('expose', () => { + test('expose w/ options api', async () => { + const E = defineCustomElement({ + data() { + return { + value: 0, + } + }, + methods: { + foo() { + ;(this as any).value++ + }, + }, + expose: ['foo'], + render(_ctx: any) { + return h('div', null, _ctx.value) + }, + }) + customElements.define('my-el-expose-options-api', E) + + container.innerHTML = `` + const e = container.childNodes[0] as VueElement & { + foo: () => void + } + expect(e.shadowRoot!.innerHTML).toBe(`
0
`) + e.foo() + await nextTick() + expect(e.shadowRoot!.innerHTML).toBe(`
1
`) + }) test('expose attributes and callback', async () => { type SetValue = (value: string) => void let fn: MockedFunction @@ -1542,6 +1594,29 @@ describe('defineCustomElement', () => { expect(e.shadowRoot?.innerHTML).toBe('
app-injected
') }) + // #12448 + test('work with async component', async () => { + const AsyncComp = defineAsyncComponent(() => { + return Promise.resolve({ + render() { + const msg: string | undefined = inject('msg') + return h('div', {}, msg) + }, + } as any) + }) + const E = defineCustomElement(AsyncComp, { + configureApp(app) { + app.provide('msg', 'app-injected') + }, + }) + customElements.define('my-async-element-with-app', E) + + container.innerHTML = `` + const e = container.childNodes[0] as VueElement + await new Promise(r => setTimeout(r)) + expect(e.shadowRoot?.innerHTML).toBe('
app-injected
') + }) + test('with hmr reload', async () => { const __hmrId = '__hmrWithApp' const def = defineComponent({ diff --git a/packages/runtime-dom/__tests__/helpers/useCssVars.spec.ts b/packages/runtime-dom/__tests__/helpers/useCssVars.spec.ts index 1fb4cc65fd0..e2102e0c7b8 100644 --- a/packages/runtime-dom/__tests__/helpers/useCssVars.spec.ts +++ b/packages/runtime-dom/__tests__/helpers/useCssVars.spec.ts @@ -465,4 +465,27 @@ describe('useCssVars', () => { render(h(App), root) expect(colorInOnMount).toBe(`red`) }) + + test('should set vars as `initial` for nullish values', async () => { + // `getPropertyValue` cannot reflect the real value for white spaces and JSDOM also + // doesn't 100% reflect the real behavior of browsers, so we only keep the test for + // `initial` value here. + // The value normalization is tested in packages/shared/__tests__/cssVars.spec.ts. + const state = reactive>({ + foo: undefined, + bar: null, + }) + const root = document.createElement('div') + const App = { + setup() { + useCssVars(() => state) + return () => h('div') + }, + } + render(h(App), root) + await nextTick() + const style = (root.children[0] as HTMLElement).style + expect(style.getPropertyValue('--foo')).toBe('initial') + expect(style.getPropertyValue('--bar')).toBe('initial') + }) }) diff --git a/packages/runtime-dom/package.json b/packages/runtime-dom/package.json index 5764f176320..729406f7a68 100644 --- a/packages/runtime-dom/package.json +++ b/packages/runtime-dom/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-dom", - "version": "3.5.15", + "version": "3.5.18", "description": "@vue/runtime-dom", "main": "index.js", "module": "dist/runtime-dom.esm-bundler.js", diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index 56b86a5fd9e..edf7c431353 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -404,9 +404,10 @@ export class VueElement const asyncDef = (this._def as ComponentOptions).__asyncLoader if (asyncDef) { - this._pendingResolve = asyncDef().then(def => - resolve((this._def = def), true), - ) + this._pendingResolve = asyncDef().then((def: InnerComponentDef) => { + def.configureApp = this._def.configureApp + resolve((this._def = def), true) + }) } else { resolve(this._def) } diff --git a/packages/runtime-dom/src/helpers/useCssVars.ts b/packages/runtime-dom/src/helpers/useCssVars.ts index e2bc6de9278..3032143d9a7 100644 --- a/packages/runtime-dom/src/helpers/useCssVars.ts +++ b/packages/runtime-dom/src/helpers/useCssVars.ts @@ -10,14 +10,16 @@ import { warn, watch, } from '@vue/runtime-core' -import { NOOP, ShapeFlags } from '@vue/shared' +import { NOOP, ShapeFlags, normalizeCssVarValue } from '@vue/shared' export const CSS_VAR_TEXT: unique symbol = Symbol(__DEV__ ? 'CSS_VAR_TEXT' : '') /** * Runtime helper for SFC's CSS variable injection feature. * @private */ -export function useCssVars(getter: (ctx: any) => Record): void { +export function useCssVars( + getter: (ctx: any) => Record, +): void { if (!__BROWSER__ && !__TEST__) return const instance = getCurrentInstance() @@ -64,7 +66,7 @@ export function useCssVars(getter: (ctx: any) => Record): void { }) } -function setVarsOnVNode(vnode: VNode, vars: Record) { +function setVarsOnVNode(vnode: VNode, vars: Record) { if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) { const suspense = vnode.suspense! vnode = suspense.activeBranch! @@ -94,13 +96,14 @@ function setVarsOnVNode(vnode: VNode, vars: Record) { } } -function setVarsOnNode(el: Node, vars: Record) { +function setVarsOnNode(el: Node, vars: Record) { if (el.nodeType === 1) { const style = (el as HTMLElement).style let cssText = '' for (const key in vars) { - style.setProperty(`--${key}`, vars[key]) - cssText += `--${key}: ${vars[key]};` + const value = normalizeCssVarValue(vars[key]) + style.setProperty(`--${key}`, value) + cssText += `--${key}: ${value};` } ;(style as any)[CSS_VAR_TEXT] = cssText } diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index ca9a307dd98..c69375983d8 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -58,8 +58,8 @@ declare module '@vue/runtime-core' { vOn: VOnDirective vBind: VModelDirective vIf: Directive - VOnce: Directive - VSlot: Directive + vOnce: Directive + vSlot: Directive } } diff --git a/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts b/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts index 9f33866e5a8..984387bb864 100644 --- a/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts +++ b/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts @@ -203,4 +203,19 @@ describe('ssr: renderStyle', () => { }), ).toBe(`color:"><script;`) }) + + test('useCssVars handling', () => { + expect( + ssrRenderStyle({ + fontSize: null, + ':--v1': undefined, + ':--v2': null, + ':--v3': '', + ':--v4': ' ', + ':--v5': 'foo', + ':--v6': 0, + '--foo': 1, + }), + ).toBe(`--v1:initial;--v2:initial;--v3: ;--v4: ;--v5:foo;--v6:0;--foo:1;`) + }) }) diff --git a/packages/server-renderer/__tests__/ssrSlot.spec.ts b/packages/server-renderer/__tests__/ssrSlot.spec.ts index 4cc7fd97ef2..214e6ee840b 100644 --- a/packages/server-renderer/__tests__/ssrSlot.spec.ts +++ b/packages/server-renderer/__tests__/ssrSlot.spec.ts @@ -111,26 +111,106 @@ describe('ssr: slot', () => { }) test('transition slot', async () => { + const ReusableTransition = { + template: ``, + } + + const ReusableTransitionWithAppear = { + template: ``, + } + expect( await renderToString( createApp({ components: { - one: { - template: ``, - }, + one: ReusableTransition, }, template: `
foo
`, }), ), ).toBe(``) + expect(await renderToString(createApp(ReusableTransition))).toBe(``) + + expect(await renderToString(createApp(ReusableTransitionWithAppear))).toBe( + ``, + ) + expect( await renderToString( createApp({ components: { - one: { - template: ``, - }, + one: ReusableTransition, + }, + template: ``, + }), + ), + ).toBe(``) + + expect( + await renderToString( + createApp({ + components: { + one: ReusableTransitionWithAppear, + }, + template: ``, + }), + ), + ).toBe(``) + + expect( + await renderToString( + createApp({ + render() { + return h(ReusableTransition, null, { + default: () => null, + }) + }, + }), + ), + ).toBe(``) + + expect( + await renderToString( + createApp({ + render() { + return h(ReusableTransitionWithAppear, null, { + default: () => null, + }) + }, + }), + ), + ).toBe(``) + + expect( + await renderToString( + createApp({ + render() { + return h(ReusableTransitionWithAppear, null, { + default: () => [], + }) + }, + }), + ), + ).toBe(``) + + expect( + await renderToString( + createApp({ + render() { + return h(ReusableTransition, null, { + default: () => [], + }) + }, + }), + ), + ).toBe(``) + + expect( + await renderToString( + createApp({ + components: { + one: ReusableTransition, }, template: `
foo
`, }), diff --git a/packages/server-renderer/package.json b/packages/server-renderer/package.json index d798e4335c5..0fe43c72cee 100644 --- a/packages/server-renderer/package.json +++ b/packages/server-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@vue/server-renderer", - "version": "3.5.15", + "version": "3.5.18", "description": "@vue/server-renderer", "main": "index.js", "module": "dist/server-renderer.esm-bundler.js", diff --git a/packages/server-renderer/src/helpers/ssrRenderAttrs.ts b/packages/server-renderer/src/helpers/ssrRenderAttrs.ts index 9689b4185c6..b082da03fe8 100644 --- a/packages/server-renderer/src/helpers/ssrRenderAttrs.ts +++ b/packages/server-renderer/src/helpers/ssrRenderAttrs.ts @@ -1,5 +1,7 @@ import { escapeHtml, + isArray, + isObject, isRenderableAttrValue, isSVGTag, stringifyStyle, @@ -12,6 +14,7 @@ import { isString, makeMap, normalizeClass, + normalizeCssVarValue, normalizeStyle, propsToAttrMap, } from '@vue/shared' @@ -93,6 +96,22 @@ export function ssrRenderStyle(raw: unknown): string { if (isString(raw)) { return escapeHtml(raw) } - const styles = normalizeStyle(raw) + const styles = normalizeStyle(ssrResetCssVars(raw)) return escapeHtml(stringifyStyle(styles)) } + +function ssrResetCssVars(raw: unknown) { + if (!isArray(raw) && isObject(raw)) { + const res: Record = {} + for (const key in raw) { + // `:` prefixed keys are coming from `ssrCssVars` + if (key.startsWith(':--')) { + res[key.slice(1)] = normalizeCssVarValue(raw[key]) + } else { + res[key] = raw[key] + } + } + return res + } + return raw +} diff --git a/packages/server-renderer/src/helpers/ssrRenderSlot.ts b/packages/server-renderer/src/helpers/ssrRenderSlot.ts index 19aa4ce63b7..2f93a12de9d 100644 --- a/packages/server-renderer/src/helpers/ssrRenderSlot.ts +++ b/packages/server-renderer/src/helpers/ssrRenderSlot.ts @@ -74,6 +74,8 @@ export function ssrRenderSlotInner( ) } else if (fallbackRenderFn) { fallbackRenderFn() + } else if (transition) { + push(``) } } else { // ssr slot. @@ -110,13 +112,19 @@ export function ssrRenderSlotInner( end-- } - for (let i = start; i < end; i++) { - push(slotBuffer[i]) + if (start < end) { + for (let i = start; i < end; i++) { + push(slotBuffer[i]) + } + } else if (transition) { + push(``) } } } } else if (fallbackRenderFn) { fallbackRenderFn() + } else if (transition) { + push(``) } } diff --git a/packages/shared/__tests__/cssVars.spec.ts b/packages/shared/__tests__/cssVars.spec.ts new file mode 100644 index 00000000000..747ab067d25 --- /dev/null +++ b/packages/shared/__tests__/cssVars.spec.ts @@ -0,0 +1,27 @@ +import { normalizeCssVarValue } from '../src' + +describe('utils/cssVars', () => { + test('should normalize css binding values correctly', () => { + expect(normalizeCssVarValue(null)).toBe('initial') + expect(normalizeCssVarValue(undefined)).toBe('initial') + expect(normalizeCssVarValue('')).toBe(' ') + expect(normalizeCssVarValue(' ')).toBe(' ') + expect(normalizeCssVarValue('foo')).toBe('foo') + expect(normalizeCssVarValue(0)).toBe('0') + }) + + test('should warn on invalid css binding values', () => { + const warning = + '[Vue warn] Invalid value used for CSS binding. Expected a string or a finite number but received:' + expect(normalizeCssVarValue(NaN)).toBe('NaN') + expect(warning).toHaveBeenWarnedTimes(1) + expect(normalizeCssVarValue(Infinity)).toBe('Infinity') + expect(warning).toHaveBeenWarnedTimes(2) + expect(normalizeCssVarValue(-Infinity)).toBe('-Infinity') + expect(warning).toHaveBeenWarnedTimes(3) + expect(normalizeCssVarValue({})).toBe('[object Object]') + expect(warning).toHaveBeenWarnedTimes(4) + expect(normalizeCssVarValue([])).toBe('') + expect(warning).toHaveBeenWarnedTimes(5) + }) +}) diff --git a/packages/shared/package.json b/packages/shared/package.json index c5ffec0d314..63a10598ce8 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@vue/shared", - "version": "3.5.15", + "version": "3.5.18", "description": "internal utils shared across @vue packages", "main": "index.js", "module": "dist/shared.esm-bundler.js", diff --git a/packages/shared/src/cssVars.ts b/packages/shared/src/cssVars.ts new file mode 100644 index 00000000000..0c69b606f5d --- /dev/null +++ b/packages/shared/src/cssVars.ts @@ -0,0 +1,24 @@ +/** + * Normalize CSS var value created by `v-bind` in `