import { extend, hyphenate, isArray, isObject, isString, makeMap, normalizeClass, normalizeStyle, ShapeFlags, toHandlerKey } from '@vue/shared' import { Component, ComponentInternalInstance, ComponentOptions, Data, InternalRenderFunction } from '../component' import { currentRenderingInstance } from '../componentRenderContext' import { DirectiveArguments, withDirectives } from '../directives' import { resolveDirective, resolveDynamicComponent } from '../helpers/resolveAssets' import { Comment, createVNode, isVNode, normalizeChildren, VNode, VNodeArrayChildren, VNodeProps } from '../vnode' import { checkCompatEnabled, DeprecationTypes, isCompatEnabled } from './compatConfig' import { compatModelEventPrefix } from './componentVModel' const v3CompiledRenderFnRE = /^(?:function \w*)?\(_ctx, _cache/ export function convertLegacyRenderFn(instance: ComponentInternalInstance) { const Component = instance.type as ComponentOptions const render = Component.render as InternalRenderFunction | undefined // v3 runtime compiled, or already checked / wrapped if (!render || render._rc || render._compatChecked || render._compatWrapped) { return } if (v3CompiledRenderFnRE.test(render.toString())) { // v3 pre-compiled function render._compatChecked = true return } // v2 render function, try to provide compat if (checkCompatEnabled(DeprecationTypes.RENDER_FUNCTION, instance)) { const wrapped = (Component.render = function compatRender() { // @ts-ignore return render.call(this, compatH) }) // @ts-ignore wrapped._compatWrapped = true } } interface LegacyVNodeProps { key?: string | number ref?: string refInFor?: boolean staticClass?: string class?: unknown staticStyle?: Record style?: Record attrs?: Record domProps?: Record on?: Record nativeOn?: Record directives?: LegacyVNodeDirective[] // component only props?: Record slot?: string scopedSlots?: Record model?: { value: any callback: (v: any) => void expression: string } } interface LegacyVNodeDirective { name: string value: unknown arg?: string modifiers?: Record } type LegacyVNodeChildren = | string | number | boolean | VNode | VNodeArrayChildren export function compatH( type: string | Component, children?: LegacyVNodeChildren ): VNode export function compatH( type: string | Component, props?: Data & LegacyVNodeProps, children?: LegacyVNodeChildren ): VNode export function compatH( type: any, propsOrChildren?: any, children?: any ): VNode { if (!type) { type = Comment } // to support v2 string component name look!up if (typeof type === 'string') { const t = hyphenate(type) if (t === 'transition' || t === 'transition-group' || t === 'keep-alive') { // since transition and transition-group are runtime-dom-specific, // we cannot import them directly here. Instead they are registered using // special keys in @vue/compat entry. type = `__compat__${t}` } type = resolveDynamicComponent(type) } const l = arguments.length const is2ndArgArrayChildren = isArray(propsOrChildren) if (l === 2 || is2ndArgArrayChildren) { if (isObject(propsOrChildren) && !is2ndArgArrayChildren) { // single vnode without props if (isVNode(propsOrChildren)) { return convertLegacySlots(createVNode(type, null, [propsOrChildren])) } // props without children return convertLegacySlots( convertLegacyDirectives( createVNode(type, convertLegacyProps(propsOrChildren, type)), propsOrChildren ) ) } else { // omit props return convertLegacySlots(createVNode(type, null, propsOrChildren)) } } else { if (isVNode(children)) { children = [children] } return convertLegacySlots( convertLegacyDirectives( createVNode(type, convertLegacyProps(propsOrChildren, type), children), propsOrChildren ) ) } } const skipLegacyRootLevelProps = /*#__PURE__*/ makeMap( 'staticStyle,staticClass,directives,model,hook' ) function convertLegacyProps( legacyProps: LegacyVNodeProps | undefined, type: any ): Data & VNodeProps | null { if (!legacyProps) { return null } const converted: Data & VNodeProps = {} for (const key in legacyProps) { if (key === 'attrs' || key === 'domProps' || key === 'props') { extend(converted, legacyProps[key]) } else if (key === 'on' || key === 'nativeOn') { const listeners = legacyProps[key] for (const event in listeners) { let handlerKey = convertLegacyEventKey(event) if (key === 'nativeOn') handlerKey += `Native` const existing = converted[handlerKey] const incoming = listeners[event] if (existing !== incoming) { if (existing) { converted[handlerKey] = [].concat(existing as any, incoming as any) } else { converted[handlerKey] = incoming } } } } else if (!skipLegacyRootLevelProps(key)) { converted[key] = legacyProps[key as keyof LegacyVNodeProps] } } if (legacyProps.staticClass) { converted.class = normalizeClass([legacyProps.staticClass, converted.class]) } if (legacyProps.staticStyle) { converted.style = normalizeStyle([legacyProps.staticStyle, converted.style]) } if (legacyProps.model && isObject(type)) { // v2 compiled component v-model const { prop = 'value', event = 'input' } = (type as any).model || {} converted[prop] = legacyProps.model.value converted[compatModelEventPrefix + event] = legacyProps.model.callback } 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 ): VNode { if (props && props.directives) { return withDirectives( vnode, props.directives.map(({ name, value, arg, modifiers }) => { return [ resolveDirective(name)!, value, arg, modifiers ] as DirectiveArguments[number] }) ) } return vnode } function convertLegacySlots(vnode: VNode): VNode { const { props, children } = vnode let slots: Record | undefined if (vnode.shapeFlag & ShapeFlags.COMPONENT && isArray(children)) { slots = {} // check "slot" property on vnodes and turn them into v3 function slots for (let i = 0; i < children.length; i++) { const child = children[i] const slotName = (isVNode(child) && child.props && child.props.slot) || 'default' const slot = slots[slotName] || (slots[slotName] = [] as any[]) if (isVNode(child) && child.type === 'template') { slot.push(child.children) } else { slot.push(child) } } if (slots) { for (const key in slots) { const slotChildren = slots[key] slots[key] = () => slotChildren slots[key]._nonScoped = true } } } const scopedSlots = props && props.scopedSlots if (scopedSlots) { delete props!.scopedSlots if (slots) { extend(slots, scopedSlots) } else { slots = scopedSlots } } if (slots) { normalizeChildren(vnode, slots) } return vnode } export function defineLegacyVNodeProperties(vnode: VNode) { /* istanbul ignore if */ if ( isCompatEnabled( DeprecationTypes.RENDER_FUNCTION, currentRenderingInstance, true /* enable for built-ins */ ) && isCompatEnabled( DeprecationTypes.PRIVATE_APIS, currentRenderingInstance, true /* enable for built-ins */ ) ) { const context = currentRenderingInstance const getInstance = () => vnode.component && vnode.component.proxy let componentOptions: any Object.defineProperties(vnode, { tag: { get: () => vnode.type }, data: { get: () => vnode.props || {}, set: p => (vnode.props = p) }, elm: { get: () => vnode.el }, componentInstance: { get: getInstance }, child: { get: getInstance }, text: { get: () => (isString(vnode.children) ? vnode.children : null) }, context: { get: () => context && context.proxy }, componentOptions: { get: () => { if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { if (componentOptions) { return componentOptions } return (componentOptions = { Ctor: vnode.type, propsData: vnode.props, children: vnode.children }) } } } }) } }