Compare commits

...

145 Commits

Author SHA1 Message Date
Stefano Nepa e9c676ff2b
chore(runtime-dom): export nodeOps and patchProp for better accessibility (#13753) 2025-11-10 09:38:05 +08:00
daiwei e131369833 release: v3.5.24
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details
size data / upload (push) Has been cancelled Details
2025-11-07 16:02:40 +08:00
殷谊辉 90ce838a94
chore(reactivity): remove duplicated ReactiveEffectRunner interface (#14063) 2025-11-07 14:04:03 +08:00
edison 11ec51aa5a
Revert "fix(compiler-core): correctly handle ts type assertions in expression…" (#14062)
This reverts commit e6544ac292.
Close #14060
2025-11-07 08:52:07 +08:00
daiwei 5cf0097f33 release: v3.5.23
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details
size data / upload (push) Has been cancelled Details
2025-11-06 08:27:08 +08:00
edison f411c6604c
fix(suspense): clear placeholder and fallback el after resolve to enable GC (#13928)
ci / test (push) Waiting to run Details
ci / continuous-release (push) Waiting to run Details
size data / upload (push) Waiting to run Details
2025-11-05 21:53:06 +08:00
Jooies dc4dd594fb
fix(TransitionGroup): use offsetLeft and offsetTop instead of getBoundingClientRect to avoid transform scale affect animation (#6108)
ci / test (push) Waiting to run Details
ci / continuous-release (push) Waiting to run Details
size data / upload (push) Waiting to run Details
close #6105
2025-11-05 17:20:25 +08:00
indykoning 40c4b2a876
fix(runtime-core): pass props and children to loadingComponent (#13997) 2025-11-05 17:18:20 +08:00
zhiyuanzmj e6544ac292
fix(compiler-core): correctly handle ts type assertions in expressions (#13397)
similar to #13395
2025-11-05 17:17:49 +08:00
山吹色御守 75d44c7189
fix(compiler-sfc): resolve numeric literals and template literals without expressions as static property key (#13998) 2025-11-05 17:13:04 +08:00
沈青川 dcc6f36257
fix(compiler): using guard instead of non-nullish assertion (#13982) 2025-11-05 17:12:23 +08:00
rzzf 8fbe48fe39
fix(v-model): handle number modifier on change (#13959)
close #13958
2025-11-05 17:11:35 +08:00
edison 6cbdf7823b
fix(hydration): avoid mismatch during hydrate text with newlines in interpolation (#9232)
close #9229
2025-11-05 17:05:50 +08:00
Alex Snezhko 006a0c1011
fix(compiler-ssr): textarea with v-text directive SSR (#13975) 2025-11-05 17:05:29 +08:00
skirtle b8aab3d209
refactor(runtime-core): check feature flag when forwarding `data` properties (#13966) 2025-11-05 17:04:55 +08:00
edison 84ca349fef
fix(custom-element): optimize slot retrieval to avoid duplicates (#13961)
close #13955
2025-11-05 17:04:33 +08:00
Vida Xie 8ca2b3fbb7
chore(lint): replace deprecated `tseslint.config` and `prefer-ts-expect-error` (#13942) 2025-11-05 17:04:12 +08:00
clay jenson 5689884c8e
fix(runtime-dom): ensure iframe sandbox is handled as an attribute to prevent unintended behavior (#13950)
close #13946
2025-11-05 16:53:58 +08:00
edison b3cca2611c
fix(compiler-core): fix v-bind shorthand handling for in-DOM templates (#13933)
close #13930
2025-11-05 16:51:29 +08:00
Dylan Lathrum 8ec7cb12e4
types(runtime-core): add `undefined` to `NativeType` type (#13594)
close #13593
2025-11-05 16:50:58 +08:00
Alex Snezhko c13e674fb9
fix(custom-element): batch custom element prop patching (#13478)
close #12619
2025-11-05 16:50:00 +08:00
zhiyuanzmj 1df8990504
types(jsx-runtime): use interface instead of type for ReservedProps (#12385) 2025-11-05 16:35:37 +08:00
renovate[bot] d715e5f6f1
fix(deps): update dependency monaco-editor to ^0.54.0 (#13985)
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details
size data / upload (push) Has been cancelled Details
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 11:28:30 +08:00
renovate[bot] 475539c154
chore(deps): update actions/setup-node action to v6 (#13999)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 11:27:57 +08:00
renovate[bot] cd7c9a371c
chore(deps): update dependency pretty-bytes to v7 (#13968)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 11:27:35 +08:00
renovate[bot] c35e880f7f
chore(deps): update actions/upload-artifact action to v5 (#14022)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 11:27:10 +08:00
renovate[bot] 90d3ff4dec
chore(deps): update compiler (#14021)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 11:26:42 +08:00
renovate[bot] 7065cee4fd
chore(deps): update build (#13939)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 11:21:53 +08:00
renovate[bot] f00e5c7885
chore(deps): update all non-major dependencies (#13967)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 11:12:05 +08:00
renovate[bot] 2d65306949
chore(deps): update test (#13940)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 11:06:23 +08:00
edison 45547e69b2
docs: remove COMPILER_V_BIND_PROP (#13986)
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
.prop was removed in 3.0. It was reintroduced in 3.2. 
see vuejs/core@1c7d737
2025-10-13 15:03:10 +08:00
skirtle 079010a38c
test(v-model): mutating an array or set checkbox value (#13974)
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details
size data / upload (push) Has been cancelled Details
2025-10-09 10:16:11 +08:00
abeer0 2dbe30177f
chore: fix typo (#13973) 2025-10-09 09:28:26 +08:00
王二狗 c16f8a94c7
chore: fix typo. (#13948)
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details
size data / upload (push) Has been cancelled Details
2025-10-04 09:46:30 +08:00
daiwei 5a8aa0b2ba release: v3.5.22
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details
size data / upload (push) Has been cancelled Details
2025-09-25 09:05:13 +08:00
Tobias Messner 1be5ddfe87
fix(transition-group): run `forceReflow` on the correct document (fix #13849) (#13853)
close #13849
2025-09-25 08:42:52 +08:00
renovate[bot] d44a5a98c8
chore(deps): update build (#13856)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-24 21:46:10 +08:00
renovate[bot] c8a99172cc
chore(deps): update dependency jsdom to v27 (#13913)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-24 21:45:42 +08:00
renovate[bot] b46481a47f
chore(deps): update compiler (#13857)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-24 21:40:59 +08:00
renovate[bot] 8593647e37
chore(deps): update test (#13882)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-24 21:40:29 +08:00
renovate[bot] f2487d86ea
chore(deps): update actions/github-script action to v8 (#13885) 2025-09-24 21:37:46 +08:00
renovate[bot] b374ec7ca9
chore(deps): update actions/setup-node action to v5 (#13912)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-24 21:36:44 +08:00
renovate[bot] 9612b95220
chore(deps): update all non-major dependencies (#13883)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-24 21:34:31 +08:00
Tony Wang 5953c9ff90
fix(compiler-core): identifiers in switch-case should not be inferred as references (#13923) 2025-09-24 21:33:48 +08:00
edison 565741a9b2
refactor(compiler): add separate transform for vbind shorthand (#13438)
close #13169
close #13170
close #11321
close #12298
close #12828

use tests from #13170 and #12298 and #12828
2025-09-24 21:23:07 +08:00
Matthias Hryniszak 47e628df1c
feat(custom-element): allow specifying additional options for `shadowRoot` in custom elements (#12965)
close #12964
2025-09-24 21:14:54 +08:00
edison 6b68f72673
Revert "fix(hmr): prevent __VUE_HMR_RUNTIME__ from being overwritten by vue runtime in 3rd-party libraries" (#13925)
This reverts commit 1392734ae5.
2025-09-24 18:02:05 +08:00
Massimiliano Torromeo 8bb8fb2362
fix(types): more precise types for Events and added missing definitions (#9675) 2025-09-24 17:56:28 +08:00
Alex Snezhko c4a88cdd0d
fix(custom-element): set prop runs pending mutations before disconnect (#13897)
close #13315
2025-09-24 17:42:11 +08:00
edison e388f1a09f
fix(compiler-sfc): enhance inferRuntimeType to support TSMappedType with indexed access (#13848)
close #13847
2025-09-24 17:29:38 +08:00
Arthur Darkstone fda47ac702
chore(types): improve type safety in watch functions and instanceWatch (#13918) 2025-09-24 17:21:41 +08:00
linzhe 5e1e791880
fix(custom-element): properly mount multiple Teleports in custom element component w/ shadowRoot false (#13900)
close #13899
2025-09-24 17:15:36 +08:00
Arman Tang 95c1975604
fix(compiler-dom): nodes with v-once shouldn't be stringified (#13878) 2025-09-24 17:13:44 +08:00
czhlin 4b7170625d
fix(types): widen directive arg type from string to any (#13758)
closes #13757
2025-09-24 17:12:25 +08:00
Daniel Roe 9c279517b9
fix(compiler-sfc): ensure css custom properties do not start with a digit (#13870) 2025-09-24 17:11:36 +08:00
edison aba7feda17
fix(reactivity): respect readonly during ref unwrapping (#13905)
close #13903
2025-09-24 17:10:49 +08:00
edison ba7f7f90f6
fix(compiler-sfc): add support for @vue-ignore in runtime type resolution (#13906) 2025-09-24 17:10:20 +08:00
edison 5358bca4a8
fix(custom-element): use PatchFlags.BAIL for slot when props are present (#13907)
close #13904
2025-09-24 17:08:25 +08:00
linzhe 836b82976f
fix(compiler-ssr): ensure v-show has a higher priority in SSR (#12171)
close #12162
2025-09-24 17:06:03 +08:00
山吹色御守 8620a616eb
fix(types): set dom stub type to `never` instead of `{}` (#13915)
re-fix #11564
2025-09-24 17:04:51 +08:00
yangdan8 2078f8b756
fix(reactivity): update iterator to check for completion instead of value presence (#13761) 2025-09-24 17:04:15 +08:00
edison abd563822a
fix(compiler-sfc): ensure props bindings register before compiling template (#13922)
close #13920
2025-09-24 17:03:47 +08:00
renovate[bot] b555f02eed
fix(deps): update playground (#13884) 2025-09-15 10:41:09 +08:00
daiwei 8c1f61d050 chore: format 2025-09-15 10:18:59 +08:00
codelo e5a6fe42ea
chore(docs): add missing commas 2025-09-15 10:08:36 +08:00
edison 75220c7995
fix(runtime-core): simplify block-tracking disabling in h() (#13841) 2025-09-03 09:13:09 +08:00
daiwei 4b6cb1f52a release: v3.5.21 2025-09-02 17:59:45 +08:00
yangxiuxiu 5d75a170c8
fix(Suspence): handle Suspense + KeepAlive HMR updating edge case (#13076)
close #13075
2025-09-02 17:44:13 +08:00
Alex Snezhko 55922ff316
fix(compiler-sfc): check lang before attempt to compile script (#13508)
close #8368
2025-09-02 17:39:29 +08:00
山吹色御守 1e8b65aa49
perf: improve regexp performance with non-capturing groups (#13567) 2025-09-02 17:30:02 +08:00
skirtle f2699a5cb3
fix(watch): use maximum depth for duplicates (#13434) 2025-09-02 17:29:08 +08:00
edison 99d54b28b4
fix(compiler-core): force dynamic slots when slot referencing scope vars (#9427)
close #9380
2025-09-02 17:24:56 +08:00
Red Huang 15fc75f403
fix(runtime-core): use separate emits caches for components and mixins (#11661) 2025-09-02 17:15:46 +08:00
Yang Mingshan 4810f1489f
chore(types): compatible with TS 5.8 (#12973) 2025-09-02 17:13:08 +08:00
edison 7171defb45
refactor: remove canary release workflows (#13794)
now using continuous release with pkg.pr.new
2025-09-02 17:12:42 +08:00
edison 26bce3dc6c
chore: update side effect annotations to use standardized format (#13839) 2025-09-02 17:12:19 +08:00
Andrei L 842a392ae5
types(jsx): add undefined to optional properties (#12771)
close #6068
2025-09-02 17:11:18 +08:00
edison 1392734ae5
fix(hmr): prevent __VUE_HMR_RUNTIME__ from being overwritten by vue runtime in 3rd-party libraries (#13817)
close vitejs/vite-plugin-vue#644
2025-09-02 17:10:30 +08:00
edison 8696e346b4
fix(compiler-sfc): support `${configDir}` in paths for TypeScript 5.5+ (#13491)
close #13484
2025-09-02 17:09:44 +08:00
edison 93ba107672
fix(templateRef): prevent unnecessary set ref on dynamic ref change or component unmount (#12642)
close #12639
2025-09-02 17:08:53 +08:00
linzhe 00978f7d14
fix(Teleport): hydrate disabled Teleport with undefined target (#11235)
close #11230
2025-09-02 17:08:15 +08:00
edison ef20b86b36
fix(hmr): prevent update unmounting component during HMR reload (#13815)
close vitejs/vite-plugin-vue#599
2025-09-02 17:07:36 +08:00
Daniel Roe 35da3c6dcb
fix(compiler-sfc): support global augments with named exports (#13789) 2025-09-02 17:03:16 +08:00
edison 8f6b505051
fix(runtime-core): disable tracking block in h function (#8213)
close #6913
2025-09-02 16:59:57 +08:00
Folee e322436887
fix(custom-element): prevent defineCustomElement from mutating the options object (#13791) 2025-09-02 16:56:33 +08:00
renovate[bot] d11cdd4a01
chore(deps): update build (#13799)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-01 16:44:46 +08:00
renovate[bot] ce9e6d1f4c
chore(deps): update test (#13801)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-01 16:44:14 +08:00
renovate[bot] bbf0f4cc44
chore(deps): update dependency npm-run-all2 to v8 (#13802)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-01 16:43:40 +08:00
renovate[bot] a28794edfa
chore(deps): update dependency magic-string to ^0.30.18 (#13800)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-01 15:32:53 +08:00
王二狗 63279661e8
chore: fix typo (#13833) 2025-09-01 14:37:31 +08:00
Ari Perkkiö 233b1250ce
chore(test): migrate to Vitest inline projects (#13838) 2025-09-01 13:59:34 +08:00
Zhong 24fccb4ee4
types(runtime-dom): improve event types (#13804)
close #13796
2025-08-25 15:25:10 +08:00
daiwei 3aa782df38 release: v3.5.20 2025-08-25 15:08:32 +08:00
edison 1031e8de08
fix(runtime-dom): add name to vShow for prop mismatch check (#13806)
close #13805
re-fix #13744
revert #13777

The implementation in #13777 requires users to configure __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__, otherwise errors like #13805 will occur.
2025-08-25 14:52:50 +08:00
Zhong 0f916d8c39
types(compiler-sfc): add explicit return type to genModelProps (#13441) 2025-08-23 21:32:53 +08:00
Zhong 952886e299
chore(compat): rename legacyresolveScopedSlots to legacyResolveScopedSlots
The changes correct the casing of a function name from legacyresolveScopedSlots to legacyResolveScopedSlots in both its definition and usage. No logic, control flow, or public API behavior is altered; only the symbol's casing is updated for consistency.
2025-08-23 21:30:50 +08:00
yangdan8 a48ffdad65
chore(reactivity): optimize size retrieval in createInstrumentations (#13759) 2025-08-21 17:52:55 +08:00
吴杨帆 cde15b07bf
chore: fix typo 2025-08-21 17:39:55 +08:00
daiwei 20b888bd59 release: v3.5.19 2025-08-21 10:29:08 +08:00
equt 0a202d890f
fix(compiler-ssr): disable v-memo transform in ssr vdom fallback branch (#13725)
close #13724
2025-08-21 10:03:16 +08:00
edison d9dd628800
fix(compiler-sfc): improve type inference for generic type aliases types (#12876)
close #12872
2025-08-21 09:48:40 +08:00
Alex Snezhko 4a2953f57b
fix(runtime-core): avoid setting direct ref of useTemplateRef in dev (#13449)
close 12852
2025-08-21 08:46:10 +08:00
renovate[bot] 19a0cbd431
chore(deps): update build (#13748)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-20 22:21:14 +08:00
renovate[bot] 40d8d61c64
chore(deps): update test (#13734)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-20 22:20:44 +08:00
renovate[bot] 5bdb2b4693
chore(deps): update dawidd6/action-download-artifact action to v11 (#13774)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-20 22:15:44 +08:00
renovate[bot] be7c7e57ac
chore(deps): update actions/checkout action to v5 (#13773)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-20 22:14:57 +08:00
renovate[bot] 40654d4aa4
chore(deps): update compiler (#13713)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-20 22:14:32 +08:00
renovate[bot] 10edfb5fc0
chore(deps): update all non-major dependencies (#13733)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-20 22:14:18 +08:00
renovate[bot] 2a0382ca7a
chore(deps): update build (#13712)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-20 22:14:03 +08:00
renovate[bot] 5eed143dd1
fix(deps): update dependency @vue/repl to ^4.6.3 (#13747)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-20 22:13:36 +08:00
edison a8713159ee
fix(suspense): don't immediately resolve suspense on last dep unmount (#13456)
close #13453
2025-08-20 22:11:16 +08:00
Adrian Cerbaro 0562548ab3
fix(compiler-sfc): throw mismatched script langs error before invoking babel (#13194)
Close #13193
2025-08-20 21:05:52 +08:00
skirtle d7283f3b7f
fix(runtime-core): improve consistency of `PublicInstanceProxyHandlers.has` (#13507) 2025-08-20 21:05:26 +08:00
edison 3190b179b0
fix(Transition): handle KeepAlive + transition leaving edge case (#13152)
close #13153
2025-08-20 20:56:08 +08:00
edison 7f60ef83e7
fix(compiler-core): prevent cached array children from retaining detached dom nodes (#13691)
fix element-plus/element-plus#21408
Re-fix #13211
2025-08-20 20:51:04 +08:00
edison 6e5143d963
fix(hmr): prevent updating unmounting component during HMR rerender (#13775)
close #13771
close #13772
2025-08-20 20:49:59 +08:00
Tycho 1498821ed9
fix(reactivity): warn on nested readonly ref update during unwrapping (#12141) 2025-08-20 20:45:01 +08:00
edison 439e1a543e
fix(hydration): also set vShow name if __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__ flag is enabled (#13777)
close #13744
2025-08-20 20:41:07 +08:00
edison 7420564b20
chore(ci): trusted publisher (#13768)
Related to e18e/ecosystem-issues#201
2025-08-20 20:39:07 +08:00
alentide 8963b7979a
test(runtime-core): remove incorrect suspense test in vnode spec (#13782) 2025-08-20 20:36:37 +08:00
awaken1ng c875019d49
fix(devtools): clear performance measures (#13701)
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
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 #13700
2025-07-25 08:48:57 +08:00
zhangenming 31f798581c
chore(runtime-core): use NO instead of ()=>false (#13695) 2025-07-25 08:45:38 +08:00
linzhe 911e67045e
fix(compiler-core): adjacent v-else should cause a compiler error (#13699)
close #13698
2025-07-25 08:30:05 +08:00
daiwei c486536105 release: v3.5.18
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details
size data / upload (push) Has been cancelled Details
2025-07-23 08:57:59 +08:00
Alex Snezhko 7343f7c95f
dx(runtime-core): fix warning message for useSlots, useAttrs invocation with missing instance (#13647)
ci / test (push) Waiting to run Details
ci / continuous-release (push) Waiting to run Details
size data / upload (push) Waiting to run Details
2025-07-23 08:42:50 +08:00
edison 8cfc10a80b
fix(ssr): ensure empty slots render as a comment node in Transition (#13396)
close #13394
2025-07-23 08:42:34 +08:00
linzhe 7f2994393d
fix(runtime-core): ensure correct anchor el for unresolved async components (#13560)
close #13559
2025-07-23 08:42:10 +08:00
zhiyuanzmj 9b029239ed
fix(compiler-core): identifiers in function parameters should not be inferred as references (#13548) 2025-07-23 08:41:50 +08:00
edison d8e40ef7e1
fix(compiler-sfc): transform empty srcset w/ includeAbsolute: true (#13639)
close vitejs/vite-plugin-vue#631
2025-07-23 08:41:17 +08:00
linzhe 90573b06bf
fix(custom-element): ensure exposed methods are accessible from custom elements by making them enumerable (#13634)
close #13632
2025-07-23 08:40:40 +08:00
edison c5f7db1154
fix(slots): refine internal key checking to support slot names starting with an underscore (#13612)
close #13611
2025-07-23 08:40:20 +08:00
edison a9269c642b
fix(hydration): prevent lazy hydration for updated components (#13511)
close #13510
2025-07-23 08:36:47 +08:00
edison 00695a5b41
fix(compiler-core): avoid cached text vnodes retaining detached DOM nodes (#13662)
close #13661
2025-07-23 08:36:15 +08:00
renovate[bot] da1f8d7987
chore(deps): update all non-major dependencies (#13627)
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-22 15:01:21 +08:00
renovate[bot] 0b6616a9c1
chore(deps): update dependency @babel/types to ^7.28.1 (#13628)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 15:01:02 +08:00
renovate[bot] 42b272da57
chore(deps): update build (#13670)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 14:58:11 +08:00
山吹色御守 e60edc06f2
chore(test): report correct value of `__EXTEND_POINT__` when subsequent error codes is less than it (#13213)
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-07-18 16:24:29 +08:00
山吹色御守 21b685ad9d
fix(compiler-core): avoid self updates of `v-pre` (#12556) 2025-07-18 16:22:56 +08:00
山吹色御守 ce933390ad
fix(compiler-core): recognize empty string as non-identifier (#12553) 2025-07-18 15:58:50 +08:00
山吹色御守 d3af67e878
fix(compiler-core): transform empty `v-bind` dynamic argument content correctly (#12554) 2025-07-18 15:56:01 +08:00
edison e0e8221d7f
chore(sfc-playground): import vaporInteropPlugin only if Vapor mode is supported (#13645)
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details
size data / upload (push) Has been cancelled Details
2025-07-17 10:03:13 +08:00
山吹色御守 347ef1d3f5
chore(compiler-sfc): optimize the regular expression for matching `@keyframes` (#13566)
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-07-09 10:31:20 +08:00
renovate[bot] f97c4d4e6e
chore(deps): update compiler to ^7.28.0 (#13575)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: edison <daiwei521@126.com>
2025-07-09 09:38:17 +08:00
Wick a0bd1f518e
refactor: migrate to getCurrentInstance API (#12958)
ci / test (push) Waiting to run Details
ci / continuous-release (push) Waiting to run Details
size data / upload (push) Waiting to run Details
2025-07-08 14:30:43 +08:00
renovate[bot] 01a122283f
chore(deps): update build (#13574)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-08 14:20:35 +08:00
158 changed files with 5245 additions and 2244 deletions

View File

@ -11,13 +11,13 @@ jobs:
env:
PUPPETEER_SKIP_DOWNLOAD: 'true'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- name: Install Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version-file: '.node-version'
registry-url: 'https://registry.npmjs.org'

View File

@ -1,33 +0,0 @@
name: canary minor release
on:
# Runs every Monday at 1 AM UTC (9:00 AM in Singapore)
schedule:
- cron: 0 1 * * MON
workflow_dispatch:
jobs:
canary:
# prevents this action from running on forks
if: github.repository == 'vuejs/core'
runs-on: ubuntu-latest
environment: Release
steps:
- uses: actions/checkout@v4
with:
ref: minor
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.node-version'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
- run: pnpm install
- run: pnpm release --canary --publish --tag minor
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@ -1,31 +0,0 @@
name: canary release
on:
# Runs every Monday at 1 AM UTC (9:00 AM in Singapore)
schedule:
- cron: 0 1 * * MON
workflow_dispatch:
jobs:
canary:
# prevents this action from running on forks
if: github.repository == 'vuejs/core'
runs-on: ubuntu-latest
environment: Release
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.node-version'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
- run: pnpm install
- run: pnpm release --canary --publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@ -20,13 +20,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Install Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version-file: '.node-version'
registry-url: 'https://registry.npmjs.org'

View File

@ -10,7 +10,7 @@ jobs:
if: github.repository == 'vuejs/core' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run')
steps:
- name: Check user permission
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const user = context.payload.sender.login
@ -45,7 +45,7 @@ jobs:
throw new Error('not allowed')
}
- name: Get PR info
uses: actions/github-script@v7
uses: actions/github-script@v8
id: get-pr-data
with:
script: |
@ -62,7 +62,7 @@ jobs:
commit: pr.head.sha
}
- name: Trigger run
uses: actions/github-script@v7
uses: actions/github-script@v8
id: trigger
env:
COMMENT: ${{ github.event.comment.body }}

View File

@ -21,13 +21,13 @@ jobs:
environment: Release
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Install Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version-file: '.node-version'
registry-url: 'https://registry.npmjs.org'
@ -36,12 +36,13 @@ jobs:
- name: Install deps
run: pnpm install
- name: Update npm
run: npm i -g npm@latest
- name: Build and publish
id: publish
run: |
pnpm release --publishOnly
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Create GitHub release
id: release_tag

View File

@ -22,13 +22,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- name: Install Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version-file: '.node-version'
cache: pnpm
@ -45,7 +45,7 @@ jobs:
echo ${{ github.base_ref }} > ./temp/size/base.txt
- name: Upload Size Data
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: size-data
path: temp/size

View File

@ -22,13 +22,13 @@ jobs:
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- name: Install Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version-file: '.node-version'
cache: pnpm
@ -37,7 +37,7 @@ jobs:
run: pnpm install
- name: Download Size Data
uses: dawidd6/action-download-artifact@v9
uses: dawidd6/action-download-artifact@v11
with:
name: size-data
run_id: ${{ github.event.workflow_run.id }}
@ -56,7 +56,7 @@ jobs:
path: temp/size/base.txt
- name: Download Previous Size Data
uses: dawidd6/action-download-artifact@v9
uses: dawidd6/action-download-artifact@v11
with:
branch: ${{ steps.pr-base.outputs.content }}
workflow: size-data.yml

View File

@ -11,13 +11,13 @@ jobs:
env:
PUPPETEER_SKIP_DOWNLOAD: 'true'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- name: Install Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version-file: '.node-version'
cache: 'pnpm'
@ -32,13 +32,13 @@ jobs:
env:
PUPPETEER_SKIP_DOWNLOAD: 'true'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- name: Install Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version-file: '.node-version'
cache: 'pnpm'
@ -54,7 +54,7 @@ jobs:
e2e-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Setup cache for Chromium binary
uses: actions/cache@v4
@ -63,10 +63,10 @@ jobs:
key: chromium-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- name: Install Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version-file: '.node-version'
cache: 'pnpm'
@ -85,13 +85,13 @@ jobs:
env:
PUPPETEER_SKIP_DOWNLOAD: 'true'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- name: Install Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version-file: '.node-version'
cache: 'pnpm'

View File

@ -4,7 +4,7 @@
"cSpell.enabledLanguageIds": ["markdown", "plaintext", "text", "yml"],
// Use prettier to format typescript, javascript and JSON files
// Use prettier to format TypeScript, JavaScript and JSON files
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},

View File

@ -1,3 +1,144 @@
## [3.5.24](https://github.com/vuejs/core/compare/v3.5.23...v3.5.24) (2025-11-07)
### Reverts
* Revert "fix(compiler-core): correctly handle ts type assertions in expression…" (#14062) ([11ec51a](https://github.com/vuejs/core/commit/11ec51aa5a7914745fee10ed2b9f9464fab4d02c)), closes [#14062](https://github.com/vuejs/core/issues/14062) [#14060](https://github.com/vuejs/core/issues/14060)
## [3.5.23](https://github.com/vuejs/core/compare/v3.5.22...v3.5.23) (2025-11-06)
### Bug Fixes
* **compiler-core:** correctly handle ts type assertions in expressions ([#13397](https://github.com/vuejs/core/issues/13397)) ([e6544ac](https://github.com/vuejs/core/commit/e6544ac292b5b473274f87cdb83ebeac3e7e61a4)), closes [#13395](https://github.com/vuejs/core/issues/13395)
* **compiler-core:** fix v-bind shorthand handling for in-DOM templates ([#13933](https://github.com/vuejs/core/issues/13933)) ([b3cca26](https://github.com/vuejs/core/commit/b3cca2611c656b85f0c4e737b9ec248d2627dded)), closes [#13930](https://github.com/vuejs/core/issues/13930)
* **compiler-sfc:** resolve numeric literals and template literals without expressions as static property key ([#13998](https://github.com/vuejs/core/issues/13998)) ([75d44c7](https://github.com/vuejs/core/commit/75d44c718981f91843e197265cc68e82fe2532dd))
* **compiler-ssr:** textarea with v-text directive SSR ([#13975](https://github.com/vuejs/core/issues/13975)) ([006a0c1](https://github.com/vuejs/core/commit/006a0c1011a224bcbf21195c6df76812c3a7e757))
* **compiler:** using guard instead of non-nullish assertion ([#13982](https://github.com/vuejs/core/issues/13982)) ([dcc6f36](https://github.com/vuejs/core/commit/dcc6f362577ed86ccad31c2623c6cf75137dd27a))
* **custom-element:** batch custom element prop patching ([#13478](https://github.com/vuejs/core/issues/13478)) ([c13e674](https://github.com/vuejs/core/commit/c13e674fb9f92ab9339d28a862d18de460faf56e)), closes [#12619](https://github.com/vuejs/core/issues/12619)
* **custom-element:** optimize slot retrieval to avoid duplicates ([#13961](https://github.com/vuejs/core/issues/13961)) ([84ca349](https://github.com/vuejs/core/commit/84ca349fef73f6f55fc98299fcfa5c1eeef721db)), closes [#13955](https://github.com/vuejs/core/issues/13955)
* **hydration:** avoid mismatch during hydrate text with newlines in interpolation ([#9232](https://github.com/vuejs/core/issues/9232)) ([6cbdf78](https://github.com/vuejs/core/commit/6cbdf7823b0c961190bee5b7c117b7f2bbeb832f)), closes [#9229](https://github.com/vuejs/core/issues/9229)
* **runtime-core:** pass props and children to loadingComponent ([#13997](https://github.com/vuejs/core/issues/13997)) ([40c4b2a](https://github.com/vuejs/core/commit/40c4b2a876ce606973521dfc3024e26bfc10953a))
* **runtime-dom:** ensure iframe sandbox is handled as an attribute to prevent unintended behavior ([#13950](https://github.com/vuejs/core/issues/13950)) ([5689884](https://github.com/vuejs/core/commit/5689884c8e32cda6a802ac36b4d23218f67b38ed)), closes [#13946](https://github.com/vuejs/core/issues/13946)
* **suspense:** clear placeholder and fallback el after resolve to enable GC ([#13928](https://github.com/vuejs/core/issues/13928)) ([f411c66](https://github.com/vuejs/core/commit/f411c6604c12c531883aa0d30b81a7f69092f8a6))
* **transition-group:** use offsetLeft and offsetTop instead of getBoundingClientRect to avoid transform scale affect animation ([#6108](https://github.com/vuejs/core/issues/6108)) ([dc4dd59](https://github.com/vuejs/core/commit/dc4dd594fbecce6ed7f44ffa69dc8b5d022287b6)), closes [#6105](https://github.com/vuejs/core/issues/6105)
* **v-model:** handle number modifier on change ([#13959](https://github.com/vuejs/core/issues/13959)) ([8fbe48f](https://github.com/vuejs/core/commit/8fbe48fe396d830999afd07f9413d899157d5f5e)), closes [#13958](https://github.com/vuejs/core/issues/13958)
## [3.5.22](https://github.com/vuejs/core/compare/v3.5.21...v3.5.22) (2025-09-25)
### Bug Fixes
* **compiler-core:** identifiers in switch-case should not be inferred as references ([#13923](https://github.com/vuejs/core/issues/13923)) ([5953c9f](https://github.com/vuejs/core/commit/5953c9ff90090e128372f645d377bd99137a5fb4))
* **compiler-dom:** nodes with v-once shouldn't be stringified ([#13878](https://github.com/vuejs/core/issues/13878)) ([95c1975](https://github.com/vuejs/core/commit/95c197560409f5d39a0d376c0a43d89a47a604e8))
* **compiler-sfc:** add support for `@vue-ignore` in runtime type resolution ([#13906](https://github.com/vuejs/core/issues/13906)) ([ba7f7f9](https://github.com/vuejs/core/commit/ba7f7f90f689f6e7e0417a192d081db542de28ec))
* **compiler-sfc:** enhance inferRuntimeType to support TSMappedType with indexed access ([#13848](https://github.com/vuejs/core/issues/13848)) ([e388f1a](https://github.com/vuejs/core/commit/e388f1a09fde78cf006450f060813d972ac8c23d)), closes [#13847](https://github.com/vuejs/core/issues/13847)
* **compiler-sfc:** ensure css custom properties do not start with a digit ([#13870](https://github.com/vuejs/core/issues/13870)) ([9c27951](https://github.com/vuejs/core/commit/9c279517b9bc1f4c250c555ec9b9eb6104756d56))
* **compiler-sfc:** ensure props bindings register before compiling template ([#13922](https://github.com/vuejs/core/issues/13922)) ([abd5638](https://github.com/vuejs/core/commit/abd563822abafe63047f7b599bff266380ee2b64)), closes [#13920](https://github.com/vuejs/core/issues/13920)
* **compiler-ssr:** ensure v-show has a higher priority in SSR ([#12171](https://github.com/vuejs/core/issues/12171)) ([836b829](https://github.com/vuejs/core/commit/836b82976ffb7aa0ea9cbe417bef07deae3ca47c)), closes [#12162](https://github.com/vuejs/core/issues/12162)
* **custom-element:** properly mount multiple Teleports in custom element component w/ shadowRoot false ([#13900](https://github.com/vuejs/core/issues/13900)) ([5e1e791](https://github.com/vuejs/core/commit/5e1e791880238380a1038ae2c505e206ceb34d77)), closes [#13899](https://github.com/vuejs/core/issues/13899)
* **custom-element:** set prop runs pending mutations before disconnect ([#13897](https://github.com/vuejs/core/issues/13897)) ([c4a88cd](https://github.com/vuejs/core/commit/c4a88cdd0dfed3ef46a8aa9be448c01781fdc4f0)), closes [#13315](https://github.com/vuejs/core/issues/13315)
* **custom-element:** use `PatchFlags.BAIL` for slot when props are present ([#13907](https://github.com/vuejs/core/issues/13907)) ([5358bca](https://github.com/vuejs/core/commit/5358bca4a80cf52d19ed91967eeaa025a786083d)), closes [#13904](https://github.com/vuejs/core/issues/13904)
* **reactivity:** respect readonly during ref unwrapping ([#13905](https://github.com/vuejs/core/issues/13905)) ([aba7fed](https://github.com/vuejs/core/commit/aba7feda1703e69e5a7c37f784718de0371adadc)), closes [#13903](https://github.com/vuejs/core/issues/13903)
* **reactivity:** update iterator to check for completion instead of value presence ([#13761](https://github.com/vuejs/core/issues/13761)) ([2078f8b](https://github.com/vuejs/core/commit/2078f8b7565cf637f47fcd5b0abdfb2b264225bb))
* **runtime-core:** simplify block-tracking disabling in `h` helper ([#13841](https://github.com/vuejs/core/issues/13841)) ([75220c7](https://github.com/vuejs/core/commit/75220c7995a13a483ae9599a739075be1c8e17f8))
* **transition-group:** run `forceReflow` on the correct document (fix [#13849](https://github.com/vuejs/core/issues/13849)) ([#13853](https://github.com/vuejs/core/issues/13853)) ([1be5ddf](https://github.com/vuejs/core/commit/1be5ddfe878c8bfddaa2c50e82105b247f50b9ba))
* **types:** more precise types for Events and added missing definitions ([#9675](https://github.com/vuejs/core/issues/9675)) ([8bb8fb2](https://github.com/vuejs/core/commit/8bb8fb236257c03bfa0bccadcfffe3eb4592f71b))
* **types:** set dom stub type to `never` instead of `{}` ([#13915](https://github.com/vuejs/core/issues/13915)) ([8620a61](https://github.com/vuejs/core/commit/8620a616eb02a64fe32dd52d9be68e360687ef9d)), closes [#11564](https://github.com/vuejs/core/issues/11564)
* **types:** widen directive arg type from string to any ([#13758](https://github.com/vuejs/core/issues/13758)) ([4b71706](https://github.com/vuejs/core/commit/4b7170625d0bc93b26a3343aeda98850c1138f82)), closes [#13757](https://github.com/vuejs/core/issues/13757)
### Features
* **custom-element:** allow specifying additional options for `shadowRoot` in custom elements ([#12965](https://github.com/vuejs/core/issues/12965)) ([47e628d](https://github.com/vuejs/core/commit/47e628df1ce1914c5677010ad5bddd18d037cb3c)), closes [#12964](https://github.com/vuejs/core/issues/12964)
### Reverts
* Revert "fix(hmr): prevent __VUE_HMR_RUNTIME__ from being overwritten by vue runtime in 3rd-party libraries" (#13925) ([6b68f72](https://github.com/vuejs/core/commit/6b68f72673dac5db349f26eeefb2f2e0e342586b)), closes [#13925](https://github.com/vuejs/core/issues/13925)
## [3.5.21](https://github.com/vuejs/core/compare/v3.5.20...v3.5.21) (2025-09-02)
### Bug Fixes
* **compiler-core:** force dynamic slots when slot referencing scope vars ([#9427](https://github.com/vuejs/core/issues/9427)) ([99d54b2](https://github.com/vuejs/core/commit/99d54b28b46dbea006205dff71c383a31dd1b87a)), closes [#9380](https://github.com/vuejs/core/issues/9380)
* **compiler-sfc:** check lang before attempt to compile script ([#13508](https://github.com/vuejs/core/issues/13508)) ([55922ff](https://github.com/vuejs/core/commit/55922ff3168a1397ad72f18946eb1c4051cdab3b)), closes [#8368](https://github.com/vuejs/core/issues/8368)
* **compiler-sfc:** support `${configDir}` in paths for TypeScript 5.5+ ([#13491](https://github.com/vuejs/core/issues/13491)) ([8696e34](https://github.com/vuejs/core/commit/8696e346b4780d88247464490f1a992cc0c3658c)), closes [#13484](https://github.com/vuejs/core/issues/13484)
* **compiler-sfc:** support global augments with named exports ([#13789](https://github.com/vuejs/core/issues/13789)) ([35da3c6](https://github.com/vuejs/core/commit/35da3c6dcb30030ef60fa22e30aa83a56e396c60))
* **custom-element:** prevent defineCustomElement from mutating the options object ([#13791](https://github.com/vuejs/core/issues/13791)) ([e322436](https://github.com/vuejs/core/commit/e322436887549c129e61eb58a0084167103451bb))
* **hmr:** prevent `__VUE_HMR_RUNTIME__` from being overwritten by vue runtime in 3rd-party libraries ([#13817](https://github.com/vuejs/core/issues/13817)) ([1392734](https://github.com/vuejs/core/commit/1392734ae5d5a3b2be124753e198eafa324f6815)), closes [vitejs/vite-plugin-vue#644](https://github.com/vitejs/vite-plugin-vue/issues/644)
* **hmr:** prevent update unmounting component during HMR reload ([#13815](https://github.com/vuejs/core/issues/13815)) ([ef20b86](https://github.com/vuejs/core/commit/ef20b86b36a127e317f8981df970dc8efd277053)), closes [vitejs/vite-plugin-vue#599](https://github.com/vitejs/vite-plugin-vue/issues/599)
* **runtime-core:** disable tracking block in h function ([#8213](https://github.com/vuejs/core/issues/8213)) ([8f6b505](https://github.com/vuejs/core/commit/8f6b5050518441a5047d128138da44f798836002)), closes [#6913](https://github.com/vuejs/core/issues/6913)
* **runtime-core:** use separate emits caches for components and mixins ([#11661](https://github.com/vuejs/core/issues/11661)) ([15fc75f](https://github.com/vuejs/core/commit/15fc75f4031dea805c3bbb67a75e48a9dc307c11))
* **Suspence:** handle Suspense + KeepAlive HMR updating edge case ([#13076](https://github.com/vuejs/core/issues/13076)) ([5d75a17](https://github.com/vuejs/core/commit/5d75a170c8d23acd11ef2513173d4cbc4d0b54de)), closes [#13075](https://github.com/vuejs/core/issues/13075)
* **Teleport:** hydrate disabled Teleport with undefined target ([#11235](https://github.com/vuejs/core/issues/11235)) ([00978f7](https://github.com/vuejs/core/commit/00978f7d14e85b49d9d334ea92fa8c03733ce64c)), closes [#11230](https://github.com/vuejs/core/issues/11230)
* **templateRef:** prevent unnecessary set ref on dynamic ref change or component unmount ([#12642](https://github.com/vuejs/core/issues/12642)) ([93ba107](https://github.com/vuejs/core/commit/93ba10767230872fcdca974a1e19e8bd69b7eb6a)), closes [#12639](https://github.com/vuejs/core/issues/12639)
* **watch:** use maximum depth for duplicates ([#13434](https://github.com/vuejs/core/issues/13434)) ([f2699a5](https://github.com/vuejs/core/commit/f2699a5cb376ffa452a54feb171c14411c67287c))
### Performance Improvements
* improve regexp performance with non-capturing groups ([#13567](https://github.com/vuejs/core/issues/13567)) ([1e8b65a](https://github.com/vuejs/core/commit/1e8b65aa4934c94ef6142b4f49cdfb13ba5e6ce5))
## [3.5.20](https://github.com/vuejs/core/compare/v3.5.19...v3.5.20) (2025-08-25)
### Bug Fixes
* **runtime-dom:** add name to vShow for prop mismatch check ([#13806](https://github.com/vuejs/core/issues/13806)) ([1031e8d](https://github.com/vuejs/core/commit/1031e8de08b735059217b1ad0057f62565c99c4f)), closes [#13805](https://github.com/vuejs/core/issues/13805) re-fix [#13744](https://github.com/vuejs/core/issues/13744) revert [#13777](https://github.com/vuejs/core/issues/13777)
## [3.5.19](https://github.com/vuejs/core/compare/v3.5.18...v3.5.19) (2025-08-21)
### Bug Fixes
* **compiler-core:** adjacent v-else should cause a compiler error ([#13699](https://github.com/vuejs/core/issues/13699)) ([911e670](https://github.com/vuejs/core/commit/911e67045e2a63e0ecbd198ed4f567530f6d1c17)), closes [#13698](https://github.com/vuejs/core/issues/13698)
* **compiler-core:** prevent cached array children from retaining detached dom nodes ([#13691](https://github.com/vuejs/core/issues/13691)) ([7f60ef8](https://github.com/vuejs/core/commit/7f60ef83e735dbd29d323347acecf69f22b06d53)), closes [element-plus/element-plus#21408](https://github.com/element-plus/element-plus/issues/21408) [#13211](https://github.com/vuejs/core/issues/13211)
* **compiler-sfc:** improve type inference for generic type aliases types ([#12876](https://github.com/vuejs/core/issues/12876)) ([d9dd628](https://github.com/vuejs/core/commit/d9dd628800ae32e673bdfabfe79f1988037991d0)), closes [#12872](https://github.com/vuejs/core/issues/12872)
* **compiler-sfc:** throw mismatched script langs error before invoking babel ([#13194](https://github.com/vuejs/core/issues/13194)) ([0562548](https://github.com/vuejs/core/commit/0562548ab3a040073386021222225e0e9d43c632)), closes [#13193](https://github.com/vuejs/core/issues/13193)
* **compiler-ssr:** disable v-memo transform in ssr vdom fallback branch ([#13725](https://github.com/vuejs/core/issues/13725)) ([0a202d8](https://github.com/vuejs/core/commit/0a202d890ff2a564b1fab51e4ac621708640818e)), closes [#13724](https://github.com/vuejs/core/issues/13724)
* **devtools:** clear performance measures ([#13701](https://github.com/vuejs/core/issues/13701)) ([c875019](https://github.com/vuejs/core/commit/c875019d49b4c36a88d929ccadc31ad414747c7b)), closes [#13700](https://github.com/vuejs/core/issues/13700)
* **hmr:** prevent updating unmounting component during HMR rerender ([#13775](https://github.com/vuejs/core/issues/13775)) ([6e5143d](https://github.com/vuejs/core/commit/6e5143d9635dac3f20fb394a827109df30e232ae)), closes [#13771](https://github.com/vuejs/core/issues/13771) [#13772](https://github.com/vuejs/core/issues/13772)
* **hydration:** also set vShow name if `__FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__` flag is enabled ([#13777](https://github.com/vuejs/core/issues/13777)) ([439e1a5](https://github.com/vuejs/core/commit/439e1a543e62de4dbf7658d78d05c358c9677c86)), closes [#13744](https://github.com/vuejs/core/issues/13744)
* **reactivity:** warn on nested readonly ref update during unwrapping ([#12141](https://github.com/vuejs/core/issues/12141)) ([1498821](https://github.com/vuejs/core/commit/1498821ed9eeb22a0767e53ddc1f6a2840598a29))
* **runtime-core:** avoid setting direct ref of useTemplateRef in dev ([#13449](https://github.com/vuejs/core/issues/13449)) ([4a2953f](https://github.com/vuejs/core/commit/4a2953f57b90dfc24e34ff1a87cc1ebb0b97636d))
* **runtime-core:** improve consistency of `PublicInstanceProxyHandlers.has` ([#13507](https://github.com/vuejs/core/issues/13507)) ([d7283f3](https://github.com/vuejs/core/commit/d7283f3b7f0631c8b8a4a31a05983dac9f078c4f))
* **suspense:** don't immediately resolve suspense on last dep unmount ([#13456](https://github.com/vuejs/core/issues/13456)) ([a871315](https://github.com/vuejs/core/commit/a8713159ee24602c7c2b70c5fd52d2e5cd37dca5)), closes [#13453](https://github.com/vuejs/core/issues/13453)
* **transition:** handle KeepAlive + transition leaving edge case ([#13152](https://github.com/vuejs/core/issues/13152)) ([3190b17](https://github.com/vuejs/core/commit/3190b179b0545a3dc4549737793eec630cf9f0d1)), closes [#13153](https://github.com/vuejs/core/issues/13153)
## [3.5.18](https://github.com/vuejs/core/compare/v3.5.17...v3.5.18) (2025-07-23)
### Bug Fixes
* **compiler-core:** avoid cached text vnodes retaining detached DOM nodes ([#13662](https://github.com/vuejs/core/issues/13662)) ([00695a5](https://github.com/vuejs/core/commit/00695a5b41b2d032deaeada83831ff83aa6bfd4e)), closes [#13661](https://github.com/vuejs/core/issues/13661)
* **compiler-core:** avoid self updates of `v-pre` ([#12556](https://github.com/vuejs/core/issues/12556)) ([21b685a](https://github.com/vuejs/core/commit/21b685ad9d9d0e6060fc7d07b719bf35f2d9ae1f))
* **compiler-core:** identifiers in function parameters should not be inferred as references ([#13548](https://github.com/vuejs/core/issues/13548)) ([9b02923](https://github.com/vuejs/core/commit/9b029239edf88558465b941e1e4c085f92b1ebff))
* **compiler-core:** recognize empty string as non-identifier ([#12553](https://github.com/vuejs/core/issues/12553)) ([ce93339](https://github.com/vuejs/core/commit/ce933390ad1c72bed258f7ad959a78f0e8acdf57))
* **compiler-core:** transform empty `v-bind` dynamic argument content correctly ([#12554](https://github.com/vuejs/core/issues/12554)) ([d3af67e](https://github.com/vuejs/core/commit/d3af67e878790892f9d34cfea15d13625aabe733))
* **compiler-sfc:** transform empty srcset w/ includeAbsolute: true ([#13639](https://github.com/vuejs/core/issues/13639)) ([d8e40ef](https://github.com/vuejs/core/commit/d8e40ef7e1c20ee86b294e7cf78e2de60d12830e)), closes [vitejs/vite-plugin-vue#631](https://github.com/vitejs/vite-plugin-vue/issues/631)
* **css-vars:** nullish v-bind in style should not lead to unexpected inheritance ([#12461](https://github.com/vuejs/core/issues/12461)) ([c85f1b5](https://github.com/vuejs/core/commit/c85f1b5a132eb8ec25f71b250e25e65a5c20964f)), closes [#12434](https://github.com/vuejs/core/issues/12434) [#12439](https://github.com/vuejs/core/issues/12439) [#7474](https://github.com/vuejs/core/issues/7474) [#7475](https://github.com/vuejs/core/issues/7475)
* **custom-element:** ensure exposed methods are accessible from custom elements by making them enumerable ([#13634](https://github.com/vuejs/core/issues/13634)) ([90573b0](https://github.com/vuejs/core/commit/90573b06bf6fb6c14c6bbff6c4e34e0ab108953a)), closes [#13632](https://github.com/vuejs/core/issues/13632)
* **hydration:** prevent lazy hydration for updated components ([#13511](https://github.com/vuejs/core/issues/13511)) ([a9269c6](https://github.com/vuejs/core/commit/a9269c642bf944560bc29adb5dae471c11cd9ee8)), closes [#13510](https://github.com/vuejs/core/issues/13510)
* **runtime-core:** ensure correct anchor el for unresolved async components ([#13560](https://github.com/vuejs/core/issues/13560)) ([7f29943](https://github.com/vuejs/core/commit/7f2994393dcdb82cacbf62e02b5ba5565f32588b)), closes [#13559](https://github.com/vuejs/core/issues/13559)
* **slots:** refine internal key checking to support slot names starting with an underscore ([#13612](https://github.com/vuejs/core/issues/13612)) ([c5f7db1](https://github.com/vuejs/core/commit/c5f7db11542bb2246363aef78c88a8e6cef0ee93)), closes [#13611](https://github.com/vuejs/core/issues/13611)
* **ssr:** ensure empty slots render as a comment node in Transition ([#13396](https://github.com/vuejs/core/issues/13396)) ([8cfc10a](https://github.com/vuejs/core/commit/8cfc10a80b9cbf5d801ab149e49b8506d192e7e1)), closes [#13394](https://github.com/vuejs/core/issues/13394)
## [3.5.17](https://github.com/vuejs/core/compare/v3.5.16...v3.5.17) (2025-06-18)
@ -253,7 +394,7 @@
* **compiler-core:** fix handling of delimiterOpen in VPre ([#11915](https://github.com/vuejs/core/issues/11915)) ([706d4ac](https://github.com/vuejs/core/commit/706d4ac1d0210b2d9134b3228280187fe02fc971)), closes [#11913](https://github.com/vuejs/core/issues/11913)
* **compiler-dom:** fix stringify static edge for partially eligible chunks in cached parent ([1d99d61](https://github.com/vuejs/core/commit/1d99d61c1bd77f9ea6743f6214a82add8346a121)), closes [#11879](https://github.com/vuejs/core/issues/11879) [#11890](https://github.com/vuejs/core/issues/11890)
* **compiler-dom:** should ignore leading newline in <textarea> per spec ([3c4bf76](https://github.com/vuejs/core/commit/3c4bf7627649ec1e3220f8c4e4163c20d2afb367))
* **compiler-dom:** should ignore leading newline in `<textarea>` per spec ([3c4bf76](https://github.com/vuejs/core/commit/3c4bf7627649ec1e3220f8c4e4163c20d2afb367))
* **compiler-sfc:** nested css supports atrule and comment ([#11899](https://github.com/vuejs/core/issues/11899)) ([0e7bc71](https://github.com/vuejs/core/commit/0e7bc717e6640644f062957ec5031506f0dab215)), closes [#11896](https://github.com/vuejs/core/issues/11896)
* **custom-element:** handle nested customElement mount w/ shadowRoot false ([#11861](https://github.com/vuejs/core/issues/11861)) ([f2d8019](https://github.com/vuejs/core/commit/f2d801918841e7673ff3f048d0d895592a2f7e23)), closes [#11851](https://github.com/vuejs/core/issues/11851) [#11871](https://github.com/vuejs/core/issues/11871)
* **hmr:** reload async child wrapped in Suspense + KeepAlive ([#11907](https://github.com/vuejs/core/issues/11907)) ([10a2c60](https://github.com/vuejs/core/commit/10a2c6053bd30d160d0214bb3566f540187e6874)), closes [#11868](https://github.com/vuejs/core/issues/11868)

View File

@ -1,5 +1,6 @@
import importX from 'eslint-plugin-import-x'
import tseslint from 'typescript-eslint'
import { defineConfig } from 'eslint/config'
import vitest from '@vitest/eslint-plugin'
import { builtinModules } from 'node:module'
@ -12,7 +13,7 @@ const banConstEnum = {
'Please use non-const enums. This project automatically inlines enums.',
}
export default tseslint.config(
export default defineConfig(
{
files: ['**/*.js', '**/*.ts', '**/*.tsx'],
extends: [tseslint.configs.base],
@ -60,7 +61,10 @@ export default tseslint.config(
],
// This rule enforces the preference for using '@ts-expect-error' comments in TypeScript
// code to indicate intentional type errors, improving code clarity and maintainability.
'@typescript-eslint/prefer-ts-expect-error': 'error',
'@typescript-eslint/ban-ts-comment': [
'error',
{ minimumDescriptionLength: 0 },
],
// Enforce the use of 'import type' for importing types
'@typescript-eslint/consistent-type-imports': [
'error',

View File

@ -1,7 +1,7 @@
{
"private": true,
"version": "3.5.17",
"packageManager": "pnpm@10.12.4",
"version": "3.5.24",
"packageManager": "pnpm@10.20.0",
"type": "module",
"scripts": {
"dev": "node scripts/dev.js",
@ -17,11 +17,11 @@
"format": "prettier --write --cache .",
"format-check": "prettier --check --cache .",
"test": "vitest",
"test-unit": "vitest --project unit",
"test-unit": "vitest --project unit*",
"test-e2e": "node scripts/build.js vue -f global -d && vitest --project e2e",
"test-dts": "run-s build-dts test-dts-only",
"test-dts-only": "tsc -p packages-private/dts-built-test/tsconfig.json && tsc -p ./packages-private/dts-test/tsconfig.test.json",
"test-coverage": "vitest run --project unit --coverage",
"test-coverage": "vitest run --project unit* --coverage",
"prebench": "node scripts/build.js -pf esm-browser reactivity",
"prebench-compare": "node scripts/build.js -pf esm-browser reactivity",
"bench": "vitest bench --project=unit --outputJson=temp/bench.json",
@ -65,44 +65,44 @@
"@babel/parser": "catalog:",
"@babel/types": "catalog:",
"@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-commonjs": "^28.0.6",
"@rollup/plugin-commonjs": "^28.0.9",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-replace": "5.0.4",
"@swc/core": "^1.12.9",
"@swc/core": "^1.14.0",
"@types/hash-sum": "^1.0.2",
"@types/node": "^22.16.0",
"@types/semver": "^7.7.0",
"@types/node": "^22.19.0",
"@types/semver": "^7.7.1",
"@types/serve-handler": "^6.1.4",
"@vitest/coverage-v8": "^3.1.4",
"@vitest/eslint-plugin": "^1.2.1",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/eslint-plugin": "^1.4.0",
"@vue/consolidate": "1.0.0",
"conventional-changelog-cli": "^5.0.0",
"enquirer": "^2.4.1",
"esbuild": "^0.25.5",
"esbuild": "^0.25.12",
"esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^9.27.0",
"eslint-plugin-import-x": "^4.13.1",
"estree-walker": "catalog:",
"jsdom": "^26.1.0",
"jsdom": "^27.1.0",
"lint-staged": "^16.0.0",
"lodash": "^4.17.21",
"magic-string": "^0.30.17",
"magic-string": "^0.30.21",
"markdown-table": "^3.0.4",
"marked": "13.0.3",
"npm-run-all2": "^7.0.2",
"npm-run-all2": "^8.0.4",
"picocolors": "^1.1.1",
"prettier": "^3.5.3",
"pretty-bytes": "^6.1.1",
"pretty-bytes": "^7.1.0",
"pug": "^3.0.3",
"puppeteer": "~24.9.0",
"rimraf": "^6.0.1",
"rollup": "^4.44.1",
"rollup-plugin-dts": "^6.2.1",
"puppeteer": "~24.28.0",
"rimraf": "^6.1.0",
"rollup": "^4.52.5",
"rollup-plugin-dts": "^6.2.3",
"rollup-plugin-esbuild": "^6.2.1",
"rollup-plugin-polyfill-node": "^0.13.0",
"semver": "^7.7.2",
"serve": "^14.2.4",
"semver": "^7.7.3",
"serve": "^14.2.5",
"serve-handler": "^6.1.6",
"simple-git-hooks": "^2.13.0",
"todomvc-app-css": "^2.4.3",
@ -110,6 +110,6 @@
"typescript": "~5.6.2",
"typescript-eslint": "^8.32.1",
"vite": "catalog:",
"vitest": "^3.1.4"
"vitest": "^3.2.4"
}
}

View File

@ -13,7 +13,7 @@ type ExtractBinding<T> = T extends (
declare function testDirective<
Value,
Modifiers extends string = string,
Arg extends string = string,
Arg = any,
>(): ExtractBinding<Directive<any, Value, Modifiers, Arg>>
describe('vmodel', () => {
@ -44,7 +44,7 @@ describe('custom', () => {
value: number
oldValue: number | null
arg?: 'Arg'
modifiers: Record<'a' | 'b', boolean>
modifiers: Partial<Record<'a' | 'b', boolean>>
// @ts-expect-error
}>(testDirective<number, 'a' | 'b', 'Argx'>())
@ -52,7 +52,29 @@ describe('custom', () => {
value: number
oldValue: number | null
arg?: 'Arg'
modifiers: Record<'a' | 'b', boolean>
modifiers: Partial<Record<'a' | 'b', boolean>>
// @ts-expect-error
}>(testDirective<string, 'a' | 'b', 'Arg'>())
expectType<{
value: number
oldValue: number | null
arg?: HTMLElement
modifiers: Partial<Record<'a' | 'b', boolean>>
}>(testDirective<number, 'a' | 'b', HTMLElement>())
expectType<{
value: number
oldValue: number | null
arg?: HTMLElement
modifiers: Partial<Record<'a' | 'b', boolean>>
// @ts-expect-error
}>(testDirective<number, 'a' | 'b', string>())
expectType<{
value: number
oldValue: number | null
arg?: HTMLElement
modifiers: Partial<Record<'a' | 'b', boolean>>
}>(testDirective<number, 'a' | 'b'>())
})

View File

@ -0,0 +1,31 @@
import { nextTick } from 'vue'
import { describe, expectType } from './utils'
describe('nextTick', async () => {
expectType<Promise<void>>(nextTick())
expectType<Promise<string>>(nextTick(() => 'foo'))
expectType<Promise<string>>(nextTick(() => Promise.resolve('foo')))
expectType<Promise<string>>(
nextTick(() => Promise.resolve(Promise.resolve('foo'))),
)
expectType<void>(await nextTick())
expectType<string>(await nextTick(() => 'foo'))
expectType<string>(await nextTick(() => Promise.resolve('foo')))
expectType<string>(
await nextTick(() => Promise.resolve(Promise.resolve('foo'))),
)
nextTick().then(value => {
expectType<void>(value)
})
nextTick(() => 'foo').then(value => {
expectType<string>(value)
})
nextTick(() => Promise.resolve('foo')).then(value => {
expectType<string>(value)
})
nextTick(() => Promise.resolve(Promise.resolve('foo'))).then(value => {
expectType<string>(value)
})
})

View File

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

View File

@ -2,7 +2,7 @@
import Header from './Header.vue'
import { Repl, useStore, SFCOptions, useVueImportMap } from '@vue/repl'
import Monaco from '@vue/repl/monaco-editor'
import { ref, watchEffect, onMounted, computed } from 'vue'
import { ref, watchEffect, onMounted, computed, watch } from 'vue'
const replRef = ref<InstanceType<typeof Repl>>()
@ -115,6 +115,34 @@ onMounted(() => {
// @ts-expect-error process shim for old versions of @vue/compiler-sfc dependency
window.process = { env: {} }
})
const isVaporSupported = ref(false)
watch(
() => store.vueVersion,
(version, oldVersion) => {
const [major, minor] = (version || store.compiler.version)
.split('.')
.map((v: string) => parseInt(v, 10))
isVaporSupported.value = major > 3 || (major === 3 && minor >= 6)
if (oldVersion) reloadPage()
},
{ immediate: true, flush: 'pre' },
)
const previewOptions = computed(() => ({
customCode: {
importCode: `import { initCustomFormatter${isVaporSupported.value ? ', vaporInteropPlugin' : ''} } from 'vue'`,
useCode: `
${isVaporSupported.value ? 'app.use(vaporInteropPlugin)' : ''}
if (window.devtoolsFormatters) {
const index = window.devtoolsFormatters.findIndex((v) => v.__vue_custom_formatter)
window.devtoolsFormatters.splice(index, 1)
initCustomFormatter()
} else {
initCustomFormatter()
}`,
},
}))
</script>
<template>
@ -145,18 +173,7 @@ onMounted(() => {
:showOpenSourceMap="true"
:autoResize="true"
:clearConsole="false"
:preview-options="{
customCode: {
importCode: `import { initCustomFormatter } from 'vue'`,
useCode: `if (window.devtoolsFormatters) {
const index = window.devtoolsFormatters.findIndex((v) => v.__vue_custom_formatter)
window.devtoolsFormatters.splice(index, 1)
initCustomFormatter()
} else {
initCustomFormatter()
}`,
},
}"
:preview-options="previewOptions"
/>
</template>

View File

@ -11,7 +11,7 @@
"vue": "latest"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.4",
"vite": "^6.3.5"
"@vitejs/plugin-vue": "^6.0.1",
"vite": "^7.1.12"
}
}

View File

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

View File

@ -7,9 +7,9 @@ return function render(_ctx, _cache) {
with (_ctx) {
const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("div", { key: "foo" }, null, -1 /* CACHED */)
])))
]))]))
}
}"
`;
@ -21,7 +21,7 @@ return function render(_ctx, _cache) {
with (_ctx) {
const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("p", null, [
_createElementVNode("span"),
_createElementVNode("span")
@ -30,7 +30,7 @@ return function render(_ctx, _cache) {
_createElementVNode("span"),
_createElementVNode("span")
], -1 /* CACHED */)
])))
]))]))
}
}"
`;
@ -42,11 +42,11 @@ return function render(_ctx, _cache) {
with (_ctx) {
const { createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("div", null, [
_createCommentVNode("comment")
], -1 /* CACHED */)
])))
]))]))
}
}"
`;
@ -58,11 +58,11 @@ return function render(_ctx, _cache) {
with (_ctx) {
const { createElementVNode: _createElementVNode, createTextVNode: _createTextVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("span", null, null, -1 /* CACHED */),
_createTextVNode("foo"),
_createTextVNode("foo", -1 /* CACHED */),
_createElementVNode("div", null, null, -1 /* CACHED */)
])))
]))]))
}
}"
`;
@ -74,9 +74,9 @@ return function render(_ctx, _cache) {
with (_ctx) {
const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("span", { class: "inline" }, "hello", -1 /* CACHED */)
])))
]))]))
}
}"
`;
@ -147,9 +147,9 @@ return function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("span", null, "foo " + _toDisplayString(1) + " " + _toDisplayString(true), -1 /* CACHED */)
])))
]))]))
}
}"
`;
@ -161,9 +161,9 @@ return function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("span", { foo: 0 }, _toDisplayString(1), -1 /* CACHED */)
])))
]))]))
}
}"
`;
@ -215,9 +215,9 @@ return function render(_ctx, _cache) {
const _directive_foo = _resolveDirective("foo")
return (_openBlock(), _createElementBlock("div", null, [
_withDirectives((_openBlock(), _createElementBlock("svg", null, _cache[0] || (_cache[0] = [
_withDirectives((_openBlock(), _createElementBlock("svg", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("path", { d: "M2,3H5.5L12" }, null, -1 /* CACHED */)
]))), [
]))])), [
[_directive_foo]
])
]))
@ -401,9 +401,9 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
ok
? (_openBlock(), _createElementBlock("div", _hoisted_1, _cache[0] || (_cache[0] = [
? (_openBlock(), _createElementBlock("div", _hoisted_1, [...(_cache[0] || (_cache[0] = [
_createElementVNode("span", null, null, -1 /* CACHED */)
])))
]))]))
: _createCommentVNode("v-if", true)
]))
}
@ -422,7 +422,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createCommentVNode("comment"),
_createElementVNode("div", _hoisted_1, _cache[0] || (_cache[0] = [
_createElementVNode("div", _hoisted_1, [...(_cache[0] || (_cache[0] = [
_createElementVNode("div", { id: "b" }, [
_createElementVNode("div", { id: "c" }, [
_createElementVNode("div", { id: "d" }, [
@ -430,7 +430,7 @@ return function render(_ctx, _cache) {
])
])
], -1 /* CACHED */)
]))
]))])
], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
}
}"
@ -448,9 +448,9 @@ 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] = [
return (_openBlock(), _createElementBlock("div", _hoisted_1, [...(_cache[0] || (_cache[0] = [
_createElementVNode("span", null, null, -1 /* CACHED */)
])))
]))]))
}), 256 /* UNKEYED_FRAGMENT */))
]))
}

View File

@ -27,7 +27,7 @@ import { PatchFlags } from '@vue/shared'
const cachedChildrenArrayMatcher = (
tags: string[],
needArraySpread = false,
needArraySpread = true,
) => ({
type: NodeTypes.JS_CACHE_EXPRESSION,
needArraySpread,
@ -170,11 +170,6 @@ describe('compiler: cacheStatic transform', () => {
{
/* _ slot flag */
},
{
type: NodeTypes.JS_PROPERTY,
key: { content: '__' },
value: { content: '[0]' },
},
],
})
})
@ -202,11 +197,6 @@ describe('compiler: cacheStatic transform', () => {
{
/* _ slot flag */
},
{
type: NodeTypes.JS_PROPERTY,
key: { content: '__' },
value: { content: '[0]' },
},
],
})
})

View File

@ -716,4 +716,42 @@ describe('compiler: expression transform', () => {
})
})
})
describe('switch case variable declarations', () => {
test('should handle const declarations in switch case without braces', () => {
const { code } = compile(
`{{ (() => { switch (1) { case 1: const foo = "bar"; return \`\${foo}\`; } })() }}`,
)
expect(code).toMatch(`const foo = "bar";`)
expect(code).toMatch(`return \`\${foo}\`;`)
expect(code).not.toMatch(`_ctx.foo`)
})
test('should handle const declarations in switch case with braces (existing behavior)', () => {
const { code } = compile(
`{{ (() => {
switch (true) {
case true: {
const foo = "bar";
return \`\${foo}\`;
}
}
})() }}`,
)
expect(code).toMatch(`const foo = "bar";`)
expect(code).toMatch(`return \`\${foo}\`;`)
expect(code).not.toMatch(`_ctx.foo`)
})
test('should parse switch case test as local scoped variables', () => {
const { code } = compile(
`{{ (() => { switch (foo) { case bar: return \`\${bar}\`; } })() }}`,
)
expect(code).toMatch('_ctx.foo')
expect(code).toMatch(`_ctx.bar`)
})
})
})

View File

@ -17,6 +17,7 @@ import {
helperNameMap,
} from '../../src/runtimeHelpers'
import { transformExpression } from '../../src/transforms/transformExpression'
import { transformVBindShorthand } from '../../src/transforms/transformVBindShorthand'
function parseWithVBind(
template: string,
@ -25,6 +26,7 @@ function parseWithVBind(
const ast = parse(template)
transform(ast, {
nodeTransforms: [
transformVBindShorthand,
...(options.prefixIdentifiers ? [transformExpression] : []),
transformElement,
],
@ -110,6 +112,27 @@ describe('compiler: transform v-bind', () => {
})
})
test('no expression (shorthand) in-DOM templates', () => {
try {
__BROWSER__ = true
// :id in in-DOM templates will be parsed into :id="" by browser
const node = parseWithVBind(`<div :id="" />`)
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
content: `id`,
isStatic: true,
},
value: {
content: `id`,
isStatic: false,
},
})
} finally {
__BROWSER__ = false
}
})
test('dynamic arg', () => {
const node = parseWithVBind(`<div v-bind:[id]="id"/>`)
const props = (node.codegenNode as VNodeCall).props as CallExpression

View File

@ -21,6 +21,7 @@ import { type CompilerOptions, generate } from '../../src'
import { FRAGMENT, RENDER_LIST, RENDER_SLOT } from '../../src/runtimeHelpers'
import { PatchFlags } from '@vue/shared'
import { createObjectMatcher } from '../testUtils'
import { transformVBindShorthand } from '../../src/transforms/transformVBindShorthand'
export function parseWithForTransform(
template: string,
@ -32,6 +33,7 @@ export function parseWithForTransform(
const ast = parse(template, options)
transform(ast, {
nodeTransforms: [
transformVBindShorthand,
transformIf,
transformFor,
...(options.prefixIdentifiers ? [transformExpression] : []),

View File

@ -17,7 +17,12 @@ import {
type VNodeCall,
} from '../../src/ast'
import { ErrorCodes } from '../../src/errors'
import { type CompilerOptions, TO_HANDLERS, generate } from '../../src'
import {
type CompilerOptions,
TO_HANDLERS,
generate,
transformVBindShorthand,
} from '../../src'
import {
CREATE_COMMENT,
FRAGMENT,
@ -35,7 +40,12 @@ function parseWithIfTransform(
) {
const ast = parse(template, options)
transform(ast, {
nodeTransforms: [transformIf, transformSlotOutlet, transformElement],
nodeTransforms: [
transformVBindShorthand,
transformIf,
transformSlotOutlet,
transformElement,
],
...options,
})
if (!options.onError) {
@ -209,6 +219,16 @@ describe('compiler: v-if', () => {
content: `_ctx.ok`,
})
})
//#11321
test('v-if + :key shorthand', () => {
const { node } = parseWithIfTransform(`<div v-if="ok" :key></div>`)
expect(node.type).toBe(NodeTypes.IF)
expect(node.branches[0].userKey).toMatchObject({
arg: { content: 'key' },
exp: { content: 'key' },
})
})
})
describe('errors', () => {
@ -301,6 +321,25 @@ describe('compiler: v-if', () => {
])
})
test('error on adjacent v-else', () => {
const onError = vi.fn()
const {
node: { branches },
} = parseWithIfTransform(
`<div v-if="false"/><div v-else/><div v-else/>`,
{ onError },
0,
)
expect(onError.mock.calls[0]).toMatchObject([
{
code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF,
loc: branches[branches.length - 1].loc,
},
])
})
test('error on user key', () => {
const onError = vi.fn()
// dynamic

View File

@ -478,7 +478,10 @@ describe('compiler: transform component slots', () => {
})
test('should only force dynamic slots when actually using scope vars w/ prefixIdentifiers: true', () => {
function assertDynamicSlots(template: string, shouldForce: boolean) {
function assertDynamicSlots(
template: string,
expectedPatchFlag?: PatchFlags,
) {
const { root } = parseWithSlots(template, { prefixIdentifiers: true })
let flag: any
if (root.children[0].type === NodeTypes.FOR) {
@ -491,8 +494,8 @@ describe('compiler: transform component slots', () => {
.children[0] as ComponentNode
flag = (innerComp.codegenNode as VNodeCall).patchFlag
}
if (shouldForce) {
expect(flag).toBe(PatchFlags.DYNAMIC_SLOTS)
if (expectedPatchFlag) {
expect(flag).toBe(expectedPatchFlag)
} else {
expect(flag).toBeUndefined()
}
@ -502,14 +505,13 @@ describe('compiler: transform component slots', () => {
`<div v-for="i in list">
<Comp v-slot="bar">foo</Comp>
</div>`,
false,
)
assertDynamicSlots(
`<div v-for="i in list">
<Comp v-slot="bar">{{ i }}</Comp>
</div>`,
true,
PatchFlags.DYNAMIC_SLOTS,
)
// reference the component's own slot variable should not force dynamic slots
@ -517,14 +519,13 @@ describe('compiler: transform component slots', () => {
`<Comp v-slot="foo">
<Comp v-slot="bar">{{ bar }}</Comp>
</Comp>`,
false,
)
assertDynamicSlots(
`<Comp v-slot="foo">
<Comp v-slot="bar">{{ foo }}</Comp>
</Comp>`,
true,
PatchFlags.DYNAMIC_SLOTS,
)
// #2564
@ -532,14 +533,35 @@ describe('compiler: transform component slots', () => {
`<div v-for="i in list">
<Comp v-slot="bar"><button @click="fn(i)" /></Comp>
</div>`,
true,
PatchFlags.DYNAMIC_SLOTS,
)
assertDynamicSlots(
`<div v-for="i in list">
<Comp v-slot="bar"><button @click="fn()" /></Comp>
</div>`,
false,
)
// #9380
assertDynamicSlots(
`<div v-for="i in list">
<Comp :i="i">foo</Comp>
</div>`,
PatchFlags.PROPS,
)
assertDynamicSlots(
`<div v-for="i in list">
<Comp v-slot="{ value = i }"><button @click="fn()" /></Comp>
</div>`,
PatchFlags.DYNAMIC_SLOTS,
)
assertDynamicSlots(
`<div v-for="i in list">
<Comp v-slot:[i]><button @click="fn()" /></Comp>
</div>`,
PatchFlags.DYNAMIC_SLOTS,
)
})

View File

@ -1,4 +1,9 @@
import type { ExpressionNode, TransformContext } from '../src'
import { babelParse, walkIdentifiers } from '@vue/compiler-sfc'
import {
type ExpressionNode,
type TransformContext,
isReferencedIdentifier,
} from '../src'
import { type Position, createSimpleExpression } from '../src/ast'
import {
advancePositionWithClone,
@ -115,3 +120,18 @@ test('toValidAssetId', () => {
'_component_test_2797935797_1',
)
})
describe('isReferencedIdentifier', () => {
test('identifiers in function parameters should not be inferred as references', () => {
expect.assertions(4)
const ast = babelParse(`(({ title }) => [])`)
walkIdentifiers(
ast.program.body[0],
(node, parent, parentStack, isReference) => {
expect(isReference).toBe(false)
expect(isReferencedIdentifier(node, parent, parentStack)).toBe(false)
},
true,
)
})
})

View File

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

View File

@ -10,6 +10,8 @@ import type {
Node,
ObjectProperty,
Program,
SwitchCase,
SwitchStatement,
} from '@babel/types'
import { walk } from 'estree-walker'
@ -80,14 +82,31 @@ export function walkIdentifiers(
markScopeIdentifier(node, id, knownIds),
)
}
} else if (node.type === 'SwitchStatement') {
if (node.scopeIds) {
node.scopeIds.forEach(id => markKnownIds(id, knownIds))
} else {
// record switch case block-level local variables
walkSwitchStatement(node, false, id =>
markScopeIdentifier(node, id, knownIds),
)
}
} else if (node.type === 'CatchClause' && node.param) {
for (const id of extractIdentifiers(node.param)) {
markScopeIdentifier(node, id, knownIds)
if (node.scopeIds) {
node.scopeIds.forEach(id => markKnownIds(id, knownIds))
} else {
for (const id of extractIdentifiers(node.param)) {
markScopeIdentifier(node, id, knownIds)
}
}
} else if (isForStatement(node)) {
walkForStatement(node, false, id =>
markScopeIdentifier(node, id, knownIds),
)
if (node.scopeIds) {
node.scopeIds.forEach(id => markKnownIds(id, knownIds))
} else {
walkForStatement(node, false, id =>
markScopeIdentifier(node, id, knownIds),
)
}
}
},
leave(node: Node & { scopeIds?: Set<string> }, parent: Node | null) {
@ -122,7 +141,7 @@ export function isReferencedIdentifier(
return false
}
if (isReferenced(id, parent)) {
if (isReferenced(id, parent, parentStack[parentStack.length - 2])) {
return true
}
@ -132,7 +151,8 @@ export function isReferencedIdentifier(
case 'AssignmentExpression':
case 'AssignmentPattern':
return true
case 'ObjectPattern':
case 'ObjectProperty':
return parent.key !== id && isInDestructureAssignment(parent, parentStack)
case 'ArrayPattern':
return isInDestructureAssignment(parent, parentStack)
}
@ -186,10 +206,11 @@ export function walkFunctionParams(
}
export function walkBlockDeclarations(
block: BlockStatement | Program,
block: BlockStatement | SwitchCase | Program,
onIdent: (node: Identifier) => void,
): void {
for (const stmt of block.body) {
const body = block.type === 'SwitchCase' ? block.consequent : block.body
for (const stmt of body) {
if (stmt.type === 'VariableDeclaration') {
if (stmt.declare) continue
for (const decl of stmt.declarations) {
@ -205,6 +226,8 @@ export function walkBlockDeclarations(
onIdent(stmt.id)
} else if (isForStatement(stmt)) {
walkForStatement(stmt, true, onIdent)
} else if (stmt.type === 'SwitchStatement') {
walkSwitchStatement(stmt, true, onIdent)
}
}
}
@ -238,6 +261,28 @@ function walkForStatement(
}
}
function walkSwitchStatement(
stmt: SwitchStatement,
isVar: boolean,
onIdent: (id: Identifier) => void,
) {
for (const cs of stmt.cases) {
for (const stmt of cs.consequent) {
if (
stmt.type === 'VariableDeclaration' &&
(stmt.kind === 'var' ? isVar : !isVar)
) {
for (const decl of stmt.declarations) {
for (const id of extractIdentifiers(decl.id)) {
onIdent(id)
}
}
}
}
walkBlockDeclarations(cs, onIdent)
}
}
export function extractIdentifiers(
param: Node,
nodes: Identifier[] = [],

View File

@ -22,6 +22,7 @@ import { transformModel } from './transforms/vModel'
import { transformFilter } from './compat/transformFilter'
import { ErrorCodes, createCompilerError, defaultOnError } from './errors'
import { transformMemo } from './transforms/vMemo'
import { transformVBindShorthand } from './transforms/transformVBindShorthand'
export type TransformPreset = [
NodeTransform[],
@ -33,6 +34,7 @@ export function getBaseTransformPreset(
): TransformPreset {
return [
[
transformVBindShorthand,
transformOnce,
transformIf,
transformMemo,

View File

@ -66,6 +66,7 @@ export {
buildDirectiveArgs,
type PropsExpression,
} from './transforms/transformElement'
export { transformVBindShorthand } from './transforms/transformVBindShorthand'
export { processSlotOutlet } from './transforms/transformSlotOutlet'
export { getConstantType } from './transforms/cacheStatic'
export { generateCodeFrame } from '@vue/shared'

View File

@ -43,6 +43,7 @@ import {
isCoreComponent,
isSimpleIdentifier,
isStaticArgOf,
isVPre,
} from './utils'
import { decodeHTML } from 'entities/lib/decode.js'
import {
@ -246,7 +247,7 @@ const tokenizer = new Tokenizer(stack, {
ondirarg(start, end) {
if (start === end) return
const arg = getSlice(start, end)
if (inVPre) {
if (inVPre && !isVPre(currentProp!)) {
;(currentProp as AttributeNode).name += arg
setLocEnd((currentProp as AttributeNode).nameLoc, end)
} else {
@ -262,7 +263,7 @@ const tokenizer = new Tokenizer(stack, {
ondirmodifier(start, end) {
const mod = getSlice(start, end)
if (inVPre) {
if (inVPre && !isVPre(currentProp!)) {
;(currentProp as AttributeNode).name += '.' + mod
setLocEnd((currentProp as AttributeNode).nameLoc, end)
} else if ((currentProp as DirectiveNode).name === 'slot') {
@ -1053,7 +1054,7 @@ export function baseParse(input: string, options?: ParserOptions): RootNode {
`[@vue/compiler-core] decodeEntities option is passed but will be ` +
`ignored in non-browser builds.`,
)
} else if (__BROWSER__ && !currentOptions.decodeEntities) {
} else if (__BROWSER__ && !__TEST__ && !currentOptions.decodeEntities) {
throw new Error(
`[@vue/compiler-core] decodeEntities option is required in browser builds.`,
)

View File

@ -12,19 +12,22 @@ import {
type RootNode,
type SimpleExpressionNode,
type SlotFunctionExpression,
type SlotsObjectProperty,
type TemplateChildNode,
type TemplateNode,
type TextCallNode,
type VNodeCall,
createArrayExpression,
createObjectProperty,
createSimpleExpression,
getVNodeBlockHelper,
getVNodeHelper,
} from '../ast'
import type { TransformContext } from '../transform'
import { PatchFlags, isArray, isString, isSymbol } from '@vue/shared'
import {
PatchFlagNames,
PatchFlags,
isArray,
isString,
isSymbol,
} from '@vue/shared'
import { findDir, isSlotOutlet } from '../utils'
import {
GUARD_REACTIVE_PROPS,
@ -109,6 +112,15 @@ function walk(
? ConstantTypes.NOT_CONSTANT
: getConstantType(child, context)
if (constantType >= ConstantTypes.CAN_CACHE) {
if (
child.codegenNode.type === NodeTypes.JS_CALL_EXPRESSION &&
child.codegenNode.arguments.length > 0
) {
child.codegenNode.arguments.push(
PatchFlags.CACHED +
(__DEV__ ? ` /* ${PatchFlagNames[PatchFlags.CACHED]} */` : ``),
)
}
toCache.push(child)
continue
}
@ -142,7 +154,6 @@ function walk(
}
let cachedAsArray = false
const slotCacheKeys = []
if (toCache.length === children.length && node.type === NodeTypes.ELEMENT) {
if (
node.tagType === ElementTypes.ELEMENT &&
@ -166,7 +177,6 @@ function walk(
// default slot
const slot = getSlotNode(node.codegenNode, 'default')
if (slot) {
slotCacheKeys.push(context.cached.length)
slot.returns = getCacheExpression(
createArrayExpression(slot.returns as TemplateChildNode[]),
)
@ -190,7 +200,6 @@ function walk(
slotName.arg &&
getSlotNode(parent.codegenNode, slotName.arg)
if (slot) {
slotCacheKeys.push(context.cached.length)
slot.returns = getCacheExpression(
createArrayExpression(slot.returns as TemplateChildNode[]),
)
@ -201,39 +210,22 @@ function walk(
if (!cachedAsArray) {
for (const child of toCache) {
slotCacheKeys.push(context.cached.length)
child.codegenNode = context.cache(child.codegenNode!)
}
}
// put the slot cached keys on the slot object, so that the cache
// can be removed when component unmounting to prevent memory leaks
if (
slotCacheKeys.length &&
node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.COMPONENT &&
node.codegenNode &&
node.codegenNode.type === NodeTypes.VNODE_CALL &&
node.codegenNode.children &&
!isArray(node.codegenNode.children) &&
node.codegenNode.children.type === NodeTypes.JS_OBJECT_EXPRESSION
) {
node.codegenNode.children.properties.push(
createObjectProperty(
`__`,
createSimpleExpression(JSON.stringify(slotCacheKeys), false),
) as SlotsObjectProperty,
)
}
function getCacheExpression(value: JSChildNode): CacheExpression {
const exp = context.cache(value)
// #6978, #7138, #7114
// a cached children array inside v-for can caused HMR errors since
// it might be mutated when mounting the first item
if (inFor && context.hmr) {
exp.needArraySpread = true
}
// #13221
// fix memory leak in cached array:
// cached vnodes get replaced by cloned ones during mountChildren,
// which bind DOM elements. These DOM references persist after unmount,
// preventing garbage collection. Array spread avoids mutating cached
// array, preventing memory leaks.
exp.needArraySpread = true
return exp
}

View File

@ -0,0 +1,48 @@
import { camelize } from '@vue/shared'
import {
NodeTypes,
type SimpleExpressionNode,
createSimpleExpression,
} from '../ast'
import type { NodeTransform } from '../transform'
import { ErrorCodes, createCompilerError } from '../errors'
import { validFirstIdentCharRE } from '../utils'
export const transformVBindShorthand: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT) {
for (const prop of node.props) {
// same-name shorthand - :arg is expanded to :arg="arg"
if (
prop.type === NodeTypes.DIRECTIVE &&
prop.name === 'bind' &&
(!prop.exp ||
// #13930 :foo in in-DOM templates will be parsed into :foo="" by browser
(__BROWSER__ &&
prop.exp.type === NodeTypes.SIMPLE_EXPRESSION &&
!prop.exp.content.trim())) &&
prop.arg
) {
const arg = prop.arg
if (arg.type !== NodeTypes.SIMPLE_EXPRESSION || !arg.isStatic) {
// only simple expression is allowed for same-name shorthand
context.onError(
createCompilerError(
ErrorCodes.X_V_BIND_INVALID_SAME_NAME_ARGUMENT,
arg.loc,
),
)
prop.exp = createSimpleExpression('', true, arg.loc)
} else {
const propName = camelize((arg as SimpleExpressionNode).content)
if (
validFirstIdentCharRE.test(propName[0]) ||
// allow hyphen first char for https://github.com/vuejs/language-tools/pull/3424
propName[0] === '-'
) {
prop.exp = createSimpleExpression(propName, false, arg.loc)
}
}
}
}
}
}

View File

@ -1,16 +1,13 @@
import type { DirectiveTransform, TransformContext } from '../transform'
import type { DirectiveTransform } from '../transform'
import {
type DirectiveNode,
type ExpressionNode,
NodeTypes,
type SimpleExpressionNode,
createObjectProperty,
createSimpleExpression,
} from '../ast'
import { ErrorCodes, createCompilerError } from '../errors'
import { camelize } from '@vue/shared'
import { CAMELIZE } from '../runtimeHelpers'
import { processExpression } from './transformExpression'
// v-bind without arg is handled directly in ./transformElement.ts due to its affecting
// codegen for the entire props object. This transform here is only for v-bind
@ -40,32 +37,11 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => {
}
}
// same-name shorthand - :arg is expanded to :arg="arg"
if (!exp) {
if (arg.type !== NodeTypes.SIMPLE_EXPRESSION || !arg.isStatic) {
// only simple expression is allowed for same-name shorthand
context.onError(
createCompilerError(
ErrorCodes.X_V_BIND_INVALID_SAME_NAME_ARGUMENT,
arg.loc,
),
)
return {
props: [
createObjectProperty(arg, createSimpleExpression('', true, loc)),
],
}
}
transformBindShorthand(dir, context)
exp = dir.exp!
}
if (arg.type !== NodeTypes.SIMPLE_EXPRESSION) {
arg.children.unshift(`(`)
arg.children.push(`) || ""`)
} else if (!arg.isStatic) {
arg.content = `${arg.content} || ""`
arg.content = arg.content ? `${arg.content} || ""` : `""`
}
// .sync is replaced by v-model:arg
@ -92,20 +68,7 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => {
}
return {
props: [createObjectProperty(arg, exp)],
}
}
export const transformBindShorthand = (
dir: DirectiveNode,
context: TransformContext,
): void => {
const arg = dir.arg!
const propName = camelize((arg as SimpleExpressionNode).content)
dir.exp = createSimpleExpression(propName, false, arg.loc)
if (!__BROWSER__) {
dir.exp = processExpression(dir.exp, context)
props: [createObjectProperty(arg, exp!)],
}
}

View File

@ -48,7 +48,6 @@ import {
import { processExpression } from './transformExpression'
import { validateBrowserExpression } from '../validateExpression'
import { PatchFlags } from '@vue/shared'
import { transformBindShorthand } from './vBind'
export const transformFor: NodeTransform = createStructuralDirectiveTransform(
'for',
@ -64,10 +63,6 @@ export const transformFor: NodeTransform = createStructuralDirectiveTransform(
const memo = findDir(node, 'memo')
const keyProp = findProp(node, `key`, false, true)
const isDirKey = keyProp && keyProp.type === NodeTypes.DIRECTIVE
if (isDirKey && !keyProp.exp) {
// resolve :key shorthand #10882
transformBindShorthand(keyProp, context)
}
let keyExp =
keyProp &&
(keyProp.type === NodeTypes.ATTRIBUTE

View File

@ -36,7 +36,7 @@ import { findDir, findProp, getMemoedVNodeCall, injectProp } from '../utils'
import { PatchFlags } from '@vue/shared'
export const transformIf: NodeTransform = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
/^(?:if|else|else-if)$/,
(node, dir, context) => {
return processIf(node, dir, context, (ifNode, branch, isRoot) => {
// #1587: We need to dynamically increment the key based on the current
@ -141,9 +141,9 @@ export function processIf(
}
if (sibling && sibling.type === NodeTypes.IF) {
// Check if v-else was followed by v-else-if
// Check if v-else was followed by v-else-if or there are two adjacent v-else
if (
dir.name === 'else-if' &&
(dir.name === 'else-if' || dir.name === 'else') &&
sibling.branches[sibling.branches.length - 1].condition === undefined
) {
context.onError(

View File

@ -16,7 +16,7 @@ const seen = new WeakSet()
export const transformMemo: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT) {
const dir = findDir(node, 'memo')
if (!dir || seen.has(node)) {
if (!dir || seen.has(node) || context.inSSR) {
return
}
seen.add(node)

View File

@ -131,9 +131,17 @@ export function buildSlots(
// since it likely uses a scope variable.
let hasDynamicSlots = context.scopes.vSlot > 0 || context.scopes.vFor > 0
// with `prefixIdentifiers: true`, this can be further optimized to make
// it dynamic only when the slot actually uses the scope variables.
// it dynamic when
// 1. the slot arg or exp uses the scope variables.
// 2. the slot children use the scope variables.
if (!__BROWSER__ && !context.ssr && context.prefixIdentifiers) {
hasDynamicSlots = hasScopeRef(node, context.identifiers)
hasDynamicSlots =
node.props.some(
prop =>
isVSlot(prop) &&
(hasScopeRef(prop.arg, context.identifiers) ||
hasScopeRef(prop.exp, context.identifiers)),
) || children.some(child => hasScopeRef(child, context.identifiers))
}
// 1. Check for slot with slotProps on component itself.
@ -215,7 +223,7 @@ export function buildSlots(
),
)
} else if (
(vElse = findDir(slotElement, /^else(-if)?$/, true /* allowEmpty */))
(vElse = findDir(slotElement, /^else(?:-if)?$/, true /* allowEmpty */))
) {
// find adjacent v-if
let j = i
@ -226,7 +234,7 @@ export function buildSlots(
break
}
}
if (prev && isTemplateNode(prev) && findDir(prev, /^(else-)?if$/)) {
if (prev && isTemplateNode(prev) && findDir(prev, /^(?:else-)?if$/)) {
__TEST__ && assert(dynamicSlots.length > 0)
// attach this slot to previous conditional
let conditional = dynamicSlots[

View File

@ -63,7 +63,7 @@ export function isCoreComponent(tag: string): symbol | void {
}
}
const nonIdentifierRE = /^\d|[^\$\w\xA0-\uFFFF]/
const nonIdentifierRE = /^$|^\d|[^\$\w\xA0-\uFFFF]/
export const isSimpleIdentifier = (name: string): boolean =>
!nonIdentifierRE.test(name)
@ -74,7 +74,7 @@ enum MemberExpLexState {
inString,
}
const validFirstIdentCharRE = /[A-Za-z_$\xA0-\uFFFF]/
export const validFirstIdentCharRE: RegExp = /[A-Za-z_$\xA0-\uFFFF]/
const validIdentCharRE = /[\.\?\w$\xA0-\uFFFF]/
const whitespaceRE = /\s+[.[]\s*|\s*[.[]\s+/g
@ -189,7 +189,7 @@ export const isMemberExpression: (
) => boolean = __BROWSER__ ? isMemberExpressionBrowser : isMemberExpressionNode
const fnExpRE =
/^\s*(async\s*)?(\([^)]*?\)|[\w$_]+)\s*(:[^=]+)?=>|^\s*(async\s+)?function(?:\s+[\w$]+)?\s*\(/
/^\s*(?:async\s*)?(?:\([^)]*?\)|[\w$_]+)\s*(?::[^=]+)?=>|^\s*(?:async\s+)?function(?:\s+[\w$]+)?\s*\(/
export const isFnExpressionBrowser: (exp: ExpressionNode) => boolean = exp =>
fnExpRE.test(getExpSource(exp))
@ -343,6 +343,10 @@ export function isText(
return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT
}
export function isVPre(p: ElementNode['props'][0]): p is DirectiveNode {
return p.type === NodeTypes.DIRECTIVE && p.name === 'pre'
}
export function isVSlot(p: ElementNode['props'][0]): p is DirectiveNode {
return p.type === NodeTypes.DIRECTIVE && p.name === 'slot'
}

View File

@ -4,11 +4,29 @@ exports[`stringify static html > eligible content (elements > 20) + non-eligible
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
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 /* 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)
])))
]))]))
}"
`;
exports[`stringify static html > eligible content + v-once node 1`] = `
"const { setBlockTracking: _setBlockTracking, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || (
_setBlockTracking(-1, true),
(_cache[0] = _createElementVNode("div", null, [
_createTextVNode(_toDisplayString(_ctx.msg), 1 /* TEXT */)
])).cacheIndex = 0,
_setBlockTracking(1),
_cache[0]
),
_cache[1] || (_cache[1] = _createStaticVNode("<span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span>", 5))
]))
}"
`;
@ -16,9 +34,9 @@ exports[`stringify static html > escape 1`] = `
"const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createStaticVNode("<div><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span></div>", 1)
])))
]))]))
}"
`;
@ -26,9 +44,9 @@ exports[`stringify static html > serializing constant bindings 1`] = `
"const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createStaticVNode("<div style=\\"color:red;\\"><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span></div>", 1)
])))
]))]))
}"
`;
@ -36,9 +54,9 @@ exports[`stringify static html > serializing template string style 1`] = `
"const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createStaticVNode("<div style=\\"color:red;\\"><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span></div>", 1)
])))
]))]))
}"
`;
@ -46,7 +64,7 @@ exports[`stringify static html > should bail for <option> elements with null val
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("select", null, [
_createElementVNode("option", { value: null }),
_createElementVNode("option", { value: "1" }),
@ -55,7 +73,7 @@ return function render(_ctx, _cache) {
_createElementVNode("option", { value: "1" }),
_createElementVNode("option", { value: "1" })
], -1 /* CACHED */)
])))
]))]))
}"
`;
@ -63,7 +81,7 @@ exports[`stringify static html > should bail for <option> elements with number v
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("select", null, [
_createElementVNode("option", { value: 1 }),
_createElementVNode("option", { value: 1 }),
@ -71,7 +89,7 @@ return function render(_ctx, _cache) {
_createElementVNode("option", { value: 1 }),
_createElementVNode("option", { value: 1 })
], -1 /* CACHED */)
])))
]))]))
}"
`;
@ -95,7 +113,7 @@ exports[`stringify static html > should bail on bindings that are cached but not
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("div", null, [
_createElementVNode("span", { class: "foo" }, "foo"),
_createElementVNode("span", { class: "foo" }, "foo"),
@ -104,7 +122,7 @@ return function render(_ctx, _cache) {
_createElementVNode("span", { class: "foo" }, "foo"),
_createElementVNode("img", { src: _imports_0_ })
], -1 /* CACHED */)
])))
]))]))
}"
`;
@ -112,9 +130,9 @@ exports[`stringify static html > should work for <option> elements with string v
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createStaticVNode("<select><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option></select>", 1)
])))
]))]))
}"
`;
@ -122,9 +140,9 @@ exports[`stringify static html > should work for multiple adjacent nodes 1`] = `
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createStaticVNode("<span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span>", 5)
])))
]))]))
}"
`;
@ -132,9 +150,9 @@ exports[`stringify static html > should work on eligible content (elements > 20)
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createStaticVNode("<div><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></div>", 1)
])))
]))]))
}"
`;
@ -142,9 +160,9 @@ exports[`stringify static html > should work on eligible content (elements with
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createStaticVNode("<div><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span></div>", 1)
])))
]))]))
}"
`;
@ -152,9 +170,9 @@ exports[`stringify static html > should work with bindings that are non-static b
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createStaticVNode("<div><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><img src=\\"" + _imports_0_ + "\\"></div>", 1)
])))
]))]))
}"
`;

View File

@ -48,6 +48,22 @@ return function render(_ctx, _cache) {
}"
`;
exports[`compiler: transform v-model > input with v-bind shorthand type after v-model should use dynamic model 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { vModelDynamic: _vModelDynamic, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return _withDirectives((_openBlock(), _createElementBlock("input", {
"onUpdate:modelValue": $event => ((model) = $event)
}, null, 8 /* PROPS */, ["onUpdate:modelValue"])), [
[_vModelDynamic, model]
])
}
}"
`;
exports[`compiler: transform v-model > modifiers > .lazy 1`] = `
"const _Vue = Vue

View File

@ -525,4 +525,14 @@ describe('stringify static html', () => {
expect(code).toMatchSnapshot()
})
test('eligible content + v-once node', () => {
const { code } = compileWithStringify(
`<div>
<div v-once>{{ msg }}</div>
${repeat(`<span class="foo">foo</span>`, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT)}
</div>`,
)
expect(code).toMatchSnapshot()
})
})

View File

@ -3,6 +3,7 @@ import {
generate,
baseParse as parse,
transform,
transformVBindShorthand,
} from '@vue/compiler-core'
import { transformModel } from '../../src/transforms/vModel'
import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
@ -18,7 +19,7 @@ import {
function transformWithModel(template: string, options: CompilerOptions = {}) {
const ast = parse(template)
transform(ast, {
nodeTransforms: [transformElement],
nodeTransforms: [transformVBindShorthand, transformElement],
directiveTransforms: {
model: transformModel,
},
@ -63,6 +64,14 @@ describe('compiler: transform v-model', () => {
expect(generate(root).code).toMatchSnapshot()
})
// #13169
test('input with v-bind shorthand type after v-model should use dynamic model', () => {
const root = transformWithModel('<input v-model="model" :type/>')
expect(root.helpers).toContain(V_MODEL_DYNAMIC)
expect(generate(root).code).toMatchSnapshot()
})
test('input w/ dynamic v-bind', () => {
const root = transformWithModel('<input v-bind="obj" v-model="model" />')

View File

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

View File

@ -42,7 +42,7 @@ if (__TEST__) {
if (DOMErrorCodes.X_V_HTML_NO_EXPRESSION < ErrorCodes.__EXTEND_POINT__) {
throw new Error(
`DOMErrorCodes need to be updated to ${
ErrorCodes.__EXTEND_POINT__ + 1
ErrorCodes.__EXTEND_POINT__
} to match extension point from core ErrorCodes.`,
)
}

View File

@ -17,6 +17,7 @@ import {
type TextCallNode,
type TransformContext,
createCallExpression,
findDir,
isStaticArgOf,
} from '@vue/compiler-core'
import {
@ -184,7 +185,7 @@ const getCachedNode = (
}
}
const dataAriaRE = /^(data|aria)-/
const dataAriaRE = /^(?:data|aria)-/
const isStringifiableAttr = (name: string, ns: Namespaces) => {
return (
(ns === Namespaces.HTML
@ -213,6 +214,11 @@ function analyzeNode(node: StringifiableNode): [number, number] | false {
return false
}
// v-once nodes should not be stringified
if (node.type === NodeTypes.ELEMENT && findDir(node, 'once', true)) {
return false
}
if (node.type === NodeTypes.TEXT_CALL) {
return [1, 0]
}

View File

@ -12,6 +12,27 @@ export function render(_ctx, _cache) {
}"
`;
exports[`prefixing props edge case in inline mode 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
import { unref as _unref, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export default /*@__PURE__*/_defineComponent({
props: {
Foo: { type: Object, required: true }
},
setup(__props: any) {
return (_ctx: any,_cache: any) => {
return (_openBlock(), _createBlock(_unref(__props["Foo"]).Bar))
}
}
})"
`;
exports[`should not hoist srcset URLs in SSR mode 1`] = `
"import { resolveComponent as _resolveComponent, withCtx as _withCtx, createVNode as _createVNode } from "vue"
import { ssrRenderAttr as _ssrRenderAttr, ssrRenderComponent as _ssrRenderComponent } from "vue/server-renderer"

View File

@ -81,9 +81,9 @@ import _imports_1 from '/bar.png'
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createStaticVNode("<img src=\\"" + _imports_0 + "\\"><img src=\\"" + _imports_1 + "\\"><img src=\\"https://foo.bar/baz.png\\"><img src=\\"//foo.bar/baz.png\\"><img src=\\"" + _imports_0 + "\\">", 5)
])))
]))]))
}"
`;

View File

@ -16,6 +16,16 @@ export function render(_ctx, _cache) {
}"
`;
exports[`compiler sfc: transform srcset > transform empty srcset w/ includeAbsolute: true 1`] = `
"import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = { srcset: " " }
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("img", _hoisted_1))
}"
`;
exports[`compiler sfc: transform srcset > transform srcset 1`] = `
"import { createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
import _imports_0 from './logo.png'
@ -228,8 +238,8 @@ const _hoisted_8 = _imports_1 + ', ' + _imports_1 + ' 2x'
const _hoisted_9 = _imports_1 + ', ' + _imports_0 + ' 2x'
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createStaticVNode("<img src=\\"./logo.png\\" srcset=\\"\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_1 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_2 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_3 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_4 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_5 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_6 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_7 + "\\"><img src=\\"/logo.png\\" srcset=\\"" + _hoisted_8 + "\\"><img src=\\"https://example.com/logo.png\\" srcset=\\"https://example.com/logo.png, https://example.com/logo.png 2x\\"><img src=\\"/logo.png\\" srcset=\\"" + _hoisted_9 + "\\"><img src=\\"data:image/png;base64,i\\" srcset=\\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\">", 12)
])))
]))]))
}"
`;

View File

@ -913,6 +913,13 @@ describe('SFC compile <script setup>', () => {
expect(() =>
compile(`<script>foo()</script><script setup lang="ts">bar()</script>`),
).toThrow(`<script> and <script setup> must have the same language type`)
// #13193 must check lang before parsing with babel
expect(() =>
compile(
`<script lang="ts">const a = 1</script><script setup lang="tsx">const Comp = () => <p>test</p></script>`,
),
).toThrow(`<script> and <script setup> must have the same language type`)
})
const moduleErrorMsg = `cannot contain ES module exports`
@ -1543,4 +1550,19 @@ describe('compileScript', () => {
)
assertCode(content)
})
test('should not compile unrecognized language', () => {
const { content, lang, scriptAst } = compile(
`<script lang="coffee">
export default
data: ->
myVal: 0
</script>`,
)
expect(content).toMatch(`export default
data: ->
myVal: 0`)
expect(lang).toBe('coffee')
expect(scriptAst).not.toBeDefined()
})
})

View File

@ -20,6 +20,8 @@ describe('resolveType', () => {
foo: number // property
bar(): void // method
'baz': string // string literal key
[\`qux\`]: boolean // template literal key
123: symbol // numeric literal key
(e: 'foo'): void // call signature
(e: 'bar'): void
}>()`)
@ -27,6 +29,8 @@ describe('resolveType', () => {
foo: ['Number'],
bar: ['Function'],
baz: ['String'],
qux: ['Boolean'],
123: ['Symbol'],
})
expect(calls?.length).toBe(2)
})
@ -149,6 +153,17 @@ describe('resolveType', () => {
})
})
test('TSPropertySignature with ignore', () => {
expect(
resolve(`
type Foo = string
defineProps<{ foo: /* @vue-ignore */ Foo }>()
`).props,
).toStrictEqual({
foo: ['Unknown'],
})
})
// #7553
test('union type', () => {
expect(
@ -184,7 +199,7 @@ describe('resolveType', () => {
type T = 'foo' | 'bar'
type S = 'x' | 'y'
defineProps<{
[\`_\${T}_\${S}_\`]: string
[K in \`_\${T}_\${S}_\`]: string
}>()
`).props,
).toStrictEqual({
@ -538,7 +553,7 @@ describe('resolveType', () => {
expect(props).toStrictEqual({
foo: ['Symbol', 'String', 'Number'],
bar: [UNKNOWN_TYPE],
bar: ['String', 'Number'],
})
})
@ -731,6 +746,22 @@ describe('resolveType', () => {
})
})
test('TSMappedType with indexed access', () => {
const { props } = resolve(
`
type Prettify<T> = { [K in keyof T]: T[K] } & {}
type Side = 'top' | 'right' | 'bottom' | 'left'
type AlignedPlacement = \`\${Side}-\${Alignment}\`
type Alignment = 'start' | 'end'
type Placement = Prettify<Side | AlignedPlacement>
defineProps<{placement?: Placement}>()
`,
)
expect(props).toStrictEqual({
placement: ['String', 'Object'],
})
})
describe('type alias declaration', () => {
// #13240
test('function type', () => {
@ -749,7 +780,7 @@ describe('resolveType', () => {
})
})
test('fallback to Unknown', () => {
test('with intersection type', () => {
expect(
resolve(`
type Brand<T> = T & {};
@ -758,7 +789,18 @@ describe('resolveType', () => {
}>()
`).props,
).toStrictEqual({
foo: [UNKNOWN_TYPE],
foo: ['String', 'Object'],
})
})
test('with union type', () => {
expect(
resolve(`
type Wrapped<T> = T | symbol | number
defineProps<{foo?: Wrapped<boolean>}>()
`).props,
).toStrictEqual({
foo: ['Boolean', 'Symbol', 'Number'],
})
})
})
@ -1187,6 +1229,45 @@ describe('resolveType', () => {
expect(deps && [...deps]).toStrictEqual(['/user.ts'])
})
// #13484
test('ts module resolve w/ project reference & extends & ${configDir}', () => {
const files = {
'/tsconfig.json': JSON.stringify({
files: [],
references: [{ path: './tsconfig.app.json' }],
}),
'/tsconfig.app.json': JSON.stringify({
extends: ['./tsconfigs/base.json'],
}),
'/tsconfigs/base.json': JSON.stringify({
compilerOptions: {
paths: {
'@/*': ['${configDir}/src/*'],
},
},
include: ['${configDir}/src/**/*.ts', '${configDir}/src/**/*.vue'],
}),
'/src/types.ts':
'export type BaseProps = { foo?: string, bar?: string }',
}
const { props, deps } = resolve(
`
import { BaseProps } from '@/types.ts';
defineProps<BaseProps>()
`,
files,
{},
'/src/components/Foo.vue',
)
expect(props).toStrictEqual({
foo: ['String'],
bar: ['String'],
})
expect(deps && [...deps]).toStrictEqual(['/src/types.ts'])
})
test('ts module resolve w/ project reference folder', () => {
const files = {
'/tsconfig.json': JSON.stringify({
@ -1331,6 +1412,33 @@ describe('resolveType', () => {
expect(deps && [...deps]).toStrictEqual(Object.keys(files))
})
test('global types with named exports', () => {
const files = {
'/global.d.ts': `
declare global {
export interface ExportedInterface { foo: number }
export type ExportedType = { bar: boolean }
}
export {}
`,
}
const globalTypeFiles = { globalTypeFiles: Object.keys(files) }
expect(
resolve(`defineProps<ExportedInterface>()`, files, globalTypeFiles)
.props,
).toStrictEqual({
foo: ['Number'],
})
expect(
resolve(`defineProps<ExportedType>()`, files, globalTypeFiles).props,
).toStrictEqual({
bar: ['Boolean'],
})
})
test('global types with ambient references', () => {
const files = {
// with references

View File

@ -512,3 +512,22 @@ test('non-identifier expression in legacy filter syntax', () => {
babelParse(compilationResult.code, { sourceType: 'module' })
}).not.toThrow()
})
test('prefixing props edge case in inline mode', () => {
const src = `
<script setup lang="ts">
defineProps<{ Foo: { Bar: unknown } }>()
</script>
<template>
<Foo.Bar/>
</template>
`
const { descriptor } = parse(src)
const { content } = compileScript(descriptor, {
id: 'xxx',
inlineTemplate: true,
})
expect(content).toMatchSnapshot()
expect(content).toMatch(`__props["Foo"]).Bar`)
})

View File

@ -109,8 +109,8 @@ describe('CSS vars injection', () => {
{ isProd: true },
)
expect(content).toMatch(`_useCssVars(_ctx => ({
"4003f1a6": (_ctx.color),
"41b6490a": (_ctx.font.size)
"v4003f1a6": (_ctx.color),
"v41b6490a": (_ctx.font.size)
}))}`)
const { code } = compileStyle({
@ -124,8 +124,8 @@ describe('CSS vars injection', () => {
})
expect(code).toMatchInlineSnapshot(`
".foo {
color: var(--4003f1a6);
font-size: var(--41b6490a);
color: var(--v4003f1a6);
font-size: var(--v41b6490a);
}"
`)
})

View File

@ -72,6 +72,14 @@ describe('compiler sfc: transform srcset', () => {
).toMatchSnapshot()
})
test('transform empty srcset w/ includeAbsolute: true', () => {
expect(
compileWithSrcset(`<img srcset=" " />`, {
includeAbsolute: true,
}).code,
).toMatchSnapshot()
})
test('transform srcset w/ stringify', () => {
const code = compileWithSrcset(
`<div>${src}</div>`,

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-sfc",
"version": "3.5.17",
"version": "3.5.24",
"description": "@vue/compiler-sfc",
"main": "dist/compiler-sfc.cjs.js",
"module": "dist/compiler-sfc.esm-browser.js",
@ -58,10 +58,10 @@
"hash-sum": "^2.0.0",
"lru-cache": "10.1.0",
"merge-source-map": "^1.1.0",
"minimatch": "~10.0.3",
"minimatch": "~10.1.1",
"postcss-modules": "^6.0.1",
"postcss-selector-parser": "^7.1.0",
"pug": "^3.0.3",
"sass": "^1.89.2"
"sass": "^1.93.3"
}
}

View File

@ -18,6 +18,7 @@ import type {
Declaration,
ExportSpecifier,
Identifier,
LVal,
Node,
ObjectPattern,
Statement,
@ -55,7 +56,13 @@ import { DEFINE_EXPOSE, processDefineExpose } from './script/defineExpose'
import { DEFINE_OPTIONS, processDefineOptions } from './script/defineOptions'
import { DEFINE_SLOTS, processDefineSlots } from './script/defineSlots'
import { DEFINE_MODEL, processDefineModel } from './script/defineModel'
import { getImportedName, isCallOf, isLiteralNode } from './script/utils'
import {
getImportedName,
isCallOf,
isJS,
isLiteralNode,
isTS,
} from './script/utils'
import { analyzeScriptBindings } from './script/analyzeScriptBindings'
import { isImportUsed } from './script/importUsageCheck'
import { processAwait } from './script/topLevelAwait'
@ -167,33 +174,43 @@ export function compileScript(
)
}
const ctx = new ScriptCompileContext(sfc, options)
const { script, scriptSetup, source, filename } = sfc
const hoistStatic = options.hoistStatic !== false && !script
const scopeId = options.id ? options.id.replace(/^data-v-/, '') : ''
const scriptLang = script && script.lang
const scriptSetupLang = scriptSetup && scriptSetup.lang
const isJSOrTS =
isJS(scriptLang, scriptSetupLang) || isTS(scriptLang, scriptSetupLang)
if (!scriptSetup) {
if (!script) {
throw new Error(`[@vue/compiler-sfc] SFC contains no <script> tags.`)
}
// normal <script> only
return processNormalScript(ctx, scopeId)
}
if (script && scriptLang !== scriptSetupLang) {
if (script && scriptSetup && scriptLang !== scriptSetupLang) {
throw new Error(
`[@vue/compiler-sfc] <script> and <script setup> must have the same ` +
`language type.`,
)
}
if (scriptSetupLang && !ctx.isJS && !ctx.isTS) {
if (!scriptSetup) {
if (!script) {
throw new Error(`[@vue/compiler-sfc] SFC contains no <script> tags.`)
}
// normal <script> only
if (script.lang && !isJSOrTS) {
// do not process non js/ts script blocks
return script
}
const ctx = new ScriptCompileContext(sfc, options)
return processNormalScript(ctx, scopeId)
}
if (scriptSetupLang && !isJSOrTS) {
// do not process non js/ts script blocks
return scriptSetup
}
const ctx = new ScriptCompileContext(sfc, options)
// metadata that needs to be returned
// const ctx.bindingMetadata: BindingMetadata = {}
const scriptBindings: Record<string, BindingTypes> = Object.create(null)
@ -540,7 +557,7 @@ export function compileScript(
}
// defineProps
const isDefineProps = processDefineProps(ctx, init, decl.id)
const isDefineProps = processDefineProps(ctx, init, decl.id as LVal)
if (ctx.propsDestructureRestId) {
setupBindings[ctx.propsDestructureRestId] =
BindingTypes.SETUP_REACTIVE_CONST
@ -548,10 +565,10 @@ export function compileScript(
// defineEmits
const isDefineEmits =
!isDefineProps && processDefineEmits(ctx, init, decl.id)
!isDefineProps && processDefineEmits(ctx, init, decl.id as LVal)
!isDefineEmits &&
(processDefineSlots(ctx, init, decl.id) ||
processDefineModel(ctx, init, decl.id))
(processDefineSlots(ctx, init, decl.id as LVal) ||
processDefineModel(ctx, init, decl.id as LVal))
if (
isDefineProps &&
@ -816,6 +833,8 @@ export function compileScript(
let templateMap
// 9. generate return statement
let returned
// ensure props bindings register before compile template in inline mode
const propsDecl = genRuntimeProps(ctx)
if (
!options.inlineTemplate ||
(!sfc.template && ctx.hasDefaultExportRender)
@ -948,7 +967,6 @@ export function compileScript(
runtimeOptions += `\n __ssrInlineRender: true,`
}
const propsDecl = genRuntimeProps(ctx)
if (propsDecl) runtimeOptions += `\n props: ${propsDecl},`
const emitsDecl = genRuntimeEmits(ctx)

View File

@ -9,6 +9,7 @@ import type { BindingMetadata } from '../../../compiler-core/src'
import MagicString from 'magic-string'
import type { TypeScope } from './resolveType'
import { warn } from '../warn'
import { isJS, isTS } from './utils'
export class ScriptCompileContext {
isJS: boolean
@ -87,16 +88,8 @@ export class ScriptCompileContext {
const scriptLang = script && script.lang
const scriptSetupLang = scriptSetup && scriptSetup.lang
this.isJS =
scriptLang === 'js' ||
scriptLang === 'jsx' ||
scriptSetupLang === 'js' ||
scriptSetupLang === 'jsx'
this.isTS =
scriptLang === 'ts' ||
scriptLang === 'tsx' ||
scriptSetupLang === 'ts' ||
scriptSetupLang === 'tsx'
this.isJS = isJS(scriptLang, scriptSetupLang)
this.isTS = isTS(scriptLang, scriptSetupLang)
const customElement = options.customElement
const filename = this.descriptor.filename

View File

@ -114,7 +114,7 @@ export function processDefineModel(
return true
}
export function genModelProps(ctx: ScriptCompileContext) {
export function genModelProps(ctx: ScriptCompileContext): string | undefined {
if (!ctx.hasDefineModelCall) return
const isProd = !!ctx.options.isProd

View File

@ -12,10 +12,6 @@ export function processNormalScript(
scopeId: string,
): SFCScriptBlock {
const script = ctx.descriptor.script!
if (script.lang && !ctx.isJS && !ctx.isTS) {
// do not process non js/ts script blocks
return script
}
try {
let content = script.content
let map = script.map

View File

@ -29,6 +29,7 @@ import {
createGetCanonicalFileName,
getId,
getImportedName,
getStringLiteralKey,
joinPaths,
normalizePath,
} from './utils'
@ -336,13 +337,9 @@ function typeElementsToMap(
Object.assign(scope.types, typeParameters)
}
;(e as MaybeWithScope)._ownerScope = scope
const name = getId(e.key)
if (name && !e.computed) {
const name = getStringLiteralKey(e)
if (name !== null) {
res.props[name] = e as ResolvedElements['props'][string]
} else if (e.key.type === 'TemplateLiteral') {
for (const key of resolveTemplateKeys(ctx, e.key, scope)) {
res.props[key] = e as ResolvedElements['props'][string]
}
} else {
ctx.error(
`Unsupported computed key in type referenced by a macro`,
@ -853,7 +850,7 @@ export function registerTS(_loadTS: () => typeof TS): void {
) {
throw new Error(
'Failed to load TypeScript, which is required for resolving imported types. ' +
'Please make sure "typescript" is installed as a project dependency.',
'Please make sure "TypeScript" is installed as a project dependency.',
)
} else {
throw new Error(
@ -951,7 +948,7 @@ function importSourceToScope(
if (!ts) {
return ctx.error(
`Failed to resolve import source ${JSON.stringify(source)}. ` +
`typescript is required as a peer dep for vue in order ` +
`TypeScript is required as a peer dep for vue in order ` +
`to support resolving types from module imports.`,
node,
scope,
@ -1029,6 +1026,14 @@ function resolveWithTS(
if (configs.length === 1) {
matchedConfig = configs[0]
} else {
const [major, minor] = ts.versionMajorMinor.split('.').map(Number)
const getPattern = (base: string, p: string) => {
// ts 5.5+ supports ${configDir} in paths
const supportsConfigDir = major > 5 || (major === 5 && minor >= 5)
return p.startsWith('${configDir}') && supportsConfigDir
? normalizePath(p.replace('${configDir}', dirname(configPath!)))
: joinPaths(base, p)
}
// resolve which config matches the current file
for (const c of configs) {
const base = normalizePath(
@ -1039,11 +1044,11 @@ function resolveWithTS(
const excluded: string[] | undefined = c.config.raw?.exclude
if (
(!included && (!base || containingFile.startsWith(base))) ||
included?.some(p => isMatch(containingFile, joinPaths(base, p)))
included?.some(p => isMatch(containingFile, getPattern(base, p)))
) {
if (
excluded &&
excluded.some(p => isMatch(containingFile, joinPaths(base, p)))
excluded.some(p => isMatch(containingFile, getPattern(base, p)))
) {
continue
}
@ -1296,7 +1301,12 @@ function recordTypes(
}
} else if (stmt.type === 'TSModuleDeclaration' && stmt.global) {
for (const s of (stmt.body as TSModuleBlock).body) {
recordType(s, types, declares)
if (s.type === 'ExportNamedDeclaration' && s.declaration) {
// Handle export declarations inside declare global
recordType(s.declaration, types, declares)
} else {
recordType(s, types, declares)
}
}
}
} else {
@ -1500,7 +1510,15 @@ export function inferRuntimeType(
node: Node & MaybeWithScope,
scope: TypeScope = node._ownerScope || ctxToScope(ctx),
isKeyOf = false,
typeParameters?: Record<string, Node>,
): string[] {
if (
node.leadingComments &&
node.leadingComments.some(c => c.value.includes('@vue-ignore'))
) {
return [UNKNOWN_TYPE]
}
try {
switch (node.type) {
case 'TSStringKeyword':
@ -1588,19 +1606,43 @@ 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']
if (resolved.type === 'TSTypeAliasDeclaration') {
// #13240
// Special case for function type aliases to ensure correct runtime behavior
// other type aliases still fallback to unknown as before
if (resolved.typeAnnotation.type === 'TSFunctionType') {
return ['Function']
}
if (node.typeParameters) {
const typeParams: Record<string, Node> = Object.create(null)
if (resolved.typeParameters) {
resolved.typeParameters.params.forEach((p, i) => {
typeParams![p.name] = node.typeParameters!.params[i]
})
}
return inferRuntimeType(
ctx,
resolved.typeAnnotation,
resolved._ownerScope,
isKeyOf,
typeParams,
)
}
}
return inferRuntimeType(ctx, resolved, resolved._ownerScope, isKeyOf)
}
if (node.typeName.type === 'Identifier') {
if (typeParameters && typeParameters[node.typeName.name]) {
return inferRuntimeType(
ctx,
typeParameters[node.typeName.name],
scope,
isKeyOf,
typeParameters,
)
}
if (isKeyOf) {
switch (node.typeName.name) {
case 'String':
@ -1733,11 +1775,56 @@ export function inferRuntimeType(
return inferRuntimeType(ctx, node.typeAnnotation, scope)
case 'TSUnionType':
return flattenTypes(ctx, node.types, scope, isKeyOf)
return flattenTypes(ctx, node.types, scope, isKeyOf, typeParameters)
case 'TSIntersectionType': {
return flattenTypes(ctx, node.types, scope, isKeyOf).filter(
t => t !== UNKNOWN_TYPE,
)
return flattenTypes(
ctx,
node.types,
scope,
isKeyOf,
typeParameters,
).filter(t => t !== UNKNOWN_TYPE)
}
case 'TSMappedType': {
// only support { [K in keyof T]: T[K] }
const { typeAnnotation, typeParameter } = node
if (
typeAnnotation &&
typeAnnotation.type === 'TSIndexedAccessType' &&
typeParameter &&
typeParameter.constraint &&
typeParameters
) {
const constraint = typeParameter.constraint
if (
constraint.type === 'TSTypeOperator' &&
constraint.operator === 'keyof' &&
constraint.typeAnnotation &&
constraint.typeAnnotation.type === 'TSTypeReference' &&
constraint.typeAnnotation.typeName.type === 'Identifier'
) {
const typeName = constraint.typeAnnotation.typeName.name
const index = typeAnnotation.indexType
const obj = typeAnnotation.objectType
if (
obj &&
obj.type === 'TSTypeReference' &&
obj.typeName.type === 'Identifier' &&
obj.typeName.name === typeName &&
index &&
index.type === 'TSTypeReference' &&
index.typeName.type === 'Identifier' &&
index.typeName.name === typeParameter.name
) {
const targetType = typeParameters[typeName]
if (targetType) {
return inferRuntimeType(ctx, targetType, scope)
}
}
}
}
return [UNKNOWN_TYPE]
}
case 'TSEnumDeclaration':
@ -1808,14 +1895,17 @@ function flattenTypes(
types: TSType[],
scope: TypeScope,
isKeyOf: boolean = false,
typeParameters: Record<string, Node> | undefined = undefined,
): string[] {
if (types.length === 1) {
return inferRuntimeType(ctx, types[0], scope, isKeyOf)
return inferRuntimeType(ctx, types[0], scope, isKeyOf, typeParameters)
}
return [
...new Set(
([] as string[]).concat(
...types.map(t => inferRuntimeType(ctx, t, scope, isKeyOf)),
...types.map(t =>
inferRuntimeType(ctx, t, scope, isKeyOf, typeParameters),
),
),
),
]
@ -1936,8 +2026,7 @@ function findStaticPropertyType(node: TSTypeLiteral, key: string) {
const prop = node.members.find(
m =>
m.type === 'TSPropertySignature' &&
!m.computed &&
getId(m.key) === key &&
getStringLiteralKey(m) === key &&
m.typeAnnotation,
)
return prop && prop.typeAnnotation!.typeAnnotation

View File

@ -7,6 +7,8 @@ import type {
ImportSpecifier,
Node,
StringLiteral,
TSMethodSignature,
TSPropertySignature,
} from '@babel/types'
import path from 'path'
@ -79,6 +81,22 @@ export function getId(node: Expression) {
: null
}
export function getStringLiteralKey(
node: TSPropertySignature | TSMethodSignature,
): string | null {
return node.computed
? node.key.type === 'TemplateLiteral' && !node.key.expressions.length
? node.key.quasis.map(q => q.value.cooked).join('')
: null
: node.key.type === 'Identifier'
? node.key.name
: node.key.type === 'StringLiteral'
? node.key.value
: node.key.type === 'NumericLiteral'
? String(node.key.value)
: null
}
const identity = (str: string) => str
const fileNameLowerCaseRegExp = /[^\u0130\u0131\u00DFa-z0-9\\/:\-_\. ]+/g
const toLowerCase = (str: string) => str.toLowerCase()
@ -121,3 +139,8 @@ export const propNameEscapeSymbolsRE: RegExp =
export function getEscapedPropName(key: string): string {
return propNameEscapeSymbolsRE.test(key) ? JSON.stringify(key) : key
}
export const isJS = (...langs: (string | null | undefined)[]): boolean =>
langs.some(lang => lang === 'js' || lang === 'jsx')
export const isTS = (...langs: (string | null | undefined)[]): boolean =>
langs.some(lang => lang === 'ts' || lang === 'tsx')

View File

@ -40,7 +40,8 @@ function genVarName(
isSSR = false,
): string {
if (isProd) {
return hash(id + raw)
// hash must not start with a digit to comply with CSS custom property naming rules
return hash(id + raw).replace(/^\d/, r => `v${r}`)
} else {
// escape ASCII Punctuation & Symbols
// #7823 need to double-escape in SSR because the attributes are rendered

View File

@ -8,8 +8,9 @@ import {
import selectorParser from 'postcss-selector-parser'
import { warn } from '../warn'
const animationNameRE = /^(-\w+-)?animation-name$/
const animationRE = /^(-\w+-)?animation$/
const animationNameRE = /^(?:-\w+-)?animation-name$/
const animationRE = /^(?:-\w+-)?animation$/
const keyframesRE = /^(?:-\w+-)?keyframes$/
const scopedPlugin: PluginCreator<string> = (id = '') => {
const keyframes = Object.create(null)
@ -21,10 +22,7 @@ const scopedPlugin: PluginCreator<string> = (id = '') => {
processRule(id, rule)
},
AtRule(node) {
if (
/-?keyframes$/.test(node.name) &&
!node.params.endsWith(`-${shortId}`)
) {
if (keyframesRE.test(node.name) && !node.params.endsWith(`-${shortId}`)) {
// register keyframes
keyframes[node.params] = node.params = node.params + '-' + shortId
}
@ -72,7 +70,7 @@ function processRule(id: string, rule: Rule) {
processedRules.has(rule) ||
(rule.parent &&
rule.parent.type === 'atrule' &&
/-?keyframes$/.test((rule.parent as AtRule).name))
keyframesRE.test((rule.parent as AtRule).name))
) {
return
}

View File

@ -6,7 +6,7 @@ export function isRelativeUrl(url: string): boolean {
return firstChar === '.' || firstChar === '~' || firstChar === '@'
}
const externalRE = /^(https?:)?\/\//
const externalRE = /^(?:https?:)?\/\//
export function isExternalUrl(url: string): boolean {
return externalRE.test(url)
}

View File

@ -71,6 +71,7 @@ export const transformSrcset: NodeTransform = (
const shouldProcessUrl = (url: string) => {
return (
url &&
!isExternalUrl(url) &&
!isDataUrl(url) &&
(options.includeAbsolute || isRelativeUrl(url))

View File

@ -317,6 +317,35 @@ describe('ssr: components', () => {
`)
})
// #13724
test('slot content with v-memo', () => {
const { code } = compile(`<foo><bar v-memo="[]" /></foo>`)
expect(code).not.toMatch(`_cache`)
expect(compile(`<foo><bar v-memo="[]" /></foo>`).code)
.toMatchInlineSnapshot(`
"const { resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode } = require("vue")
const { ssrRenderComponent: _ssrRenderComponent } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
const _component_foo = _resolveComponent("foo")
const _component_bar = _resolveComponent("bar")
_push(_ssrRenderComponent(_component_foo, _attrs, {
default: _withCtx((_, _push, _parent, _scopeId) => {
if (_push) {
_push(_ssrRenderComponent(_component_bar, null, null, _parent, _scopeId))
} else {
return [
_createVNode(_component_bar)
]
}
}),
_: 1 /* STABLE */
}, _parent))
}"
`)
})
describe('built-in fallthroughs', () => {
test('transition', () => {
expect(compile(`<transition><div/></transition>`).code)

View File

@ -101,6 +101,28 @@ describe('transition-group', () => {
`)
})
test('with dynamic tag shorthand', () => {
expect(
compile(
`<transition-group :tag><div v-for="i in list"/></transition-group>`,
).code,
).toMatchInlineSnapshot(`
"const { ssrRenderAttrs: _ssrRenderAttrs, ssrRenderList: _ssrRenderList } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<\${
_ctx.tag
}\${
_ssrRenderAttrs(_attrs)
}>\`)
_ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`)
})
_push(\`</\${_ctx.tag}>\`)
}"
`)
})
test('with multi fragments children', () => {
expect(
compile(

View File

@ -11,9 +11,9 @@ describe('ssr: v-show', () => {
const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${_ssrRenderAttrs(_mergeProps({
_push(\`<div\${_ssrRenderAttrs(_mergeProps(_attrs, {
style: (_ctx.foo) ? null : { display: "none" }
}, _attrs))}></div>\`)
}))}></div>\`)
}"
`)
})
@ -92,6 +92,24 @@ describe('ssr: v-show', () => {
`)
})
test('with style + display', () => {
expect(compileWithWrapper(`<div v-show="foo" style="display:flex" />`).code)
.toMatchInlineSnapshot(`
"const { ssrRenderStyle: _ssrRenderStyle, ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${
_ssrRenderAttrs(_attrs)
}><div style="\${
_ssrRenderStyle([
{"display":"flex"},
(_ctx.foo) ? null : { display: "none" }
])
}"></div></div>\`)
}"
`)
})
test('with v-bind', () => {
expect(
compileWithWrapper(

View File

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

View File

@ -29,7 +29,7 @@ if (__TEST__) {
if (SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME < DOMErrorCodes.__EXTEND_POINT__) {
throw new Error(
`SSRErrorCodes need to be updated to ${
DOMErrorCodes.__EXTEND_POINT__ + 1
DOMErrorCodes.__EXTEND_POINT__
} to match extension point from core DOMErrorCodes.`,
)
}

View File

@ -13,6 +13,7 @@ import {
transformExpression,
transformOn,
transformStyle,
transformVBindShorthand,
} from '@vue/compiler-dom'
import { ssrCodegenTransform } from './ssrCodegenTransform'
import { ssrTransformElement } from './transforms/ssrTransformElement'
@ -55,6 +56,7 @@ export function compile(
...options,
hoistStatic: false,
nodeTransforms: [
transformVBindShorthand,
ssrTransformIf,
ssrTransformFor,
trackVForSlotScopes,

View File

@ -88,6 +88,17 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
const hasCustomDir = node.props.some(
p => p.type === NodeTypes.DIRECTIVE && !isBuiltInDirective(p.name),
)
// v-show has a higher priority in ssr
const vShowPropIndex = node.props.findIndex(
i => i.type === NodeTypes.DIRECTIVE && i.name === 'show',
)
if (vShowPropIndex !== -1) {
const vShowProp = node.props[vShowPropIndex]
node.props.splice(vShowPropIndex, 1)
node.props.push(vShowProp)
}
const needMergeProps = hasDynamicVBind || hasCustomDir
if (needMergeProps) {
const { props, directives } = buildProps(
@ -111,8 +122,13 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
| InterpolationNode
| undefined
// If interpolation, this is dynamic <textarea> content, potentially
// injected by v-model and takes higher priority than v-bind value
if (!existingText || existingText.type !== NodeTypes.INTERPOLATION) {
// injected by v-model and takes higher priority than v-bind value.
// Additionally, directives with content overrides (v-text/v-html)
// have higher priority than the merged props.
if (
!hasContentOverrideDirective(node) &&
(!existingText || existingText.type !== NodeTypes.INTERPOLATION)
) {
// <textarea> with dynamic v-bind. We don't know if the final props
// will contain .value, so we will have to do something special:
// assign the merged props to a temp variable, and check whether
@ -165,9 +181,8 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
]
}
} else if (directives.length && !node.children.length) {
// v-text directive has higher priority than the merged props
const vText = findDir(node, 'text')
if (!vText) {
// v-text/v-html have higher priority than the merged props
if (!hasContentOverrideDirective(node)) {
const tempId = `_temp${context.temps++}`
propsExp.arguments = [
createAssignmentExpression(
@ -438,6 +453,10 @@ function findVModel(node: PlainElementNode): DirectiveNode | undefined {
) as DirectiveNode | undefined
}
function hasContentOverrideDirective(node: PlainElementNode): boolean {
return !!findDir(node, 'text') || !!findDir(node, 'html')
}
export function ssrProcessElement(
node: PlainElementNode,
context: SSRTransformContext,

View File

@ -17,7 +17,7 @@ import {
// Plugin for the first transform pass, which simply constructs the AST node
export const ssrTransformIf: NodeTransform = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
/^(?:if|else|else-if)$/,
processIf,
)

View File

@ -498,9 +498,10 @@ describe('reactivity/readonly', () => {
const r = ref(false)
const ror = readonly(r)
const obj = reactive({ ror })
expect(() => {
obj.ror = true
}).toThrow()
obj.ror = true
expect(
`Set operation on key "ror" failed: target is readonly.`,
).toHaveBeenWarned()
expect(obj.ror).toBe(false)
})
@ -521,6 +522,16 @@ describe('reactivity/readonly', () => {
expect(obj.r).toBe(ro)
expect(r.value).toBe(ro)
})
test('should keep nested ref readonly', () => {
const items = ref(['one', 'two', 'three'])
const obj = {
o: readonly({
items,
}),
}
expect(isReadonly(obj.o.items)).toBe(true)
})
})
test('should be able to trigger with triggerRef', () => {

View File

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

View File

@ -107,7 +107,7 @@ export const arrayInstrumentations: Record<string | symbol, Function> = <any>{
return reactiveReadArray(this).join(separator)
},
// keys() iterator only reads `length`, no optimisation required
// keys() iterator only reads `length`, no optimization required
lastIndexOf(...args: unknown[]) {
return searchProxy(this, 'lastIndexOf', args)
@ -200,7 +200,7 @@ function iterator(
wrapValue: (value: any) => unknown,
) {
// note that taking ARRAY_ITERATE dependency here is not strictly equivalent
// to calling iterate on the proxified array.
// to calling iterate on the proxied array.
// creating the iterator does not access any array property:
// it is only when .next() is called that length and indexes are accessed.
// pushed to the extreme, an iterator could be created in one effect scope,
@ -215,7 +215,7 @@ function iterator(
iter._next = iter.next
iter.next = () => {
const result = iter._next()
if (result.value) {
if (!result.done) {
result.value = wrapValue(result.value)
}
return result

View File

@ -119,7 +119,8 @@ class BaseReactiveHandler implements ProxyHandler<Target> {
if (isRef(res)) {
// ref unwrapping - skip unwrap for Array + integer key.
return targetIsArray && isIntegerKey(key) ? res : res.value
const value = targetIsArray && isIntegerKey(key) ? res : res.value
return isReadonly && isObject(value) ? readonly(value) : value
}
if (isObject(res)) {
@ -153,7 +154,13 @@ class MutableReactiveHandler extends BaseReactiveHandler {
}
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
if (isOldValueReadonly) {
return false
if (__DEV__) {
warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target[key],
)
}
return true
} else {
oldValue.value = value
return true

View File

@ -125,7 +125,7 @@ function createInstrumentations(
get size() {
const target = (this as unknown as IterableCollections)[ReactiveFlags.RAW]
!readonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.get(target, 'size', target)
return target.size
},
has(this: CollectionTypes, key: unknown): boolean {
const target = this[ReactiveFlags.RAW]

View File

@ -311,7 +311,7 @@ function prepareDeps(sub: Subscriber) {
}
function cleanupDeps(sub: Subscriber) {
// Cleanup unsued deps
// Cleanup unused deps
let head
let tail = sub.depsTail
let link = tail
@ -470,11 +470,6 @@ function removeDep(link: Link) {
}
}
export interface ReactiveEffectRunner<T = any> {
(): T
effect: ReactiveEffect
}
export function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions,

View File

@ -275,7 +275,7 @@ export function proxyRefs<T extends object>(
objectWithRefs: T,
): ShallowUnwrapRef<T> {
return isReactive(objectWithRefs)
? objectWithRefs
? (objectWithRefs as ShallowUnwrapRef<T>)
: new Proxy(objectWithRefs, shallowUnwrapHandlers)
}

View File

@ -331,17 +331,17 @@ export function watch(
export function traverse(
value: unknown,
depth: number = Infinity,
seen?: Set<unknown>,
seen?: Map<unknown, number>,
): unknown {
if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
return value
}
seen = seen || new Set()
if (seen.has(value)) {
seen = seen || new Map()
if ((seen.get(value) || 0) >= depth) {
return value
}
seen.add(value)
seen.set(value, depth)
depth--
if (isRef(value)) {
traverse(value.value, depth, seen)

View File

@ -1689,6 +1689,57 @@ describe('api: watch', () => {
expect(cb).toHaveBeenCalledTimes(4)
})
test('watching the same object at different depths', async () => {
const arr1: any[] = reactive([[[{ foo: {} }]]])
const arr2 = arr1[0]
const arr3 = arr2[0]
const obj = arr3[0]
arr1.push(arr3)
const cb1 = vi.fn()
const cb2 = vi.fn()
const cb3 = vi.fn()
const cb4 = vi.fn()
watch(arr1, cb1, { deep: 1 })
watch(arr1, cb2, { deep: 2 })
watch(arr1, cb3, { deep: 3 })
watch(arr1, cb4, { deep: 4 })
await nextTick()
expect(cb1).toHaveBeenCalledTimes(0)
expect(cb2).toHaveBeenCalledTimes(0)
expect(cb3).toHaveBeenCalledTimes(0)
expect(cb4).toHaveBeenCalledTimes(0)
obj.foo = {}
await nextTick()
expect(cb1).toHaveBeenCalledTimes(0)
expect(cb2).toHaveBeenCalledTimes(0)
expect(cb3).toHaveBeenCalledTimes(1)
expect(cb4).toHaveBeenCalledTimes(1)
obj.foo.bar = 1
await nextTick()
expect(cb1).toHaveBeenCalledTimes(0)
expect(cb2).toHaveBeenCalledTimes(0)
expect(cb3).toHaveBeenCalledTimes(1)
expect(cb4).toHaveBeenCalledTimes(2)
arr3.push(obj.foo)
await nextTick()
expect(cb1).toHaveBeenCalledTimes(0)
expect(cb2).toHaveBeenCalledTimes(1)
expect(cb3).toHaveBeenCalledTimes(2)
expect(cb4).toHaveBeenCalledTimes(3)
obj.foo.bar = 2
await nextTick()
expect(cb1).toHaveBeenCalledTimes(0)
expect(cb2).toHaveBeenCalledTimes(1)
expect(cb3).toHaveBeenCalledTimes(3)
expect(cb4).toHaveBeenCalledTimes(4)
})
test('pause / resume', async () => {
const count = ref(0)
const cb = vi.fn()

View File

@ -3,6 +3,7 @@
import {
type ComponentPublicInstance,
createApp,
defineComponent,
h,
nextTick,
@ -598,4 +599,45 @@ describe('component: emit', () => {
render(h(ComponentC), el)
expect(renderFn).toHaveBeenCalledTimes(1)
})
test('merging emits for a component that is also used as a mixin', () => {
const render = () => h('div')
const CompA = {
render,
}
const validateByMixin = vi.fn(() => true)
const validateByGlobalMixin = vi.fn(() => true)
const mixin = {
emits: {
one: validateByMixin,
},
}
const CompB = defineComponent({
mixins: [mixin, CompA],
created(this) {
this.$emit('one', 1)
},
render,
})
const app = createApp({
render() {
return [h(CompA), h(CompB)]
},
})
app.mixin({
emits: {
one: validateByGlobalMixin,
two: null,
},
})
const root = nodeOps.createElement('div')
app.mount(root)
expect(validateByMixin).toHaveBeenCalledTimes(1)
expect(validateByGlobalMixin).not.toHaveBeenCalled()
})
})

View File

@ -167,13 +167,26 @@ describe('component: proxy', () => {
data() {
return {
foo: 0,
$foo: 0,
}
},
computed: {
cmp: () => {
throw new Error('value of cmp should not be accessed')
},
$cmp: () => {
throw new Error('value of $cmp should not be read')
},
},
setup() {
return {
bar: 1,
}
},
__cssModules: {
$style: {},
cssStyles: {},
},
mounted() {
instanceProxy = this
},
@ -181,6 +194,7 @@ describe('component: proxy', () => {
const app = createApp(Comp, { msg: 'hello' })
app.config.globalProperties.global = 1
app.config.globalProperties.$global = 1
app.mount(nodeOps.createElement('div'))
@ -188,12 +202,20 @@ describe('component: proxy', () => {
expect('msg' in instanceProxy).toBe(true)
// data
expect('foo' in instanceProxy).toBe(true)
// ctx
expect('$foo' in instanceProxy).toBe(false)
// setupState
expect('bar' in instanceProxy).toBe(true)
// ctx
expect('cmp' in instanceProxy).toBe(true)
expect('$cmp' in instanceProxy).toBe(true)
// public properties
expect('$el' in instanceProxy).toBe(true)
// CSS modules
expect('$style' in instanceProxy).toBe(true)
expect('cssStyles' in instanceProxy).toBe(true)
// global properties
expect('global' in instanceProxy).toBe(true)
expect('$global' in instanceProxy).toBe(true)
// non-existent
expect('$foobar' in instanceProxy).toBe(false)
@ -202,11 +224,15 @@ describe('component: proxy', () => {
// #4962 triggering getter should not cause non-existent property to
// pass the has check
instanceProxy.baz
instanceProxy.$baz
expect('baz' in instanceProxy).toBe(false)
expect('$baz' in instanceProxy).toBe(false)
// set non-existent (goes into proxyTarget sink)
instanceProxy.baz = 1
expect('baz' in instanceProxy).toBe(true)
instanceProxy.$baz = 1
expect('$baz' in instanceProxy).toBe(true)
// dev mode ownKeys check for console inspection
// should only expose own keys
@ -214,7 +240,10 @@ describe('component: proxy', () => {
'msg',
'bar',
'foo',
'cmp',
'$cmp',
'baz',
'$baz',
])
})

View File

@ -6,6 +6,7 @@ import {
nodeOps,
ref,
render,
serializeInner,
useSlots,
} from '@vue/runtime-test'
import { createBlock, normalizeVNode } from '../src/vnode'
@ -55,14 +56,10 @@ describe('component: slots', () => {
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] }
const slots = { foo: () => {}, _: 1 }
render(createBlock(Comp, null, slots), nodeOps.createElement('div'))
})
@ -74,6 +71,10 @@ describe('component: slots', () => {
footer: ['f1', 'f2'],
})
expect(
'[Vue warn]: Non-function value encountered for slot "_inner". Prefer function slots for better performance.',
).toHaveBeenWarned()
expect(
'[Vue warn]: Non-function value encountered for slot "header". Prefer function slots for better performance.',
).toHaveBeenWarned()
@ -82,8 +83,8 @@ describe('component: slots', () => {
'[Vue warn]: Non-function value encountered for slot "footer". Prefer function slots for better performance.',
).toHaveBeenWarned()
expect(slots).not.toHaveProperty('_inner')
expect(slots).not.toHaveProperty('foo')
expect(slots._inner()).toMatchObject([normalizeVNode('_inner')])
expect(slots.header()).toMatchObject([normalizeVNode('header')])
expect(slots.footer()).toMatchObject([
normalizeVNode('f1'),
@ -442,4 +443,22 @@ describe('component: slots', () => {
'Slot "default" invoked outside of the render function',
).toHaveBeenWarned()
})
test('slot name starts with underscore', () => {
const Comp = {
setup(_: any, { slots }: any) {
return () => slots._foo()
},
}
const App = {
setup() {
return () => h(Comp, null, { _foo: () => 'foo' })
},
}
const root = nodeOps.createElement('div')
createApp(App).mount(root)
expect(serializeInner(root)).toBe('foo')
})
})

View File

@ -17,6 +17,8 @@ import {
onUnmounted,
ref,
render,
renderList,
renderSlot,
resolveDynamicComponent,
serializeInner,
shallowRef,
@ -2161,6 +2163,80 @@ describe('Suspense', () => {
await Promise.all(deps)
})
// #13453
test('add new async deps during patching', async () => {
const getComponent = (type: string) => {
if (type === 'A') {
return defineAsyncComponent({
setup() {
return () => h('div', 'A')
},
})
}
return defineAsyncComponent({
setup() {
return () => h('div', 'B')
},
})
}
const types = ref(['A'])
const add = async () => {
types.value.push('B')
}
const update = async () => {
// mount Suspense B
// [Suspense A] -> [Suspense A(pending), Suspense B(pending)]
await add()
// patch Suspense B (still pending)
// [Suspense A(pending), Suspense B(pending)] -> [Suspense B(pending)]
types.value.shift()
}
const Comp = {
render(this: any) {
return h(Fragment, null, [
renderList(types.value, type => {
return h(
Suspense,
{ key: type },
{
default: () => [
renderSlot(this.$slots, 'default', { type: type }),
],
},
)
}),
])
},
}
const App = {
setup() {
return () =>
h(Comp, null, {
default: (params: any) => [h(getComponent(params.type))],
})
},
}
const root = nodeOps.createElement('div')
render(h(App), root)
expect(serializeInner(root)).toBe(`<!---->`)
await Promise.all(deps)
expect(serializeInner(root)).toBe(`<div>A</div>`)
update()
await nextTick()
// wait for both A and B to resolve
await Promise.all(deps)
// wait for new B to resolve
await Promise.all(deps)
expect(serializeInner(root)).toBe(`<div>B</div>`)
})
describe('warnings', () => {
// base function to check if a combination of slots warns or not
function baseCheckWarn(
@ -2230,5 +2306,57 @@ describe('Suspense', () => {
fallback: [h('div'), h('div')],
})
})
// #13559
test('renders multiple async components in Suspense with v-for and updates on items change', async () => {
const CompAsyncSetup = defineAsyncComponent({
props: ['item'],
render(ctx: any) {
return h('div', ctx.item.name)
},
})
const items = ref([
{ id: 1, name: '111' },
{ id: 2, name: '222' },
{ id: 3, name: '333' },
])
const Comp = {
setup() {
return () =>
h(Suspense, null, {
default: () =>
h(
Fragment,
null,
items.value.map(item =>
h(CompAsyncSetup, { item, key: item.id }),
),
),
})
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
await nextTick()
await Promise.all(deps)
expect(serializeInner(root)).toBe(
`<div>111</div><div>222</div><div>333</div>`,
)
items.value = [
{ id: 4, name: '444' },
{ id: 5, name: '555' },
{ id: 6, name: '666' },
]
await nextTick()
await Promise.all(deps)
expect(serializeInner(root)).toBe(
`<div>444</div><div>555</div><div>666</div>`,
)
})
})
})

View File

@ -106,6 +106,134 @@ describe('useTemplateRef', () => {
expect(tRef!.value).toBe(null)
})
test('should work when used with direct ref value with ref_key', () => {
let tRef: ShallowRef
const key = 'refKey'
const Comp = {
setup() {
tRef = useTemplateRef(key)
return () => h('div', { ref: tRef, ref_key: key })
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect('target is readonly').not.toHaveBeenWarned()
expect(tRef!.value).toBe(root.children[0])
})
test('should work when used with direct ref value with ref_key and ref_for', () => {
let tRef: ShallowRef
const key = 'refKey'
const Comp = {
setup() {
tRef = useTemplateRef(key)
},
render() {
return h(
'div',
[1, 2, 3].map(x =>
h('span', { ref: tRef, ref_key: key, ref_for: true }, x.toString()),
),
)
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect('target is readonly').not.toHaveBeenWarned()
expect(tRef!.value).toHaveLength(3)
})
test('should work when used with direct ref value with ref_key and dynamic value', async () => {
const refMode = ref('h1-ref')
let tRef: ShallowRef
const key = 'refKey'
const Comp = {
setup() {
tRef = useTemplateRef(key)
},
render() {
switch (refMode.value) {
case 'h1-ref':
return h('h1', { ref: tRef, ref_key: key })
case 'h2-ref':
return h('h2', { ref: tRef, ref_key: key })
case 'no-ref':
return h('span')
case 'nothing':
return null
}
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(tRef!.value.tag).toBe('h1')
refMode.value = 'h2-ref'
await nextTick()
expect(tRef!.value.tag).toBe('h2')
refMode.value = 'no-ref'
await nextTick()
expect(tRef!.value).toBeNull()
refMode.value = 'nothing'
await nextTick()
expect(tRef!.value).toBeNull()
expect('target is readonly').not.toHaveBeenWarned()
})
test('should work when used with dynamic direct refs and ref_keys', async () => {
const refKey = ref('foo')
let tRefs: Record<string, ShallowRef>
const Comp = {
setup() {
tRefs = {
foo: useTemplateRef('foo'),
bar: useTemplateRef('bar'),
}
},
render() {
return h('div', { ref: tRefs[refKey.value], ref_key: refKey.value })
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(tRefs!['foo'].value).toBe(root.children[0])
expect(tRefs!['bar'].value).toBeNull()
refKey.value = 'bar'
await nextTick()
expect(tRefs!['foo'].value).toBeNull()
expect(tRefs!['bar'].value).toBe(root.children[0])
expect('target is readonly').not.toHaveBeenWarned()
})
test('should not work when used with direct ref value without ref_key (in dev mode)', () => {
let tRef: ShallowRef
const Comp = {
setup() {
tRef = useTemplateRef('refKey')
return () => h('div', { ref: tRef })
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(tRef!.value).toBeNull()
})
test('should work when used as direct ref value (compiled in prod mode)', () => {
__DEV__ = false
try {
@ -125,4 +253,65 @@ describe('useTemplateRef', () => {
__DEV__ = true
}
})
test('should work when used as direct ref value with ref_key and ref_for (compiled in prod mode)', () => {
__DEV__ = false
try {
let tRef: ShallowRef
const key = 'refKey'
const Comp = {
setup() {
tRef = useTemplateRef(key)
},
render() {
return h(
'div',
[1, 2, 3].map(x =>
h(
'span',
{ ref: tRef, ref_key: key, ref_for: true },
x.toString(),
),
),
)
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect('target is readonly').not.toHaveBeenWarned()
expect(tRef!.value).toHaveLength(3)
} finally {
__DEV__ = true
}
})
test('should work when used as direct ref value with ref_for but without ref_key (compiled in prod mode)', () => {
__DEV__ = false
try {
let tRef: ShallowRef
const Comp = {
setup() {
tRef = useTemplateRef('refKey')
},
render() {
return h(
'div',
[1, 2, 3].map(x =>
h('span', { ref: tRef, ref_for: true }, x.toString()),
),
)
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect('target is readonly').not.toHaveBeenWarned()
expect(tRef!.value).toHaveLength(3)
} finally {
__DEV__ = true
}
})
})

View File

@ -894,4 +894,150 @@ describe('hot module replacement', () => {
await timeout()
expect(serializeInner(root)).toBe('<div>bar</div>')
})
test('multi reload child wrapped in Suspense + KeepAlive', async () => {
const id = 'test-child-reload-3'
const Child: ComponentOptions = {
__hmrId: id,
setup() {
const count = ref(0)
return { count }
},
render: compileToFunction(`<div>{{ count }}</div>`),
}
createRecord(id, Child)
const appId = 'test-app-id'
const App: ComponentOptions = {
__hmrId: appId,
components: { Child },
render: compileToFunction(`
<KeepAlive>
<Suspense>
<Child />
</Suspense>
</KeepAlive>
`),
}
const root = nodeOps.createElement('div')
render(h(App), root)
expect(serializeInner(root)).toBe('<div>0</div>')
await timeout()
reload(id, {
__hmrId: id,
setup() {
const count = ref(1)
return { count }
},
render: compileToFunction(`<div>{{ count }}</div>`),
})
await timeout()
expect(serializeInner(root)).toBe('<div>1</div>')
reload(id, {
__hmrId: id,
setup() {
const count = ref(2)
return { count }
},
render: compileToFunction(`<div>{{ count }}</div>`),
})
await timeout()
expect(serializeInner(root)).toBe('<div>2</div>')
})
test('rerender for nested component', () => {
const id = 'child-nested-rerender'
const Foo: ComponentOptions = {
__hmrId: id,
render() {
return this.$slots.default()
},
}
createRecord(id, Foo)
const parentId = 'parent-nested-rerender'
const Parent: ComponentOptions = {
__hmrId: parentId,
render() {
return h(Foo, null, {
default: () => this.$slots.default(),
_: 3 /* FORWARDED */,
})
},
}
const appId = 'app-nested-rerender'
const App: ComponentOptions = {
__hmrId: appId,
render: () =>
h(Parent, null, {
default: () => [
h(Foo, null, {
default: () => ['foo'],
}),
],
}),
}
createRecord(parentId, App)
const root = nodeOps.createElement('div')
render(h(App), root)
expect(serializeInner(root)).toBe('foo')
rerender(id, () => 'bar')
expect(serializeInner(root)).toBe('bar')
})
// https://github.com/vitejs/vite-plugin-vue/issues/599
// Both Outer and Inner are reloaded when './server.js' changes
test('reload nested components from single update', async () => {
const innerId = 'nested-reload-inner'
const outerId = 'nested-reload-outer'
let Inner = {
__hmrId: innerId,
render() {
return h('div', 'foo')
},
}
let Outer = {
__hmrId: outerId,
render() {
return h(Inner)
},
}
createRecord(innerId, Inner)
createRecord(outerId, Outer)
const App = {
render: () => h(Outer),
}
const root = nodeOps.createElement('div')
render(h(App), root)
expect(serializeInner(root)).toBe('<div>foo</div>')
Inner = {
__hmrId: innerId,
render() {
return h('div', 'bar')
},
}
Outer = {
__hmrId: outerId,
render() {
return h(Inner)
},
}
// trigger reload for both Outer and Inner
reload(outerId, Outer)
reload(innerId, Inner)
await nextTick()
expect(serializeInner(root)).toBe('<div>bar</div>')
})
})

View File

@ -82,6 +82,11 @@ describe('SSR hydration', () => {
expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned()
})
test('text w/ newlines', async () => {
mountWithHydration('<div>1\n2\n3</div>', () => h('div', '1\r\n2\r3'))
expect(`Hydration text mismatch`).not.toHaveBeenWarned()
})
test('comment', () => {
const { vnode, container } = mountWithHydration('<!---->', () => null)
expect(vnode.el).toBe(container.firstChild)
@ -1160,6 +1165,69 @@ describe('SSR hydration', () => {
)
})
// #13510
test('update async component after parent mount before async component resolve', async () => {
const Comp = {
props: ['toggle'],
render(this: any) {
return h('h1', [
this.toggle ? 'Async component' : 'Updated async component',
])
},
}
let serverResolve: any
let AsyncComp = defineAsyncComponent(
() =>
new Promise(r => {
serverResolve = r
}),
)
const toggle = ref(true)
const App = {
setup() {
onMounted(() => {
// change state, after mount and before async component resolve
nextTick(() => (toggle.value = false))
})
return () => {
return h(AsyncComp, { toggle: toggle.value })
}
},
}
// server render
const htmlPromise = renderToString(h(App))
serverResolve(Comp)
const html = await htmlPromise
expect(html).toMatchInlineSnapshot(`"<h1>Async component</h1>"`)
// hydration
let clientResolve: any
AsyncComp = defineAsyncComponent(
() =>
new Promise(r => {
clientResolve = r
}),
)
const container = document.createElement('div')
container.innerHTML = html
createSSRApp(App).mount(container)
// resolve
clientResolve(Comp)
await new Promise(r => setTimeout(r))
// prevent lazy hydration since the component has been patched
expect('Skipping lazy hydration for component').toHaveBeenWarned()
expect(`Hydration node mismatch`).not.toHaveBeenWarned()
expect(container.innerHTML).toMatchInlineSnapshot(
`"<h1>Updated async component</h1>"`,
)
})
test('hydrate safely when property used by async setup changed before render', async () => {
const toggle = ref(true)
@ -1677,6 +1745,35 @@ describe('SSR hydration', () => {
expect(`mismatch`).not.toHaveBeenWarned()
})
// #13394
test('transition appear work with empty content', async () => {
const show = ref(true)
const { vnode, container } = mountWithHydration(
`<template><!----></template>`,
function (this: any) {
return h(
Transition,
{ appear: true },
{
default: () =>
show.value
? renderSlot(this.$slots, 'default')
: createTextVNode('foo'),
},
)
},
)
// empty slot render as a comment node
expect(container.firstChild!.nodeType).toBe(Node.COMMENT_NODE)
expect(vnode.el).toBe(container.firstChild)
expect(`mismatch`).not.toHaveBeenWarned()
show.value = false
await nextTick()
expect(container.innerHTML).toBe('foo')
})
test('transition appear with v-if', () => {
const show = false
const { vnode, container } = mountWithHydration(
@ -2265,6 +2362,30 @@ describe('SSR hydration', () => {
expect(`Hydration style mismatch`).not.toHaveBeenWarned()
})
test('with disabled teleport + undefined target', async () => {
const container = document.createElement('div')
const isOpen = ref(false)
const App = {
setup() {
return { isOpen }
},
template: `
<Teleport :to="undefined" :disabled="true">
<div v-if="isOpen">
Menu is open...
</div>
</Teleport>`,
}
container.innerHTML = await renderToString(h(App))
const app = createSSRApp(App)
app.mount(container)
isOpen.value = true
await nextTick()
expect(container.innerHTML).toBe(
`<!--teleport start--><div> Menu is open... </div><!--teleport end-->`,
)
})
test('escape css var name', () => {
const container = document.createElement('div')
container.innerHTML = `<div style="padding: 4px;--foo\\.bar:red;"></div>`

View File

@ -10,6 +10,7 @@ import {
render,
serializeInner,
shallowRef,
watch,
} from '@vue/runtime-test'
describe('api: template refs', () => {
@ -179,6 +180,89 @@ describe('api: template refs', () => {
expect(el.value).toBe(null)
})
// #12639
it('update and unmount child in the same tick', async () => {
const root = nodeOps.createElement('div')
const el = ref(null)
const toggle = ref(true)
const show = ref(true)
const Comp = defineComponent({
emits: ['change'],
props: ['show'],
setup(props, { emit }) {
watch(
() => props.show,
() => {
emit('change')
},
)
return () => h('div', 'hi')
},
})
const App = {
setup() {
return {
refKey: el,
}
},
render() {
return toggle.value
? h(Comp, {
ref: 'refKey',
show: show.value,
onChange: () => (toggle.value = false),
})
: null
},
}
render(h(App), root)
expect(el.value).not.toBe(null)
show.value = false
await nextTick()
expect(el.value).toBe(null)
})
it('set and change ref in the same tick', async () => {
const root = nodeOps.createElement('div')
const show = ref(false)
const refName = ref('a')
const Child = defineComponent({
setup() {
refName.value = 'b'
return () => {}
},
})
const Comp = {
render() {
return h(Child, {
ref: refName.value,
})
},
updated(this: any) {
expect(this.$refs.a).toBe(null)
expect(this.$refs.b).not.toBe(null)
},
}
const App = {
render() {
return show.value ? h(Comp) : null
},
}
render(h(App), root)
expect(refName.value).toBe('a')
show.value = true
await nextTick()
expect(refName.value).toBe('b')
})
it('unset old ref when new ref is absent', async () => {
const root1 = nodeOps.createElement('div')
const root2 = nodeOps.createElement('div')

View File

@ -553,18 +553,6 @@ describe('vnode', () => {
expect(vnode.dynamicChildren).toStrictEqual([vnode1])
})
test('with suspense', () => {
const hoist = createVNode('div')
let vnode1
const vnode =
(openBlock(),
createBlock('div', null, [
hoist,
(vnode1 = createVNode(() => {}, null, 'text')),
]))
expect(vnode.dynamicChildren).toStrictEqual([vnode1])
})
// #1039
// <component :is="foo">{{ bar }}</component>
// - content is compiled as slot

View File

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

View File

@ -43,7 +43,7 @@ export interface AsyncComponentOptions<T = any> {
export const isAsyncWrapper = (i: ComponentInternalInstance | VNode): boolean =>
!!(i.type as ComponentOptions).__asyncLoader
/*! #__NO_SIDE_EFFECTS__ */
/*@__NO_SIDE_EFFECTS__*/
export function defineAsyncComponent<
T extends Component = { new (): ComponentPublicInstance },
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
@ -123,28 +123,30 @@ export function defineAsyncComponent<
__asyncHydrate(el, instance, hydrate) {
let patched = false
;(instance.bu || (instance.bu = [])).push(() => (patched = true))
const performHydrate = () => {
// skip hydration if the component has been patched
if (patched) {
if (__DEV__) {
warn(
`Skipping lazy hydration for component '${getComponentName(resolvedComp!) || resolvedComp!.__file}': ` +
`it was updated before lazy hydration performed.`,
)
}
return
}
hydrate()
}
const doHydrate = hydrateStrategy
? () => {
const performHydrate = () => {
// skip hydration if the component has been patched
if (__DEV__ && patched) {
warn(
`Skipping lazy hydration for component '${getComponentName(resolvedComp!)}': ` +
`it was updated before lazy hydration performed.`,
)
return
}
hydrate()
}
const teardown = hydrateStrategy(performHydrate, cb =>
forEachElement(el, cb),
)
if (teardown) {
;(instance.bum || (instance.bum = [])).push(teardown)
}
;(instance.u || (instance.u = [])).push(() => (patched = true))
}
: hydrate
: performHydrate
if (resolvedComp) {
doHydrate()
} else {
@ -239,7 +241,10 @@ export function defineAsyncComponent<
error: error.value,
})
} else if (loadingComponent && !delayed.value) {
return createVNode(loadingComponent)
return createInnerComp(
loadingComponent as ConcreteComponent,
instance,
)
}
}
},

View File

@ -50,7 +50,7 @@ export interface App<HostElement = any> {
HostElement = any,
Value = any,
Modifiers extends string = string,
Arg extends string = string,
Arg = any,
>(
name: string,
): Directive<HostElement, Value, Modifiers, Arg> | undefined
@ -58,7 +58,7 @@ export interface App<HostElement = any> {
HostElement = any,
Value = any,
Modifiers extends string = string,
Arg extends string = string,
Arg = any,
>(
name: string,
directive: Directive<HostElement, Value, Modifiers, Arg>,

View File

@ -301,7 +301,7 @@ export function defineComponent<
>
// implementation, close to no-op
/*! #__NO_SIDE_EFFECTS__ */
/*@__NO_SIDE_EFFECTS__*/
export function defineComponent(
options: unknown,
extraOptions?: ComponentOptions,

View File

@ -1,6 +1,5 @@
import { isFunction } from '@vue/shared'
import { currentInstance } from './component'
import { currentRenderingInstance } from './componentRenderContext'
import { currentInstance, getCurrentInstance } from './component'
import { currentApp } from './apiCreateApp'
import { warn } from './warning'
@ -51,7 +50,7 @@ export function inject(
) {
// fallback to `currentRenderingInstance` so that this can be called in
// a functional component
const instance = currentInstance || currentRenderingInstance
const instance = getCurrentInstance()
// also support looking up from app-level provides w/ `app.runWithContext()`
if (instance || currentApp) {
@ -90,5 +89,5 @@ export function inject(
* user. One example is `useRoute()` in `vue-router`.
*/
export function hasInjectionContext(): boolean {
return !!(currentInstance || currentRenderingInstance || currentApp)
return !!(getCurrentInstance() || currentApp)
}

View File

@ -319,7 +319,14 @@ type InferDefaults<T> = {
[K in keyof T]?: InferDefault<T, T[K]>
}
type NativeType = null | number | string | boolean | symbol | Function
type NativeType =
| null
| undefined
| number
| string
| boolean
| symbol
| Function
type InferDefault<P, T> =
| ((props: P) => T & {})
@ -382,17 +389,17 @@ export function withDefaults<
}
export function useSlots(): SetupContext['slots'] {
return getContext().slots
return getContext('useSlots').slots
}
export function useAttrs(): SetupContext['attrs'] {
return getContext().attrs
return getContext('useAttrs').attrs
}
function getContext(): SetupContext {
function getContext(calledFunctionName: string): SetupContext {
const i = getCurrentInstance()!
if (__DEV__ && !i) {
warn(`useContext() called without active instance.`)
warn(`${calledFunctionName}() called without active instance.`)
}
return i.setupContext || (i.setupContext = createSetupContext(i))
}

View File

@ -21,6 +21,7 @@ import { queuePostRenderEffect } from './renderer'
import { warn } from './warning'
import type { ObjectWatchOptionItem } from './componentOptions'
import { useSSRContext } from './helpers/useSsrContext'
import type { ComponentPublicInstance } from './componentPublicInstance'
export type {
WatchHandle,
@ -66,7 +67,9 @@ export function watchPostEffect(
return doWatch(
effect,
null,
__DEV__ ? extend({}, options as any, { flush: 'post' }) : { flush: 'post' },
__DEV__
? extend({}, options as WatchEffectOptions, { flush: 'post' })
: { flush: 'post' },
)
}
@ -77,7 +80,9 @@ export function watchSyncEffect(
return doWatch(
effect,
null,
__DEV__ ? extend({}, options as any, { flush: 'sync' }) : { flush: 'sync' },
__DEV__
? extend({}, options as WatchEffectOptions, { flush: 'sync' })
: { flush: 'sync' },
)
}
@ -243,11 +248,11 @@ export function instanceWatch(
value: WatchCallback | ObjectWatchOptionItem,
options?: WatchOptions,
): WatchHandle {
const publicThis = this.proxy as any
const publicThis = this.proxy
const getter = isString(source)
? source.includes('.')
? createPathGetter(publicThis, source)
: () => publicThis[source]
? createPathGetter(publicThis!, source)
: () => publicThis![source as keyof typeof publicThis]
: source.bind(publicThis, publicThis)
let cb
if (isFunction(value)) {
@ -262,12 +267,15 @@ export function instanceWatch(
return res
}
export function createPathGetter(ctx: any, path: string) {
export function createPathGetter(
ctx: ComponentPublicInstance,
path: string,
): () => WatchSource | WatchSource[] | WatchEffect | object {
const segments = path.split('.')
return (): any => {
return (): WatchSource | WatchSource[] | WatchEffect | object => {
let cur = ctx
for (let i = 0; i < segments.length && cur; i++) {
cur = cur[segments[i]]
cur = cur[segments[i] as keyof typeof cur]
}
return cur
}

View File

@ -536,7 +536,7 @@ function installCompatMount(
if (__DEV__) {
for (let i = 0; i < container.attributes.length; i++) {
const attr = container.attributes[i]
if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
if (attr.name !== 'v-cloak' && /^(?:v-|:|@)/.test(attr.name)) {
warnDeprecation(DeprecationTypes.GLOBAL_MOUNT_CONTAINER, null)
break
}

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