Skip to content

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a problem

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

elaborate?

Original file line number Diff line number Diff line change
Expand Up @@ -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(`<search/>`, {
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(
`<search/>`,
{
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(`<search/>`, {
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(`<Example/>`, {
inline: true,
Expand Down
5 changes: 5 additions & 0 deletions packages/compiler-core/src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
OPEN_BLOCK,
type RENDER_LIST,
type RENDER_SLOT,
RESOLVE_LATE_ADDED_TAG,
WITH_DIRECTIVES,
type WITH_MEMO,
} from './runtimeHelpers'
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions packages/compiler-core/src/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
type TemplateLiteral,
type TextNode,
type VNodeCall,
getResolveLateAddedTagHelper,
getVNodeBlockHelper,
getVNodeHelper,
locStub,
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler-core/src/runtimeHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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` : ``,
)
Expand Down Expand Up @@ -98,6 +101,7 @@ export const helperNameMap: Record<symbol, string> = {
[CREATE_TEXT]: `createTextVNode`,
[CREATE_STATIC]: `createStaticVNode`,
[RESOLVE_COMPONENT]: `resolveComponent`,
[RESOLVE_LATE_ADDED_TAG]: `resolveLateAddedTag`,
[RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,
[RESOLVE_DIRECTIVE]: `resolveDirective`,
[RESOLVE_FILTER]: `resolveFilter`,
Expand Down
24 changes: 18 additions & 6 deletions packages/compiler-core/src/transforms/transformElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ import {
createObjectProperty,
createSimpleExpression,
createVNodeCall,
getResolveLateAddedTagHelper,
} from '../ast'
import {
PatchFlags,
camelize,
capitalize,
isBuiltInDirective,
isLateTag,
isObject,
isOn,
isReservedProp,
Expand Down Expand Up @@ -85,7 +87,6 @@ export const transformElement: NodeTransform = (node, context) => {
) {
return
}

const { tag, props } = node
const isComponent = node.tagType === ElementTypes.COMPONENT

Expand Down Expand Up @@ -344,28 +345,39 @@ 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 =
checkType(BindingTypes.SETUP_LET) ||
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})`
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
74 changes: 71 additions & 3 deletions packages/compiler-dom/src/htmlNesting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,75 @@ const onlyValidChildren: Record<string, Set<string>> = {
'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']),
Expand All @@ -74,10 +141,10 @@ const onlyValidChildren: Record<string, Set<string>> = {
// 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 */
Expand All @@ -104,6 +171,7 @@ const onlyValidParents: Record<string, Set<string>> = {
// 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 */
Expand Down
45 changes: 39 additions & 6 deletions packages/runtime-core/src/helpers/resolveAssets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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}`)
}

Expand All @@ -144,3 +153,27 @@ function resolve(registry: Record<string, any> | 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
}
1 change: 1 addition & 0 deletions packages/runtime-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export {
resolveComponent,
resolveDirective,
resolveDynamicComponent,
resolveLateAddedTag,
} from './helpers/resolveAssets'
// For integration with runtime compiler
export { registerRuntimeCompiler, isRuntimeOnly } from './component'
Expand Down
5 changes: 5 additions & 0 deletions packages/shared/src/domTagConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,' +
Expand Down Expand Up @@ -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)
Loading