fix(compiler-core/compat): fix is prop usage on components

also fix v-bind:is usage on plain element in compat mode

fix #3934
This commit is contained in:
Evan You 2021-06-21 16:16:49 -04:00
parent 4de5d24aa7
commit 08e93220f1
4 changed files with 144 additions and 57 deletions

View File

@ -57,8 +57,9 @@ function parseWithElementTransform(
} }
} }
function parseWithBind(template: string) { function parseWithBind(template: string, options?: CompilerOptions) {
return parseWithElementTransform(template, { return parseWithElementTransform(template, {
...options,
directiveTransforms: { directiveTransforms: {
bind: transformBind bind: transformBind
} }
@ -914,6 +915,18 @@ describe('compiler: element transform', () => {
directives: undefined directives: undefined
}) })
}) })
// #3934
test('normal component with is prop', () => {
const { node, root } = parseWithBind(`<custom-input is="foo" />`, {
isNativeTag: () => false
})
expect(root.helpers).toContain(RESOLVE_COMPONENT)
expect(root.helpers).not.toContain(RESOLVE_DYNAMIC_COMPONENT)
expect(node).toMatchObject({
tag: '_component_custom_input'
})
})
}) })
test('<svg> should be forced into blocks', () => { test('<svg> should be forced into blocks', () => {

View File

@ -10,7 +10,8 @@ import {
assert, assert,
advancePositionWithMutation, advancePositionWithMutation,
advancePositionWithClone, advancePositionWithClone,
isCoreComponent isCoreComponent,
isBindKey
} from './utils' } from './utils'
import { import {
Namespaces, Namespaces,
@ -596,46 +597,11 @@ function parseTag(
} }
let tagType = ElementTypes.ELEMENT let tagType = ElementTypes.ELEMENT
const options = context.options if (!context.inVPre) {
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 (
__COMPAT__ &&
checkCompatEnabled(
CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
context,
p.loc
)
) {
return true
}
})
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'
) {
tagType = ElementTypes.COMPONENT
}
if (tag === 'slot') { if (tag === 'slot') {
tagType = ElementTypes.SLOT tagType = ElementTypes.SLOT
} else if ( } else if (tag === 'template') {
tag === 'template' && if (
props.some( props.some(
p => p =>
p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name) p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
@ -643,6 +609,9 @@ function parseTag(
) { ) {
tagType = ElementTypes.TEMPLATE tagType = ElementTypes.TEMPLATE
} }
} else if (isComponent(tag, props, context)) {
tagType = ElementTypes.COMPONENT
}
} }
return { 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( function parseAttributes(
context: ParserContext, context: ParserContext,
type: TagType type: TagType

View File

@ -240,16 +240,16 @@ export function resolveComponentType(
// 1. dynamic component // 1. dynamic component
const isExplicitDynamic = isComponentTag(tag) const isExplicitDynamic = isComponentTag(tag)
const isProp = const isProp = findProp(node, 'is')
findProp(node, 'is') || (!isExplicitDynamic && findDir(node, 'is'))
if (isProp) { if (isProp) {
if (!isExplicitDynamic && isProp.type === NodeTypes.ATTRIBUTE) { if (
// <button is="vue:xxx"> isExplicitDynamic ||
// if not <component>, only is value that starts with "vue:" will be (__COMPAT__ &&
// treated as component by the parse phase and reach here, unless it's isCompatEnabled(
// compat mode where all is values are considered components CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
tag = isProp.value!.content.replace(/^vue:/, '') context
} else { ))
) {
const exp = const exp =
isProp.type === NodeTypes.ATTRIBUTE isProp.type === NodeTypes.ATTRIBUTE
? isProp.value && createSimpleExpression(isProp.value.content, true) ? isProp.value && createSimpleExpression(isProp.value.content, true)
@ -259,9 +259,26 @@ export function resolveComponentType(
exp exp
]) ])
} }
} else if (
isProp.type === NodeTypes.ATTRIBUTE &&
isProp.value!.content.startsWith('vue:')
) {
// <button is="vue:xxx">
// if not <component>, only is value that starts with "vue:" will be
// treated as component by the parse phase and reach here, unless it's
// compat mode where all is values are considered components
tag = isProp.value!.content.slice(4)
} }
} }
// 1.5 v-is (TODO: Deprecate)
const isDir = !isExplicitDynamic && findDir(node, 'is')
if (isDir && isDir.exp) {
return createCallExpression(context.helper(RESOLVE_DYNAMIC_COMPONENT), [
isDir.exp
])
}
// 2. built-in components (Teleport, Transition, KeepAlive, Suspense...) // 2. built-in components (Teleport, Transition, KeepAlive, Suspense...)
const builtIn = isCoreComponent(tag) || context.isBuiltInComponent(tag) const builtIn = isCoreComponent(tag) || context.isBuiltInComponent(tag)
if (builtIn) { if (builtIn) {
@ -433,7 +450,13 @@ export function buildProps(
// skip is on <component>, or is="vue:xxx" // skip is on <component>, or is="vue:xxx"
if ( if (
name === 'is' && name === 'is' &&
(isComponentTag(tag) || (value && value.content.startsWith('vue:'))) (isComponentTag(tag) ||
(value && value.content.startsWith('vue:')) ||
(__COMPAT__ &&
isCompatEnabled(
CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
context
)))
) { ) {
continue continue
} }
@ -473,7 +496,14 @@ export function buildProps(
// skip v-is and :is on <component> // skip v-is and :is on <component>
if ( if (
name === 'is' || name === 'is' ||
(isVBind && isComponentTag(tag) && isBindKey(arg, 'is')) (isVBind &&
isBindKey(arg, 'is') &&
(isComponentTag(tag) ||
(__COMPAT__ &&
isCompatEnabled(
CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
context
))))
) { ) {
continue continue
} }

View File

@ -35,6 +35,22 @@ test('COMPILER_IS_ON_ELEMENT', () => {
expect(CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT).toHaveBeenWarned() expect(CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT).toHaveBeenWarned()
}) })
test('COMPILER_IS_ON_ELEMENT (dynamic)', () => {
const MyButton = {
template: `<div><slot/></div>`
}
const vm = new Vue({
template: `<button :is="'MyButton'">text</button>`,
components: {
MyButton
}
}).$mount()
expect(vm.$el.outerHTML).toBe(`<div>text</div>`)
expect(CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT).toHaveBeenWarned()
})
test('COMPILER_V_BIND_SYNC', async () => { test('COMPILER_V_BIND_SYNC', async () => {
const MyButton = { const MyButton = {
props: ['foo'], props: ['foo'],