From 6a617eb35886be44d7f34b2e576410a8aa739cc8 Mon Sep 17 00:00:00 2001
From: Rizumu Ayaka <rizumu@ayaka.moe>
Date: Wed, 20 Dec 2023 23:05:18 +0800
Subject: [PATCH] feat: support simple case of using components

---
 packages/compiler-vapor/src/generate.ts         | 17 +++++++++++++++++
 packages/compiler-vapor/src/ir.ts               |  8 ++++++++
 .../src/transforms/transformElement.ts          | 16 ++++++++++++++--
 .../runtime-vapor/src/apiCreateComponent.ts     |  8 ++++++++
 packages/runtime-vapor/src/index.ts             |  1 +
 packages/runtime-vapor/src/render.ts            | 15 +++++++++------
 6 files changed, 57 insertions(+), 8 deletions(-)
 create mode 100644 packages/runtime-vapor/src/apiCreateComponent.ts

diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts
index b79ac2901..459b39448 100644
--- a/packages/compiler-vapor/src/generate.ts
+++ b/packages/compiler-vapor/src/generate.ts
@@ -28,6 +28,7 @@ import {
   type PrependNodeIRNode,
   type AppendNodeIRNode,
   IRNodeTypes,
+  CreateComponentIRNode,
 } from './ir'
 import { SourceMapGenerator } from 'source-map-js'
 import { camelize, isGloballyAllowed, isString, makeMap } from '@vue/shared'
@@ -376,6 +377,8 @@ function genOperation(oper: OperationNode, context: CodegenContext) {
       return genSetHtml(oper, context)
     case IRNodeTypes.CREATE_TEXT_NODE:
       return genCreateTextNode(oper, context)
+    case IRNodeTypes.CREATE_COMPONENT_NODE:
+      return genCreateComponentNode(oper, context)
     case IRNodeTypes.INSERT_NODE:
       return genInsertNode(oper, context)
     case IRNodeTypes.PREPEND_NODE:
@@ -437,6 +440,20 @@ function genCreateTextNode(
   )
 }
 
+function genCreateComponentNode(
+  oper: CreateComponentIRNode,
+  context: CodegenContext,
+) {
+  const { pushNewline, pushFnCall, vaporHelper } = context
+  pushNewline(`const n${oper.id} = `)
+  // TODO: support props
+  pushFnCall(
+    vaporHelper('createComponent'),
+    () => genExpression(oper.tag, context),
+    '{}',
+  )
+}
+
 function genInsertNode(oper: InsertNodeIRNode, context: CodegenContext) {
   const { newline, pushFnCall, vaporHelper } = context
   const elements = ([] as number[]).concat(oper.element)
diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts
index 2af5455d6..a217ce6d6 100644
--- a/packages/compiler-vapor/src/ir.ts
+++ b/packages/compiler-vapor/src/ir.ts
@@ -22,6 +22,7 @@ export enum IRNodeTypes {
   PREPEND_NODE,
   APPEND_NODE,
   CREATE_TEXT_NODE,
+  CREATE_COMPONENT_NODE,
 
   WITH_DIRECTIVE,
 }
@@ -123,6 +124,12 @@ export interface WithDirectiveIRNode extends BaseIRNode {
   dir: VaporDirectiveNode
 }
 
+export interface CreateComponentIRNode extends BaseIRNode {
+  type: IRNodeTypes.CREATE_COMPONENT_NODE
+  id: number
+  tag: string
+}
+
 export type IRNode =
   | OperationNode
   | RootIRNode
@@ -138,6 +145,7 @@ export type OperationNode =
   | PrependNodeIRNode
   | AppendNodeIRNode
   | WithDirectiveIRNode
+  | CreateComponentIRNode
 
 export interface IRDynamicInfo {
   id: number | null
diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts
index 4bca80478..8d8392c64 100644
--- a/packages/compiler-vapor/src/transforms/transformElement.ts
+++ b/packages/compiler-vapor/src/transforms/transformElement.ts
@@ -25,7 +25,17 @@ export const transformElement: NodeTransform = (node, ctx) => {
     const { tag, props } = node
     const isComponent = node.tagType === ElementTypes.COMPONENT
 
-    ctx.template += `<${tag}`
+    if (isComponent) {
+      ctx.dynamic.ghost = true
+      ctx.registerOperation({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        loc: node.loc,
+        id: ctx.reference(),
+        tag,
+      })
+    } else {
+      ctx.template += `<${tag}`
+    }
     if (props.length) {
       buildProps(
         node,
@@ -34,7 +44,9 @@ export const transformElement: NodeTransform = (node, ctx) => {
         isComponent,
       )
     }
-    ctx.template += `>` + ctx.childrenTemplate.join('')
+    if (!isComponent) {
+      ctx.template += `>` + ctx.childrenTemplate.join('')
+    }
 
     // TODO remove unnecessary close tag, e.g. if it's the last element of the template
     if (!isVoidTag(tag)) {
diff --git a/packages/runtime-vapor/src/apiCreateComponent.ts b/packages/runtime-vapor/src/apiCreateComponent.ts
new file mode 100644
index 000000000..70d7b37f1
--- /dev/null
+++ b/packages/runtime-vapor/src/apiCreateComponent.ts
@@ -0,0 +1,8 @@
+import { Data } from '@vue/shared'
+import { Component } from './component'
+import { render } from './render'
+
+export function createComponent(comp: Component, props: Data = {}) {
+  const instance = render(comp, props)
+  return instance.block
+}
diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts
index 7f6165677..4a10f5226 100644
--- a/packages/runtime-vapor/src/index.ts
+++ b/packages/runtime-vapor/src/index.ts
@@ -45,4 +45,5 @@ export * from './directive'
 export * from './dom'
 export * from './directives/vShow'
 export * from './apiLifecycle'
+export * from './apiCreateComponent'
 export { getCurrentInstance, type ComponentInternalInstance } from './component'
diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts
index a2f505ecd..66b524ec9 100644
--- a/packages/runtime-vapor/src/render.ts
+++ b/packages/runtime-vapor/src/render.ts
@@ -20,11 +20,12 @@ export type BlockFn = (props: any, ctx: any) => Block
 export function render(
   comp: Component,
   props: Data,
-  container: string | ParentNode,
+  container?: string | ParentNode,
 ): ComponentInternalInstance {
   const instance = createComponentInstance(comp)
   initProps(instance, props)
-  return mountComponent(instance, (container = normalizeContainer(container)))
+  if (container) container = normalizeContainer(container)
+  return mountComponent(instance, container as ParentNode)
 }
 
 export function normalizeContainer(container: string | ParentNode): ParentNode {
@@ -35,9 +36,9 @@ export function normalizeContainer(container: string | ParentNode): ParentNode {
 
 export function mountComponent(
   instance: ComponentInternalInstance,
-  container: ParentNode,
+  container?: ParentNode,
 ) {
-  instance.container = container
+  if (container) instance.container = container
 
   setCurrentInstance(instance)
   const block = instance.scope.run(() => {
@@ -68,8 +69,10 @@ export function mountComponent(
   bm && invokeArrayFns(bm)
   invokeDirectiveHook(instance, 'beforeMount')
 
-  insert(block, instance.container)
-  instance.isMountedRef.value = true
+  if (instance.container) {
+    insert(block, instance.container)
+    instance.isMountedRef.value = true
+  }
 
   // hook: mounted
   invokeDirectiveHook(instance, 'mounted')