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/compiler-dom/__tests__/transforms/validateHtmlNesting.spec.ts b/packages/compiler-dom/__tests__/transforms/validateHtmlNesting.spec.ts index 46d69846bae..3cc49851770 100644 --- a/packages/compiler-dom/__tests__/transforms/validateHtmlNesting.spec.ts +++ b/packages/compiler-dom/__tests__/transforms/validateHtmlNesting.spec.ts @@ -34,6 +34,20 @@ describe('validate html nesting', () => { * with ISC license */ describe('isValidHTMLNesting', () => { + test('customizable select', () => { + // invalid + expect(isValidHTMLNesting('select', 'selectedcontent')).toBe(false) + expect(isValidHTMLNesting('option', 'button')).toBe(false) + + // using example from https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/Customizable_select + + // valid + expect(isValidHTMLNesting('select', 'button')).toBe(true) + expect(isValidHTMLNesting('button', 'selectedcontent')).toBe(true) + expect(isValidHTMLNesting('select', 'option')).toBe(true) + expect(isValidHTMLNesting('option', 'span')).toBe(true) + }) + test('form', () => { // invalid expect(isValidHTMLNesting('form', 'form')).toBe(false) diff --git a/packages/compiler-dom/src/htmlNesting.ts b/packages/compiler-dom/src/htmlNesting.ts index 5f924880bd0..d496e45cbc1 100644 --- a/packages/compiler-dom/src/htmlNesting.ts +++ b/packages/compiler-dom/src/htmlNesting.ts @@ -62,8 +62,75 @@ const onlyValidChildren: Record> = { 'script', 'template', ]), - optgroup: new Set(['option']), - select: new Set(['optgroup', 'option', 'hr']), + option: new Set([ + 'div', + 'a', + 'abbr', + 'area', + 'audio', + 'b', + 'bdi', + 'bdo', + 'br', + 'canvas', + 'cite', + 'code', + 'data', + 'del', + 'dfn', + 'em', + 'i', + 'img', + 'input', + 'ins', + 'kbd', + 'link', + 'map', + 'mark', + 'math', + 'meta', + 'meter', + 'noscript', + 'output', + 'picture', + 'progress', + 'q', + 'ruby', + 's', + 'samp', + 'script', + 'slot', + 'small', + 'span', + 'strong', + 'sub', + 'sup', + 'svg', + 'template', + 'time', + 'u', + 'var', + 'video', + 'wbr', + ]), + optgroup: new Set([ + 'option', + 'legend', + 'script', + 'template', + 'noscript', + 'div', + ]), + select: new Set([ + 'optgroup', + 'option', + 'hr', + 'button', + 'script', + 'template', + 'noscript', + 'div', + ]), // table table: new Set(['caption', 'colgroup', 'tbody', 'tfoot', 'thead']), tr: new Set(['td', 'th']), @@ -74,10 +141,10 @@ const onlyValidChildren: Record> = { // these elements can not have any children elements script: emptySet, iframe: emptySet, - option: emptySet, textarea: emptySet, style: emptySet, title: emptySet, + selectedcontent: emptySet, } /** maps elements to set of elements which can be it's parent, no other */ @@ -104,6 +171,7 @@ const onlyValidParents: Record> = { // li: new Set(["ul", "ol"]), summary: new Set(['details']), area: new Set(['map']), + selectedcontent: new Set(['button']), } as const /** maps element to set of elements that can not be it's children, others can */ 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..a0e5d9f03ba 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,selectedcontent' + // 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)