2
0
Эх сурвалжийг харах

Merge branch 'minor' into rolldown

edison 3 сар өмнө
parent
commit
963ecee8ef

+ 132 - 0
packages/compiler-vapor/__tests__/abbreviation.spec.ts

@@ -129,3 +129,135 @@ test('deeply nested', () => {
     '<div><div><span>a</span><span>b</span></div></div>',
   )
 })
+
+test('always close tags', () => {
+  // button always needs closing tag unless on rightmost path
+  checkAbbr(
+    '<div><button>click</button></div>',
+    '<div><button>click',
+    '<div><button>click</button></div>',
+  )
+  checkAbbr(
+    '<div><button>click</button><span>sibling</span></div>',
+    '<div><button>click</button><span>sibling',
+    '<div><button>click</button><span>sibling</span></div>',
+  )
+
+  // select always needs closing tag unless rightmost
+  checkAbbr(
+    '<div><select></select></div>',
+    '<div><select>',
+    '<div><select></select></div>',
+  )
+  checkAbbr(
+    '<div><select></select><span>sibling</span></div>',
+    '<div><select></select><span>sibling',
+    '<div><select></select><span>sibling</span></div>',
+  )
+
+  // table always needs closing tag unless rightmost
+  checkAbbr(
+    '<div><table></table></div>',
+    '<div><table>',
+    '<div><table></table></div>',
+  )
+  checkAbbr(
+    '<div><table></table><span>sibling</span></div>',
+    '<div><table></table><span>sibling',
+    '<div><table></table><span>sibling</span></div>',
+  )
+
+  // textarea always needs closing tag unless rightmost
+  checkAbbr(
+    '<div><textarea></textarea></div>',
+    '<div><textarea>',
+    '<div><textarea></textarea></div>',
+  )
+  checkAbbr(
+    '<div><textarea></textarea><span>sibling</span></div>',
+    '<div><textarea></textarea><span>sibling',
+    '<div><textarea></textarea><span>sibling</span></div>',
+  )
+
+  // template always needs closing tag unless rightmost
+  checkAbbr(
+    '<div><template></template></div>',
+    '<div><template>',
+    '<div><template></template></div>',
+  )
+  checkAbbr(
+    '<div><template></template><span>sibling</span></div>',
+    '<div><template></template><span>sibling',
+    '<div><template></template><span>sibling</span></div>',
+  )
+
+  // script always needs closing tag unless rightmost
+  checkAbbr(
+    '<div><script></script></div>',
+    '<div><script>',
+    '<div><script></script></div>',
+  )
+  checkAbbr(
+    '<div><script></script><span>sibling</span></div>',
+    '<div><script></script><span>sibling',
+    '<div><script></script><span>sibling</span></div>',
+  )
+
+  // without always-close elements, normal abbreviation should work
+  checkAbbr(
+    '<div><form><input></form></div>',
+    '<div><form><input>',
+    '<div><form><input></form></div>',
+  )
+})
+
+test('inline/block ancestor relationships', () => {
+  // Inline element containing block element with sibling after inline
+  // The block element must close because inline ancestor needs to close
+  checkAbbr(
+    '<div><span><div>text</div></span><p>after</p></div>',
+    '<div><span><div>text</div></span><p>after',
+    '<div><span><div>text</div></span><p>after</p></div>',
+  )
+
+  // Same situation but deeper nesting
+  checkAbbr(
+    '<div><span><p>text</p></span><span>after</span></div>',
+    '<div><span><p>text</p></span><span>after',
+    '<div><span><p>text</p></span><span>after</span></div>',
+  )
+
+  // Inline containing block on rightmost path - can omit
+  checkAbbr(
+    '<div><span><div>text</div></span></div>',
+    '<div><span><div>text',
+    '<div><span><div>text</div></span></div>',
+  )
+
+  // Normal case - no inline/block issue
+  checkAbbr('<div><p>text</p></div>', '<div><p>text', '<div><p>text</p></div>')
+
+  // Sibling after parent but no inline/block issue
+  checkAbbr(
+    '<div><div><p>text</p></div><span>after</span></div>',
+    '<div><div><p>text</div><span>after',
+    '<div><div><p>text</p></div><span>after</span></div>',
+  )
+
+  // Multi-level inline nesting with block inside
+  // Outer span is not rightmost -> Needs close -> Inner block needs close
+  checkAbbr(
+    '<div><span><b><div>text</div></b></span><p>after</p></div>',
+    '<div><span><b><div>text</div></b></span><p>after',
+    '<div><span><b><div>text</div></b></span><p>after</p></div>',
+  )
+
+  // Mixed nesting: div > span > div > span > div
+  // The middle div is inside a span that needs closing (because of outer structure)
+  // Both inner divs need closing because they are inside spans that need closing
+  checkAbbr(
+    '<div><span><div><span><div>text</div></span></div></span><p>after</p></div>',
+    '<div><span><div><span><div>text</div></div></span><p>after',
+    '<div><span><div><span><div>text</div></span></div></span><p>after</p></div>',
+  )
+})

+ 1 - 4
packages/compiler-vapor/src/generators/template.ts

@@ -71,7 +71,6 @@ export function genChildren(
 
   let offset = 0
   let prev: [variable: string, elementIndex: number] | undefined
-  let ifBranchCount = 0
   let prependCount = 0
 
   for (const [index, child] of children.entries()) {
@@ -83,8 +82,6 @@ export function genChildren(
     }
     if (child.flags & DynamicFlag.NON_TEMPLATE) {
       offset--
-    } else if (child.ifBranch) {
-      ifBranchCount++
     }
 
     const id =
@@ -100,7 +97,7 @@ export function genChildren(
     }
 
     const elementIndex = index + offset
-    const logicalIndex = elementIndex - ifBranchCount + prependCount
+    const logicalIndex = elementIndex + prependCount
     // p for "placeholder" variables that are meant for possible reuse by
     // other access paths
     const variable =

+ 0 - 1
packages/compiler-vapor/src/ir/index.ts

@@ -268,7 +268,6 @@ export interface IRDynamicInfo {
   template?: number
   hasDynamicChild?: boolean
   operation?: OperationNode
-  ifBranch?: boolean
 }
 
 export interface IREffect {

+ 31 - 1
packages/compiler-vapor/src/transform.ts

@@ -15,7 +15,14 @@ import {
   getSelfName,
   isVSlot,
 } from '@vue/compiler-dom'
-import { EMPTY_OBJ, NOOP, extend, isArray, isString } from '@vue/shared'
+import {
+  EMPTY_OBJ,
+  NOOP,
+  extend,
+  isArray,
+  isInlineTag,
+  isString,
+} from '@vue/shared'
 import {
   type BlockIRNode,
   DynamicFlag,
@@ -98,6 +105,9 @@ export class TransformContext<T extends AllNode = AllNode> {
   // whether this node is on the rightmost path of the tree
   // (all ancestors are also last effective children)
   isOnRightmostPath: boolean = true
+  // whether there is an inline ancestor that needs closing
+  // (i.e. is an inline tag and not on the rightmost path)
+  hasInlineAncestorNeedingClose: boolean = false
 
   private globalId = 0
   private nextIdMap: Map<number, number> | null = null
@@ -228,6 +238,25 @@ export class TransformContext<T extends AllNode = AllNode> {
     const isLastEffectiveChild = this.isEffectivelyLastChild(index)
     const isOnRightmostPath = this.isOnRightmostPath && isLastEffectiveChild
 
+    // propagate the inline ancestor status
+    let hasInlineAncestorNeedingClose = this.hasInlineAncestorNeedingClose
+    if (this.node.type === NodeTypes.ELEMENT) {
+      if (this.node.tag === 'template') {
+        // <template> acts as a boundary ensuring its content is parsed as a fragment,
+        // protecting inner blocks from outer inline contexts.
+        hasInlineAncestorNeedingClose = false
+      } else if (
+        !hasInlineAncestorNeedingClose &&
+        !this.isOnRightmostPath &&
+        isInlineTag(this.node.tag)
+      ) {
+        // Logic: if current node (parent of the node being created) is inline
+        // AND it's not on the rightmost path, then it needs closing.
+        // Any block child inside will need to be careful.
+        hasInlineAncestorNeedingClose = true
+      }
+    }
+
     return Object.assign(Object.create(TransformContext.prototype), this, {
       node,
       parent: this as any,
@@ -239,6 +268,7 @@ export class TransformContext<T extends AllNode = AllNode> {
       effectiveParent,
       isLastEffectiveChild,
       isOnRightmostPath,
+      hasInlineAncestorNeedingClose,
     } satisfies Partial<TransformContext<T>>)
   }
 

+ 14 - 0
packages/compiler-vapor/src/transforms/transformElement.ts

@@ -20,6 +20,8 @@ import {
   camelize,
   capitalize,
   extend,
+  isAlwaysCloseTag,
+  isBlockTag,
   isBuiltInDirective,
   isFormattingTag,
   isVoidTag,
@@ -138,6 +140,12 @@ function canOmitEndTag(
     return true
   }
 
+  // Elements in the alwaysClose list cannot have their end tags omitted
+  // unless they are on the rightmost path.
+  if (isAlwaysCloseTag(node.tag) && !context.isOnRightmostPath) {
+    return false
+  }
+
   // Formatting tags and same-name nested tags require explicit closing
   // unless on the rightmost path of the tree:
   // - Formatting tags: https://html.spec.whatwg.org/multipage/parsing.html#reconstruct-the-active-formatting-elements
@@ -149,6 +157,12 @@ function canOmitEndTag(
     return context.isOnRightmostPath
   }
 
+  // For inline element containing block element, if the inline ancestor
+  // is not on rightmost path, the block must close to avoid parsing issues
+  if (isBlockTag(node.tag) && context.hasInlineAncestorNeedingClose) {
+    return false
+  }
+
   return context.isLastEffectiveChild
 }
 

+ 0 - 1
packages/compiler-vapor/src/transforms/vIf.ts

@@ -59,7 +59,6 @@ export function processIf(
   } else {
     // check the adjacent v-if
     const siblingIf = getSiblingIf(context, true)
-    context.dynamic.ifBranch = true
 
     const siblings = context.parent && context.parent.dynamic.children
     let lastIfNode

+ 5 - 4
packages/runtime-core/src/renderer.ts

@@ -1454,7 +1454,7 @@ function baseCreateRenderer(
       if (!instance.isMounted) {
         let vnodeHook: VNodeHook | null | undefined
         const { el, props } = initialVNode
-        const { bm, m, parent, root, type } = instance
+        const { bm, parent, root, type } = instance
         const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)
 
         toggleRecurse(instance, false)
@@ -1549,9 +1549,10 @@ function baseCreateRenderer(
           }
           initialVNode.el = subTree.el
         }
-        // mounted hook
-        if (m) {
-          queuePostRenderEffect(m, undefined, parentSuspense)
+        // Mounted hooks may be added to `instance.m` while mounting.
+        // Reading `instance.m` here ensures those hooks are still scheduled.
+        if (instance.m) {
+          queuePostRenderEffect(instance.m, undefined, parentSuspense)
         }
         // onVnodeMounted
         if (

+ 30 - 0
packages/runtime-dom/__tests__/helpers/useCssVars.spec.ts

@@ -488,4 +488,34 @@ describe('useCssVars', () => {
     expect(style.getPropertyValue('--foo')).toBe('initial')
     expect(style.getPropertyValue('--bar')).toBe('initial')
   })
+
+  test('with v-if initial false then update css vars', async () => {
+    const state = reactive({ color: 'red' })
+    const root = document.createElement('div')
+    const show = ref(false)
+
+    const App = {
+      setup() {
+        useCssVars(() => state)
+        return () => (show.value ? h('div') : null)
+      },
+    }
+
+    render(h(App), root)
+    await nextTick()
+    expect(root.children.length).toBe(0)
+
+    // toggle v-if to true
+    show.value = true
+    await nextTick()
+    expect(root.children.length).toBe(1)
+    let el = root.children[0] as HTMLElement
+    expect(el.style.getPropertyValue(`--color`)).toBe('red')
+
+    // update css vars
+    state.color = 'green'
+    await nextTick()
+    el = root.children[0] as HTMLElement
+    expect(el.style.getPropertyValue(`--color`)).toBe('green')
+  })
 })

+ 15 - 6
packages/runtime-dom/src/helpers/useCssVars.ts

@@ -11,7 +11,7 @@ import {
   warn,
   watch,
 } from '@vue/runtime-core'
-import { NOOP, ShapeFlags, normalizeCssVarValue } from '@vue/shared'
+import { NOOP, ShapeFlags, extend, normalizeCssVarValue } from '@vue/shared'
 
 export const CSS_VAR_TEXT: unique symbol = Symbol(__DEV__ ? 'CSS_VAR_TEXT' : '')
 /**
@@ -99,8 +99,7 @@ export function baseUseCssVars(
     ).forEach(node => setVarsOnNode(node, vars))
   })
 
-  const applyCssCars = () => {
-    const vars = getVars()
+  const applyCssVars = (vars = getVars()) => {
     setVars(vars)
     updateTeleports(vars)
   }
@@ -108,13 +107,23 @@ export function baseUseCssVars(
   // handle cases where child component root is affected
   // and triggers reflow in onMounted
   onBeforeUpdate(() => {
-    queuePostFlushCb(applyCssCars)
+    queuePostFlushCb(applyCssVars)
   })
 
   onMounted(() => {
     // run setVars synchronously here, but run as post-effect on changes
-    watch(applyCssCars, NOOP, { flush: 'post' })
-    const ob = new MutationObserver(applyCssCars)
+    watch(
+      () => {
+        const vars = getVars()
+        // access all properties to ensure dependency tracking
+        // even if there are no DOM elements to receive vars yet
+        extend({}, vars)
+        applyCssVars(vars)
+      },
+      NOOP,
+      { flush: 'post' },
+    )
+    const ob = new MutationObserver(() => applyCssVars())
     ob.observe(getParentNode(), { childList: true })
     onUnmounted(() => ob.disconnect())
   })

+ 33 - 0
packages/runtime-vapor/__tests__/componentSlots.spec.ts

@@ -2186,5 +2186,38 @@ describe('component: slots', () => {
           '<div><!--slot--></div>',
       )
     })
+
+    test('should work with null and undefined', async () => {
+      const loop = ref<number[] | null | undefined>(undefined)
+
+      let instance: any
+      const Child = () => {
+        instance = currentInstance
+        return template('child')()
+      }
+
+      const { render } = define({
+        setup() {
+          return createComponent(Child, null, {
+            $: [
+              () =>
+                createForSlots(loop.value as any, (item, i) => ({
+                  name: item,
+                  fn: () => template(item + i)(),
+                })),
+            ],
+          })
+        },
+      })
+      render()
+
+      expect(instance.slots).toEqual({})
+      loop.value = [1]
+      await nextTick()
+      expect(instance.slots).toHaveProperty('1')
+      loop.value = null
+      await nextTick()
+      expect(instance.slots).toEqual({})
+    })
   })
 })

+ 34 - 0
packages/runtime-vapor/__tests__/for.spec.ts

@@ -691,6 +691,40 @@ describe('createFor', () => {
     expectCalledTimesToBe('Clear rows', 1, 0, 0, 0)
   })
 
+  test('should work with null and undefined', async () => {
+    const list = ref<{ name: string }[] | null | undefined>(undefined)
+
+    const { host } = define(() => {
+      const n1 = createFor(
+        () => list.value as any,
+        (item, key, index) => {
+          const span = document.createElement('li')
+          renderEffect(() => {
+            span.innerHTML = `${key.value}. ${item.value.name}`
+
+            // index should be undefined if source is not an object
+            expect(index.value).toBe(undefined)
+          })
+          return span
+        },
+        item => item.name,
+      )
+      return n1
+    }).render()
+
+    expect(host.innerHTML).toBe('<!--for-->')
+
+    // set to valid array
+    list.value = [{ name: '1' }]
+    await nextTick()
+    expect(host.innerHTML).toBe('<li>0. 1</li><!--for-->')
+
+    // null
+    list.value = null
+    await nextTick()
+    expect(host.innerHTML).toBe('<!--for-->')
+  })
+
   describe('readonly source', () => {
     test('should not allow mutation', () => {
       const arr = readonly(reactive([{ foo: 1 }]))

+ 33 - 1
packages/runtime-vapor/__tests__/helpers/useCssVars.spec.ts

@@ -297,7 +297,7 @@ describe('useVaporCssVars', () => {
     expect(host.children[0].outerHTML.includes('data-v-owner')).toBe(true)
   })
 
-  test('with teleport and nested component', async () => {
+  test('with teleport and nested fragment', async () => {
     const state = reactive({ color: 'red' })
     const target = document.createElement('div')
     document.body.appendChild(target)
@@ -517,4 +517,36 @@ describe('useVaporCssVars', () => {
 
     expect(root.innerHTML).toBe(`<!--for-->`)
   })
+
+  test('with v-if initial false then update css vars', async () => {
+    const state = reactive({ color: 'red' })
+    const root = document.createElement('div')
+    const toggle = ref(false)
+
+    define({
+      setup() {
+        useVaporCssVars(() => state)
+        return createIf(
+          () => toggle.value,
+          () => template('<div></div>')(),
+        )
+      },
+    }).render({}, root)
+
+    await nextTick()
+    expect(root.children.length).toBe(0)
+
+    // toggle v-if to true
+    toggle.value = true
+    await nextTick()
+    expect(root.children.length).toBe(1)
+    let el = root.children[0] as HTMLElement
+    expect(el.style.getPropertyValue(`--color`)).toBe('red')
+
+    // update css vars
+    state.color = 'green'
+    await nextTick()
+    el = root.children[0] as HTMLElement
+    expect(el.style.getPropertyValue(`--color`)).toBe('green')
+  })
 })

+ 44 - 0
packages/runtime-vapor/__tests__/hydration.spec.ts

@@ -1,4 +1,5 @@
 import {
+  VaporTeleport,
   child,
   createComponent,
   createPlainElement,
@@ -3584,6 +3585,49 @@ describe('Vapor Mode hydration', () => {
         `<!--teleport start--><span>bar</span><!--teleport end-->`,
       )
     })
+
+    test('should apply css vars after hydration', async () => {
+      const state = reactive({ color: 'red' })
+
+      const teleportContainer = document.createElement('div')
+      teleportContainer.id = 'teleport-css-vars'
+      teleportContainer.innerHTML =
+        `<!--teleport start anchor-->` +
+        `<span>content</span>` +
+        `<!--teleport anchor-->`
+      document.body.appendChild(teleportContainer)
+
+      const App = defineVaporComponent({
+        setup() {
+          useVaporCssVars(() => state)
+          return createComponent(
+            VaporTeleport,
+            { to: () => '#teleport-css-vars' },
+            { default: () => template('<span>content</span>', true)() },
+          )
+        },
+      })
+
+      const container = document.createElement('div')
+      container.innerHTML = '<!--teleport start--><!--teleport end-->'
+      document.body.appendChild(container)
+
+      const app = createVaporSSRApp(App)
+      app.mount(container)
+
+      await nextTick()
+
+      // css vars should be applied after hydration
+      const span = teleportContainer.querySelector('span') as HTMLElement
+      expect(span).toBeTruthy()
+      expect(span.style.getPropertyValue('--color')).toBe('red')
+      expect(span.hasAttribute('data-v-owner')).toBe(true)
+
+      // css vars should update reactively
+      state.color = 'green'
+      await nextTick()
+      expect(span.style.getPropertyValue('--color')).toBe('green')
+    })
   })
 
   describe('async component', async () => {

+ 79 - 0
packages/runtime-vapor/__tests__/vdomInterop.spec.ts

@@ -28,6 +28,7 @@ import {
   child,
   createComponent,
   createDynamicComponent,
+  createSlot,
   defineVaporAsyncComponent,
   defineVaporComponent,
   renderEffect,
@@ -127,6 +128,84 @@ describe('vdomInterop', () => {
       await nextTick()
       expect(html()).toBe('<h1>bar</h1><input>')
     })
+
+    test('slot v-model should persist when switching vapor/vdom child', async () => {
+      const VaporComp1 = defineVaporComponent({
+        name: 'VaporComp1',
+        setup() {
+          return [document.createTextNode('comp1: '), createSlot('default')]
+        },
+      })
+
+      const VDomComp2 = defineComponent({
+        name: 'VDomComp2',
+        setup(_, { slots }) {
+          return () =>
+            h('div', [
+              'comp2: ',
+              // vdom <slot/>
+              renderSlot(slots, 'default'),
+            ])
+        },
+      })
+
+      const VaporParent = defineVaporComponent({
+        name: 'VaporParent',
+        props: {
+          show: Boolean,
+          modelValue: {},
+          modelModifiers: {},
+        },
+        emits: ['update:modelValue'],
+        setup(__props) {
+          const modelValue = useModel(__props, 'modelValue')
+          return createDynamicComponent(
+            () => (__props.show ? VaporComp1 : VDomComp2),
+            null,
+            {
+              default: () => {
+                const input = template('<input>')() as any
+                applyTextModel(
+                  input,
+                  () => modelValue.value,
+                  _value => (modelValue.value = _value),
+                )
+                return input
+              },
+            },
+            true,
+          )
+        },
+      })
+
+      const show = ref(true)
+      const msg = ref('')
+
+      const { host } = define({
+        setup() {
+          return () =>
+            h(VaporParent as any, {
+              show: show.value,
+              modelValue: msg.value,
+              'onUpdate:modelValue': (value: string) => {
+                msg.value = value
+              },
+            })
+        },
+      }).render()
+
+      const input1 = host.querySelector('input')!
+      input1.value = 'hello'
+      input1.dispatchEvent(new Event('input'))
+      await nextTick()
+      expect(msg.value).toBe('hello')
+
+      show.value = false
+      await nextTick()
+
+      const input2 = host.querySelector('input')!
+      expect(input2.value).toBe('hello')
+    })
   })
 
   describe('emit', () => {

+ 2 - 0
packages/runtime-vapor/src/apiCreateFor.ts

@@ -609,6 +609,8 @@ function normalizeSource(source: any): ResolvedSource {
         values[i] = source[keys[i]]
       }
     }
+  } else {
+    values = []
   }
   return {
     values,

+ 1 - 0
packages/runtime-vapor/src/components/Teleport.ts

@@ -344,6 +344,7 @@ export class TeleportFragment extends VaporFragment {
       this.hydrateDisabledTeleport(currentHydrationNode!)
     }
 
+    updateCssVars(this)
     advanceHydrationNode(this.anchor!)
   }
 }

+ 40 - 0
packages/shared/src/domTagConfig.ts

@@ -41,6 +41,28 @@ const VOID_TAGS =
 // https://html.spec.whatwg.org/multipage/parsing.html#formatting
 const FORMATTING_TAGS = 'a,b,big,code,em,font,i,nobr,s,small,strike,strong,tt,u'
 
+// Elements that always require explicit closing tags due to HTML parsing rules.
+// These include:
+// - Formatting elements (a, b, i, etc.) - handled by FORMATTING_TAGS
+// - Elements with special parsing rules
+// - Scope boundary elements
+const ALWAYS_CLOSE_TAGS =
+  'title,style,script,noscript,template,' + // raw text / special parsing
+  'object,table,button,textarea,select,iframe,fieldset' // scope boundary / form elements
+
+// Inline elements
+const INLINE_TAGS =
+  'a,abbr,acronym,b,bdi,bdo,big,br,button,canvas,cite,code,data,datalist,' +
+  'del,dfn,em,embed,i,iframe,img,input,ins,kbd,label,map,mark,meter,' +
+  'noscript,object,output,picture,progress,q,ruby,s,samp,script,select,' +
+  'small,span,strong,sub,sup,svg,textarea,time,u,tt,var,video'
+
+// Block elements
+const BLOCK_TAGS =
+  'address,article,aside,blockquote,dd,details,dialog,div,dl,dt,fieldset,' +
+  'figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,hr,li,' +
+  'main,menu,nav,ol,p,pre,section,table,ul'
+
 /**
  * Compiler only.
  * Do NOT use in runtime code paths unless behind `__DEV__` flag.
@@ -71,3 +93,21 @@ export const isVoidTag: (key: string) => boolean =
  */
 export const isFormattingTag: (key: string) => boolean =
   /*@__PURE__*/ makeMap(FORMATTING_TAGS)
+/**
+ * Compiler only.
+ * Do NOT use in runtime code paths unless behind `__DEV__` flag.
+ */
+export const isAlwaysCloseTag: (key: string) => boolean =
+  /*@__PURE__*/ makeMap(ALWAYS_CLOSE_TAGS)
+/**
+ * Compiler only.
+ * Do NOT use in runtime code paths unless behind `__DEV__` flag.
+ */
+export const isInlineTag: (key: string) => boolean =
+  /*@__PURE__*/ makeMap(INLINE_TAGS)
+/**
+ * Compiler only.
+ * Do NOT use in runtime code paths unless behind `__DEV__` flag.
+ */
+export const isBlockTag: (key: string) => boolean =
+  /*@__PURE__*/ makeMap(BLOCK_TAGS)