Compare commits

..

72 Commits

Author SHA1 Message Date
renovate[bot] eca0e1ccff
chore(deps): update build (#13542)
ci / test (push) Waiting to run Details
ci / continuous-release (push) Waiting to run Details
size data / upload (push) Waiting to run Details
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-04 11:47:51 +08:00
GU Yiling c85f1b5a13
fix(css-vars): nullish v-bind in style should not lead to unexpected inheritance (#12461)
ci / test (push) Waiting to run Details
ci / continuous-release (push) Waiting to run Details
size data / upload (push) Waiting to run Details
close #12434
close #12439
close #7474
close #7475
2025-07-03 16:20:28 +08:00
renovate[bot] 7e133dbe01
chore(deps): update all non-major dependencies (#13541)
ci / test (push) Waiting to run Details
ci / continuous-release (push) Waiting to run Details
size data / upload (push) Waiting to run Details
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-03 08:16:56 +08:00
renovate[bot] ba391f5fdf
chore(deps): update dependency vite to v5.4.19 [security] (#13517)
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details
size data / upload (push) Has been cancelled Details
Lock Closed Issues / action (push) Has been cancelled Details
Auto close issues with "can't reproduce" label / close-issues (push) Has been cancelled Details
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 14:33:04 +08:00
renovate[bot] 50a1c30899
chore(deps): update build (#13516)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 14:32:41 +08:00
renovate[bot] b8b926cdee
chore(deps): update all non-major dependencies (#13515)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 14:04:52 +08:00
daiwei 5f8314cb7f release: v3.5.17
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details
size data / upload (push) Has been cancelled Details
2025-06-18 21:14:18 +08:00
edison 15520954f9
fix(ssr): handle initial selected state for select with v-model + v-for/v-if option (#13487)
close #13486
2025-06-18 20:54:32 +08:00
Tycho f3479aac96
fix(compiler-sfc): improved type resolution for function type aliases (#13452)
close #13444
2025-06-18 20:54:09 +08:00
edison 919c44744b
fix(slots): make cache indexes marker non-enumerable (#13469)
close #13468
2025-06-18 20:53:48 +08:00
Mark Florian cb14b860f1
fix(compat): allow v-model built in modifiers on component (#12654)
close #12652
2025-06-18 20:53:25 +08:00
renovate[bot] 9818456f20
fix(deps): update compiler (#13480)
ci / test (push) Waiting to run Details
ci / continuous-release (push) Waiting to run Details
size data / upload (push) Waiting to run Details
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 15:08:15 +08:00
renovate[bot] 52571655f8
chore(deps): update dependency vite to v5.4.19 [security] (#13281)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 15:08:00 +08:00
renovate[bot] c0c9c5baea
chore(deps): update build (#13479)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 15:02:46 +08:00
renovate[bot] 532cfae349
chore(deps): update autofix-ci/action digest to 635ffb0 (#13450)
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details
size data / upload (push) Has been cancelled Details
Lock Closed Issues / action (push) Has been cancelled Details
Auto close issues with "can't reproduce" label / close-issues (push) Has been cancelled Details
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 09:42:04 +08:00
renovate[bot] c91afec9c2
fix(deps): update dependency @vue/repl to ^4.6.1 (#13471)
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details
size data / upload (push) Has been cancelled Details
Lock Closed Issues / action (push) Has been cancelled Details
Auto close issues with "can't reproduce" label / close-issues (push) Has been cancelled Details
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-13 11:54:02 +08:00
edison 4e9e9ceeb1
chore(deps): update @vue/repl to version 4.6.0 (#13470) 2025-06-13 11:03:48 +08:00
renovate[bot] a6e2032f43
chore(deps): update all non-major dependencies (#13451)
ci / test (push) Waiting to run Details
ci / continuous-release (push) Waiting to run Details
size data / upload (push) Waiting to run Details
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-13 08:26:12 +08:00
xiejiahe 6c68421e4d
chore(build): replace node:fs repeated import (#13467) 2025-06-13 08:24:57 +08:00
linzhe c7d3207cde
chore: export `jsxs` type (#13463) 2025-06-13 08:24:27 +08:00
pengbo cdffaf6b9e
chore(compile-core): removed the optional tag parameter from condenseWhitespace's signature (#13437)
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details
size data / upload (push) Has been cancelled Details
Lock Closed Issues / action (push) Has been cancelled Details
Auto close issues with "can't reproduce" label / close-issues (push) Has been cancelled Details
canary release / canary (push) Has been cancelled Details
canary minor release / canary (push) Has been cancelled Details
2025-06-06 08:25:02 +08:00
renovate[bot] a47832e75e
chore(deps): update dependency esbuild to ^0.25.5 (#13427)
ci / test (push) Waiting to run Details
ci / continuous-release (push) Waiting to run Details
size data / upload (push) Waiting to run Details
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 10:39:31 +08:00
renovate[bot] e416f84d74
chore(deps): update all non-major dependencies (#13426)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 10:38:49 +08:00
daiwei 1b98aafa05 chore: update snap 2025-06-05 10:26:52 +08:00
Alex Snezhko 55dad625ac
fix(compiler-core): prevent comments from blocking static node hoisting (#13345)
close #13344
2025-06-05 10:23:00 +08:00
inottn 47ddf98602
fix(runtime-core): unset old ref during patching when new ref is absent (#12900)
fix #12898
2025-06-05 10:19:48 +08:00
SerKo e8d8f5f604
fix(reactivity): add `__v_skip` flag to `Dep` to prevent reactive conversion (#12804)
close #12803
2025-06-05 10:19:16 +08:00
edison 5ba1afba09
fix(custom-element): ensure configureApp is applied to async component (#12607)
close #12448
2025-06-05 10:10:52 +08:00
edison 73055d8d95
fix(custom-element): prevent injecting child styles if shadowRoot is false (#12769)
close #12630
2025-06-05 10:02:26 +08:00
Arman Tang 4aa7a4aba3
chore(shared): update patch flag name (#12831) 2025-06-05 10:01:51 +08:00
edison 4eb46e443f
fix(compile-sfc): handle mapped types work with omit and pick (#12648)
close #12647
2025-06-05 10:01:22 +08:00
edison 10ebcef8c8
fix(compiler-core): ignore whitespace when matching adjacent v-if (#12321)
close #9173
2025-06-05 09:44:25 +08:00
山吹色御守 f05a8d613b
fix(compiler-core): do not increase newlines in `InEntity` state (#13362) 2025-06-05 09:39:05 +08:00
山吹色御守 762fae4b57
fix(types): typo of `vOnce` and `vSlot` (#13343) 2025-06-05 09:36:47 +08:00
skirtle e53a4ffbe0
chore(hydration): reuse existing variable (#13412)
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details
size data / upload (push) Has been cancelled Details
canary release / canary (push) Has been cancelled Details
canary minor release / canary (push) Has been cancelled Details
2025-05-30 14:43:23 +08:00
btea 62f2aa11a2
test(compiler-sfc): direct descendant wildcard rule selector (#13411)
add test case for #13387
2025-05-30 10:35:27 +08:00
daiwei a60f2bd371 chore: update changelog
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details
size data / upload (push) Has been cancelled Details
2025-05-29 09:04:53 +08:00
daiwei e7381761cc release: v3.5.16 2025-05-29 08:24:16 +08:00
edison 19f23b180b
Revert "fix(compiler-sfc): add scoping tag to trailing universal selector (#1…" (#13406)
This reverts commit 949df80880.
2025-05-29 08:21:19 +08:00
Arpit Jain d9bd436b1a
chore: fix typos
ci / test (push) Waiting to run Details
ci / continuous-release (push) Waiting to run Details
size data / upload (push) Waiting to run Details
2025-05-28 08:26:28 +08:00
edison 42f879fcab
Revert "fix(compiler-sfc): add error handling for defineModel() without varia…" (#13390)
ci / test (push) Waiting to run Details
ci / continuous-release (push) Waiting to run Details
size data / upload (push) Waiting to run Details
This reverts commit 00734afef5.
2025-05-27 18:49:34 +08:00
daiwei d5ada3d235 release: v3.5.15
ci / test (push) Waiting to run Details
ci / continuous-release (push) Waiting to run Details
size data / upload (push) Waiting to run Details
2025-05-26 20:38:56 +08:00
renovate[bot] c017c7b8d3
chore(deps): update dependency lint-staged to v16 (#13381)
ci / test (push) Waiting to run Details
ci / continuous-release (push) Waiting to run Details
size data / upload (push) Waiting to run Details
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-26 09:45:16 +08:00
renovate[bot] 004f488a67
chore(deps): update dependency eslint-plugin-import-x to ^4.13.1 (#13380)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-26 09:41:35 +08:00
renovate[bot] 1ee3fcf929
chore(deps): update dependency @vitest/eslint-plugin to ^1.2.1 (#13379)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-26 09:28:23 +08:00
renovate[bot] c46d9f37ae
chore(deps): update build (#13378)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-26 09:27:29 +08:00
edison 9fa787cfd2
chore(workflow): add TypeScript type checking step to CI pipeline (#13367)
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details
size data / upload (push) Has been cancelled Details
Lock Closed Issues / action (push) Has been cancelled Details
Auto close issues with "can't reproduce" label / close-issues (push) Has been cancelled Details
2025-05-22 15:48:20 +08:00
edison 2c6c0794a1
chore: fix type check error (#13366)
ci / test (push) Waiting to run Details
ci / continuous-release (push) Waiting to run Details
size data / upload (push) Waiting to run Details
2025-05-22 11:38:58 +08:00
yangxiuxiu 93949e6587
fix(compat): should not warn COMPILER_V_BIND_OBJECT_ORDER when using v-bind together with v-for (#12993)
fix #12992
2025-05-22 08:44:34 +08:00
thecodewarrior 949df80880
fix(compiler-sfc): add scoping tag to trailing universal selector (#12918)
close #12906
2025-05-22 08:41:27 +08:00
edison a683c80cf4
fix(custom-element): properly resolve props for sync component defs (#12855)
close #12854
2025-05-22 08:38:27 +08:00
edison 1d41d4de7f
fix(custom-element): ensure proper remount and prevent redundant slot parsing with shadowRoot false (#13201)
close #13199
2025-05-22 08:05:39 +08:00
Tycho 5179d328d9
fix(types): exclude `undefined` from inferred prop types with default values (#13007)
close #13006
2025-05-22 08:03:33 +08:00
renovate[bot] d53daf1f29
chore(deps): update all non-major dependencies (#13353)
ci / test (push) Waiting to run Details
ci / continuous-release (push) Waiting to run Details
size data / upload (push) Waiting to run Details
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-21 09:20:45 +08:00
renovate[bot] 2b894746bd
chore(deps): update test (#13354)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-21 09:17:42 +08:00
renovate[bot] 64d2ba9f47
chore(deps): update dependency rollup to ^4.41.0 (#13356)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-21 09:07:01 +08:00
renovate[bot] dc18a159e8
chore(deps): update lint (#13357)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-21 09:06:37 +08:00
Runyasak Chaengnaimuang 00734afef5
fix(compiler-sfc): add error handling for defineModel() without variable assignment (#13352)
close #13280
2025-05-21 09:06:05 +08:00
edison 89edc6cdcb
fix(compile-sfc): handle inline template source map in prod build (#12701)
ci / test (push) Waiting to run Details
ci / continuous-release (push) Waiting to run Details
size data / upload (push) Waiting to run Details
close #12682
close vitejs/vite-plugin-vue#500
2025-05-20 08:46:01 +08:00
Tycho f44feed6fa
fix(types): avoid merging component instance into `$props` in `ComponentInstance` (#12870)
close #12751
2025-05-20 08:44:35 +08:00
Tycho c69c4bb59c
fix(watch): update `oldValue` before running `cb` to prevent stale value (#12296)
close #12294
2025-05-20 08:44:13 +08:00
edison 1a664749d4
fix(compat): ensure false value on input retains value attribute (#13216)
close #13205
2025-05-20 08:43:51 +08:00
edison 013749e75e
fix(custom-element): preserve appContext during update (#12455)
close #12453
2025-05-20 08:34:36 +08:00
edison 35aeae7fa3
fix(hydration): handle transition appear hydration edge case (#13339)
close #13335
2025-05-20 08:28:43 +08:00
edison d15dce3142
fix(teleport): handle deferred teleport updates before and after mount (#13350)
close #13349
2025-05-20 08:27:54 +08:00
edison 163b3651d1
fix(compiler-dom): improve HTML nesting validation to allow any child element within template tag (#13320)
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details
size data / upload (push) Has been cancelled Details
Lock Closed Issues / action (push) Has been cancelled Details
Auto close issues with "can't reproduce" label / close-issues (push) Has been cancelled Details
close #13318
2025-05-16 09:05:31 +08:00
Teages f7ce5ae666
fix(compiler-sfc): simulate `allowArbitraryExtensions` on resolving type (#13301)
close #13295
2025-05-16 08:38:47 +08:00
edison 772b0087cb
fix(suspense): handle edge case in patching list nodes within Suspense (#13306)
close #13305
2025-05-16 08:32:55 +08:00
linzhe cf5a5e0edf
fix(compiler-sfc): improve type inference for TSTypeAliasDeclaration with better runtime type detection (#13245)
close #13240
2025-05-16 08:32:13 +08:00
edison d37a2ac59d
fix(compiler-core): ensure mapping is added only if node source is available (#13285)
close #13261
close vitejs/vite-plugin-vue#368
2025-05-16 08:22:37 +08:00
edison 80055fddfb
fix(hydration): skip lazy hydration for patched components (#13283)
close #13255
2025-05-16 08:22:01 +08:00
Adrian Cerbaro b9910755a5
fix(custom-element): allow injecting values ​​from app context in nested elements (#13219)
close #13212)
2025-05-16 08:07:32 +08:00
98 changed files with 2623 additions and 1243 deletions

View File

@ -31,4 +31,4 @@ jobs:
- name: Run prettier
run: pnpm run format
- uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27

View File

@ -104,5 +104,8 @@ jobs:
- name: Run prettier
run: pnpm run format-check
- name: Run tsc
run: pnpm run check
- name: Run type declaration tests
run: pnpm run test-dts

View File

@ -1,3 +1,62 @@
## [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)
### Bug Fixes
* **compat:** ensure false value on input retains value attribute ([#13216](https://github.com/vuejs/core/issues/13216)) ([1a66474](https://github.com/vuejs/core/commit/1a664749d4d65a345589a6d78106ede7574cb2e1)), closes [#13205](https://github.com/vuejs/core/issues/13205)
* **compat:** should not warn COMPILER_V_BIND_OBJECT_ORDER when using v-bind together with v-for ([#12993](https://github.com/vuejs/core/issues/12993)) ([93949e6](https://github.com/vuejs/core/commit/93949e6587ee019bccd5b8b9d76f0e1ed6ea16fc)), closes [#12992](https://github.com/vuejs/core/issues/12992)
* **compile-sfc:** handle inline template source map in prod build ([#12701](https://github.com/vuejs/core/issues/12701)) ([89edc6c](https://github.com/vuejs/core/commit/89edc6cdcbd34ea6394927ecbfaa61dc4f871de7)), closes [#12682](https://github.com/vuejs/core/issues/12682) [vitejs/vite-plugin-vue#500](https://github.com/vitejs/vite-plugin-vue/issues/500)
* **compiler-core:** ensure mapping is added only if node source is available ([#13285](https://github.com/vuejs/core/issues/13285)) ([d37a2ac](https://github.com/vuejs/core/commit/d37a2ac59d904ac0e3257ba552b6c04920a363f0)), closes [#13261](https://github.com/vuejs/core/issues/13261) [vitejs/vite-plugin-vue#368](https://github.com/vitejs/vite-plugin-vue/issues/368)
* **compiler-dom:** improve HTML nesting validation to allow any child element within template tag ([#13320](https://github.com/vuejs/core/issues/13320)) ([163b365](https://github.com/vuejs/core/commit/163b3651d174321911648a164052effa9249a2aa)), closes [#13318](https://github.com/vuejs/core/issues/13318)
* **compiler-sfc:** add error handling for defineModel() without variable assignment ([#13352](https://github.com/vuejs/core/issues/13352)) ([00734af](https://github.com/vuejs/core/commit/00734afef5f7bddbdaee52aa5359a6ef989f32d3)), closes [#13280](https://github.com/vuejs/core/issues/13280)
* **compiler-sfc:** add scoping tag to trailing universal selector ([#12918](https://github.com/vuejs/core/issues/12918)) ([949df80](https://github.com/vuejs/core/commit/949df808809fd7cccf7718797beab0654aa68302)), closes [#12906](https://github.com/vuejs/core/issues/12906)
* **compiler-sfc:** improve type inference for TSTypeAliasDeclaration with better runtime type detection ([#13245](https://github.com/vuejs/core/issues/13245)) ([cf5a5e0](https://github.com/vuejs/core/commit/cf5a5e0edf0efcab25c27aa2d13eba91f7372d39)), closes [#13240](https://github.com/vuejs/core/issues/13240)
* **compiler-sfc:** simulate `allowArbitraryExtensions` on resolving type ([#13301](https://github.com/vuejs/core/issues/13301)) ([f7ce5ae](https://github.com/vuejs/core/commit/f7ce5ae666129339c006b339437c2dff6bceffe0)), closes [#13295](https://github.com/vuejs/core/issues/13295)
* **custom-element:** allow injecting values from app context in nested elements ([#13219](https://github.com/vuejs/core/issues/13219)) ([b991075](https://github.com/vuejs/core/commit/b9910755a50c7d6c52b28c3aef20cf97810295c9)), closes [#13212](https://github.com/vuejs/core/issues/13212)
* **custom-element:** ensure proper remount and prevent redundant slot parsing with shadowRoot false ([#13201](https://github.com/vuejs/core/issues/13201)) ([1d41d4d](https://github.com/vuejs/core/commit/1d41d4de7f64a37160c8171d0137fd8d35c346c9)), closes [#13199](https://github.com/vuejs/core/issues/13199)
* **custom-element:** preserve appContext during update ([#12455](https://github.com/vuejs/core/issues/12455)) ([013749e](https://github.com/vuejs/core/commit/013749e75ef3b51762a86da379ea4ba4501b54ae)), closes [#12453](https://github.com/vuejs/core/issues/12453)
* **custom-element:** properly resolve props for sync component defs ([#12855](https://github.com/vuejs/core/issues/12855)) ([a683c80](https://github.com/vuejs/core/commit/a683c80cf44ecc482f8ac9c76bf2381443c1b0bb)), closes [#12854](https://github.com/vuejs/core/issues/12854)
* **hydration:** handle transition appear hydration edge case ([#13339](https://github.com/vuejs/core/issues/13339)) ([35aeae7](https://github.com/vuejs/core/commit/35aeae7fa3168adcf9ed95fd35495d17c8b93eeb)), closes [#13335](https://github.com/vuejs/core/issues/13335)
* **hydration:** skip lazy hydration for patched components ([#13283](https://github.com/vuejs/core/issues/13283)) ([80055fd](https://github.com/vuejs/core/commit/80055fddfb3ca1e2a44f19c7f0ffaeba00de5140)), closes [#13255](https://github.com/vuejs/core/issues/13255)
* **suspense:** handle edge case in patching list nodes within Suspense ([#13306](https://github.com/vuejs/core/issues/13306)) ([772b008](https://github.com/vuejs/core/commit/772b0087cb7be151c514a1d30365fb0f61a652ba)), closes [#13305](https://github.com/vuejs/core/issues/13305)
* **teleport:** handle deferred teleport updates before and after mount ([#13350](https://github.com/vuejs/core/issues/13350)) ([d15dce3](https://github.com/vuejs/core/commit/d15dce3142474f2ef9fffed38383acdadcb26c4c)), closes [#13349](https://github.com/vuejs/core/issues/13349)
* **types:** avoid merging component instance into `$props` in `ComponentInstance` ([#12870](https://github.com/vuejs/core/issues/12870)) ([f44feed](https://github.com/vuejs/core/commit/f44feed6fa461a9c4c724e9631c19e9e214c0a20)), closes [#12751](https://github.com/vuejs/core/issues/12751)
* **types:** exclude `undefined` from inferred prop types with default values ([#13007](https://github.com/vuejs/core/issues/13007)) ([5179d32](https://github.com/vuejs/core/commit/5179d328d950015e7fb2a74fe1a8518fd8d2c94e)), closes [#13006](https://github.com/vuejs/core/issues/13006)
* **watch:** update `oldValue` before running `cb` to prevent stale value ([#12296](https://github.com/vuejs/core/issues/12296)) ([c69c4bb](https://github.com/vuejs/core/commit/c69c4bb59c114f2b5e03733b55ef9ace3087b5c3)), closes [#12294](https://github.com/vuejs/core/issues/12294)
## [3.5.14](https://github.com/vuejs/core/compare/v3.5.13...v3.5.14) (2025-05-15)

View File

@ -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 `<script setup>` sfc template ([3f99e23](https://github.com/vuejs/core/commit/3f99e239e03a8861c462d4ee91feb82066ab3e28))
- **runtime-core:** explicit expose API ([0e59770](https://github.com/vuejs/core/commit/0e59770b9282992f6a5af4d8fef33dafb948fc8b))
@ -282,7 +282,7 @@ may cause build issues in projects still using TS 3.x.
- **runtime-core:** fix directive merging on component root ([4d1ebb5](https://github.com/vuejs/core/commit/4d1ebb5deb4c1cb2a02e8482bf8f9cc87197b088)), closes [#2298](https://github.com/vuejs/core/issues/2298)
- **runtime-core:** fix duplicated unmount traversal in optimized mode ([376883d](https://github.com/vuejs/core/commit/376883d1cfea6ed92807cce1f1209f943a04b625)), closes [#2169](https://github.com/vuejs/core/issues/2169)
- **runtime-core:** fix provide function data access in extends/mixins ([f06518a](https://github.com/vuejs/core/commit/f06518a8c9201b4fa2a956595aa9d89a192fcd20)), closes [#2300](https://github.com/vuejs/core/issues/2300)
- **runtime-core:** fix SSR memoery leak due to props normalization cache ([a66e53a](https://github.com/vuejs/core/commit/a66e53a24f445b688eef6812ecb872dc53cf2702)), closes [#2225](https://github.com/vuejs/core/issues/2225)
- **runtime-core:** fix SSR memory leak due to props normalization cache ([a66e53a](https://github.com/vuejs/core/commit/a66e53a24f445b688eef6812ecb872dc53cf2702)), closes [#2225](https://github.com/vuejs/core/issues/2225)
- **runtime-core:** make errorCaptured return value handling consistent with Vue 2 ([#2289](https://github.com/vuejs/core/issues/2289)) ([4d20ac8](https://github.com/vuejs/core/commit/4d20ac8173f84c87288255dcc03c62a6ee862a23)), closes [#2267](https://github.com/vuejs/core/issues/2267)
- **runtime-core:** use consistent camelCase event casing for render functions ([#2278](https://github.com/vuejs/core/issues/2278)) ([62f2617](https://github.com/vuejs/core/commit/62f26173ba715fd8bf2b131e19d94275106e830d)), closes [#2249](https://github.com/vuejs/core/issues/2249)
- **runtime-core:** vnode.el is null in watcher after rerendering ([#2295](https://github.com/vuejs/core/issues/2295)) ([28d5fd7](https://github.com/vuejs/core/commit/28d5fd7a2871c10df3427dfbbe0e203c2a976cb4)), closes [#2170](https://github.com/vuejs/core/issues/2170)
@ -450,7 +450,7 @@ may cause build issues in projects still using TS 3.x.
- **compiler-core:** should attach key to single element child of `<template v-for>` ([#1910](https://github.com/vuejs/core/issues/1910)) ([69cfed6](https://github.com/vuejs/core/commit/69cfed6b313821d1ae7ecb02b63b0aaccb5599c6))
- **reactivity:** unwrap non-index accessed refs on reactive arrays ([#1859](https://github.com/vuejs/core/issues/1859)) ([3c05f8b](https://github.com/vuejs/core/commit/3c05f8bbd6cd0e01bbc5830730852f9a93d8de8a)), closes [#1846](https://github.com/vuejs/core/issues/1846)
- **runtime-core:** correctly track dynamic nodes in renderSlot ([#1911](https://github.com/vuejs/core/issues/1911)) ([7ffb79c](https://github.com/vuejs/core/commit/7ffb79c56318861075a47bd2357e34cde8a6dad9))
- **runtime-core:** disable block tracking when calling compiled slot function in tempalte expressions ([f02e2f9](https://github.com/vuejs/core/commit/f02e2f99d9c2ca95f4fd984d7bd62178eceaa214)), closes [#1745](https://github.com/vuejs/core/issues/1745) [#1918](https://github.com/vuejs/core/issues/1918)
- **runtime-core:** disable block tracking when calling compiled slot function in template expressions ([f02e2f9](https://github.com/vuejs/core/commit/f02e2f99d9c2ca95f4fd984d7bd62178eceaa214)), closes [#1745](https://github.com/vuejs/core/issues/1745) [#1918](https://github.com/vuejs/core/issues/1918)
- **teleport:** only inherit el for non-patched nodes ([d4cc7b2](https://github.com/vuejs/core/commit/d4cc7b2496f9ed21ef6cac426697eac058da76bb)), closes [#1903](https://github.com/vuejs/core/issues/1903)
### Performance Improvements
@ -631,7 +631,7 @@ may cause build issues in projects still using TS 3.x.
- **runtime-dom/v-on:** only block event handlers based on attach timestamp ([8b320cc](https://github.com/vuejs/core/commit/8b320cc12f74aafea9ec69f7ce70231d4f0d08fd)), closes [#1565](https://github.com/vuejs/core/issues/1565)
- **slots:** differentiate dynamic/static compiled slots ([65beba9](https://github.com/vuejs/core/commit/65beba98fe5793133d3218945218b9e3f8d136eb)), closes [#1557](https://github.com/vuejs/core/issues/1557)
- **v-on:** capitalize dynamic event names ([9152a89](https://github.com/vuejs/core/commit/9152a8901653d7cef864a52a3c618afcc70d827d))
- **v-on:** refactor DOM event options modifer handling ([380c679](https://github.com/vuejs/core/commit/380c6792d8899f1a43a9e6400c5df483c63290b6)), closes [#1567](https://github.com/vuejs/core/issues/1567)
- **v-on:** refactor DOM event options modifier handling ([380c679](https://github.com/vuejs/core/commit/380c6792d8899f1a43a9e6400c5df483c63290b6)), closes [#1567](https://github.com/vuejs/core/issues/1567)
### Features
@ -743,7 +743,7 @@ may cause build issues in projects still using TS 3.x.
- **compiler-core:** fix parsing for directive with dynamic argument containing dots ([0d26413](https://github.com/vuejs/core/commit/0d26413433d41389f5525a0ef2c2dd7cfbb454d4))
- **compiler-core:** support static slot names containing dots for 2.x compat ([825ec15](https://github.com/vuejs/core/commit/825ec1500feda8b0c43245e7e92074af7f9dcca2)), closes [#1241](https://github.com/vuejs/core/issues/1241)
- **hmr:** force full update on nested child components ([#1312](https://github.com/vuejs/core/issues/1312)) ([8f2a748](https://github.com/vuejs/core/commit/8f2a7489b7c74f5cfc1844697c60287c37fc0eb8))
- **reactivity:** fix toRaw for objects prototype inherting reactive ([10bb34b](https://github.com/vuejs/core/commit/10bb34bb869a47c37d945f8c80abf723fac9fc1a)), closes [#1246](https://github.com/vuejs/core/issues/1246)
- **reactivity:** fix toRaw for objects prototype inheriting reactive ([10bb34b](https://github.com/vuejs/core/commit/10bb34bb869a47c37d945f8c80abf723fac9fc1a)), closes [#1246](https://github.com/vuejs/core/issues/1246)
- **runtime-core:** should pass instance to patchProp on mount for event error handling ([#1337](https://github.com/vuejs/core/issues/1337)) ([aac9b03](https://github.com/vuejs/core/commit/aac9b03c11c9be0c67b924004364a42d04d78195)), closes [#1336](https://github.com/vuejs/core/issues/1336)
- **runtime-core:** track access to $attrs ([6abac87](https://github.com/vuejs/core/commit/6abac87b3d1b7a22df80b7a70a10101a7f3d3732)), closes [#1346](https://github.com/vuejs/core/issues/1346)
- always treat spellcheck and draggable as attributes ([4492b88](https://github.com/vuejs/core/commit/4492b88938922a7f1bcc36a608375ad99f16b22e)), closes [#1350](https://github.com/vuejs/core/issues/1350)
@ -863,7 +863,7 @@ may cause build issues in projects still using TS 3.x.
### Bug Fixes
- **compiler:** bail strigification on runtime constant expressions ([f9a3766](https://github.com/vuejs/core/commit/f9a3766fd68dc6996cdbda6475287c4005f55243))
- **compiler:** bail stringification on runtime constant expressions ([f9a3766](https://github.com/vuejs/core/commit/f9a3766fd68dc6996cdbda6475287c4005f55243))
- **transitionGroup:** fix transition children resolving condition ([f05aeea](https://github.com/vuejs/core/commit/f05aeea7aec2e6cd859f40edc6236afd0ce2ea7d))
### Features

View File

@ -28,7 +28,7 @@
- **build:** avoid using async/await syntax ([438754a](https://github.com/vuejs/core/commit/438754a0d1428d10e27d1a290beb4b81da5fdaeb))
- **build:** fix generated code containing unprocessed class field syntax ([2788154](https://github.com/vuejs/core/commit/2788154f7707928f1dd3e4d9bd144f758a8c0478)), closes [#4052](https://github.com/vuejs/core/issues/4052) [vuejs/vue-cli#6562](https://github.com/vuejs/vue-cli/issues/6562)
- **codegen:** ensure valid types in genreated code when using global directives ([a44d528](https://github.com/vuejs/core/commit/a44d528af1227c05dedf610b6ec45504d8e58276)), closes [#4054](https://github.com/vuejs/core/issues/4054)
- **codegen:** ensure valid types in generated code when using global directives ([a44d528](https://github.com/vuejs/core/commit/a44d528af1227c05dedf610b6ec45504d8e58276)), closes [#4054](https://github.com/vuejs/core/issues/4054)
- **compiler-sfc:** fix parse-only mode when there is no script setup block ([253ca27](https://github.com/vuejs/core/commit/253ca2729d808fc051215876aa4af986e4caa43c))
- **runtime-core:** add useAttrs and useSlots export ([#4053](https://github.com/vuejs/core/issues/4053)) ([735ada1](https://github.com/vuejs/core/commit/735ada1507623b8d36e80b30a4f67a8af4a45c99))
- **runtime-core:** fix instance accessed via $parent chain when using expose() ([#4048](https://github.com/vuejs/core/issues/4048)) ([12cf9f4](https://github.com/vuejs/core/commit/12cf9f4ea148a59fd9002ecf9ea9d365829ce37c))
@ -114,7 +114,7 @@
### Performance Improvements
- only trigger `$attrs` update when it has actually changed ([5566d39](https://github.com/vuejs/core/commit/5566d39d467ebdd4e4234bc97d62600ff01ea28e))
- **compiler:** skip unncessary checks when parsing end tag ([048ac29](https://github.com/vuejs/core/commit/048ac299f35709b25ae1bc1efa67d2abc53dbc3b))
- **compiler:** skip unnecessary checks when parsing end tag ([048ac29](https://github.com/vuejs/core/commit/048ac299f35709b25ae1bc1efa67d2abc53dbc3b))
- avoid deopt for props/emits normalization when global mixins are used ([51d2be2](https://github.com/vuejs/core/commit/51d2be20386d4dc59006d31a1cc96676871027ce))
### Deprecations
@ -181,7 +181,7 @@
* **compat:** avoid accidentally delete the modelValue prop ([#3772](https://github.com/vuejs/core/issues/3772)) ([4f17be7](https://github.com/vuejs/core/commit/4f17be7b1ce4872ded085a36b95c1897d8c1f299))
* **compat:** enum coercion warning ([#3755](https://github.com/vuejs/core/issues/3755)) ([f01aadf](https://github.com/vuejs/core/commit/f01aadf2a16a7bef422eb039d7b157bef9ad32fc))
* **compiler-core:** fix whitespace management for slots with whitespace: 'preserve' ([#3767](https://github.com/vuejs/core/issues/3767)) ([47da921](https://github.com/vuejs/core/commit/47da92146c9fb3fa6b1e250e064ca49b74d815e4)), closes [#3766](https://github.com/vuejs/core/issues/3766)
* **compiler-dom:** comments in the v-if branchs should be ignored when used in Transition ([#3622](https://github.com/vuejs/core/issues/3622)) ([7c74feb](https://github.com/vuejs/core/commit/7c74feb3dc6beae7ff3ad22193be3b5a0f4d8aac)), closes [#3619](https://github.com/vuejs/core/issues/3619)
* **compiler-dom:** comments in the v-if branches should be ignored when used in Transition ([#3622](https://github.com/vuejs/core/issues/3622)) ([7c74feb](https://github.com/vuejs/core/commit/7c74feb3dc6beae7ff3ad22193be3b5a0f4d8aac)), closes [#3619](https://github.com/vuejs/core/issues/3619)
* **compiler-sfc:** support tsx in setup script ([#3825](https://github.com/vuejs/core/issues/3825)) ([01e8ba8](https://github.com/vuejs/core/commit/01e8ba8f873afe3857a23fb68b44fdc057e31781)), closes [#3808](https://github.com/vuejs/core/issues/3808)
* **compiler-ssr:** disable hoisting in compiler-ssr ([3ef1fcc](https://github.com/vuejs/core/commit/3ef1fcc8590da186664197a0a82e7856011c1693)), closes [#3536](https://github.com/vuejs/core/issues/3536)
* **devtools:** send update to component owning the slot ([1355ee2](https://github.com/vuejs/core/commit/1355ee27a65d466bfe8f3a7ba99aa2213e25bc50))
@ -265,7 +265,7 @@
- **compat:** avoid accidentally delete the modelValue prop ([#3772](https://github.com/vuejs/core/issues/3772)) ([4f17be7](https://github.com/vuejs/core/commit/4f17be7b1ce4872ded085a36b95c1897d8c1f299))
- **compat:** enum coercion warning ([#3755](https://github.com/vuejs/core/issues/3755)) ([f01aadf](https://github.com/vuejs/core/commit/f01aadf2a16a7bef422eb039d7b157bef9ad32fc))
- **compiler-core:** fix whitespace management for slots with whitespace: 'preserve' ([#3767](https://github.com/vuejs/core/issues/3767)) ([47da921](https://github.com/vuejs/core/commit/47da92146c9fb3fa6b1e250e064ca49b74d815e4)), closes [#3766](https://github.com/vuejs/core/issues/3766)
- **compiler-dom:** comments in the v-if branchs should be ignored when used in Transition ([#3622](https://github.com/vuejs/core/issues/3622)) ([7c74feb](https://github.com/vuejs/core/commit/7c74feb3dc6beae7ff3ad22193be3b5a0f4d8aac)), closes [#3619](https://github.com/vuejs/core/issues/3619)
- **compiler-dom:** comments in the v-if branches should be ignored when used in Transition ([#3622](https://github.com/vuejs/core/issues/3622)) ([7c74feb](https://github.com/vuejs/core/commit/7c74feb3dc6beae7ff3ad22193be3b5a0f4d8aac)), closes [#3619](https://github.com/vuejs/core/issues/3619)
- **compiler-sfc:** support tsx in setup script ([#3825](https://github.com/vuejs/core/issues/3825)) ([01e8ba8](https://github.com/vuejs/core/commit/01e8ba8f873afe3857a23fb68b44fdc057e31781)), closes [#3808](https://github.com/vuejs/core/issues/3808)
- **compiler-ssr:** disable hoisting in compiler-ssr ([3ef1fcc](https://github.com/vuejs/core/commit/3ef1fcc8590da186664197a0a82e7856011c1693)), closes [#3536](https://github.com/vuejs/core/issues/3536)
- **devtools:** send update to component owning the slot ([1355ee2](https://github.com/vuejs/core/commit/1355ee27a65d466bfe8f3a7ba99aa2213e25bc50))
@ -317,4 +317,4 @@
### Performance Improvements
- only trigger $attrs update when it has actually changed ([5566d39](https://github.com/vuejs/core/commit/5566d39d467ebdd4e4234bc97d62600ff01ea28e))
- **compiler:** skip unncessary checks when parsing end tag ([048ac29](https://github.com/vuejs/core/commit/048ac299f35709b25ae1bc1efa67d2abc53dbc3b))
- **compiler:** skip unnecessary checks when parsing end tag ([048ac29](https://github.com/vuejs/core/commit/048ac299f35709b25ae1bc1efa67d2abc53dbc3b))

View File

@ -26,7 +26,7 @@
* **reactivity-transform:** fix $$ escape edge cases ([e06d3b6](https://github.com/vuejs/core/commit/e06d3b614ea518e9cdf83fca9200fc816eb4e5a1)), closes [#6312](https://github.com/vuejs/core/issues/6312) [#6944](https://github.com/vuejs/core/issues/6944)
* **reactivity-transform:** prohibit const assignment at compile time ([#6993](https://github.com/vuejs/core/issues/6993)) ([3427052](https://github.com/vuejs/core/commit/3427052229db3448252d938292a40e960a0f4b9c)), closes [#6992](https://github.com/vuejs/core/issues/6992)
* **reactivity:** `triggerRef` working with `toRef` from reactive ([#7507](https://github.com/vuejs/core/issues/7507)) ([e64c9ae](https://github.com/vuejs/core/commit/e64c9ae957aa2606b55e8652bbde30a6ada59fb0))
* **reactivity:** ensure watch(Effect) can run independent of unmounted instance if created in a detatched effectScope (fix [#7319](https://github.com/vuejs/core/issues/7319)) ([#7330](https://github.com/vuejs/core/issues/7330)) ([cd7c887](https://github.com/vuejs/core/commit/cd7c887b755810aedf83f3d458cb956d5b147f6f))
* **reactivity:** ensure watch(Effect) can run independent of unmounted instance if created in a detached effectScope (fix [#7319](https://github.com/vuejs/core/issues/7319)) ([#7330](https://github.com/vuejs/core/issues/7330)) ([cd7c887](https://github.com/vuejs/core/commit/cd7c887b755810aedf83f3d458cb956d5b147f6f))
* **reactivity:** track hasOwnProperty ([588bd44](https://github.com/vuejs/core/commit/588bd44f036b79d7dee5d23661aa7244f70e6beb)), closes [#2619](https://github.com/vuejs/core/issues/2619) [#2621](https://github.com/vuejs/core/issues/2621)
* **runtime-core:** ensure prop type validation warning shows custom class names ([#7198](https://github.com/vuejs/core/issues/7198)) ([620327d](https://github.com/vuejs/core/commit/620327d527593c6263a21500baddbae1ebc30db8))
* **runtime-core:** fix keep-alive cache prune logic on vnodes with same type but different keys ([#7510](https://github.com/vuejs/core/issues/7510)) ([1fde49c](https://github.com/vuejs/core/commit/1fde49c0f57cc50fedf91366a274c9759d1d9a39)), closes [#7355](https://github.com/vuejs/core/issues/7355)
@ -126,7 +126,7 @@
* **transition/keep-alive:** fix unmount bug for component with out-in transition ([#6839](https://github.com/vuejs/core/issues/6839)) ([64e6d92](https://github.com/vuejs/core/commit/64e6d9221d353598b5f61c158c978d80e3b4628c)), closes [#6835](https://github.com/vuejs/core/issues/6835)
* **types/reactivity-transform:** fix type when initial value is not used ([#6821](https://github.com/vuejs/core/issues/6821)) ([fdc5902](https://github.com/vuejs/core/commit/fdc5902cce0d077c722dfd422850ca69fd51be8e)), closes [#6820](https://github.com/vuejs/core/issues/6820)
* **types:** `$watch` callback parameters type ([#6136](https://github.com/vuejs/core/issues/6136)) ([41d9c47](https://github.com/vuejs/core/commit/41d9c47300888fce9d4ff6a02f69d8a912cded8f)), closes [#6135](https://github.com/vuejs/core/issues/6135)
* **types:** ensure createBlock() helper accepts Teleport and Supsense types (fix: [#2855](https://github.com/vuejs/core/issues/2855)) ([#5458](https://github.com/vuejs/core/issues/5458)) ([e5fc7dc](https://github.com/vuejs/core/commit/e5fc7dcc02f2dd3fa8172958259049031626375f))
* **types:** ensure createBlock() helper accepts Teleport and Suspense types (fix: [#2855](https://github.com/vuejs/core/issues/2855)) ([#5458](https://github.com/vuejs/core/issues/5458)) ([e5fc7dc](https://github.com/vuejs/core/commit/e5fc7dcc02f2dd3fa8172958259049031626375f))
* **types:** export `Raw` type ([#6380](https://github.com/vuejs/core/issues/6380)) ([e9172db](https://github.com/vuejs/core/commit/e9172db68b86fad2e0bb1de9e5d0dddbe3c2a25e)), closes [#7048](https://github.com/vuejs/core/issues/7048)
* **types:** should unwrap tuple correctly ([#3820](https://github.com/vuejs/core/issues/3820)) ([e816812](https://github.com/vuejs/core/commit/e816812f10b9e3a375eef8dffd617d7f08b23c00)), closes [#3819](https://github.com/vuejs/core/issues/3819)
* **types:** stricter type condition for `EventHandlers` ([#6855](https://github.com/vuejs/core/issues/6855)) ([bad3f3c](https://github.com/vuejs/core/commit/bad3f3ce46aad1f5fec47d1d02aee26af393bcff)), closes [#6899](https://github.com/vuejs/core/issues/6899)
@ -714,7 +714,7 @@
* **compiler-core:** avoid runtime dependency on @babel/types ([1045590](https://github.com/vuejs/core/commit/1045590d4bbaf4a2b05311f11b22a0b3d22cf609)), closes [#4531](https://github.com/vuejs/core/issues/4531)
* **compiler-core:** pick last char when dynamic directive doesn't close ([#4507](https://github.com/vuejs/core/issues/4507)) ([5d262e0](https://github.com/vuejs/core/commit/5d262e08d5d5fb29f48ba5fa5b97a9a3e34b9d4b))
* **compiler:** condense whitespaces in static class attributes ([#4432](https://github.com/vuejs/core/issues/4432)) ([b8653d3](https://github.com/vuejs/core/commit/b8653d390a555e1ee3f92a1c49cfd8800c67e46a)), closes [#4251](https://github.com/vuejs/core/issues/4251)
* **runtime-dom:** style patching shoud always preserve v-show display property ([d534515](https://github.com/vuejs/core/commit/d53451583684c37bda7d30bff912216e1a58126f)), closes [#4424](https://github.com/vuejs/core/issues/4424)
* **runtime-dom:** style patching should always preserve v-show display property ([d534515](https://github.com/vuejs/core/commit/d53451583684c37bda7d30bff912216e1a58126f)), closes [#4424](https://github.com/vuejs/core/issues/4424)
* **type:** fix prop type infer ([#4530](https://github.com/vuejs/core/issues/4530)) ([4178d5d](https://github.com/vuejs/core/commit/4178d5d7d9549a0a1d19663bc2f92c8ac6a731b2)), closes [#4525](https://github.com/vuejs/core/issues/4525)
@ -741,7 +741,7 @@
* **compiler-sfc:** ensure script setup generates type-valid ts output ([bacb201](https://github.com/vuejs/core/commit/bacb2012acb4045a2db6988ba4545a7655d6ca14)), closes [#4455](https://github.com/vuejs/core/issues/4455)
* **compiler-sfc:** generate matching prop types when withDefaults is used ([#4466](https://github.com/vuejs/core/issues/4466)) ([8580796](https://github.com/vuejs/core/commit/85807967dc874e6ea6b20f341875beda938e3058)), closes [#4455](https://github.com/vuejs/core/issues/4455)
* **compiler:** generate function ref for script setup if inline is ture. ([#4492](https://github.com/vuejs/core/issues/4492)) ([4cd282b](https://github.com/vuejs/core/commit/4cd282b0a17589ef9ca2649e7beb0bdee4a73c57))
* **compiler:** generate function ref for script setup if inline is true. ([#4492](https://github.com/vuejs/core/issues/4492)) ([4cd282b](https://github.com/vuejs/core/commit/4cd282b0a17589ef9ca2649e7beb0bdee4a73c57))
* **compiler:** report invalid directive name error ([#4494](https://github.com/vuejs/core/issues/4494)) ([#4495](https://github.com/vuejs/core/issues/4495)) ([c00925e](https://github.com/vuejs/core/commit/c00925ed5c409b57a1540b79c595b7f8117e2d4c))
* **types:** include ref-macros.d.ts in npm dist files ([d7f1b77](https://github.com/vuejs/core/commit/d7f1b771f80ab9014a4701913b50458fd251a117)), closes [#4433](https://github.com/vuejs/core/issues/4433)
@ -798,7 +798,7 @@
### Bug Fixes
* **compiler-sfc:** fix import usage check for lowercase imported components ([57f1081](https://github.com/vuejs/core/commit/57f10812cc7f1e9f6c92736c36aba577943996fd)), closes [#4358](https://github.com/vuejs/core/issues/4358)
* **runtime-core:** ensure consistent arguments for tempalte and render funtion slot usage ([644971e](https://github.com/vuejs/core/commit/644971ec06642817cf7e720ad4980182d2140f53)), closes [#4367](https://github.com/vuejs/core/issues/4367)
* **runtime-core:** ensure consistent arguments for template and render function slot usage ([644971e](https://github.com/vuejs/core/commit/644971ec06642817cf7e720ad4980182d2140f53)), closes [#4367](https://github.com/vuejs/core/issues/4367)
* **runtime-core:** fix child component double update on props change ([c1f564e](https://github.com/vuejs/core/commit/c1f564e1dc40eda9af657c30cd787a8d770dde0f)), closes [#4365](https://github.com/vuejs/core/issues/4365)

View File

@ -259,7 +259,7 @@
* **sfc:** support imported types in SFC macros ([#8083](https://github.com/vuejs/core/pull/8083))
* **types/slots:** support slot presence / props type checks via `defineSlots` macro and `slots` option ([#7982](https://github.com/vuejs/core/issues/7982)) ([5a2f5d5](https://github.com/vuejs/core/commit/5a2f5d59cffa36a99e6f2feab6b3ba7958b7362f))
* **sfc:** support more ergnomic defineEmits type syntax ([#7992](https://github.com/vuejs/core/issues/7992)) ([8876dcc](https://github.com/vuejs/core/commit/8876dccf42a7f05375d97cb18c1afdfd0fc51c94))
* **sfc:** support more ergonomic defineEmits type syntax ([#7992](https://github.com/vuejs/core/issues/7992)) ([8876dcc](https://github.com/vuejs/core/commit/8876dccf42a7f05375d97cb18c1afdfd0fc51c94))
* **sfc:** introduce `defineModel` macro and `useModel` helper ([#8018](https://github.com/vuejs/core/issues/8018)) ([14f3d74](https://github.com/vuejs/core/commit/14f3d747a34d45415b0036b274517d70a27ec0d3))
* **reactivity:** improve support of getter usage in reactivity APIs ([#7997](https://github.com/vuejs/core/issues/7997)) ([59e8284](https://github.com/vuejs/core/commit/59e828448e7f37643cd0eaea924a764e9d314448))
* **compiler-sfc:** add defineOptions macro ([#5738](https://github.com/vuejs/core/issues/5738)) ([bcf5841](https://github.com/vuejs/core/commit/bcf5841ddecc64d0bdbd56ce1463eb8ebf01bb9d))
@ -483,7 +483,7 @@
* **compiler-sfc:** support arbitrary expression as withDefaults argument ([fe61944](https://github.com/vuejs/core/commit/fe619443d2e99301975de120685dbae8d66c03a6)), closes [#6459](https://github.com/vuejs/core/issues/6459)
* **reactivity:** improve support of getter usage in reactivity APIs ([#7997](https://github.com/vuejs/core/issues/7997)) ([59e8284](https://github.com/vuejs/core/commit/59e828448e7f37643cd0eaea924a764e9d314448))
* **sfc:** revert withDefaults() deprecation ([4af5d1b](https://github.com/vuejs/core/commit/4af5d1b0754035058436f9e4e5c12aedef199177))
* **sfc:** support more ergnomic defineEmits type syntax ([#7992](https://github.com/vuejs/core/issues/7992)) ([8876dcc](https://github.com/vuejs/core/commit/8876dccf42a7f05375d97cb18c1afdfd0fc51c94))
* **sfc:** support more ergonomic defineEmits type syntax ([#7992](https://github.com/vuejs/core/issues/7992)) ([8876dcc](https://github.com/vuejs/core/commit/8876dccf42a7f05375d97cb18c1afdfd0fc51c94))
* **types/slots:** support slot presence / props type checks via `defineSlots` macro and `slots` option ([#7982](https://github.com/vuejs/core/issues/7982)) ([5a2f5d5](https://github.com/vuejs/core/commit/5a2f5d59cffa36a99e6f2feab6b3ba7958b7362f))
@ -544,7 +544,7 @@
### Bug Fixes
* **runtime-core:** support `getCurrentInstance` across mutiple builds of Vue ([8d2d5bf](https://github.com/vuejs/core/commit/8d2d5bf48a24dab44e5b03cb8fa0c5faa4b696e3))
* **runtime-core:** support `getCurrentInstance` across multiple builds of Vue ([8d2d5bf](https://github.com/vuejs/core/commit/8d2d5bf48a24dab44e5b03cb8fa0c5faa4b696e3))
* **types:** ensure defineProps with generics return correct types ([c288c7b](https://github.com/vuejs/core/commit/c288c7b0bd6077d690f42153c3fc49a45454a66a))

View File

@ -167,7 +167,7 @@
### Bug Fixes
* **compat:** correctly transform non-identifier expressions in legacy filter syntax ([#10896](https://github.com/vuejs/core/issues/10896)) ([07b3c4b](https://github.com/vuejs/core/commit/07b3c4b7860009e19446f3d78571556c5737d82a)), closes [#10852](https://github.com/vuejs/core/issues/10852)
* **compat:** ensure proper handling of render fuction from SFC using Vue.extend ([#7781](https://github.com/vuejs/core/issues/7781)) ([c73847f](https://github.com/vuejs/core/commit/c73847f2becc20f03cb9c68748eea92455e688ee)), closes [#7766](https://github.com/vuejs/core/issues/7766)
* **compat:** ensure proper handling of render function from SFC using Vue.extend ([#7781](https://github.com/vuejs/core/issues/7781)) ([c73847f](https://github.com/vuejs/core/commit/c73847f2becc20f03cb9c68748eea92455e688ee)), closes [#7766](https://github.com/vuejs/core/issues/7766)
* **compat:** only warn ATTR_FALSE_VALUE when enabled ([04729ba](https://github.com/vuejs/core/commit/04729ba2163d840f0ca7866bc964696eb5557804)), closes [#11126](https://github.com/vuejs/core/issues/11126)
* **compile-sfc:** register props destructure rest id as setup bindings ([#10888](https://github.com/vuejs/core/issues/10888)) ([b2b5f57](https://github.com/vuejs/core/commit/b2b5f57c2c945edd0eebc1b545ec1b7568e51484)), closes [#10885](https://github.com/vuejs/core/issues/10885)
* **compile-sfc:** Support project reference with folder, ([#10908](https://github.com/vuejs/core/issues/10908)) ([bdeac37](https://github.com/vuejs/core/commit/bdeac377c7b85888193b49ac187e927636cc40bc)), closes [#10907](https://github.com/vuejs/core/issues/10907)
@ -218,7 +218,7 @@
### Bug Fixes
* **compat:** include legacy scoped slots ([#10868](https://github.com/vuejs/core/issues/10868)) ([8366126](https://github.com/vuejs/core/commit/83661264a4ced3cb2ff6800904a86dd9e82bbfe2)), closes [#8869](https://github.com/vuejs/core/issues/8869)
* **compiler-core:** add support for arrow aysnc function with unbracketed ([#5789](https://github.com/vuejs/core/issues/5789)) ([ca7d421](https://github.com/vuejs/core/commit/ca7d421e8775f6813f8943d32ab485e0c542f98b)), closes [#5788](https://github.com/vuejs/core/issues/5788)
* **compiler-core:** add support for arrow async function with unbracketed ([#5789](https://github.com/vuejs/core/issues/5789)) ([ca7d421](https://github.com/vuejs/core/commit/ca7d421e8775f6813f8943d32ab485e0c542f98b)), closes [#5788](https://github.com/vuejs/core/issues/5788)
* **compiler-dom:** restrict createStaticVNode usage with option elements ([#10846](https://github.com/vuejs/core/issues/10846)) ([0e3d617](https://github.com/vuejs/core/commit/0e3d6178b02d0386d779720ae2cc4eac1d1ec990)), closes [#6568](https://github.com/vuejs/core/issues/6568) [#7434](https://github.com/vuejs/core/issues/7434)
* **compiler-sfc:** handle keyof operator ([#10874](https://github.com/vuejs/core/issues/10874)) ([10d34a5](https://github.com/vuejs/core/commit/10d34a5624775f20437ccad074a97270ef74c3fb)), closes [#10871](https://github.com/vuejs/core/issues/10871)
* **hydration:** handle edge case of style mismatch without style attribute ([f2c1412](https://github.com/vuejs/core/commit/f2c1412e46a8fad3e13403bfa78335c4f704f21c)), closes [#10786](https://github.com/vuejs/core/issues/10786)
@ -417,7 +417,7 @@
* **compiler-sfc:** fix type resolution for symlinked node_modules structure w/ pnpm ([75e866b](https://github.com/vuejs/core/commit/75e866bd4ef368b4e037a4933dbaf188920dc683)), closes [#10121](https://github.com/vuejs/core/issues/10121)
* correct url for production error reference links ([c3087ff](https://github.com/vuejs/core/commit/c3087ff2cce7d96c60a870f8233441311ab4dfb4))
* **hydration:** fix incorect mismatch warning for option with non-string value and inner text ([d16a213](https://github.com/vuejs/core/commit/d16a2138a33b106b9e1499bbb9e1c67790370c97))
* **hydration:** fix incorrect mismatch warning for option with non-string value and inner text ([d16a213](https://github.com/vuejs/core/commit/d16a2138a33b106b9e1499bbb9e1c67790370c97))
* **reactivity:** re-fix [#10114](https://github.com/vuejs/core/issues/10114) ([#10123](https://github.com/vuejs/core/issues/10123)) ([c2b274a](https://github.com/vuejs/core/commit/c2b274a887f61deb7e0185d1bef3b77d31e991cc))
* **runtime-core:** should not warn out-of-render slot fn usage when mounting another app in setup ([#10125](https://github.com/vuejs/core/issues/10125)) ([6fa33e6](https://github.com/vuejs/core/commit/6fa33e67ec42af140a86fbdb86939032c3a1f345)), closes [#10124](https://github.com/vuejs/core/issues/10124)

View File

@ -1,7 +1,7 @@
{
"private": true,
"version": "3.5.14",
"packageManager": "pnpm@10.9.0",
"version": "3.5.17",
"packageManager": "pnpm@10.12.4",
"type": "module",
"scripts": {
"dev": "node scripts/dev.js",
@ -65,27 +65,27 @@
"@babel/parser": "catalog:",
"@babel/types": "catalog:",
"@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-commonjs": "^28.0.3",
"@rollup/plugin-commonjs": "^28.0.6",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-replace": "5.0.4",
"@swc/core": "^1.11.24",
"@swc/core": "^1.12.9",
"@types/hash-sum": "^1.0.2",
"@types/node": "^22.14.1",
"@types/node": "^22.16.0",
"@types/semver": "^7.7.0",
"@types/serve-handler": "^6.1.4",
"@vitest/coverage-v8": "^3.1.3",
"@vitest/eslint-plugin": "^1.1.44",
"@vitest/coverage-v8": "^3.1.4",
"@vitest/eslint-plugin": "^1.2.1",
"@vue/consolidate": "1.0.0",
"conventional-changelog-cli": "^5.0.0",
"enquirer": "^2.4.1",
"esbuild": "^0.25.4",
"esbuild": "^0.25.5",
"esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^9.25.1",
"eslint-plugin-import-x": "^4.11.0",
"eslint": "^9.27.0",
"eslint-plugin-import-x": "^4.13.1",
"estree-walker": "catalog:",
"jsdom": "^26.1.0",
"lint-staged": "^15.5.1",
"lint-staged": "^16.0.0",
"lodash": "^4.17.21",
"magic-string": "^0.30.17",
"markdown-table": "^3.0.4",
@ -95,21 +95,21 @@
"prettier": "^3.5.3",
"pretty-bytes": "^6.1.1",
"pug": "^3.0.3",
"puppeteer": "~24.8.2",
"puppeteer": "~24.9.0",
"rimraf": "^6.0.1",
"rollup": "^4.40.2",
"rollup": "^4.44.1",
"rollup-plugin-dts": "^6.2.1",
"rollup-plugin-esbuild": "^6.2.1",
"rollup-plugin-polyfill-node": "^0.13.0",
"semver": "^7.7.1",
"semver": "^7.7.2",
"serve": "^14.2.4",
"serve-handler": "^6.1.6",
"simple-git-hooks": "^2.13.0",
"todomvc-app-css": "^2.4.3",
"tslib": "^2.8.1",
"typescript": "~5.6.2",
"typescript-eslint": "^8.31.1",
"typescript-eslint": "^8.32.1",
"vite": "catalog:",
"vitest": "^3.1.3"
"vitest": "^3.1.4"
}
}

View File

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

View File

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

View File

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

View File

@ -141,6 +141,8 @@ onMounted(() => {
:editorOptions="{ autoSaveText: false }"
:store="store"
:showCompileOutput="true"
:showSsrOutput="useSSRMode"
:showOpenSourceMap="true"
:autoResize="true"
:clearConsole="false"
:preview-options="{

View File

@ -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(`<span v-if="foo &&\nbar"></span>`).children
expect(span.loc.end.line).toBe(2)
})
})
describe('decodeEntities option', () => {

View File

@ -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 */),
_createElementVNode("span", null, null, -1 /* CACHED */),
_createTextVNode("foo"),
_createElementVNode("div", null, null, -1 /* HOISTED */)
_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 */))
]))

View File

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

View File

@ -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(
`<!--comment--><div id="a"><div id="b"><div id="c"><div id="d"><div id="e">hello</div></div></div></div></div>`,
)
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(

View File

@ -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 = `
<Comp>
<template #one v-if="ok">foo</template>
<template #two v-else>baz</template>
</Comp>
`
const { root } = parseWithSlots(source, {
whitespace: 'preserve',
})
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
})
})

View File

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

View File

@ -188,7 +188,9 @@ function createCodegenContext(
name = content
}
}
addMapping(node.loc.start, name)
if (node.loc.source) {
addMapping(node.loc.start, name)
}
}
if (newlineIndex === NewlineType.Unknown) {
// multiple newlines, full iteration
@ -225,7 +227,7 @@ function createCodegenContext(
context.column = code.length - newlineIndex
}
}
if (node && node.loc !== locStub) {
if (node && node.loc !== locStub && node.loc.source) {
addMapping(node.loc.end)
}
}

View File

@ -647,7 +647,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 +832,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++) {

View File

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

View File

@ -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 <slot/>, 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.

View File

@ -41,20 +41,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(

View File

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

View File

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

View File

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

View File

@ -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("<span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>", 20),
_createElementVNode("div", { key: "1" }, "1", -1 /* HOISTED */),
_createElementVNode("div", { key: "1" }, "1", -1 /* CACHED */),
_createStaticVNode("<span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>", 20)
])))
}"
@ -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("<span class=\\"b\\"></span><span class=\\"b\\"></span><span class=\\"b\\"></span><span class=\\"b\\"></span><span class=\\"b\\"></span>", 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 */)
])))
}"
`;

View File

@ -491,6 +491,16 @@ describe('stringify static html', () => {
expect(code).toMatchSnapshot()
})
test('should bail for comments', () => {
const { code } = compileWithStringify(
`<!-- Comment 1 --><div class="a"><!-- Comment 2 -->${repeat(
`<span class="b"/>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div>`,
)
expect(code).toMatchSnapshot()
})
test('should bail for <option> elements with null values', () => {
const { ast, code } = compileWithStringify(
`<div><select><option :value="null" />${repeat(

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-dom",
"version": "3.5.14",
"version": "3.5.17",
"description": "@vue/compiler-dom",
"main": "index.js",
"module": "dist/compiler-dom.esm-bundler.js",

View File

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

View File

@ -861,7 +861,7 @@ export default {
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("div", null, _toDisplayString(count.value), 1 /* TEXT */),
_cache[0] || (_cache[0] = _createElementVNode("div", null, "static", -1 /* HOISTED */))
_cache[0] || (_cache[0] = _createElementVNode("div", null, "static", -1 /* CACHED */))
], 64 /* STABLE_FRAGMENT */))
}
}
@ -884,9 +884,9 @@ export default {
return (_ctx, _push, _parent, _attrs) => {
const _cssVars = { style: {
"--xxxxxxxx-count": (count.value),
"--xxxxxxxx-style\\\\.color": (style.color),
"--xxxxxxxx-height\\\\ \\\\+\\\\ \\\\\\"px\\\\\\"": (height.value + "px")
":--xxxxxxxx-count": (count.value),
":--xxxxxxxx-style\\\\.color": (style.color),
":--xxxxxxxx-height\\\\ \\\\+\\\\ \\\\\\"px\\\\\\"": (height.value + "px")
}}
_push(\`<!--[--><div\${
_ssrRenderAttrs(_cssVars)

View File

@ -41,8 +41,8 @@ const _hoisted_1 = _imports_0 + '#fragment'
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_cache[0] || (_cache[0] = _createElementVNode("use", { href: _hoisted_1 }, null, -1 /* HOISTED */)),
_cache[1] || (_cache[1] = _createElementVNode("use", { href: _hoisted_1 }, null, -1 /* HOISTED */))
_cache[0] || (_cache[0] = _createElementVNode("use", { href: _hoisted_1 }, null, -1 /* CACHED */)),
_cache[1] || (_cache[1] = _createElementVNode("use", { href: _hoisted_1 }, null, -1 /* CACHED */))
], 64 /* STABLE_FRAGMENT */))
}"
`;

View File

@ -10,8 +10,8 @@ const _hoisted_2 = _imports_0 + ' 1x, ' + "/foo/logo.png" + ' 2x'
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_cache[0] || (_cache[0] = _createElementVNode("img", { srcset: _hoisted_1 }, null, -1 /* HOISTED */)),
_cache[1] || (_cache[1] = _createElementVNode("img", { srcset: _hoisted_2 }, null, -1 /* HOISTED */))
_cache[0] || (_cache[0] = _createElementVNode("img", { srcset: _hoisted_1 }, null, -1 /* CACHED */)),
_cache[1] || (_cache[1] = _createElementVNode("img", { srcset: _hoisted_2 }, null, -1 /* CACHED */))
], 64 /* STABLE_FRAGMENT */))
}"
`;
@ -35,51 +35,51 @@ export function render(_ctx, _cache) {
_cache[0] || (_cache[0] = _createElementVNode("img", {
src: "./logo.png",
srcset: ""
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[1] || (_cache[1] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_1
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[2] || (_cache[2] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_2
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[3] || (_cache[3] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_3
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[4] || (_cache[4] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_4
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[5] || (_cache[5] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_5
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[6] || (_cache[6] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_6
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[7] || (_cache[7] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_7
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[8] || (_cache[8] = _createElementVNode("img", {
src: "/logo.png",
srcset: "/logo.png, /logo.png 2x"
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[9] || (_cache[9] = _createElementVNode("img", {
src: "https://example.com/logo.png",
srcset: "https://example.com/logo.png, https://example.com/logo.png 2x"
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[10] || (_cache[10] = _createElementVNode("img", {
src: "/logo.png",
srcset: _hoisted_8
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[11] || (_cache[11] = _createElementVNode("img", {
src: "data:image/png;base64,i",
srcset: "data:image/png;base64,i 1x, data:image/png;base64,i 2x"
}, null, -1 /* HOISTED */))
}, null, -1 /* CACHED */))
], 64 /* STABLE_FRAGMENT */))
}"
`;
@ -92,51 +92,51 @@ export function render(_ctx, _cache) {
_cache[0] || (_cache[0] = _createElementVNode("img", {
src: "./logo.png",
srcset: ""
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[1] || (_cache[1] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png"
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[2] || (_cache[2] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png 2x"
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[3] || (_cache[3] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png 2x"
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[4] || (_cache[4] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png, /foo/logo.png 2x"
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[5] || (_cache[5] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png 2x, /foo/logo.png"
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[6] || (_cache[6] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png 2x, /foo/logo.png 3x"
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[7] || (_cache[7] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png, /foo/logo.png 2x, /foo/logo.png 3x"
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[8] || (_cache[8] = _createElementVNode("img", {
src: "/logo.png",
srcset: "/logo.png, /logo.png 2x"
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[9] || (_cache[9] = _createElementVNode("img", {
src: "https://example.com/logo.png",
srcset: "https://example.com/logo.png, https://example.com/logo.png 2x"
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[10] || (_cache[10] = _createElementVNode("img", {
src: "/logo.png",
srcset: "/logo.png, /foo/logo.png 2x"
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[11] || (_cache[11] = _createElementVNode("img", {
src: "data:image/png;base64,i",
srcset: "data:image/png;base64,i 1x, data:image/png;base64,i 2x"
}, null, -1 /* HOISTED */))
}, null, -1 /* CACHED */))
], 64 /* STABLE_FRAGMENT */))
}"
`;
@ -162,51 +162,51 @@ export function render(_ctx, _cache) {
_cache[0] || (_cache[0] = _createElementVNode("img", {
src: "./logo.png",
srcset: ""
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[1] || (_cache[1] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_1
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[2] || (_cache[2] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_2
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[3] || (_cache[3] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_3
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[4] || (_cache[4] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_4
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[5] || (_cache[5] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_5
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[6] || (_cache[6] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_6
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[7] || (_cache[7] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_7
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[8] || (_cache[8] = _createElementVNode("img", {
src: "/logo.png",
srcset: _hoisted_8
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[9] || (_cache[9] = _createElementVNode("img", {
src: "https://example.com/logo.png",
srcset: "https://example.com/logo.png, https://example.com/logo.png 2x"
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[10] || (_cache[10] = _createElementVNode("img", {
src: "/logo.png",
srcset: _hoisted_9
}, null, -1 /* HOISTED */)),
}, null, -1 /* CACHED */)),
_cache[11] || (_cache[11] = _createElementVNode("img", {
src: "data:image/png;base64,i",
srcset: "data:image/png;base64,i 1x, data:image/png;base64,i 2x"
}, null, -1 /* HOISTED */))
}, null, -1 /* CACHED */))
], 64 /* STABLE_FRAGMENT */))
}"
`;

View File

@ -1,5 +1,11 @@
import { BindingTypes } from '@vue/compiler-core'
import { assertCode, compileSFCScript as compile, mockId } from './utils'
import {
assertCode,
compileSFCScript as compile,
getPositionInCode,
mockId,
} from './utils'
import { type RawSourceMap, SourceMapConsumer } from 'source-map-js'
describe('SFC compile <script setup>', () => {
test('should compile JS syntax', () => {
@ -646,10 +652,10 @@ describe('SFC compile <script setup>', () => {
expect(content).toMatch(`return (_ctx, _push`)
expect(content).toMatch(`ssrInterpolate`)
expect(content).not.toMatch(`useCssVars`)
expect(content).toMatch(`"--${mockId}-count": (count.value)`)
expect(content).toMatch(`"--${mockId}-style\\\\.color": (style.color)`)
expect(content).toMatch(`":--${mockId}-count": (count.value)`)
expect(content).toMatch(`":--${mockId}-style\\\\.color": (style.color)`)
expect(content).toMatch(
`"--${mockId}-height\\\\ \\\\+\\\\ \\\\\\"px\\\\\\"": (height.value + "px")`,
`":--${mockId}-height\\\\ \\\\+\\\\ \\\\\\"px\\\\\\"": (height.value + "px")`,
)
assertCode(content)
})
@ -690,6 +696,27 @@ describe('SFC compile <script setup>', () => {
expect(content).toMatch(`new (_unref(Foo)).Bar()`)
assertCode(content)
})
// #12682
test('source map', () => {
const source = `
<script setup>
const count = ref(0)
</script>
<template>
<button @click="throw new Error(\`msg\`);"></button>
</template>
`
const { content, map } = compile(source, { inlineTemplate: true })
expect(map).not.toBeUndefined()
const consumer = new SourceMapConsumer(map as RawSourceMap)
expect(
consumer.originalPositionFor(getPositionInCode(content, 'count')),
).toMatchObject(getPositionInCode(source, `count`))
expect(
consumer.originalPositionFor(getPositionInCode(content, 'Error')),
).toMatchObject(getPositionInCode(source, `Error`))
})
})
describe('with TypeScript', () => {

View File

@ -278,6 +278,23 @@ describe('resolveType', () => {
})
})
test('utility type: mapped type with Omit and Pick', () => {
expect(
resolve(`
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
interface Test {
foo: string;
bar?: string;
}
type OptionalTest = Optional<Test, 'foo'>
defineProps<OptionalTest>()
`).props,
).toStrictEqual({
foo: ['String'],
bar: ['String'],
})
})
test('utility type: ReadonlyArray', () => {
expect(
resolve(`
@ -714,6 +731,38 @@ describe('resolveType', () => {
})
})
describe('type alias declaration', () => {
// #13240
test('function type', () => {
expect(
resolve(`
type FunFoo<O> = (item: O) => boolean;
type FunBar = FunFoo<number>;
defineProps<{
foo?: FunFoo<number>;
bar?: FunBar;
}>()
`).props,
).toStrictEqual({
foo: ['Function'],
bar: ['Function'],
})
})
test('fallback to Unknown', () => {
expect(
resolve(`
type Brand<T> = T & {};
defineProps<{
foo: Brand<string>;
}>()
`).props,
).toStrictEqual({
foo: [UNKNOWN_TYPE],
})
})
})
describe('generics', () => {
test('generic with type literal', () => {
expect(
@ -1434,6 +1483,29 @@ describe('resolveType', () => {
colsLg: ['Number'],
})
})
test('allowArbitraryExtensions', () => {
const files = {
'/foo.d.vue.ts': 'export type Foo = number;',
'/foo.vue': '<template><div /></template>',
'/bar.d.css.ts': 'export type Bar = string;',
'/bar.css': ':root { --color: red; }',
}
const { props } = resolve(
`
import { Foo } from './foo.vue'
import { Bar } from './bar.css'
defineProps<{ foo: Foo; bar: Bar }>()
`,
files,
)
expect(props).toStrictEqual({
foo: ['Number'],
bar: ['String'],
})
})
})
})

View File

@ -39,6 +39,24 @@ describe('SFC scoped CSS', () => {
expect(compileScoped(`h1 .foo { color: red; }`)).toMatch(
`h1 .foo[data-v-test] { color: red;`,
)
// #13387
expect(
compileScoped(`main {
width: 100%;
> * {
max-width: 200px;
}
}`),
).toMatchInlineSnapshot(`
"main {
&[data-v-test] {
width: 100%;
}
> *[data-v-test] {
max-width: 200px;
}
}"`)
})
test('nesting selector', () => {

View File

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

View File

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

View File

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

View File

@ -23,7 +23,11 @@ import type {
Statement,
} from '@babel/types'
import { walk } from 'estree-walker'
import type { RawSourceMap } from 'source-map-js'
import {
type RawSourceMap,
SourceMapConsumer,
SourceMapGenerator,
} from 'source-map-js'
import {
normalScriptDefaultVar,
processNormalScript,
@ -809,6 +813,7 @@ export function compileScript(
args += `, { ${destructureElements.join(', ')} }`
}
let templateMap
// 9. generate return statement
let returned
if (
@ -858,7 +863,7 @@ export function compileScript(
}
// inline render function mode - we are going to compile the template and
// inline it right here
const { code, ast, preamble, tips, errors } = compileTemplate({
const { code, ast, preamble, tips, errors, map } = compileTemplate({
filename,
ast: sfc.template.ast,
source: sfc.template.content,
@ -876,6 +881,7 @@ export function compileScript(
bindingMetadata: ctx.bindingMetadata,
},
})
templateMap = map
if (tips.length) {
tips.forEach(warnOnce)
}
@ -1014,19 +1020,28 @@ export function compileScript(
)
}
const content = ctx.s.toString()
let map =
options.sourceMap !== false
? (ctx.s.generateMap({
source: filename,
hires: true,
includeContent: true,
}) as unknown as RawSourceMap)
: undefined
// merge source maps of the script setup and template in inline mode
if (templateMap && map) {
const offset = content.indexOf(returned)
const templateLineOffset =
content.slice(0, offset).split(/\r?\n/).length - 1
map = mergeSourceMaps(map, templateMap, templateLineOffset)
}
return {
...scriptSetup,
bindings: ctx.bindingMetadata,
imports: ctx.userImports,
content: ctx.s.toString(),
map:
options.sourceMap !== false
? (ctx.s.generateMap({
source: filename,
hires: true,
includeContent: true,
}) as unknown as RawSourceMap)
: undefined,
content,
map,
scriptAst: scriptAst?.body,
scriptSetupAst: scriptSetupAst?.body,
deps: ctx.deps ? [...ctx.deps] : undefined,
@ -1284,3 +1299,42 @@ function isStaticNode(node: Node): boolean {
}
return false
}
export function mergeSourceMaps(
scriptMap: RawSourceMap,
templateMap: RawSourceMap,
templateLineOffset: number,
): RawSourceMap {
const generator = new SourceMapGenerator()
const addMapping = (map: RawSourceMap, lineOffset = 0) => {
const consumer = new SourceMapConsumer(map)
;(consumer as any).sources.forEach((sourceFile: string) => {
;(generator as any)._sources.add(sourceFile)
const sourceContent = consumer.sourceContentFor(sourceFile)
if (sourceContent != null) {
generator.setSourceContent(sourceFile, sourceContent)
}
})
consumer.eachMapping(m => {
if (m.originalLine == null) return
generator.addMapping({
generated: {
line: m.generatedLine + lineOffset,
column: m.generatedColumn,
},
original: {
line: m.originalLine,
column: m.originalColumn!,
},
source: m.source,
name: m.name,
})
})
}
addMapping(scriptMap)
addMapping(templateMap, templateLineOffset)
;(generator as any)._sourceRoot = scriptMap.sourceRoot
;(generator as any)._file = scriptMap.file
return (generator as any).toJSON()
}

View File

@ -546,26 +546,43 @@ function resolveStringType(
ctx: TypeResolveContext,
node: Node,
scope: TypeScope,
typeParameters?: Record<string, Node>,
): string[] {
switch (node.type) {
case 'StringLiteral':
return [node.value]
case 'TSLiteralType':
return resolveStringType(ctx, node.literal, scope)
return resolveStringType(ctx, node.literal, scope, typeParameters)
case 'TSUnionType':
return node.types.map(t => resolveStringType(ctx, t, scope)).flat()
return node.types
.map(t => resolveStringType(ctx, t, scope, typeParameters))
.flat()
case 'TemplateLiteral': {
return resolveTemplateKeys(ctx, node, scope)
}
case 'TSTypeReference': {
const resolved = resolveTypeReference(ctx, node, scope)
if (resolved) {
return resolveStringType(ctx, resolved, scope)
return resolveStringType(ctx, resolved, scope, typeParameters)
}
if (node.typeName.type === 'Identifier') {
const name = node.typeName.name
if (typeParameters && typeParameters[name]) {
return resolveStringType(
ctx,
typeParameters[name],
scope,
typeParameters,
)
}
const getParam = (index = 0) =>
resolveStringType(ctx, node.typeParameters!.params[index], scope)
switch (node.typeName.name) {
resolveStringType(
ctx,
node.typeParameters!.params[index],
scope,
typeParameters,
)
switch (name) {
case 'Extract':
return getParam(1)
case 'Exclude': {
@ -671,6 +688,7 @@ function resolveBuiltin(
ctx,
node.typeParameters!.params[1],
scope,
typeParameters,
)
const res: ResolvedElements = { props: {}, calls: t.calls }
for (const key of picked) {
@ -683,6 +701,7 @@ function resolveBuiltin(
ctx,
node.typeParameters!.params[1],
scope,
typeParameters,
)
const res: ResolvedElements = { props: {}, calls: t.calls }
for (const key in t.props) {
@ -860,13 +879,13 @@ function resolveFS(ctx: TypeResolveContext): FS | undefined {
}
return (ctx.fs = {
fileExists(file) {
if (file.endsWith('.vue.ts')) {
if (file.endsWith('.vue.ts') && !file.endsWith('.d.vue.ts')) {
file = file.replace(/\.ts$/, '')
}
return fs.fileExists(file)
},
readFile(file) {
if (file.endsWith('.vue.ts')) {
if (file.endsWith('.vue.ts') && !file.endsWith('.d.vue.ts')) {
file = file.replace(/\.ts$/, '')
}
return fs.readFile(file)
@ -1059,7 +1078,7 @@ function resolveWithTS(
if (res.resolvedModule) {
let filename = res.resolvedModule.resolvedFileName
if (filename.endsWith('.vue.ts')) {
if (filename.endsWith('.vue.ts') && !filename.endsWith('.d.vue.ts')) {
filename = filename.replace(/\.ts$/, '')
}
return fs.realpath ? fs.realpath(filename) : filename
@ -1129,7 +1148,7 @@ export function fileToScope(
// fs should be guaranteed to exist here
const fs = resolveFS(ctx)!
const source = fs.readFile(filename) || ''
const body = parseFile(filename, source, ctx.options.babelParserPlugins)
const body = parseFile(filename, source, fs, ctx.options.babelParserPlugins)
const scope = new TypeScope(filename, source, 0, recordImports(body))
recordTypes(ctx, body, scope, asGlobal)
fileToScopeCache.set(filename, scope)
@ -1139,6 +1158,7 @@ export function fileToScope(
function parseFile(
filename: string,
content: string,
fs: FS,
parserPlugins?: SFCScriptCompileOptions['babelParserPlugins'],
): Statement[] {
const ext = extname(filename)
@ -1151,7 +1171,21 @@ function parseFile(
),
sourceType: 'module',
}).program.body
} else if (ext === '.vue') {
}
// simulate `allowArbitraryExtensions` on TypeScript >= 5.0
const isUnknownTypeSource = !/\.[cm]?[tj]sx?$/.test(filename)
const arbitraryTypeSource = `${filename.slice(0, -ext.length)}.d${ext}.ts`
const hasArbitraryTypeDeclaration =
isUnknownTypeSource && fs.fileExists(arbitraryTypeSource)
if (hasArbitraryTypeDeclaration) {
return babelParse(fs.readFile(arbitraryTypeSource)!, {
plugins: resolveParserPlugins('ts', parserPlugins, true),
sourceType: 'module',
}).program.body
}
if (ext === '.vue') {
const {
descriptor: { script, scriptSetup },
} = parse(content)
@ -1554,6 +1588,15 @@ export function inferRuntimeType(
case 'TSTypeReference': {
const resolved = resolveTypeReference(ctx, node, scope)
if (resolved) {
// #13240
// Special case for function type aliases to ensure correct runtime behavior
// other type aliases still fallback to unknown as before
if (
resolved.type === 'TSTypeAliasDeclaration' &&
resolved.typeAnnotation.type === 'TSFunctionType'
) {
return ['Function']
}
return inferRuntimeType(ctx, resolved, resolved._ownerScope, isKeyOf)
}

View File

@ -23,7 +23,12 @@ export function genCssVarsFromList(
return `{\n ${vars
.map(
key =>
`"${isSSR ? `--` : ``}${genVarName(id, key, isProd, isSSR)}": (${key})`,
// The `:` prefix here is used in `ssrRenderStyle` to distinguish whether
// a custom property comes from `ssrCssVars`. If it does, we need to reset
// its value to `initial` on the component instance to avoid unintentionally
// inheriting the same property value from a different instance of the same
// component in the outer scope.
`"${isSSR ? `:--` : ``}${genVarName(id, key, isProd, isSSR)}": (${key})`,
)
.join(',\n ')}\n}`
}

View File

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

View File

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

View File

@ -39,6 +39,18 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
}
}
const processSelectChildren = (children: TemplateChildNode[]) => {
children.forEach(child => {
if (child.type === NodeTypes.ELEMENT) {
processOption(child as PlainElementNode)
} else if (child.type === NodeTypes.FOR) {
processSelectChildren(child.children)
} else if (child.type === NodeTypes.IF) {
child.branches.forEach(b => processSelectChildren(b.children))
}
})
}
function processOption(plainNode: PlainElementNode) {
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(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -264,11 +264,11 @@ export function watch(
: oldValue,
boundCleanup,
]
oldValue = newValue
call
? call(cb!, WatchErrorCodes.WATCH_CALLBACK, args)
: // @ts-expect-error
cb!(...args)
oldValue = newValue
} finally {
activeWatcher = currentWatcher
}

View File

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

View File

@ -6,6 +6,7 @@ import {
nodeOps,
ref,
render,
useSlots,
} from '@vue/runtime-test'
import { createBlock, normalizeVNode } from '../src/vnode'
import { createSlots } from '../src/helpers/createSlots'
@ -42,6 +43,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',

View File

@ -16,6 +16,7 @@ import {
render,
serialize,
serializeInner,
useModel,
withDirectives,
} from '@vue/runtime-test'
import {
@ -144,6 +145,62 @@ describe('renderer: teleport', () => {
`"<!--teleport start--><!--teleport end--><div>Footer</div><div id="targetId"><div>bar</div></div>"`,
)
})
// #13349
test('handle deferred teleport updates before and after mount', async () => {
const root = document.createElement('div')
document.body.appendChild(root)
const show = ref(false)
const data2 = ref('2')
const data3 = ref('3')
const Comp = {
props: {
modelValue: {},
modelModifiers: {},
},
emits: ['update:modelValue'],
setup(props: any) {
const data2 = useModel(props, 'modelValue')
data2.value = '2+'
return () => h('span')
},
}
createDOMApp({
setup() {
setTimeout(() => (show.value = true), 5)
setTimeout(() => (data3.value = '3+'), 10)
},
render() {
return h(Fragment, null, [
h('span', { id: 'targetId001' }),
show.value
? h(Fragment, null, [
h(Teleport, { to: '#targetId001', defer: true }, [
createTextVNode(String(data3.value)),
]),
h(Comp, {
modelValue: data2.value,
'onUpdate:modelValue': (event: any) =>
(data2.value = event),
}),
])
: createCommentVNode('v-if'),
])
},
}).mount(root)
expect(root.innerHTML).toMatchInlineSnapshot(
`"<span id="targetId001"></span><!--v-if-->"`,
)
await new Promise(r => setTimeout(r, 10))
expect(root.innerHTML).toMatchInlineSnapshot(
`"<span id="targetId001">3+</span><!--teleport start--><!--teleport end--><span></span>"`,
)
})
})
function runSharedTests(deferMode: boolean) {

View File

@ -1654,6 +1654,29 @@ describe('SSR hydration', () => {
expect(`mismatch`).not.toHaveBeenWarned()
})
test('transition appear work with pre-existing class', () => {
const { vnode, container } = mountWithHydration(
`<template><div class="foo">foo</div></template>`,
() =>
h(
Transition,
{ appear: true },
{
default: () => h('div', { class: 'foo' }, 'foo'),
},
),
)
expect(container.firstChild).toMatchInlineSnapshot(`
<div
class="foo v-enter-from v-enter-active"
>
foo
</div>
`)
expect(vnode.el).toBe(container.firstChild)
expect(`mismatch`).not.toHaveBeenWarned()
})
test('transition appear with v-if', () => {
const show = false
const { vnode, container } = mountWithHydration(

View File

@ -861,6 +861,114 @@ describe('renderer: optimized mode', () => {
expect(inner(root)).toBe('<div><div>true</div></div>')
})
// #13305
test('patch Suspense nested in list nodes in optimized mode', async () => {
const deps: Promise<any>[] = []
const Item = {
props: {
someId: { type: Number, required: true },
},
async setup(props: any) {
const p = new Promise(resolve => setTimeout(resolve, 1))
deps.push(p)
await p
return () => (
openBlock(),
createElementBlock('li', null, [
createElementVNode(
'p',
null,
String(props.someId),
PatchFlags.TEXT,
),
])
)
},
}
const list = ref([1, 2, 3])
const App = {
setup() {
return () => (
openBlock(),
createElementBlock(
Fragment,
null,
[
createElementVNode(
'p',
null,
JSON.stringify(list.value),
PatchFlags.TEXT,
),
createElementVNode('ol', null, [
(openBlock(),
createBlock(SuspenseImpl, null, {
fallback: withCtx(() => [
createElementVNode('li', null, 'Loading…'),
]),
default: withCtx(() => [
(openBlock(true),
createElementBlock(
Fragment,
null,
renderList(list.value, id => {
return (
openBlock(),
createBlock(
Item,
{
key: id,
'some-id': id,
},
null,
PatchFlags.PROPS,
['some-id'],
)
)
}),
PatchFlags.KEYED_FRAGMENT,
)),
]),
_: 1 /* STABLE */,
})),
]),
],
PatchFlags.STABLE_FRAGMENT,
)
)
},
}
const app = createApp(App)
app.mount(root)
expect(inner(root)).toBe(`<p>[1,2,3]</p>` + `<ol><li>Loading…</li></ol>`)
await Promise.all(deps)
await nextTick()
expect(inner(root)).toBe(
`<p>[1,2,3]</p>` +
`<ol>` +
`<li><p>1</p></li>` +
`<li><p>2</p></li>` +
`<li><p>3</p></li>` +
`</ol>`,
)
list.value = [3, 1, 2]
await nextTick()
expect(inner(root)).toBe(
`<p>[3,1,2]</p>` +
`<ol>` +
`<li><p>3</p></li>` +
`<li><p>1</p></li>` +
`<li><p>2</p></li>` +
`</ol>`,
)
})
// #4183
test('should not take unmount children fast path /w Suspense', async () => {
const show = ref(true)

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import {
type ComponentOptions,
type ConcreteComponent,
currentInstance,
getComponentName,
isInSSRComponentSetup,
} from './component'
import { isFunction, isObject } from '@vue/shared'
@ -121,14 +122,27 @@ export function defineAsyncComponent<
__asyncLoader: load,
__asyncHydrate(el, instance, hydrate) {
let patched = false
const doHydrate = hydrateStrategy
? () => {
const teardown = hydrateStrategy(hydrate, cb =>
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
if (resolvedComp) {

View File

@ -22,7 +22,7 @@ import { warn } from './warning'
import { type VNode, cloneVNode, createVNode } from './vnode'
import type { RootHydrateFunction } from './hydration'
import { devtoolsInitApp, devtoolsUnmountApp } from './devtools'
import { NO, extend, isFunction, isObject } from '@vue/shared'
import { NO, extend, hasOwn, isFunction, isObject } from '@vue/shared'
import { version } from '.'
import { installAppCompatProperties } from './compat/global'
import type { NormalizedPropsOptions } from './componentProps'
@ -448,10 +448,18 @@ export function createAppAPI<HostElement>(
provide(key, value) {
if (__DEV__ && (key as string | symbol) in context.provides) {
warn(
`App already provides property with key "${String(key)}". ` +
`It will be overwritten with the new value.`,
)
if (hasOwn(context.provides, key as string | symbol)) {
warn(
`App already provides property with key "${String(key)}". ` +
`It will be overwritten with the new value.`,
)
} else {
// #13212, context.provides can inherit the provides object from parent on custom elements
warn(
`App already provides property with key "${String(key)}" inherited from its parent element. ` +
`It will be overwritten with the new value.`,
)
}
}
context.provides[key as string | symbol] = value

View File

@ -59,10 +59,12 @@ export function inject(
// to support `app.use` plugins,
// fallback to appContext's `provides` if the instance is at root
// #11488, in a nested createApp, prioritize using the provides from currentApp
const provides = currentApp
// #13212, for custom elements we must get injected values from its appContext
// as it already inherits the provides object from the parent element
let provides = currentApp
? currentApp._context.provides
: instance
? instance.parent == null
? instance.parent == null || instance.ce
? instance.vnode.appContext && instance.vnode.appContext.provides
: instance.parent.provides
: undefined

View File

@ -115,20 +115,23 @@ export type ComponentInstance<T> = T extends { new (): ComponentPublicInstance }
: T extends FunctionalComponent<infer Props, infer Emits>
? ComponentPublicInstance<Props, {}, {}, {}, {}, ShortEmitsToObject<Emits>>
: T extends Component<
infer Props,
infer PropsOrInstance,
infer RawBindings,
infer D,
infer C,
infer M
>
? // NOTE we override Props/RawBindings/D to make sure is not `unknown`
ComponentPublicInstance<
unknown extends Props ? {} : Props,
unknown extends RawBindings ? {} : RawBindings,
unknown extends D ? {} : D,
C,
M
>
? PropsOrInstance extends { $props: unknown }
? // T is returned by `defineComponent()`
PropsOrInstance
: // NOTE we override Props/RawBindings/D to make sure is not `unknown`
ComponentPublicInstance<
unknown extends PropsOrInstance ? {} : PropsOrInstance,
unknown extends RawBindings ? {} : RawBindings,
unknown extends D ? {} : D,
C,
M
>
: never // not a vue Component
/**
@ -259,7 +262,7 @@ export type ConcreteComponent<
* The constructor type is an artificial type returned by defineComponent().
*/
export type Component<
Props = any,
PropsOrInstance = any,
RawBindings = any,
D = any,
C extends ComputedOptions = ComputedOptions,
@ -267,8 +270,8 @@ export type Component<
E extends EmitsOptions | Record<string, any[]> = {},
S extends Record<string, any> = any,
> =
| ConcreteComponent<Props, RawBindings, D, C, M, E, S>
| ComponentPublicInstanceConstructor<Props>
| ConcreteComponent<PropsOrInstance, RawBindings, D, C, M, E, S>
| ComponentPublicInstanceConstructor<PropsOrInstance>
export type { ComponentOptions }
@ -582,13 +585,13 @@ export interface ComponentInternalInstance {
* For updating css vars on contained teleports
* @internal
*/
ut?: (vars?: Record<string, string>) => void
ut?: (vars?: Record<string, unknown>) => void
/**
* dev only. For style v-bind hydration mismatch checks
* @internal
*/
getCssVars?: () => Record<string, string>
getCssVars?: () => Record<string, unknown>
/**
* v2 compat only, for caching mutated $options

View File

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

View File

@ -143,7 +143,9 @@ type InferPropType<T, NullAsAny = true> = [T] extends [null]
export type ExtractPropTypes<O> = {
// use `keyof Pick<O, RequiredKeys<O>>` instead of `RequiredKeys<O>` to
// support IDE features
[K in keyof Pick<O, RequiredKeys<O>>]: InferPropType<O[K]>
[K in keyof Pick<O, RequiredKeys<O>>]: O[K] extends { default: any }
? Exclude<InferPropType<O[K]>, undefined>
: InferPropType<O[K]>
} & {
// use `keyof Pick<O, OptionalKeys<O>>` instead of `OptionalKeys<O>` to
// support IDE features

View File

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

View File

@ -164,15 +164,16 @@ export const TeleportImpl = {
}
if (isTeleportDeferred(n2.props)) {
n2.el!.__isMounted = false
queuePostRenderEffect(() => {
mountToTarget()
n2.el!.__isMounted = true
delete n2.el!.__isMounted
}, parentSuspense)
} else {
mountToTarget()
}
} else {
if (isTeleportDeferred(n2.props) && !n1.el!.__isMounted) {
if (isTeleportDeferred(n2.props) && n1.el!.__isMounted === false) {
queuePostRenderEffect(() => {
TeleportImpl.process(
n1,
@ -186,7 +187,6 @@ export const TeleportImpl = {
optimized,
internals,
)
delete n1.el!.__isMounted
}, parentSuspense)
return
}

View File

@ -28,6 +28,7 @@ import {
isReservedProp,
isString,
normalizeClass,
normalizeCssVarValue,
normalizeStyle,
stringifyStyle,
} from '@vue/shared'
@ -398,9 +399,11 @@ export function createHydrationFunctions(
parentComponent.vnode.props.appear
const content = (el as HTMLTemplateElement).content
.firstChild as Element
.firstChild as Element & { $cls?: string }
if (needCallTransitionHooks) {
const cls = content.getAttribute('class')
if (cls) content.$cls = cls
transition!.beforeEnter(content)
}
@ -786,7 +789,7 @@ export function createHydrationFunctions(
* Dev only
*/
function propHasMismatch(
el: Element,
el: Element & { $cls?: string },
key: string,
clientValue: any,
vnode: VNode,
@ -799,7 +802,12 @@ function propHasMismatch(
if (key === 'class') {
// classes might be in different order, but that doesn't affect cascade
// so we just need to check if the class lists contain the same classes.
actual = el.getAttribute('class')
if (el.$cls) {
actual = el.$cls
delete el.$cls
} else {
actual = el.getAttribute('class')
}
expected = normalizeClass(clientValue)
if (!isSetEqual(toClassSet(actual || ''), toClassSet(expected))) {
mismatchType = MismatchTypes.CLASS
@ -938,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) {
@ -990,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])
}
}

View File

@ -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<HostElement = RendererElement> {
render: RootRenderFunction<HostElement>
@ -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)
}
}
@ -961,7 +964,8 @@ function baseCreateRenderer(
// which also requires the correct parent container
!isSameVNodeType(oldVNode, newVNode) ||
// - In the case of a component, it could contain anything.
oldVNode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT))
oldVNode.shapeFlag &
(ShapeFlags.COMPONENT | ShapeFlags.TELEPORT | ShapeFlags.SUSPENSE))
? hostParentNode(oldVNode.el)!
: // In other cases, the parent container is not actually used so we
// just pass the block element here to avoid a DOM parentNode call.
@ -1347,7 +1351,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)
}

View File

@ -444,6 +444,36 @@ describe('defineCustomElement', () => {
const e = container.childNodes[0] as VueElement
expect(e.shadowRoot!.innerHTML).toBe('hello')
})
test('prop types validation', async () => {
const E = defineCustomElement({
props: {
num: {
type: [Number, String],
},
bool: {
type: Boolean,
},
},
render() {
return h('div', [
h('span', [`${this.num} is ${typeof this.num}`]),
h('span', [`${this.bool} is ${typeof this.bool}`]),
])
},
})
customElements.define('my-el-with-type-props', E)
render(h('my-el-with-type-props', { num: 1, bool: true }), container)
const e = container.childNodes[0] as VueElement
// @ts-expect-error
expect(e.num).toBe(1)
// @ts-expect-error
expect(e.bool).toBe(true)
expect(e.shadowRoot!.innerHTML).toBe(
'<div><span>1 is number</span><span>true is boolean</span></div>',
)
})
})
describe('attrs', () => {
@ -708,6 +738,101 @@ describe('defineCustomElement', () => {
`<div>changedA! changedB!</div>`,
)
})
// #13212
test('inherited from app context within nested elements', async () => {
const outerValues: (string | undefined)[] = []
const innerValues: (string | undefined)[] = []
const innerChildValues: (string | undefined)[] = []
const Outer = defineCustomElement(
{
setup() {
outerValues.push(
inject<string>('shared'),
inject<string>('outer'),
inject<string>('inner'),
)
},
render() {
return h('div', [renderSlot(this.$slots, 'default')])
},
},
{
configureApp(app) {
app.provide('shared', 'shared')
app.provide('outer', 'outer')
},
},
)
const Inner = defineCustomElement(
{
setup() {
// ensure values are not self-injected
provide('inner', 'inner-child')
innerValues.push(
inject<string>('shared'),
inject<string>('outer'),
inject<string>('inner'),
)
},
render() {
return h('div', [renderSlot(this.$slots, 'default')])
},
},
{
configureApp(app) {
app.provide('outer', 'override-outer')
app.provide('inner', 'inner')
},
},
)
const InnerChild = defineCustomElement({
setup() {
innerChildValues.push(
inject<string>('shared'),
inject<string>('outer'),
inject<string>('inner'),
)
},
render() {
return h('div')
},
})
customElements.define('provide-from-app-outer', Outer)
customElements.define('provide-from-app-inner', Inner)
customElements.define('provide-from-app-inner-child', InnerChild)
container.innerHTML =
'<provide-from-app-outer>' +
'<provide-from-app-inner>' +
'<provide-from-app-inner-child></provide-from-app-inner-child>' +
'</provide-from-app-inner>' +
'</provide-from-app-outer>'
const outer = container.childNodes[0] as VueElement
expect(outer.shadowRoot!.innerHTML).toBe('<div><slot></slot></div>')
expect('[Vue warn]: injection "inner" not found.').toHaveBeenWarnedTimes(
1,
)
expect(
'[Vue warn]: App already provides property with key "outer" inherited from its parent element. ' +
'It will be overwritten with the new value.',
).toHaveBeenWarnedTimes(1)
expect(outerValues).toEqual(['shared', 'outer', undefined])
expect(innerValues).toEqual(['shared', 'override-outer', 'inner'])
expect(innerChildValues).toEqual([
'shared',
'override-outer',
'inner-child',
])
})
})
describe('styles', () => {
@ -791,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 = `<my-foo-with-shadowroot-false></my-foo-with-shadowroot-false>`
const el = container.childNodes[0] as VueElement
const style = el.shadowRoot?.querySelector('style')
expect(style).toBeUndefined()
})
test('with nonce', () => {
const Foo = defineCustomElement(
{
@ -1131,6 +1280,92 @@ describe('defineCustomElement', () => {
expect(target.innerHTML).toBe(`<span>default</span>`)
app.unmount()
})
test('toggle nested custom element with shadowRoot: false', async () => {
customElements.define(
'my-el-child-shadow-false',
defineCustomElement(
{
render(ctx: any) {
return h('div', null, [renderSlot(ctx.$slots, 'default')])
},
},
{ shadowRoot: false },
),
)
const ChildWrapper = {
render() {
return h('my-el-child-shadow-false', null, 'child')
},
}
customElements.define(
'my-el-parent-shadow-false',
defineCustomElement(
{
props: {
isShown: { type: Boolean, required: true },
},
render(ctx: any, _: any, $props: any) {
return $props.isShown
? h('div', { key: 0 }, [renderSlot(ctx.$slots, 'default')])
: null
},
},
{ shadowRoot: false },
),
)
const ParentWrapper = {
props: {
isShown: { type: Boolean, required: true },
},
render(ctx: any, _: any, $props: any) {
return h('my-el-parent-shadow-false', { isShown: $props.isShown }, [
renderSlot(ctx.$slots, 'default'),
])
},
}
const isShown = ref(true)
const App = {
render() {
return h(ParentWrapper, { isShown: isShown.value } as any, {
default: () => [h(ChildWrapper)],
})
},
}
const container = document.createElement('div')
document.body.appendChild(container)
const app = createApp(App)
app.mount(container)
expect(container.innerHTML).toBe(
`<my-el-parent-shadow-false is-shown="" data-v-app="">` +
`<div>` +
`<my-el-child-shadow-false data-v-app="">` +
`<div>child</div>` +
`</my-el-child-shadow-false>` +
`</div>` +
`</my-el-parent-shadow-false>`,
)
isShown.value = false
await nextTick()
expect(container.innerHTML).toBe(
`<my-el-parent-shadow-false data-v-app=""><!----></my-el-parent-shadow-false>`,
)
isShown.value = true
await nextTick()
expect(container.innerHTML).toBe(
`<my-el-parent-shadow-false data-v-app="" is-shown="">` +
`<div>` +
`<my-el-child-shadow-false data-v-app="">` +
`<div>child</div>` +
`</my-el-child-shadow-false>` +
`</div>` +
`</my-el-parent-shadow-false>`,
)
})
})
describe('helpers', () => {
@ -1330,6 +1565,64 @@ describe('defineCustomElement', () => {
expect(e.shadowRoot?.innerHTML).toBe('<div>app-injected</div>')
})
// #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 = `<my-async-element-with-app></my-async-element-with-app>`
const e = container.childNodes[0] as VueElement
await new Promise(r => setTimeout(r))
expect(e.shadowRoot?.innerHTML).toBe('<div>app-injected</div>')
})
test('with hmr reload', async () => {
const __hmrId = '__hmrWithApp'
const def = defineComponent({
__hmrId,
setup() {
const msg = inject('msg')
return { msg }
},
render(this: any) {
return h('div', [h('span', this.msg), h('span', this.$foo)])
},
})
const E = defineCustomElement(def, {
configureApp(app) {
app.provide('msg', 'app-injected')
app.config.globalProperties.$foo = 'foo'
},
})
customElements.define('my-element-with-app-hmr', E)
container.innerHTML = `<my-element-with-app-hmr></my-element-with-app-hmr>`
const el = container.childNodes[0] as VueElement
expect(el.shadowRoot?.innerHTML).toBe(
`<div><span>app-injected</span><span>foo</span></div>`,
)
// hmr
__VUE_HMR_RUNTIME__.reload(__hmrId, def as any)
await nextTick()
expect(el.shadowRoot?.innerHTML).toBe(
`<div><span>app-injected</span><span>foo</span></div>`,
)
})
})
// #9885

View File

@ -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<Record<string, unknown>>({
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')
})
})

View File

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

View File

@ -269,18 +269,14 @@ export class VueElement
this._root = this
}
}
if (!(this._def as ComponentOptions).__asyncLoader) {
// for sync component defs we can immediately resolve props
this._resolveProps(this._def)
}
}
connectedCallback(): void {
// avoid resolving component if it's not connected
if (!this.isConnected) return
if (!this.shadowRoot) {
// avoid re-parsing slots if already resolved
if (!this.shadowRoot && !this._resolved) {
this._parseSlots()
}
this._connected = true
@ -298,8 +294,7 @@ export class VueElement
if (!this._instance) {
if (this._resolved) {
this._setParent()
this._update()
this._mount(this._def)
} else {
if (parent && parent._pendingResolve) {
this._pendingResolve = parent._pendingResolve.then(() => {
@ -316,7 +311,18 @@ export class VueElement
private _setParent(parent = this._parent) {
if (parent) {
this._instance!.parent = parent._instance
this._instance!.provides = parent._instance!.provides
this._inheritParentContext(parent)
}
}
private _inheritParentContext(parent = this._parent) {
// #13212, the provides object of the app context must inherit the provides
// object from the parent element so we can inject values from both places
if (parent && this._app) {
Object.setPrototypeOf(
this._app._context.provides,
parent._instance!.provides,
)
}
}
@ -380,12 +386,7 @@ export class VueElement
}
}
this._numberProps = numberProps
if (isAsync) {
// defining getter/setters on prototype
// for sync defs, this already happened in the constructor
this._resolveProps(def)
}
this._resolveProps(def)
// apply CSS
if (this.shadowRoot) {
@ -403,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)
}
@ -417,6 +419,8 @@ export class VueElement
def.name = 'VueElement'
}
this._app = this._createApp(def)
// inherit before configureApp to detect context overwrites
this._inheritParentContext()
if (def.configureApp) {
def.configureApp(this._app)
}
@ -520,7 +524,9 @@ export class VueElement
}
private _update() {
render(this._createVNode(), this._root)
const vnode = this._createVNode()
if (this._app) vnode.appContext = this._app._context
render(vnode, this._root)
}
private _createVNode(): VNode<any, any> {

View File

@ -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<string, string>): void {
export function useCssVars(
getter: (ctx: any) => Record<string, unknown>,
): void {
if (!__BROWSER__ && !__TEST__) return
const instance = getCurrentInstance()
@ -64,7 +66,7 @@ export function useCssVars(getter: (ctx: any) => Record<string, string>): void {
})
}
function setVarsOnVNode(vnode: VNode, vars: Record<string, string>) {
function setVarsOnVNode(vnode: VNode, vars: Record<string, unknown>) {
if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
const suspense = vnode.suspense!
vnode = suspense.activeBranch!
@ -94,13 +96,14 @@ function setVarsOnVNode(vnode: VNode, vars: Record<string, string>) {
}
}
function setVarsOnNode(el: Node, vars: Record<string, string>) {
function setVarsOnNode(el: Node, vars: Record<string, unknown>) {
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
}

View File

@ -58,8 +58,8 @@ declare module '@vue/runtime-core' {
vOn: VOnDirective
vBind: VModelDirective
vIf: Directive<any, boolean>
VOnce: Directive
VSlot: Directive
vOnce: Directive
vSlot: Directive
}
}

View File

@ -79,6 +79,7 @@ export function compatCoerceAttr(
}
} else if (
value === false &&
!(el.tagName === 'INPUT' && key === 'value') &&
!isSpecialBooleanAttr(key) &&
compatUtils.isCompatEnabled(DeprecationTypes.ATTR_FALSE_VALUE, instance)
) {

View File

@ -203,4 +203,19 @@ describe('ssr: renderStyle', () => {
}),
).toBe(`color:&quot;&gt;&lt;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;`)
})
})

View File

@ -49,7 +49,7 @@ test('pipeToWebWritable', async () => {
}
const { readable, writable } = new TransformStream()
pipeToWebWritable(createApp(App), {}, writable)
pipeToWebWritable(createApp(App), {}, writable as any)
const reader = readable.getReader()
const decoder = new TextDecoder()

View File

@ -1,6 +1,6 @@
{
"name": "@vue/server-renderer",
"version": "3.5.14",
"version": "3.5.17",
"description": "@vue/server-renderer",
"main": "index.js",
"module": "dist/server-renderer.esm-bundler.js",

View File

@ -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<string, unknown> = {}
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
}

View File

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

View File

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

View File

@ -0,0 +1,24 @@
/**
* Normalize CSS var value created by `v-bind` in `<style>` block
* See https://github.com/vuejs/core/pull/12461#issuecomment-2495804664
*/
export function normalizeCssVarValue(value: unknown): string {
if (value == null) {
return 'initial'
}
if (typeof value === 'string') {
return value === '' ? ' ' : value
}
if (typeof value !== 'number' || !Number.isFinite(value)) {
if (__DEV__) {
console.warn(
'[Vue warn] Invalid value used for CSS binding. Expected a string or a finite number but received:',
value,
)
}
}
return String(value)
}

View File

@ -12,3 +12,4 @@ export * from './escapeHtml'
export * from './looseEqual'
export * from './toDisplayString'
export * from './typeUtils'
export * from './cssVars'

View File

@ -139,6 +139,6 @@ export const PatchFlagNames: Record<PatchFlags, string> = {
[PatchFlags.NEED_PATCH]: `NEED_PATCH`,
[PatchFlags.DYNAMIC_SLOTS]: `DYNAMIC_SLOTS`,
[PatchFlags.DEV_ROOT_FRAGMENT]: `DEV_ROOT_FRAGMENT`,
[PatchFlags.CACHED]: `HOISTED`,
[PatchFlags.CACHED]: `CACHED`,
[PatchFlags.BAIL]: `BAIL`,
}

View File

@ -93,6 +93,16 @@ test('COMPILER_V_BIND_OBJECT_ORDER', () => {
).toHaveBeenWarned()
})
test('should not warn COMPILER_V_BIND_OBJECT_ORDER work with vFor', () => {
const vm = new Vue({
template: `<div><div v-bind="{ id: 'bar', class: 'baz' }" v-for="item in 5" /></div>`,
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLDivElement)
expect(
CompilerDeprecationTypes.COMPILER_V_BIND_OBJECT_ORDER,
).not.toHaveBeenWarned()
})
test('COMPILER_V_ON_NATIVE', () => {
const spy = vi.fn()
const vm = new Vue({

View File

@ -82,4 +82,62 @@ describe('COMPONENT_V_MODEL', () => {
template: `<input :value="input" @input="$emit('update', $event.target.value)">`,
})
})
async function runTestWithModifier(CustomInput: ComponentOptions) {
const vm = new Vue({
data() {
return {
text: ' foo ',
}
},
components: {
CustomInput,
},
template: `
<div>
<span>{{ text }}</span>
<custom-input v-model.trim="text"></custom-input>
</div>
`,
}).$mount() as any
const input = vm.$el.querySelector('input')
const span = vm.$el.querySelector('span')
expect(input.value).toBe(' foo ')
expect(span.textContent).toBe(' foo ')
expect(
(deprecationData[DeprecationTypes.COMPONENT_V_MODEL].message as Function)(
CustomInput,
),
).toHaveBeenWarned()
input.value = ' bar '
triggerEvent(input, 'input')
await nextTick()
expect(input.value).toBe('bar')
expect(span.textContent).toBe('bar')
}
test('with model modifiers', async () => {
await runTestWithModifier({
name: 'CustomInput',
props: ['value'],
template: `<input :value="value" @input="$emit('input', $event.target.value)">`,
})
})
test('with model modifiers and model option', async () => {
await runTestWithModifier({
name: 'CustomInput',
props: ['foo'],
model: {
prop: 'foo',
event: 'bar',
},
template: `<input :value="foo" @input="$emit('bar', $event.target.value)">`,
})
})
})

View File

@ -208,6 +208,20 @@ test('ATTR_FALSE_VALUE', () => {
).toHaveBeenWarned()
})
test('ATTR_FALSE_VALUE with false on input value', () => {
const vm = new Vue({
template: `<input :value="false"/>`,
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLInputElement)
expect(vm.$el.hasAttribute('value')).toBe(true)
expect(vm.$el.getAttribute('value')).toBe('false')
expect(
(deprecationData[DeprecationTypes.ATTR_FALSE_VALUE].message as Function)(
'value',
),
).not.toHaveBeenWarned()
})
test("ATTR_FALSE_VALUE with false value shouldn't throw warning", () => {
const vm = new Vue({
template: `<div :id="false" :foo="false"/>`,

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compat",
"version": "3.5.14",
"version": "3.5.17",
"description": "Vue 3 compatibility build for Vue 2",
"main": "index.js",
"module": "dist/vue.runtime.esm-bundler.js",

View File

@ -3254,7 +3254,7 @@ describe('e2e: Transition', () => {
setup: () => {
// Big arrays kick GC earlier
const test = ref([...Array(30_000_000)].map((_, i) => ({ i })))
// TODO: Use a diferent TypeScript env for testing
// TODO: Use a different TypeScript env for testing
// @ts-expect-error - Custom property and same lib as runtime is used
window.__REF__ = new WeakRef(test)
@ -3309,7 +3309,7 @@ describe('e2e: Transition', () => {
setup: () => {
// Big arrays kick GC earlier
const test = ref([...Array(30_000_000)].map((_, i) => ({ i })))
// TODO: Use a diferent TypeScript env for testing
// TODO: Use a different TypeScript env for testing
// @ts-expect-error - Custom property and same lib as runtime is used
window.__REF__ = new WeakRef(test)

View File

@ -15,13 +15,17 @@
} = Vue
const Comp = {
setup() {
props: {
value: Boolean,
},
setup(props) {
const count = ref(0)
onMounted(() => {
console.log('hydrated')
window.isHydrated = true
})
return () => {
props.value
return h('button', { onClick: () => count.value++ }, count.value)
}
},
@ -37,7 +41,9 @@
onMounted(() => {
window.isRootMounted = true
})
return () => h(AsyncComp)
const show = (window.show = ref(true))
return () => h(AsyncComp, { value: show.value })
},
}).mount('#app')
</script>

View File

@ -86,6 +86,36 @@ describe('async component hydration strategies', () => {
await assertHydrationSuccess()
})
// #13255
test('media query (patched before hydration)', async () => {
const spy = vi.fn()
const currentPage = page()
currentPage.on('pageerror', spy)
const warn: any[] = []
currentPage.on('console', e => warn.push(e.text()))
await goToCase('media')
await page().waitForFunction(() => window.isRootMounted)
expect(await page().evaluate(() => window.isHydrated)).toBe(false)
// patch
await page().evaluate(() => (window.show.value = false))
await click('button')
expect(await text('button')).toBe('1')
// resize
await page().setViewport({ width: 400, height: 600 })
await page().waitForFunction(() => window.isHydrated)
await assertHydrationSuccess('2')
expect(spy).toBeCalledTimes(0)
currentPage.off('pageerror', spy)
expect(
warn.some(w => w.includes('Skipping lazy hydration for component')),
).toBe(true)
})
test('interaction', async () => {
await goToCase('interaction')
await page().waitForFunction(() => window.isRootMounted)

View File

@ -6,7 +6,7 @@ import type { NativeElements, ReservedProps, VNode } from '@vue/runtime-dom'
* when ts compilerOptions.jsx is 'react-jsx' or 'react-jsxdev'
* https://www.typescriptlang.org/tsconfig#jsxImportSource
*/
export { h as jsx, h as jsxDEV, Fragment } from '@vue/runtime-dom'
export { h as jsx, h as jsxDEV, Fragment, h as jsxs } from '@vue/runtime-dom'
export namespace JSX {
export interface Element extends VNode {}

View File

@ -1,6 +1,6 @@
{
"name": "vue",
"version": "3.5.14",
"version": "3.5.17",
"description": "The progressive JavaScript framework for building modern web UI.",
"main": "index.js",
"module": "dist/vue.runtime.esm-bundler.js",

File diff suppressed because it is too large Load Diff

View File

@ -3,8 +3,8 @@ packages:
- 'packages-private/*'
catalog:
'@babel/parser': ^7.27.2
'@babel/types': ^7.27.1
'@babel/parser': ^7.27.5
'@babel/types': ^7.27.6
'estree-walker': ^2.0.2
'magic-string': ^0.30.17
'source-map-js': ^1.2.1

View File

@ -18,7 +18,6 @@ nr build core --formats cjs
import fs from 'node:fs'
import { parseArgs } from 'node:util'
import { existsSync, readFileSync } from 'node:fs'
import path from 'node:path'
import { brotliCompressSync, gzipSync } from 'node:zlib'
import pico from 'picocolors'
@ -163,7 +162,7 @@ async function build(target) {
? `packages-private`
: `packages`
const pkgDir = path.resolve(`${pkgBase}/${target}`)
const pkg = JSON.parse(readFileSync(`${pkgDir}/package.json`, 'utf-8'))
const pkg = JSON.parse(fs.readFileSync(`${pkgDir}/package.json`, 'utf-8'))
// if this is a full build (no specific targets), ignore private packages
if ((isRelease || !targets.length) && pkg.private) {
@ -171,7 +170,7 @@ async function build(target) {
}
// if building a specific format, do not remove dist.
if (!formats && existsSync(`${pkgDir}/dist`)) {
if (!formats && fs.existsSync(`${pkgDir}/dist`)) {
fs.rmSync(`${pkgDir}/dist`, { recursive: true })
}
@ -234,7 +233,7 @@ async function checkSize(target) {
* @returns {Promise<void>}
*/
async function checkFileSize(filePath) {
if (!existsSync(filePath)) {
if (!fs.existsSync(filePath)) {
return
}
const file = fs.readFileSync(filePath)