Skip to content

Commit 08e9322

Browse files
committed
fix(compiler-core/compat): fix is prop usage on components
also fix v-bind:is usage on plain element in compat mode fix #3934
1 parent 4de5d24 commit 08e9322

File tree

4 files changed

+141
-54
lines changed

4 files changed

+141
-54
lines changed

packages/compiler-core/__tests__/transforms/transformElement.spec.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@ function parseWithElementTransform(
5757
}
5858
}
5959

60-
function parseWithBind(template: string) {
60+
function parseWithBind(template: string, options?: CompilerOptions) {
6161
return parseWithElementTransform(template, {
62+
...options,
6263
directiveTransforms: {
6364
bind: transformBind
6465
}
@@ -914,6 +915,18 @@ describe('compiler: element transform', () => {
914915
directives: undefined
915916
})
916917
})
918+
919+
// #3934
920+
test('normal component with is prop', () => {
921+
const { node, root } = parseWithBind(`<custom-input is="foo" />`, {
922+
isNativeTag: () => false
923+
})
924+
expect(root.helpers).toContain(RESOLVE_COMPONENT)
925+
expect(root.helpers).not.toContain(RESOLVE_DYNAMIC_COMPONENT)
926+
expect(node).toMatchObject({
927+
tag: '_component_custom_input'
928+
})
929+
})
917930
})
918931

919932
test('<svg> should be forced into blocks', () => {

packages/compiler-core/src/parse.ts

+70-42
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
assert,
1111
advancePositionWithMutation,
1212
advancePositionWithClone,
13-
isCoreComponent
13+
isCoreComponent,
14+
isBindKey
1415
} from './utils'
1516
import {
1617
Namespaces,
@@ -596,53 +597,21 @@ function parseTag(
596597
}
597598

598599
let tagType = ElementTypes.ELEMENT
599-
const options = context.options
600-
if (!context.inVPre && !options.isCustomElement(tag)) {
601-
const hasVIs = props.some(p => {
602-
if (p.name !== 'is') return
603-
// v-is="xxx" (TODO: deprecate)
604-
if (p.type === NodeTypes.DIRECTIVE) {
605-
return true
606-
}
607-
// is="vue:xxx"
608-
if (p.value && p.value.content.startsWith('vue:')) {
609-
return true
610-
}
611-
// in compat mode, any is usage is considered a component
600+
if (!context.inVPre) {
601+
if (tag === 'slot') {
602+
tagType = ElementTypes.SLOT
603+
} else if (tag === 'template') {
612604
if (
613-
__COMPAT__ &&
614-
checkCompatEnabled(
615-
CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
616-
context,
617-
p.loc
605+
props.some(
606+
p =>
607+
p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
618608
)
619609
) {
620-
return true
610+
tagType = ElementTypes.TEMPLATE
621611
}
622-
})
623-
if (options.isNativeTag && !hasVIs) {
624-
if (!options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT
625-
} else if (
626-
hasVIs ||
627-
isCoreComponent(tag) ||
628-
(options.isBuiltInComponent && options.isBuiltInComponent(tag)) ||
629-
/^[A-Z]/.test(tag) ||
630-
tag === 'component'
631-
) {
612+
} else if (isComponent(tag, props, context)) {
632613
tagType = ElementTypes.COMPONENT
633614
}
634-
635-
if (tag === 'slot') {
636-
tagType = ElementTypes.SLOT
637-
} else if (
638-
tag === 'template' &&
639-
props.some(
640-
p =>
641-
p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
642-
)
643-
) {
644-
tagType = ElementTypes.TEMPLATE
645-
}
646615
}
647616

648617
return {
@@ -658,6 +627,65 @@ function parseTag(
658627
}
659628
}
660629

630+
function isComponent(
631+
tag: string,
632+
props: (AttributeNode | DirectiveNode)[],
633+
context: ParserContext
634+
) {
635+
const options = context.options
636+
if (options.isCustomElement(tag)) {
637+
return false
638+
}
639+
if (
640+
tag === 'component' ||
641+
/^[A-Z]/.test(tag) ||
642+
isCoreComponent(tag) ||
643+
(options.isBuiltInComponent && options.isBuiltInComponent(tag)) ||
644+
(options.isNativeTag && !options.isNativeTag(tag))
645+
) {
646+
return true
647+
}
648+
// at this point the tag should be a native tag, but check for potential "is"
649+
// casting
650+
for (let i = 0; i < props.length; i++) {
651+
const p = props[i]
652+
if (p.type === NodeTypes.ATTRIBUTE) {
653+
if (p.name === 'is' && p.value) {
654+
if (p.value.content.startsWith('vue:')) {
655+
return true
656+
} else if (
657+
__COMPAT__ &&
658+
checkCompatEnabled(
659+
CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
660+
context,
661+
p.loc
662+
)
663+
) {
664+
return true
665+
}
666+
}
667+
} else {
668+
// directive
669+
// v-is (TODO Deprecate)
670+
if (p.name === 'is') {
671+
return true
672+
} else if (
673+
// :is on plain element - only treat as component in compat mode
674+
p.name === 'bind' &&
675+
isBindKey(p.arg, 'is') &&
676+
__COMPAT__ &&
677+
checkCompatEnabled(
678+
CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
679+
context,
680+
p.loc
681+
)
682+
) {
683+
return true
684+
}
685+
}
686+
}
687+
}
688+
661689
function parseAttributes(
662690
context: ParserContext,
663691
type: TagType

packages/compiler-core/src/transforms/transformElement.ts

+41-11
Original file line numberDiff line numberDiff line change
@@ -240,16 +240,16 @@ export function resolveComponentType(
240240

241241
// 1. dynamic component
242242
const isExplicitDynamic = isComponentTag(tag)
243-
const isProp =
244-
findProp(node, 'is') || (!isExplicitDynamic && findDir(node, 'is'))
243+
const isProp = findProp(node, 'is')
245244
if (isProp) {
246-
if (!isExplicitDynamic && isProp.type === NodeTypes.ATTRIBUTE) {
247-
// <button is="vue:xxx">
248-
// if not <component>, only is value that starts with "vue:" will be
249-
// treated as component by the parse phase and reach here, unless it's
250-
// compat mode where all is values are considered components
251-
tag = isProp.value!.content.replace(/^vue:/, '')
252-
} else {
245+
if (
246+
isExplicitDynamic ||
247+
(__COMPAT__ &&
248+
isCompatEnabled(
249+
CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
250+
context
251+
))
252+
) {
253253
const exp =
254254
isProp.type === NodeTypes.ATTRIBUTE
255255
? isProp.value && createSimpleExpression(isProp.value.content, true)
@@ -259,9 +259,26 @@ export function resolveComponentType(
259259
exp
260260
])
261261
}
262+
} else if (
263+
isProp.type === NodeTypes.ATTRIBUTE &&
264+
isProp.value!.content.startsWith('vue:')
265+
) {
266+
// <button is="vue:xxx">
267+
// if not <component>, only is value that starts with "vue:" will be
268+
// treated as component by the parse phase and reach here, unless it's
269+
// compat mode where all is values are considered components
270+
tag = isProp.value!.content.slice(4)
262271
}
263272
}
264273

274+
// 1.5 v-is (TODO: Deprecate)
275+
const isDir = !isExplicitDynamic && findDir(node, 'is')
276+
if (isDir && isDir.exp) {
277+
return createCallExpression(context.helper(RESOLVE_DYNAMIC_COMPONENT), [
278+
isDir.exp
279+
])
280+
}
281+
265282
// 2. built-in components (Teleport, Transition, KeepAlive, Suspense...)
266283
const builtIn = isCoreComponent(tag) || context.isBuiltInComponent(tag)
267284
if (builtIn) {
@@ -433,7 +450,13 @@ export function buildProps(
433450
// skip is on <component>, or is="vue:xxx"
434451
if (
435452
name === 'is' &&
436-
(isComponentTag(tag) || (value && value.content.startsWith('vue:')))
453+
(isComponentTag(tag) ||
454+
(value && value.content.startsWith('vue:')) ||
455+
(__COMPAT__ &&
456+
isCompatEnabled(
457+
CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
458+
context
459+
)))
437460
) {
438461
continue
439462
}
@@ -473,7 +496,14 @@ export function buildProps(
473496
// skip v-is and :is on <component>
474497
if (
475498
name === 'is' ||
476-
(isVBind && isComponentTag(tag) && isBindKey(arg, 'is'))
499+
(isVBind &&
500+
isBindKey(arg, 'is') &&
501+
(isComponentTag(tag) ||
502+
(__COMPAT__ &&
503+
isCompatEnabled(
504+
CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
505+
context
506+
))))
477507
) {
478508
continue
479509
}

packages/vue-compat/__tests__/compiler.spec.ts

+16
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,22 @@ test('COMPILER_IS_ON_ELEMENT', () => {
3535
expect(CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT).toHaveBeenWarned()
3636
})
3737

38+
test('COMPILER_IS_ON_ELEMENT (dynamic)', () => {
39+
const MyButton = {
40+
template: `<div><slot/></div>`
41+
}
42+
43+
const vm = new Vue({
44+
template: `<button :is="'MyButton'">text</button>`,
45+
components: {
46+
MyButton
47+
}
48+
}).$mount()
49+
50+
expect(vm.$el.outerHTML).toBe(`<div>text</div>`)
51+
expect(CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT).toHaveBeenWarned()
52+
})
53+
3854
test('COMPILER_V_BIND_SYNC', async () => {
3955
const MyButton = {
4056
props: ['foo'],

0 commit comments

Comments
 (0)