-import { type Ref, nextTick, ref } from '@vue/runtime-dom'
import {
+ type Ref,
+ nextTick,
+ onUpdated,
+ ref,
+ withModifiers,
+} from '@vue/runtime-dom'
+import {
+ VaporTeleport,
createComponent,
createDynamicComponent,
createIf,
createSlot,
defineVaporComponent,
+ delegateEvents,
renderEffect,
setClass,
setDynamicProps,
+ setInsertionState,
setProp,
setStyle,
template,
import { setElementText } from '../src/dom/prop'
const define = makeRender<any>()
-
-// TODO: port more tests from rendererAttrsFallthrough.spec.ts
+delegateEvents('click')
describe('attribute fallthrough', () => {
it('should allow attrs to fallthrough', async () => {
expect(host.innerHTML).toBe('<div id="b">2</div>')
})
+ it('should only allow whitelisted fallthrough on functional component with optional props', async () => {
+ const click = vi.fn()
+ const childUpdated = vi.fn()
+
+ const count = ref(0)
+
+ function inc() {
+ count.value++
+ click()
+ }
+
+ const Hello = () =>
+ createComponent(Child, {
+ foo: () => count.value + 1,
+ id: () => 'test',
+ class: () => 'c' + count.value,
+ style: () => ({
+ color: count.value ? 'red' : 'green',
+ }),
+ onClick: () => inc,
+ })
+
+ const { component: Child } = define((props: any) => {
+ childUpdated()
+ const n0 = template(
+ '<div class="c2" style="font-weight: bold"></div>',
+ true,
+ )() as Element
+ renderEffect(() => setElementText(n0, props.foo))
+ return n0
+ })
+
+ const { host } = define(Hello).render()
+ expect(host.innerHTML).toBe(
+ '<div class="c2 c0" style="font-weight: bold; color: green;">1</div>',
+ )
+
+ const node = host.children[0] as HTMLElement
+
+ // not whitelisted
+ expect(node.getAttribute('id')).toBe(null)
+ expect(node.getAttribute('foo')).toBe(null)
+
+ // whitelisted: style, class, event listeners
+ expect(node.getAttribute('class')).toBe('c2 c0')
+ expect(node.style.color).toBe('green')
+ expect(node.style.fontWeight).toBe('bold')
+ node.dispatchEvent(new CustomEvent('click'))
+ expect(click).toHaveBeenCalled()
+
+ await nextTick()
+ expect(childUpdated).toHaveBeenCalled()
+ expect(node.getAttribute('id')).toBe(null)
+ expect(node.getAttribute('foo')).toBe(null)
+ expect(node.getAttribute('class')).toBe('c2 c1')
+ expect(node.style.color).toBe('red')
+ expect(node.style.fontWeight).toBe('bold')
+ })
+
+ it('should allow all attrs on functional component with declared props', async () => {
+ const click = vi.fn()
+ const childUpdated = vi.fn()
+ const count = ref(0)
+
+ function inc() {
+ count.value++
+ click()
+ }
+
+ const Hello = () =>
+ createComponent(Child, {
+ foo: () => count.value + 1,
+ id: () => 'test',
+ class: () => 'c' + count.value,
+ style: () => ({ color: count.value ? 'red' : 'green' }),
+ onClick: () => inc,
+ })
+
+ const Child = defineVaporComponent((props: any) => {
+ childUpdated()
+ const n0 = template(
+ '<div class="c2" style="font-weight: bold"></div>',
+ true,
+ )() as Element
+ renderEffect(() => setElementText(n0, props.foo))
+ return n0
+ })
+
+ Child.props = ['foo']
+
+ const { host } = define(Hello).render()
+ const node = host.children[0] as HTMLElement
+
+ expect(node.getAttribute('id')).toBe('test')
+ expect(node.getAttribute('foo')).toBe(null) // declared as prop
+ expect(node.getAttribute('class')).toBe('c2 c0')
+ expect(node.style.color).toBe('green')
+ expect(node.style.fontWeight).toBe('bold')
+ node.dispatchEvent(new CustomEvent('click'))
+ expect(click).toHaveBeenCalled()
+
+ await nextTick()
+ expect(childUpdated).toHaveBeenCalled()
+ expect(node.getAttribute('id')).toBe('test')
+ expect(node.getAttribute('foo')).toBe(null)
+ expect(node.getAttribute('class')).toBe('c2 c1')
+ expect(node.style.color).toBe('red')
+ expect(node.style.fontWeight).toBe('bold')
+ })
+
+ it('should fallthrough for nested components', async () => {
+ const click = vi.fn()
+ const childUpdated = vi.fn()
+ const grandChildUpdated = vi.fn()
+
+ const Hello = {
+ setup() {
+ const count = ref(0)
+
+ function inc() {
+ count.value++
+ click()
+ }
+
+ return createComponent(Child, {
+ foo: () => count.value + 1,
+ id: () => 'test',
+ class: () => 'c' + count.value,
+ style: () => ({
+ color: count.value ? 'red' : 'green',
+ }),
+ onClick: () => inc,
+ })
+ },
+ }
+
+ const Child = defineVaporComponent({
+ setup(props: any) {
+ onUpdated(childUpdated)
+ // HOC simply passing props down.
+ // this will result in merging the same attrs, but should be deduped by
+ // `mergeProps`.
+ return createComponent(GrandChild, props, null, true)
+ },
+ })
+
+ const GrandChild = defineVaporComponent({
+ props: {
+ id: String,
+ foo: Number,
+ },
+ setup(props) {
+ onUpdated(grandChildUpdated)
+ const n0 = template(
+ '<div class="c2" style="font-weight: bold"></div>',
+ true,
+ )() as Element
+ renderEffect(() => {
+ setProp(n0, 'id', props.id)
+ setElementText(n0, props.foo)
+ })
+ return n0
+ },
+ })
+
+ const { host } = define(Hello).render()
+ expect(host.innerHTML).toBe(
+ '<div class="c2 c0" style="font-weight: bold; color: green;" id="test">1</div>',
+ )
+
+ const node = host.children[0] as HTMLElement
+
+ // with declared props, any parent attr that isn't a prop falls through
+ expect(node.getAttribute('id')).toBe('test')
+ expect(node.getAttribute('class')).toBe('c2 c0')
+ expect(node.style.color).toBe('green')
+ expect(node.style.fontWeight).toBe('bold')
+ node.dispatchEvent(new CustomEvent('click'))
+ expect(click).toHaveBeenCalled()
+
+ // ...while declared ones remain props
+ expect(node.hasAttribute('foo')).toBe(false)
+
+ await nextTick()
+ // child should not update, due to it not accessing props
+ // this is a optimization in vapor mode
+ expect(childUpdated).not.toHaveBeenCalled()
+ expect(grandChildUpdated).toHaveBeenCalled()
+ expect(node.getAttribute('id')).toBe('test')
+ expect(node.getAttribute('class')).toBe('c2 c1')
+ expect(node.style.color).toBe('red')
+ expect(node.style.fontWeight).toBe('bold')
+
+ expect(node.hasAttribute('foo')).toBe(false)
+ })
+
+ it('should not fallthrough with inheritAttrs: false', () => {
+ const Parent = defineVaporComponent({
+ setup() {
+ return createComponent(Child, { foo: () => 1, class: () => 'parent' })
+ },
+ })
+
+ const Child = defineVaporComponent({
+ props: ['foo'],
+ inheritAttrs: false,
+ setup(props) {
+ const n0 = template('<div></div>', true)() as Element
+ renderEffect(() => setElementText(n0, props.foo))
+ return n0
+ },
+ })
+
+ const { html } = define(Parent).render()
+
+ // should not contain class
+ expect(html()).toMatch(`<div>1</div>`)
+ })
+
+ it('explicit spreading with inheritAttrs: false', () => {
+ const Parent = defineVaporComponent({
+ setup() {
+ return createComponent(Child, { foo: () => 1, class: () => 'parent' })
+ },
+ })
+
+ const Child = defineVaporComponent({
+ props: ['foo'],
+ inheritAttrs: false,
+ setup(props, { attrs }) {
+ const n0 = template('<div>', true)() as Element
+ renderEffect(() => {
+ setElementText(n0, props.foo)
+ setDynamicProps(n0, [{ class: 'child' }, attrs])
+ })
+ return n0
+ },
+ })
+
+ const { html } = define(Parent).render()
+
+ // should merge parent/child classes
+ expect(html()).toMatch(`<div class="child parent">1</div>`)
+ })
+
+ it('should warn when fallthrough fails on non-single-root', () => {
+ const Parent = {
+ setup() {
+ return createComponent(Child, {
+ foo: () => 1,
+ class: () => 'parent',
+ onBar: () => () => {},
+ })
+ },
+ }
+
+ const Child = defineVaporComponent({
+ props: ['foo'],
+ render() {
+ return [template('<div></div>')(), template('<div></div>')()]
+ },
+ })
+
+ define(Parent).render()
+
+ expect(`Extraneous non-props attributes (class)`).toHaveBeenWarned()
+ expect(`Extraneous non-emits event listeners`).toHaveBeenWarned()
+ })
+
+ it('should warn when fallthrough fails on teleport root node', () => {
+ const Parent = {
+ render() {
+ return createComponent(Child, { class: () => 'parent' })
+ },
+ }
+
+ const target = document.createElement('div')
+ const Child = defineVaporComponent({
+ render() {
+ return createComponent(
+ VaporTeleport,
+ { to: () => target },
+ {
+ default: () => template('<div></div>')(),
+ },
+ )
+ },
+ })
+
+ document.body.appendChild(target)
+ define(Parent).render()
+
+ expect(`Extraneous non-props attributes (class)`).toHaveBeenWarned()
+ })
+
+ it('should dedupe same listeners when $attrs is used during render', () => {
+ const click = vi.fn()
+ const count = ref(0)
+
+ function inc() {
+ count.value++
+ click()
+ }
+
+ const Parent = {
+ render() {
+ return createComponent(Child, { onClick: () => inc })
+ },
+ }
+
+ const Child = defineVaporComponent({
+ setup(_, { attrs }) {
+ const n0 = template('<div></div>', true)() as any
+ n0.$evtclick = withModifiers(() => {}, ['prevent', 'stop'])
+ renderEffect(() => setDynamicProps(n0, [attrs]))
+ return n0
+ },
+ })
+
+ const { host } = define(Parent).render()
+ const node = host.children[0] as HTMLElement
+ node.dispatchEvent(new CustomEvent('click'))
+ expect(click).toHaveBeenCalledTimes(1)
+ expect(count.value).toBe(1)
+ })
+
+ it('should not warn when context.attrs is used during render', () => {
+ const Parent = {
+ render() {
+ return createComponent(Child, {
+ foo: () => 1,
+ class: () => 'parent',
+ onBar: () => () => {},
+ })
+ },
+ }
+
+ const Child = defineVaporComponent({
+ props: ['foo'],
+ render(_ctx, $props, $emit, $attrs, $slots) {
+ const n0 = template('<div></div>')() as Element
+ const n1 = template('<div></div>')() as Element
+ renderEffect(() => {
+ setDynamicProps(n1, [$attrs])
+ })
+ return [n0, n1]
+ },
+ })
+
+ const { html } = define(Parent).render()
+
+ expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
+ expect(`Extraneous non-emits event listeners`).not.toHaveBeenWarned()
+
+ expect(html()).toBe(`<div></div><div class="parent"></div>`)
+ })
+
+ it('should not warn when context.attrs is used during render (functional)', () => {
+ const Parent = {
+ render() {
+ return createComponent(Child, {
+ foo: () => 1,
+ class: () => 'parent',
+ onBar: () => () => {},
+ })
+ },
+ }
+
+ const { component: Child } = define((_: any, { attrs }: any) => {
+ const n0 = template('<div></div>')() as Element
+ const n1 = template('<div></div>')() as Element
+ renderEffect(() => {
+ setDynamicProps(n1, [attrs])
+ })
+ return [n0, n1]
+ })
+
+ Child.props = ['foo']
+
+ const { html } = define(Parent).render()
+
+ expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
+ expect(`Extraneous non-emits event listeners`).not.toHaveBeenWarned()
+ expect(html()).toBe(`<div></div><div class="parent"></div>`)
+ })
+
+ it('should not warn when functional component has optional props', () => {
+ const Parent = {
+ render() {
+ return createComponent(Child, {
+ foo: () => 1,
+ class: () => 'parent',
+ onBar: () => () => {},
+ })
+ },
+ }
+
+ const { component: Child } = define((props: any) => {
+ const n0 = template('<div></div>')() as Element
+ const n1 = template('<div></div>')() as Element
+ renderEffect(() => {
+ setClass(n1, props.class)
+ })
+ return [n0, n1]
+ })
+
+ const { html } = define(Parent).render()
+
+ expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
+ expect(`Extraneous non-emits event listeners`).not.toHaveBeenWarned()
+ expect(html()).toBe(`<div></div><div class="parent"></div>`)
+ })
+
+ it('should warn when functional component has props and does not use attrs', () => {
+ const Parent = {
+ render() {
+ return createComponent(Child, {
+ foo: () => 1,
+ class: () => 'parent',
+ onBar: () => () => {},
+ })
+ },
+ }
+
+ const { component: Child } = define(() => [
+ template('<div></div>')(),
+ template('<div></div>')(),
+ ])
+
+ Child.props = ['foo']
+
+ const { html } = define(Parent).render()
+
+ expect(`Extraneous non-props attributes`).toHaveBeenWarned()
+ expect(`Extraneous non-emits event listeners`).toHaveBeenWarned()
+ expect(html()).toBe(`<div></div><div></div>`)
+ })
+
+ it('should not let listener fallthrough when declared in emits (stateful)', () => {
+ const Child = defineVaporComponent({
+ emits: ['click'],
+ render(_ctx, $props, $emit, $attrs, $slots) {
+ const n0 = template('<button>hello</button>')() as any
+ n0.$evtclick = () => {
+ // @ts-expect-error
+ $emit('click', 'custom')
+ }
+ return n0
+ },
+ })
+
+ const onClick = vi.fn()
+ const App = defineVaporComponent({
+ render() {
+ return createComponent(
+ Child,
+ {
+ onClick: () => onClick,
+ },
+ null,
+ true,
+ )
+ },
+ })
+
+ const { host } = define(App).render()
+ const node = host.children[0] as HTMLElement
+ node.click()
+ expect(onClick).toHaveBeenCalledTimes(1)
+ expect(onClick).toHaveBeenCalledWith('custom')
+ })
+
+ it('should not let listener fallthrough when declared in emits (functional)', () => {
+ const { component: Child } = define((_: any, { emit }: any) => {
+ // should not be in props
+ expect((_ as any).onClick).toBeUndefined()
+ const n0 = template('<button></button>')() as any
+ n0.$evtclick = () => {
+ emit('click', 'custom')
+ }
+ return n0
+ })
+ Child.emits = ['click']
+
+ const onClick = vi.fn()
+ const App = defineVaporComponent({
+ render() {
+ return createComponent(Child, {
+ onClick: () => onClick,
+ })
+ },
+ })
+
+ const { host } = define(App).render()
+ const node = host.children[0] as HTMLElement
+ node.click()
+ expect(onClick).toHaveBeenCalledTimes(1)
+ expect(onClick).toHaveBeenCalledWith('custom')
+ })
+
+ it('should support fallthrough for single element + comments', () => {
+ const click = vi.fn()
+
+ const Hello = defineVaporComponent({
+ render() {
+ return createComponent(Child, {
+ class: () => 'foo',
+ onClick: () => click,
+ })
+ },
+ })
+
+ const Child = defineVaporComponent({
+ render() {
+ return [
+ template('<!--hello-->')(),
+ template('<button></button>')(),
+ template('<!--world-->')(),
+ ]
+ },
+ })
+
+ const { host } = define(Hello).render()
+
+ expect(host.innerHTML).toBe(
+ `<!--hello--><button class="foo"></button><!--world-->`,
+ )
+ const button = host.children[0] as HTMLElement
+ button.dispatchEvent(new CustomEvent('click'))
+ expect(click).toHaveBeenCalled()
+ })
+
+ it('should support fallthrough for nested element + comments', async () => {
+ const toggle = ref(false)
+ const Child = defineVaporComponent({
+ setup() {
+ const n0 = template('<!-- comment A -->')() as any
+ const n1 = createIf(
+ () => toggle.value,
+ () => template('<span>Foo</span>')(),
+ () => {
+ const n2 = template('<!-- comment B -->')() as any
+ const n3 = template('<div>Bar</div>')() as any
+ return [n2, n3]
+ },
+ )
+ return [n0, n1]
+ },
+ })
+
+ const Root = defineVaporComponent({
+ setup() {
+ return createComponent(Child, { class: () => 'red' })
+ },
+ })
+
+ const { host } = define(Root).render()
+
+ expect(host.innerHTML).toBe(
+ `<!-- comment A --><!-- comment B --><div class="red">Bar</div><!--if-->`,
+ )
+
+ toggle.value = true
+ await nextTick()
+ expect(host.innerHTML).toBe(
+ `<!-- comment A --><span class=\"red\">Foo</span><!--if-->`,
+ )
+ })
+
+ it('should not fallthrough v-model listeners with corresponding declared prop', () => {
+ let textFoo = ''
+ let textBar = ''
+ const click = vi.fn()
+
+ const App = defineVaporComponent({
+ render() {
+ return createComponent(Child, {
+ modelValue: () => textFoo,
+ 'onUpdate:modelValue': () => (val: string) => {
+ textFoo = val
+ },
+ })
+ },
+ })
+
+ const Child = defineVaporComponent({
+ props: ['modelValue'],
+ setup(_props, { emit }) {
+ return createComponent(GrandChild, {
+ modelValue: () => textBar,
+ 'onUpdate:modelValue': () => (val: string) => {
+ textBar = val
+ emit('update:modelValue', 'from Child')
+ },
+ })
+ },
+ })
+
+ const GrandChild = defineVaporComponent({
+ props: ['modelValue'],
+ setup(_props, { emit }) {
+ const n0 = template('<button></button>')() as any
+ n0.$evtclick = () => {
+ click()
+ emit('update:modelValue', 'from GrandChild')
+ }
+ return n0
+ },
+ })
+
+ const { host } = define(App).render()
+ const node = host.children[0] as HTMLElement
+ node.click()
+ expect(click).toHaveBeenCalled()
+ expect(textBar).toBe('from GrandChild')
+ expect(textFoo).toBe('from Child')
+ })
+
+ it('should track this.$attrs access in slots', async () => {
+ const GrandChild = defineVaporComponent({
+ render() {
+ return createSlot('default')
+ },
+ })
+ const Child = defineVaporComponent({
+ // @ts-expect-error
+ components: { GrandChild },
+ render(_ctx, $props, $emit, $attrs, $slots) {
+ const n0 = template('<div></div>')() as any
+ setInsertionState(n0)
+ createComponent(GrandChild, null, {
+ default: () => {
+ const n1 = template(' ')()
+ renderEffect(() => setElementText(n1, $attrs.foo))
+ return n1
+ },
+ })
+ return n0
+ },
+ })
+
+ const obj = ref(1)
+ const App = defineVaporComponent({
+ render() {
+ return createComponent(Child, { foo: () => obj.value })
+ },
+ })
+
+ const { html } = define(App).render()
+ expect(html()).toBe('<div foo="1">1<!--slot--></div>')
+
+ obj.value = 2
+ await nextTick()
+ expect(html()).toBe('<div foo="2">2<!--slot--></div>')
+ })
+
it('should allow attrs to fallthrough on component with comment at root', async () => {
const t0 = template('<!--comment-->')
const t1 = template('<div>')
},
}).render()
expect(host.innerHTML).toBe('<span></span><div>1</div>')
+ expect(`Extraneous non-props attributes (id)`).toHaveBeenWarned()
})
it('should not allow attrs to fallthrough on component with single comment root', async () => {
},
}).render()
expect(host.innerHTML).toBe('<!--comment-->')
+ expect(`Extraneous non-props attributes (id)`).toHaveBeenWarned()
})
it('should not fallthrough if explicitly pass inheritAttrs: false', async () => {
currentInstance,
endMeasure,
expose,
+ getComponentName,
+ getFunctionalFallthrough,
isAsyncWrapper,
isKeepAlive,
nextUid,
startMeasure,
unregisterHMR,
warn,
+ warnExtraneousAttributes,
} from '@vue/runtime-dom'
import {
type Block,
setCurrentHydrationNode,
} from './dom/hydration'
import { _next, createElement } from './dom/node'
-import { type TeleportFragment, isVaporTeleport } from './components/Teleport'
+import { TeleportFragment, isVaporTeleport } from './components/Teleport'
import {
type KeepAliveInstance,
findParentKeepAlive,
const parentInstance = getParentInstance()
if (
- isSingleRoot &&
+ (isSingleRoot ||
+ // transition has attrs fallthrough
+ (parentInstance && isVaporTransition(parentInstance!.type))) &&
component.inheritAttrs !== false &&
isVaporComponent(parentInstance) &&
parentInstance.hasFallthrough
// check if we are the single root of the parent
// if yes, inject parent attrs as dynamic props source
const attrs = parentInstance.attrs
- if (rawProps) {
+ if (rawProps && rawProps !== EMPTY_OBJ) {
;((rawProps as RawProps).$ || ((rawProps as RawProps).$ = [])).push(
() => attrs,
)
component.inheritAttrs !== false &&
Object.keys(instance.attrs).length
) {
- renderEffect(() => applyFallthroughProps(instance.block, instance.attrs))
+ const root = getRootElement(
+ instance.block,
+ // attach attrs to root dynamic fragments for applying during each update
+ frag => (frag.attrs = instance.attrs),
+ false,
+ )
+ if (root) {
+ renderEffect(() => {
+ const attrs =
+ isFunction(component) && !isVaporTransition(component)
+ ? getFunctionalFallthrough(instance.attrs)
+ : instance.attrs
+ if (attrs) applyFallthroughProps(root, attrs)
+ })
+ } else if (
+ __DEV__ &&
+ ((!instance.accessedAttrs &&
+ isArray(instance.block) &&
+ instance.block.length) ||
+ // preventing attrs fallthrough on Teleport
+ // consistent with VDOM Teleport behavior
+ instance.block instanceof TeleportFragment)
+ ) {
+ warnExtraneousAttributes(instance.attrs)
+ }
}
setActiveSub(prevSub)
export let isApplyingFallthroughProps = false
export function applyFallthroughProps(
- block: Block,
+ el: Element,
attrs: Record<string, any>,
): void {
- const el = getRootElement(block, false)
- if (el) {
- isApplyingFallthroughProps = true
- setDynamicProps(el, [attrs])
- isApplyingFallthroughProps = false
- }
+ isApplyingFallthroughProps = true
+ setDynamicProps(el, [attrs])
+ isApplyingFallthroughProps = false
}
/**
emitsOptions?: ObjectEmitsOptions | null
isSingleRoot?: boolean
+ /**
+ * dev only flag to track whether $attrs was used during render.
+ * If $attrs was used during render then the warning for failed attrs
+ * fallthrough can be suppressed.
+ */
+ accessedAttrs: boolean = false
+
constructor(
comp: VaporComponent,
rawProps?: RawProps | null,
if (comp.ce) {
comp.ce(this)
}
+
+ if (__DEV__) {
+ // in dev, mark attrs accessed if optional props (attrs === props)
+ if (this.props === this.attrs) {
+ this.accessedAttrs = true
+ } else {
+ const attrs = this.attrs
+ const instance = this
+ this.attrs = new Proxy(attrs, {
+ get(target, key, receiver) {
+ instance.accessedAttrs = true
+ return Reflect.get(target, key, receiver)
+ },
+ })
+ }
+ }
}
/**
export function getRootElement(
block: Block,
+ onDynamicFragment?: (frag: DynamicFragment) => void,
recurse: boolean = true,
): Element | undefined {
if (block instanceof Element) {
}
if (recurse && isVaporComponent(block)) {
- return getRootElement(block.block, recurse)
+ return getRootElement(block.block, onDynamicFragment, recurse)
}
- if (isFragment(block)) {
+ if (isFragment(block) && !(block instanceof TeleportFragment)) {
+ if (block instanceof DynamicFragment && onDynamicFragment) {
+ onDynamicFragment(block)
+ }
const { nodes } = block
if (nodes instanceof Element && (nodes as any).$root) {
return nodes
}
- return getRootElement(nodes, recurse)
+ return getRootElement(nodes, onDynamicFragment, recurse)
}
// The root node contains comments. It is necessary to filter out
hasComment = true
continue
}
- const thisRoot = getRootElement(b, recurse)
+ const thisRoot = getRootElement(b, onDynamicFragment, recurse)
// only return root if there is exactly one eligible root in the array
if (!thisRoot || singleRoot) {
return
return hasComment ? singleRoot : undefined
}
}
+
+function isVaporTransition(component: VaporComponent): boolean {
+ return getComponentName(component) === 'VaporTransition'
+}