fix(compat): do not auto invoke scoped slots when accessed via `$slots`
The problem
If the component has a render function and uses this.$slots.name expression somewhere, this usage might throw if name is used as a scoped slot and the data the slot passes is destructured.
Why this happens?
If RENDER_FUNCTION compat feature is enabled and the component is using a render function (_compatWrapped), all $slots.name accesses will be intercepted by a proxy which will auto-invoke the corresponding function with no arguments:
https://github.com/vuejs/core/blob/c0c9432b64091fa15fd8619cfb06828735356a42/packages/runtime-core/src/compat/instance.ts#L96-L105
If these arguments are destructured by a component user, this weird runtime error will occur: (destructured parameter) is undefined in Firefox or Cannot destructure property '<property name>' of 'undefined' as it is undefined in Chromium.
The fix
This PR attempts to resolve the issue by avoiding invoking the slot if it has _ns (non-scoped slot) flag.
Real-world examples
-
I'm trying to migrate to Vue 3 a legacy project with
[email protected]using@vue/compat. This line, for example, is only intending to check the slot presence, but ends up invoking it with no arguments andundefinedis ended up being destructured in user code. -
The other one: https://github.com/SortableJS/vue.draggable.next/issues/122
I think this fix should not break the existing code which is written and used correctly because
- If a slot was never intended to be used as a scoped slot but is used as such, that is a component user's mistake - and runtime errors in this case are a possibility (for example caused by non auto-invoking
$slots.name) - If a slot can be used as a scoped and a non-scoped one, it is lib author's responsibility to use
$slotsor$scopedSlotsrespectively (whichvuetifyactually does).
Size Report
Bundles
| File | Size | Gzip | Brotli |
|---|---|---|---|
| runtime-dom.global.prod.js | 101 kB | 38.1 kB | 34.2 kB |
| vue.global.prod.js | 160 kB | 58 kB | 51.5 kB |
Usages
| Name | Size | Gzip | Brotli |
|---|---|---|---|
| createApp (CAPI only) | 49.1 kB | 18.9 kB | 17.3 kB |
| createApp | 55.7 kB | 21.4 kB | 19.6 kB |
| createSSRApp | 59.7 kB | 23.1 kB | 21 kB |
| defineCustomElement | 60.4 kB | 23 kB | 20.9 kB |
| overall | 69.4 kB | 26.5 kB | 24.1 kB |
@vue/compiler-dom
pnpm add https://pkg.pr.new/@vue/compiler-dom@10875
@vue/compiler-core
pnpm add https://pkg.pr.new/@vue/compiler-core@10875
@vue/compiler-sfc
pnpm add https://pkg.pr.new/@vue/compiler-sfc@10875
@vue/compiler-ssr
pnpm add https://pkg.pr.new/@vue/compiler-ssr@10875
@vue/reactivity
pnpm add https://pkg.pr.new/@vue/reactivity@10875
@vue/runtime-core
pnpm add https://pkg.pr.new/@vue/runtime-core@10875
@vue/runtime-dom
pnpm add https://pkg.pr.new/@vue/runtime-dom@10875
@vue/server-renderer
pnpm add https://pkg.pr.new/@vue/server-renderer@10875
@vue/shared
pnpm add https://pkg.pr.new/@vue/shared@10875
vue
pnpm add https://pkg.pr.new/vue@10875
@vue/compat
pnpm add https://pkg.pr.new/@vue/compat@10875
commit: 0587057
Why not use 'slotName' in this.$slots as the condition?