diff --git a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts index bf3510a052d..f93e1131b6d 100644 --- a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts +++ b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts @@ -99,6 +99,40 @@ describe('compiler: element transform', () => { expect(node.tag).toBe(`$setup["Example"]`) }) + test('resolve component from setup bindings & component', () => { + const { root, node } = parseWithElementTransform(``, { + bindingMetadata: { + search: BindingTypes.SETUP_CONST, + }, + isNativeTag: (tag: string) => tag !== 'search', + }) + expect(root.helpers).not.toContain(RESOLVE_COMPONENT) + expect(node.tag).toBe(`_resolveLateAddedTag("search", 'setupState')`) + + const { root: root2, node: node2 } = parseWithElementTransform( + ``, + { + bindingMetadata: { + search: BindingTypes.SETUP_LET, + }, + isNativeTag: (tag: string) => tag !== 'search', + }, + ) + expect(root2.helpers).not.toContain(RESOLVE_COMPONENT) + expect(node2.tag).toBe(`_resolveLateAddedTag("search", 'setupState')`) + }) + + test('resolve component from props', () => { + const { root, node } = parseWithElementTransform(``, { + bindingMetadata: { + search: BindingTypes.PROPS, + }, + isNativeTag: (tag: string) => tag !== 'search', + }) + expect(root.helpers).not.toContain(RESOLVE_COMPONENT) + expect(node.tag).toBe(`_unref(_resolveLateAddedTag("search", 'props'))`) + }) + test('resolve component from setup bindings (inline)', () => { const { root, node } = parseWithElementTransform(``, { inline: true, diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index bae13372a98..26c86f1175c 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -9,6 +9,7 @@ import { OPEN_BLOCK, type RENDER_LIST, type RENDER_SLOT, + RESOLVE_LATE_ADDED_TAG, WITH_DIRECTIVES, type WITH_MEMO, } from './runtimeHelpers' @@ -875,6 +876,10 @@ export function getVNodeBlockHelper( return ssr || isComponent ? CREATE_BLOCK : CREATE_ELEMENT_BLOCK } +export function getResolveLateAddedTagHelper(): typeof RESOLVE_LATE_ADDED_TAG { + return RESOLVE_LATE_ADDED_TAG +} + export function convertToBlock( node: VNodeCall, { helper, removeHelper, inSSR }: TransformContext, diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index 99020bcf1ae..ed3feb1341f 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -24,6 +24,7 @@ import { type TemplateLiteral, type TextNode, type VNodeCall, + getResolveLateAddedTagHelper, getVNodeBlockHelper, getVNodeHelper, locStub, @@ -336,6 +337,8 @@ export function generate( if (!__BROWSER__ && options.bindingMetadata && !options.inline) { // binding optimization args args.push('$props', '$setup', '$data', '$options') + // Add helper 'getResolveLateAddedTagHelper' for $setup + context.helper(getResolveLateAddedTagHelper()) } const signature = !__BROWSER__ && options.isTS diff --git a/packages/compiler-core/src/runtimeHelpers.ts b/packages/compiler-core/src/runtimeHelpers.ts index 7cf3757b249..21d4de02de8 100644 --- a/packages/compiler-core/src/runtimeHelpers.ts +++ b/packages/compiler-core/src/runtimeHelpers.ts @@ -26,6 +26,9 @@ export const CREATE_STATIC: unique symbol = Symbol( export const RESOLVE_COMPONENT: unique symbol = Symbol( __DEV__ ? `resolveComponent` : ``, ) +export const RESOLVE_LATE_ADDED_TAG: unique symbol = Symbol( + __DEV__ ? `resolveLateAddedTag` : ``, +) export const RESOLVE_DYNAMIC_COMPONENT: unique symbol = Symbol( __DEV__ ? `resolveDynamicComponent` : ``, ) @@ -98,6 +101,7 @@ export const helperNameMap: Record = { [CREATE_TEXT]: `createTextVNode`, [CREATE_STATIC]: `createStaticVNode`, [RESOLVE_COMPONENT]: `resolveComponent`, + [RESOLVE_LATE_ADDED_TAG]: `resolveLateAddedTag`, [RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`, [RESOLVE_DIRECTIVE]: `resolveDirective`, [RESOLVE_FILTER]: `resolveFilter`, diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts index 1dca0c514c1..4bb62909c66 100644 --- a/packages/compiler-core/src/transforms/transformElement.ts +++ b/packages/compiler-core/src/transforms/transformElement.ts @@ -21,12 +21,14 @@ import { createObjectProperty, createSimpleExpression, createVNodeCall, + getResolveLateAddedTagHelper, } from '../ast' import { PatchFlags, camelize, capitalize, isBuiltInDirective, + isLateTag, isObject, isOn, isReservedProp, @@ -85,7 +87,6 @@ export const transformElement: NodeTransform = (node, context) => { ) { return } - const { tag, props } = node const isComponent = node.tagType === ElementTypes.COMPONENT @@ -344,10 +345,13 @@ function resolveSetupReference(name: string, context: TransformContext) { checkType(BindingTypes.SETUP_REACTIVE_CONST) || checkType(BindingTypes.LITERAL_CONST) if (fromConst) { + const helper = context.helperString return context.inline ? // in inline mode, const setup bindings (e.g. imports) can be used as-is fromConst - : `$setup[${JSON.stringify(fromConst)}]` + : isLateTag(fromConst) + ? `${helper(getResolveLateAddedTagHelper())}(${JSON.stringify(fromConst)}, 'setupState')` + : `$setup[${JSON.stringify(fromConst)}]` } const fromMaybeRef = @@ -355,17 +359,25 @@ function resolveSetupReference(name: string, context: TransformContext) { checkType(BindingTypes.SETUP_REF) || checkType(BindingTypes.SETUP_MAYBE_REF) if (fromMaybeRef) { + const helper = context.helperString return context.inline ? // setup scope bindings that may be refs need to be unrefed `${context.helperString(UNREF)}(${fromMaybeRef})` - : `$setup[${JSON.stringify(fromMaybeRef)}]` + : isLateTag(fromMaybeRef) + ? `${helper(getResolveLateAddedTagHelper())}(${JSON.stringify(fromMaybeRef)}, 'setupState')` + : `$setup[${JSON.stringify(fromMaybeRef)}]` } const fromProps = checkType(BindingTypes.PROPS) if (fromProps) { - return `${context.helperString(UNREF)}(${ - context.inline ? '__props' : '$props' - }[${JSON.stringify(fromProps)}])` + const helper = context.helperString + const fromPropsStr = JSON.stringify(fromProps) + let propsCode = context.inline + ? `__props[${fromPropsStr}]` + : isLateTag(fromProps) + ? `${helper(getResolveLateAddedTagHelper())}(${fromPropsStr}, 'props')` + : `$props[${fromPropsStr}]` + return `${helper(UNREF)}(${propsCode})` } } diff --git a/packages/runtime-core/src/helpers/resolveAssets.ts b/packages/runtime-core/src/helpers/resolveAssets.ts index 910fab33424..57a74d4e72c 100644 --- a/packages/runtime-core/src/helpers/resolveAssets.ts +++ b/packages/runtime-core/src/helpers/resolveAssets.ts @@ -7,7 +7,7 @@ import { } from '../component' import { currentRenderingInstance } from '../componentRenderContext' import type { Directive } from '../directives' -import { camelize, capitalize, isString } from '@vue/shared' +import { camelize, capitalize, isLateTag, isString } from '@vue/shared' import { warn } from '../warning' import type { VNodeTypes } from '../vnode' @@ -118,12 +118,21 @@ function resolveAsset( return Component } - if (__DEV__ && warnMissing && !res) { - const extra = - type === COMPONENTS - ? `\nIf this is a native custom element, make sure to exclude it from ` + + if ( + __DEV__ && + warnMissing && + ((!res && !isLateTag(name)) || (res && isLateTag(name))) + ) { + let extra = '' + if (type === COMPONENTS) { + if (isLateTag(name)) { + extra = `\nplease do not use built-in tag names as component names.` + } else { + extra = + `\nIf this is a native custom element, make sure to exclude it from ` + `component resolution via compilerOptions.isCustomElement.` - : `` + } + } warn(`Failed to resolve ${type.slice(0, -1)}: ${name}${extra}`) } @@ -144,3 +153,27 @@ function resolve(registry: Record | undefined, name: string) { registry[capitalize(camelize(name))]) ) } + +/** + * @private + */ +export function resolveLateAddedTag( + name: string, + key: 'setupState' | 'props', +): unknown { + if (!currentRenderingInstance || !currentRenderingInstance[key]) return name + const data = currentRenderingInstance[key] + const value = data[name] + // Only the render function for the value is parsed as a component + // and a warning is reported + if ( + __DEV__ && + value && + (value as ComponentInternalInstance).render && + isLateTag(name as string) + ) { + const extra = `\nplease do not use built-in tag names as component names.` + warn(`Failed to resolve component: ${name},${extra}`) + } + return value +} diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 1ed6f21df77..42683fd4abc 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -144,6 +144,7 @@ export { resolveComponent, resolveDirective, resolveDynamicComponent, + resolveLateAddedTag, } from './helpers/resolveAssets' // For integration with runtime compiler export { registerRuntimeCompiler, isRuntimeOnly } from './component' diff --git a/packages/shared/src/domTagConfig.ts b/packages/shared/src/domTagConfig.ts index 7f9d198e569..da23b36e3dc 100644 --- a/packages/shared/src/domTagConfig.ts +++ b/packages/shared/src/domTagConfig.ts @@ -14,6 +14,8 @@ const HTML_TAGS = 'option,output,progress,select,textarea,details,dialog,menu,' + 'summary,template,blockquote,iframe,tfoot' +const LATE_ADDED_TAGS = 'search' + // https://developer.mozilla.org/en-US/docs/Web/SVG/Element const SVG_TAGS = 'svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,' + @@ -62,3 +64,6 @@ export const isMathMLTag: (key: string) => boolean = */ export const isVoidTag: (key: string) => boolean = /*@__PURE__*/ makeMap(VOID_TAGS) + +export const isLateTag: (key: string) => boolean = + /*#__PURE__*/ makeMap(LATE_ADDED_TAGS)