diff --git a/packages/runtime-core/src/compat/compatConfig.ts b/packages/runtime-core/src/compat/compatConfig.ts index 5ebe0770..25a387f1 100644 --- a/packages/runtime-core/src/compat/compatConfig.ts +++ b/packages/runtime-core/src/compat/compatConfig.ts @@ -203,9 +203,10 @@ const deprecationData: Record = { [DeprecationTypes.INSTANCE_LISTENERS]: { message: - `vm.$listeners has been removed. Parent v-on listeners are now ` + + `vm.$listeners has been removed. In Vue 3, parent v-on listeners are ` + `included in vm.$attrs and it is no longer necessary to separately use ` + - `v-on="$listeners" if you are already using v-bind="$attrs".`, + `v-on="$listeners" if you are already using v-bind="$attrs". ` + + `(Note: the Vue 3 behavior only applies if this compat config is disabled)`, link: `https://v3.vuejs.org/guide/migration/listeners-removed.html` }, diff --git a/packages/runtime-core/src/compat/instance.ts b/packages/runtime-core/src/compat/instance.ts index 60144fa6..b324d7f7 100644 --- a/packages/runtime-core/src/compat/instance.ts +++ b/packages/runtime-core/src/compat/instance.ts @@ -1,4 +1,4 @@ -import { extend, NOOP } from '@vue/shared' +import { extend, NOOP, toDisplayString, toNumber } from '@vue/shared' import { PublicPropertiesMap } from '../componentPublicInstance' import { getCompatChildren } from './instanceChildren' import { @@ -11,6 +11,14 @@ import { off, on, once } from './instanceEventEmitter' import { getCompatListeners } from './instanceListeners' import { shallowReadonly } from '@vue/reactivity' import { legacySlotProxyHandlers } from './component' +import { compatH } from './renderFn' +import { + legacyBindObjectProps, + legacyRenderSlot, + legacyRenderStatic +} from './renderHelpers' +import { createCommentVNode, createTextVNode } from '../vnode' +import { renderList } from '../helpers/renderList' export function installCompatInstanceProperties(map: PublicPropertiesMap) { const set = (target: any, key: any, val: any) => { @@ -77,6 +85,19 @@ export function installCompatInstanceProperties(map: PublicPropertiesMap) { $off: i => off.bind(null, i), $children: getCompatChildren, - $listeners: getCompatListeners + $listeners: getCompatListeners, + + // v2 render helpers + $createElement: () => compatH, + _self: i => i.proxy, + _c: () => compatH, + _n: () => toNumber, + _s: () => toDisplayString, + _l: () => renderList, + _t: i => legacyRenderSlot.bind(null, i), + _b: () => legacyBindObjectProps, + _e: () => createCommentVNode, + _v: () => createTextVNode, + _m: i => legacyRenderStatic.bind(null, i) } as PublicPropertiesMap) } diff --git a/packages/runtime-core/src/compat/renderFn.ts b/packages/runtime-core/src/compat/renderFn.ts index 458d9e4e..29180f63 100644 --- a/packages/runtime-core/src/compat/renderFn.ts +++ b/packages/runtime-core/src/compat/renderFn.ts @@ -29,6 +29,8 @@ import { } from '../vnode' import { checkCompatEnabled, DeprecationTypes } from './compatConfig' +const v3CompiledRenderFnRE = /^(?:function \w+)?\(_ctx, _cache/ + export function convertLegacyRenderFn(instance: ComponentInternalInstance) { const Component = instance.type as ComponentOptions const render = Component.render as InternalRenderFunction | undefined @@ -38,8 +40,7 @@ export function convertLegacyRenderFn(instance: ComponentInternalInstance) { return } - const string = render.toString() - if (string.startsWith('function render(_ctx') || string.startsWith('(_ctx')) { + if (v3CompiledRenderFnRE.test(render.toString())) { // v3 pre-compiled function render._compatChecked = true return @@ -128,9 +129,7 @@ export function compatH( return convertLegacySlots(createVNode(type, null, propsOrChildren)) } } else { - if (l > 3) { - children = Array.prototype.slice.call(arguments, 2) - } else if (l === 3 && isVNode(children)) { + if (isVNode(children)) { children = [children] } return convertLegacySlots( @@ -157,13 +156,20 @@ function convertLegacyProps( } else if (key === 'on' || key === 'nativeOn') { const listeners = legacyProps[key] for (const event in listeners) { - const handlerKey = toHandlerKey(event) + const handlerKey = convertLegacyEventKey(event) const existing = converted[handlerKey] const incoming = listeners[event] if (existing !== incoming) { - converted[handlerKey] = existing - ? [].concat(existing as any, incoming as any) - : incoming + if (existing) { + // for the rare case where the same handler is attached + // twice with/without .native modifier... + if (key === 'nativeOn' && String(existing) === String(incoming)) { + continue + } + converted[handlerKey] = [].concat(existing as any, incoming as any) + } else { + converted[handlerKey] = incoming + } } } } else if ( @@ -185,6 +191,20 @@ function convertLegacyProps( return converted } +function convertLegacyEventKey(event: string): string { + // normalize v2 event prefixes + if (event[0] === '&') { + event = event.slice(1) + 'Passive' + } + if (event[0] === '~') { + event = event.slice(1) + 'Once' + } + if (event[0] === '!') { + event = event.slice(1) + 'Capture' + } + return toHandlerKey(event) +} + function convertLegacyDirectives( vnode: VNode, props?: LegacyVNodeProps diff --git a/packages/runtime-core/src/compat/renderHelpers.ts b/packages/runtime-core/src/compat/renderHelpers.ts new file mode 100644 index 00000000..0f38a245 --- /dev/null +++ b/packages/runtime-core/src/compat/renderHelpers.ts @@ -0,0 +1,94 @@ +import { + camelize, + extend, + hyphenate, + isArray, + isObject, + isReservedProp, + normalizeClass +} from '@vue/shared' +import { ComponentInternalInstance } from '../component' +import { renderSlot } from '../helpers/renderSlot' +import { mergeProps, VNode } from '../vnode' + +export function legacyBindObjectProps( + data: any, + _tag: string, + value: any, + _asProp: boolean, + isSync?: boolean +) { + if (value && isObject(value)) { + if (isArray(value)) { + value = toObject(value) + } + for (const key in value) { + if (isReservedProp(key)) { + data[key] = value[key] + } else if (key === 'class') { + data.class = normalizeClass([data.class, value.class]) + } else if (key === 'style') { + data.style = normalizeClass([data.style, value.style]) + } else { + const attrs = data.attrs || (data.attrs = {}) + const camelizedKey = camelize(key) + const hyphenatedKey = hyphenate(key) + if (!(camelizedKey in attrs) && !(hyphenatedKey in attrs)) { + attrs[key] = value[key] + + if (isSync) { + const on = data.on || (data.on = {}) + on[`update:${key}`] = function($event: any) { + value[key] = $event + } + } + } + } + } + } + return data +} + +export function legacyRenderSlot( + instance: ComponentInternalInstance, + name: string, + fallback?: VNode[], + props?: any, + bindObject?: any +) { + if (bindObject) { + props = mergeProps(props, bindObject) + } + return renderSlot(instance.slots, name, props, fallback && (() => fallback)) +} + +const staticCacheMap = /*#__PURE__*/ new WeakMap< + ComponentInternalInstance, + any[] +>() + +export function legacyRenderStatic( + instance: ComponentInternalInstance, + index: number +) { + let cache = staticCacheMap.get(instance) + if (!cache) { + staticCacheMap.set(instance, (cache = [])) + } + if (cache[index]) { + return cache[index] + } + const fn = (instance.type as any).staticRenderFns[index] + const ctx = instance.proxy + return (cache[index] = fn.call(ctx, null, ctx)) +} + +function toObject(arr: Array): Object { + const res = {} + for (let i = 0; i < arr.length; i++) { + if (arr[i]) { + extend(res, arr[i]) + } + } + return res +} diff --git a/packages/runtime-core/src/compat/vModel.ts b/packages/runtime-core/src/compat/vModel.ts index c359d17c..d24f06b6 100644 --- a/packages/runtime-core/src/compat/vModel.ts +++ b/packages/runtime-core/src/compat/vModel.ts @@ -9,11 +9,6 @@ import { isCompatEnabled } from './compatConfig' -const defaultModelMapping = { - prop: 'value', - event: 'input' -} - export const compatModelEventPrefix = `onModelCompat:` const warnedTypes = new WeakSet() @@ -40,7 +35,7 @@ export function convertLegacyVModelProps(vnode: VNode) { warnedTypes.add(type as ComponentOptions) } - const { prop, event } = (type as any).model || defaultModelMapping + const { prop = 'value', event = 'input' } = (type as any).model || {} props[prop] = props.modelValue delete props.modelValue // important: update dynamic props diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts index 40765ba9..ef08c3ad 100644 --- a/packages/runtime-core/src/componentProps.ts +++ b/packages/runtime-core/src/componentProps.ts @@ -20,7 +20,8 @@ import { isReservedProp, EMPTY_ARR, def, - extend + extend, + isOn } from '@vue/shared' import { warn } from './warning' import { @@ -224,6 +225,13 @@ export function updateProps( ) } } else { + if ( + __COMPAT__ && + isOn(key) && + isCompatEnabled(DeprecationTypes.INSTANCE_LISTENERS, instance) + ) { + continue + } attrs[key] = value } } @@ -320,6 +328,13 @@ function setFullProps( // Any non-declared (either as a prop or an emitted event) props are put // into a separate `attrs` object for spreading. Make sure to preserve // original key casing + if ( + __COMPAT__ && + isOn(key) && + isCompatEnabled(DeprecationTypes.INSTANCE_LISTENERS, instance) + ) { + continue + } attrs[key] = value } } diff --git a/packages/runtime-core/src/componentSlots.ts b/packages/runtime-core/src/componentSlots.ts index f1eacd87..ab9b1fbc 100644 --- a/packages/runtime-core/src/componentSlots.ts +++ b/packages/runtime-core/src/componentSlots.ts @@ -80,7 +80,7 @@ const normalizeObjectSlots = (rawSlots: RawSlots, slots: InternalSlots) => { if (isFunction(value)) { slots[key] = normalizeSlot(key, value, ctx) } else if (value != null) { - if (__DEV__) { + if (__DEV__ && !__COMPAT__) { warn( `Non-function value encountered for slot "${key}". ` + `Prefer function slots for better performance.` diff --git a/packages/vue-compat/src/esm-index.ts b/packages/vue-compat/src/esm-index.ts new file mode 100644 index 00000000..c3caa4a9 --- /dev/null +++ b/packages/vue-compat/src/esm-index.ts @@ -0,0 +1,3 @@ +import Vue from './index' +export default Vue +export * from '@vue/runtime-dom' diff --git a/packages/vue-compat/src/esm-runtime.ts b/packages/vue-compat/src/esm-runtime.ts new file mode 100644 index 00000000..aae95dbe --- /dev/null +++ b/packages/vue-compat/src/esm-runtime.ts @@ -0,0 +1,3 @@ +import Vue from './runtime' +export default Vue +export * from '@vue/runtime-dom' diff --git a/rollup.config.js b/rollup.config.js index 844d912e..5c5d3b90 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -116,7 +116,16 @@ function createConfig(format, output, plugins = []) { // during a single build. hasTSChecked = true - const entryFile = /runtime$/.test(format) ? `src/runtime.ts` : `src/index.ts` + let entryFile = /runtime$/.test(format) ? `src/runtime.ts` : `src/index.ts` + + // the compat build needs both default AND named exports. This will cause + // Rollup to complain for non-ESM targets, so we use separate entries for + // esm vs. non-esm builds. + if (isCompatBuild && (isBrowserESMBuild || isBundlerESMBuild)) { + entryFile = /runtime$/.test(format) + ? `src/esm-runtime.ts` + : `src/esm-index.ts` + } let external = []