]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(vapor): dynamic component
author三咲智子 Kevin Deng <sxzz@sxzz.moe>
Wed, 13 Nov 2024 06:56:39 +0000 (14:56 +0800)
committer三咲智子 Kevin Deng <sxzz@sxzz.moe>
Wed, 13 Nov 2024 06:56:39 +0000 (14:56 +0800)
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts
packages/compiler-vapor/src/generators/component.ts
packages/compiler-vapor/src/ir/index.ts
packages/compiler-vapor/src/transforms/transformElement.ts
packages/runtime-vapor/src/helpers/resolveAssets.ts
packages/runtime-vapor/src/index.ts

index 63c6e2d287ae682d31091601955a5ec20f514f42..8e4509e196a142efafc571ae044561e4dd316e2b 100644 (file)
@@ -213,6 +213,54 @@ export function render(_ctx) {
 }"
 `;
 
+exports[`compiler: element transform > dynamic component > capitalized version w/ static binding 1`] = `
+"import { resolveDynamicComponent as _resolveDynamicComponent, createComponent as _createComponent } from 'vue/vapor';
+
+export function render(_ctx) {
+  const n0 = _createComponent(_resolveDynamicComponent("foo"), null, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > dynamic component > dynamic binding 1`] = `
+"import { resolveDynamicComponent as _resolveDynamicComponent, createComponent as _createComponent } from 'vue/vapor';
+
+export function render(_ctx) {
+  const n0 = _createComponent(_resolveDynamicComponent(_ctx.foo), null, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > dynamic component > dynamic binding shorthand 1`] = `
+"import { resolveDynamicComponent as _resolveDynamicComponent, createComponent as _createComponent } from 'vue/vapor';
+
+export function render(_ctx) {
+  const n0 = _createComponent(_resolveDynamicComponent(_ctx.is), null, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > dynamic component > normal component with is prop 1`] = `
+"import { resolveComponent as _resolveComponent, createComponent as _createComponent } from 'vue/vapor';
+
+export function render(_ctx) {
+  const _component_custom_input = _resolveComponent("custom-input")
+  const n0 = _createComponent(_component_custom_input, [
+    { is: () => ("foo") }
+  ], null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > dynamic component > static binding 1`] = `
+"import { resolveDynamicComponent as _resolveDynamicComponent, createComponent as _createComponent } from 'vue/vapor';
+
+export function render(_ctx) {
+  const n0 = _createComponent(_resolveDynamicComponent("foo"), null, null, true)
+  return n0
+}"
+`;
+
 exports[`compiler: element transform > empty template 1`] = `
 "
 export function render(_ctx) {
index 746ac44cc6e7b1a81f5e17552ab35b8ab7128093..dacfab9825ad940beaabf406a11067383281b321 100644 (file)
@@ -423,6 +423,117 @@ describe('compiler: element transform', () => {
     })
   })
 
+  describe('dynamic component', () => {
+    test('static binding', () => {
+      const { code, ir, vaporHelpers } = compileWithElementTransform(
+        `<component is="foo" />`,
+      )
+      expect(code).toMatchSnapshot()
+      expect(vaporHelpers).toContain('resolveDynamicComponent')
+      expect(ir.block.operation).toMatchObject([
+        {
+          type: IRNodeTypes.CREATE_COMPONENT_NODE,
+          tag: 'component',
+          asset: true,
+          root: true,
+          props: [[]],
+          dynamic: {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: 'foo',
+            isStatic: true,
+          },
+        },
+      ])
+    })
+
+    test('capitalized version w/ static binding', () => {
+      const { code, ir, vaporHelpers } = compileWithElementTransform(
+        `<Component is="foo" />`,
+      )
+      expect(code).toMatchSnapshot()
+      expect(vaporHelpers).toContain('resolveDynamicComponent')
+      expect(ir.block.operation).toMatchObject([
+        {
+          type: IRNodeTypes.CREATE_COMPONENT_NODE,
+          tag: 'Component',
+          asset: true,
+          root: true,
+          props: [[]],
+          dynamic: {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: 'foo',
+            isStatic: true,
+          },
+        },
+      ])
+    })
+
+    test('dynamic binding', () => {
+      const { code, ir, vaporHelpers } = compileWithElementTransform(
+        `<component :is="foo" />`,
+      )
+      expect(code).toMatchSnapshot()
+      expect(vaporHelpers).toContain('resolveDynamicComponent')
+      expect(ir.block.operation).toMatchObject([
+        {
+          type: IRNodeTypes.CREATE_COMPONENT_NODE,
+          tag: 'component',
+          asset: true,
+          root: true,
+          props: [[]],
+          dynamic: {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: 'foo',
+            isStatic: false,
+          },
+        },
+      ])
+    })
+
+    test('dynamic binding shorthand', () => {
+      const { code, ir, vaporHelpers } =
+        compileWithElementTransform(`<component :is />`)
+      expect(code).toMatchSnapshot()
+      expect(vaporHelpers).toContain('resolveDynamicComponent')
+      expect(ir.block.operation).toMatchObject([
+        {
+          type: IRNodeTypes.CREATE_COMPONENT_NODE,
+          tag: 'component',
+          asset: true,
+          root: true,
+          props: [[]],
+          dynamic: {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: 'is',
+            isStatic: false,
+          },
+        },
+      ])
+    })
+
+    // #3934
+    test('normal component with is prop', () => {
+      const { code, ir, vaporHelpers } = compileWithElementTransform(
+        `<custom-input is="foo" />`,
+        {
+          isNativeTag: () => false,
+        },
+      )
+      expect(code).toMatchSnapshot()
+      expect(vaporHelpers).toContain('resolveComponent')
+      expect(vaporHelpers).not.toContain('resolveDynamicComponent')
+      expect(ir.block.operation).toMatchObject([
+        {
+          type: IRNodeTypes.CREATE_COMPONENT_NODE,
+          tag: 'custom-input',
+          asset: true,
+          root: true,
+          props: [[{ key: { content: 'is' }, values: [{ content: 'foo' }] }]],
+        },
+      ])
+    })
+  })
+
   test('static props', () => {
     const { code, ir } = compileWithElementTransform(
       `<div id="foo" class="bar" />`,
index 8b2534b9fb1371b99bf2615764d762c149cf34d4..a0df807902aec4efad4bb8f61110f287ae4845ea 100644 (file)
@@ -65,7 +65,12 @@ export function genCreateComponent(
   ]
 
   function genTag() {
-    if (oper.asset) {
+    if (oper.dynamic) {
+      return genCall(
+        vaporHelper('resolveDynamicComponent'),
+        genExpression(oper.dynamic, context),
+      )
+    } else if (oper.asset) {
       return toValidAssetId(oper.tag, 'component')
     } else {
       return genExpression(
index 8e10ab0a7c2c3b8d6b97e208a98bf698d85a79b4..0b0b87fc4b037768af7c48c49e660e0508ffd18a 100644 (file)
@@ -194,6 +194,7 @@ export interface CreateComponentIRNode extends BaseIRNode {
   asset: boolean
   root: boolean
   once: boolean
+  dynamic?: SimpleExpressionNode
 }
 
 export interface DeclareOldRefIRNode extends BaseIRNode {
index 60d7f602348c28291f791591c02c9fb09ee56cf6..1af0ff8725e6db9038f943b8ed8306c1da6a57e0 100644 (file)
@@ -1,13 +1,16 @@
 import { isValidHTMLNesting } from '../html-nesting'
 import {
   type AttributeNode,
+  type ComponentNode,
   type ElementNode,
   ElementTypes,
   ErrorCodes,
   NodeTypes,
+  type PlainElementNode,
   type SimpleExpressionNode,
   createCompilerError,
   createSimpleExpression,
+  isStaticArgOf,
 } from '@vue/compiler-dom'
 import {
   camelize,
@@ -33,6 +36,7 @@ import {
   type VaporDirectiveNode,
 } from '../ir'
 import { EMPTY_EXPRESSION } from './utils'
+import { findProp } from '../utils'
 
 export const isReservedProp: (key: string) => boolean = /*#__PURE__*/ makeMap(
   // the leading comma is intentional so empty string "" is also included
@@ -51,46 +55,56 @@ export const transformElement: NodeTransform = (node, context) => {
     )
       return
 
-    const { tag, tagType } = node
-    const isComponent = tagType === ElementTypes.COMPONENT
+    const isComponent = node.tagType === ElementTypes.COMPONENT
+    const isDynamicComponent = isComponentTag(node.tag)
     const propsResult = buildProps(
       node,
       context as TransformContext<ElementNode>,
       isComponent,
+      isDynamicComponent,
     )
 
     ;(isComponent ? transformComponentElement : transformNativeElement)(
-      tag,
+      node as any,
       propsResult,
       context as TransformContext<ElementNode>,
+      isDynamicComponent,
     )
   }
 }
 
 function transformComponentElement(
-  tag: string,
+  node: ComponentNode,
   propsResult: PropsResult,
   context: TransformContext,
+  isDynamicComponent: boolean,
 ) {
-  let asset = true
+  const dynamicComponent = isDynamicComponent
+    ? resolveDynamicComponent(node)
+    : undefined
 
-  const fromSetup = resolveSetupReference(tag, context)
-  if (fromSetup) {
-    tag = fromSetup
-    asset = false
-  }
+  let { tag } = node
+  let asset = true
 
-  const dotIndex = tag.indexOf('.')
-  if (dotIndex > 0) {
-    const ns = resolveSetupReference(tag.slice(0, dotIndex), context)
-    if (ns) {
-      tag = ns + tag.slice(dotIndex)
+  if (!dynamicComponent) {
+    const fromSetup = resolveSetupReference(tag, context)
+    if (fromSetup) {
+      tag = fromSetup
       asset = false
     }
-  }
 
-  if (asset) {
-    context.component.add(tag)
+    const dotIndex = tag.indexOf('.')
+    if (dotIndex > 0) {
+      const ns = resolveSetupReference(tag.slice(0, dotIndex), context)
+      if (ns) {
+        tag = ns + tag.slice(dotIndex)
+        asset = false
+      }
+    }
+
+    if (asset) {
+      context.component.add(tag)
+    }
   }
 
   context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
@@ -106,10 +120,28 @@ function transformComponentElement(
     root,
     slots: [...context.slots],
     once: context.inVOnce,
+    dynamic: dynamicComponent,
   })
   context.slots = []
 }
 
+function resolveDynamicComponent(node: ComponentNode) {
+  const isProp = findProp(node, 'is', false, true /* allow empty */)
+  if (!isProp) return
+
+  if (isProp.type === NodeTypes.ATTRIBUTE) {
+    return isProp.value && createSimpleExpression(isProp.value.content, true)
+  } else {
+    return (
+      isProp.exp ||
+      // #10469 handle :is shorthand
+      extend(createSimpleExpression(`is`, false, isProp.arg!.loc), {
+        ast: null,
+      })
+    )
+  }
+}
+
 function resolveSetupReference(name: string, context: TransformContext) {
   const bindings = context.options.bindingMetadata
   if (!bindings || bindings.__isScriptSetup === false) {
@@ -128,10 +160,11 @@ function resolveSetupReference(name: string, context: TransformContext) {
 }
 
 function transformNativeElement(
-  tag: string,
+  node: PlainElementNode,
   propsResult: PropsResult,
   context: TransformContext<ElementNode>,
 ) {
+  const { tag } = node
   const { scopeId } = context.options
 
   let template = ''
@@ -189,6 +222,7 @@ export function buildProps(
   node: ElementNode,
   context: TransformContext<ElementNode>,
   isComponent: boolean,
+  isDynamicComponent: boolean,
 ): PropsResult {
   const props = node.props as (VaporDirectiveNode | AttributeNode)[]
   if (props.length === 0) return [false, []]
@@ -252,6 +286,18 @@ export function buildProps(
       }
     }
 
+    // exclude `is` prop for <component>
+    if (
+      (isDynamicComponent &&
+        prop.type === NodeTypes.ATTRIBUTE &&
+        prop.name === 'is') ||
+      (prop.type === NodeTypes.DIRECTIVE &&
+        prop.name === 'bind' &&
+        isStaticArgOf(prop.arg, 'is'))
+    ) {
+      continue
+    }
+
     const result = transformProp(prop, node, context)
     if (result) {
       dynamicExpr.push(result.key, result.value)
@@ -362,3 +408,7 @@ function mergePropValues(existing: IRProp, incoming: IRProp) {
   const newValues = incoming.values
   existing.values.push(...newValues)
 }
+
+function isComponentTag(tag: string) {
+  return tag === 'component' || tag === 'Component'
+}
index 06bd31f57f5f6796d3b405f2432220d51ed13445..b3281b5542be68155ba4ae9168f1dcb5a442d6a3 100644 (file)
@@ -1,4 +1,4 @@
-import { camelize, capitalize } from '@vue/shared'
+import { camelize, capitalize, isString } from '@vue/shared'
 import { type Directive, warn } from '..'
 import { type Component, currentInstance } from '../component'
 import { getComponentName } from '../component'
@@ -79,3 +79,16 @@ function resolve(registry: Record<string, any> | undefined, name: string) {
       registry[capitalize(camelize(name))])
   )
 }
+
+/**
+ * @private
+ */
+export function resolveDynamicComponent(
+  component: string | Component,
+): string | Component {
+  if (isString(component)) {
+    return resolveAsset(COMPONENTS, component, false) || component
+  } else {
+    return component
+  }
+}
index b7b7592f59511704eeaeccb05ed8d459b15889df..0a652b2081dc05c76d2e7a54d5462c495c7d632a 100644 (file)
@@ -131,7 +131,11 @@ export { createFor, createForSlots } from './apiCreateFor'
 export { createComponent } from './apiCreateComponent'
 export { createSelector } from './apiCreateSelector'
 
-export { resolveComponent, resolveDirective } from './helpers/resolveAssets'
+export {
+  resolveComponent,
+  resolveDirective,
+  resolveDynamicComponent,
+} from './helpers/resolveAssets'
 export { toHandlers } from './helpers/toHandlers'
 
 export { withDestructure } from './destructure'