diff --git a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts index 33fdb7dba19..a00cf0c5da9 100644 --- a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts +++ b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts @@ -57,8 +57,9 @@ function parseWithElementTransform( } } -function parseWithBind(template: string) { +function parseWithBind(template: string, options?: CompilerOptions) { return parseWithElementTransform(template, { + ...options, directiveTransforms: { bind: transformBind } @@ -914,6 +915,18 @@ describe('compiler: element transform', () => { directives: undefined }) }) + + // #3934 + test('normal component with is prop', () => { + const { node, root } = parseWithBind(``, { + isNativeTag: () => false + }) + expect(root.helpers).toContain(RESOLVE_COMPONENT) + expect(root.helpers).not.toContain(RESOLVE_DYNAMIC_COMPONENT) + expect(node).toMatchObject({ + tag: '_component_custom_input' + }) + }) }) test(' should be forced into blocks', () => { diff --git a/packages/compiler-core/src/parse.ts b/packages/compiler-core/src/parse.ts index 9580bdea232..ebae0ce214d 100644 --- a/packages/compiler-core/src/parse.ts +++ b/packages/compiler-core/src/parse.ts @@ -10,7 +10,8 @@ import { assert, advancePositionWithMutation, advancePositionWithClone, - isCoreComponent + isCoreComponent, + isBindKey } from './utils' import { Namespaces, @@ -596,53 +597,21 @@ function parseTag( } let tagType = ElementTypes.ELEMENT - const options = context.options - if (!context.inVPre && !options.isCustomElement(tag)) { - const hasVIs = props.some(p => { - if (p.name !== 'is') return - // v-is="xxx" (TODO: deprecate) - if (p.type === NodeTypes.DIRECTIVE) { - return true - } - // is="vue:xxx" - if (p.value && p.value.content.startsWith('vue:')) { - return true - } - // in compat mode, any is usage is considered a component + if (!context.inVPre) { + if (tag === 'slot') { + tagType = ElementTypes.SLOT + } else if (tag === 'template') { if ( - __COMPAT__ && - checkCompatEnabled( - CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT, - context, - p.loc + props.some( + p => + p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name) ) ) { - return true + tagType = ElementTypes.TEMPLATE } - }) - if (options.isNativeTag && !hasVIs) { - if (!options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT - } else if ( - hasVIs || - isCoreComponent(tag) || - (options.isBuiltInComponent && options.isBuiltInComponent(tag)) || - /^[A-Z]/.test(tag) || - tag === 'component' - ) { + } else if (isComponent(tag, props, context)) { tagType = ElementTypes.COMPONENT } - - if (tag === 'slot') { - tagType = ElementTypes.SLOT - } else if ( - tag === 'template' && - props.some( - p => - p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name) - ) - ) { - tagType = ElementTypes.TEMPLATE - } } return { @@ -658,6 +627,65 @@ function parseTag( } } +function isComponent( + tag: string, + props: (AttributeNode | DirectiveNode)[], + context: ParserContext +) { + const options = context.options + if (options.isCustomElement(tag)) { + return false + } + if ( + tag === 'component' || + /^[A-Z]/.test(tag) || + isCoreComponent(tag) || + (options.isBuiltInComponent && options.isBuiltInComponent(tag)) || + (options.isNativeTag && !options.isNativeTag(tag)) + ) { + return true + } + // at this point the tag should be a native tag, but check for potential "is" + // casting + for (let i = 0; i < props.length; i++) { + const p = props[i] + if (p.type === NodeTypes.ATTRIBUTE) { + if (p.name === 'is' && p.value) { + if (p.value.content.startsWith('vue:')) { + return true + } else if ( + __COMPAT__ && + checkCompatEnabled( + CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT, + context, + p.loc + ) + ) { + return true + } + } + } else { + // directive + // v-is (TODO Deprecate) + if (p.name === 'is') { + return true + } else if ( + // :is on plain element - only treat as component in compat mode + p.name === 'bind' && + isBindKey(p.arg, 'is') && + __COMPAT__ && + checkCompatEnabled( + CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT, + context, + p.loc + ) + ) { + return true + } + } + } +} + function parseAttributes( context: ParserContext, type: TagType diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts index dba1a99b487..36396c5d7d1 100644 --- a/packages/compiler-core/src/transforms/transformElement.ts +++ b/packages/compiler-core/src/transforms/transformElement.ts @@ -240,16 +240,16 @@ export function resolveComponentType( // 1. dynamic component const isExplicitDynamic = isComponentTag(tag) - const isProp = - findProp(node, 'is') || (!isExplicitDynamic && findDir(node, 'is')) + const isProp = findProp(node, 'is') if (isProp) { - if (!isExplicitDynamic && isProp.type === NodeTypes.ATTRIBUTE) { - //
` + } + + const vm = new Vue({ + template: ``, + components: { + MyButton + } + }).$mount() + + expect(vm.$el.outerHTML).toBe(`
text
`) + expect(CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT).toHaveBeenWarned() +}) + test('COMPILER_V_BIND_SYNC', async () => { const MyButton = { props: ['foo'],