]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler-vapor): `v-if` (#96)
authorRizumu Ayaka <rizumu@ayaka.moe>
Sat, 27 Jan 2024 17:31:20 +0000 (01:31 +0800)
committerGitHub <noreply@github.com>
Sat, 27 Jan 2024 17:31:20 +0000 (01:31 +0800)
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
12 files changed:
README.md
packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/vIf.spec.ts [new file with mode: 0644]
packages/compiler-vapor/src/compile.ts
packages/compiler-vapor/src/generate.ts
packages/compiler-vapor/src/generators/if.ts [new file with mode: 0644]
packages/compiler-vapor/src/index.ts
packages/compiler-vapor/src/ir.ts
packages/compiler-vapor/src/transform.ts
packages/compiler-vapor/src/transforms/vIf.ts [new file with mode: 0644]
packages/runtime-vapor/src/dom.ts

index 8ff103f92cdf67e83c463937cb808bdf9e5eb8b4..143d7f371d308935a3ce5ab5f68d7c433ada50b4 100644 (file)
--- a/README.md
+++ b/README.md
@@ -44,6 +44,10 @@ PR are welcome!
     - #17
     - needs #19 first
   - [ ] `v-if` / `v-else` / `v-else-if`
+    - [x] `v-if`
+    - [ ] `v-else`
+    - [ ] `v-else-if`
+    - [ ] with `<template>`
     - #9
   - [ ] `v-for`
     - #21
index 8de407e750aec702204eb5680f58c211cc14ae5d..35bc588ce168dbf2832b79abe48154246bd2630f 100644 (file)
@@ -175,7 +175,6 @@ exports[`compile > dynamic root 1`] = `
 
 export function render(_ctx) {
   const t0 = _fragment()
-
   const n0 = t0()
   const n1 = _createTextNode(1)
   const n2 = _createTextNode(2)
@@ -217,7 +216,6 @@ export function render(_ctx) {
 exports[`compile > expression parsing > interpolation 1`] = `
 "(() => {
   const t0 = _fragment()
-
   const n0 = t0()
   const n1 = _createTextNode(a + b.value)
   _append(n0, n1)
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap
new file mode 100644 (file)
index 0000000..441f734
--- /dev/null
@@ -0,0 +1,61 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler: v-if > basic v-if 1`] = `
+"import { template as _template, fragment as _fragment, createIf as _createIf, children as _children, renderEffect as _renderEffect, setText as _setText, append as _append } from 'vue/vapor';
+
+export function render(_ctx) {
+  const t0 = _template("<div></div>")
+  const t1 = _fragment()
+  const n0 = t1()
+  const n1 = _createIf(() => (_ctx.ok), () => {
+    const n2 = t0()
+    const { 0: [n3],} = _children(n2)
+    _renderEffect(() => {
+      _setText(n3, _ctx.msg)
+    })
+    return n2
+  })
+  _append(n0, n1)
+  return n0
+}"
+`;
+
+exports[`compiler: v-if > dedupe same template 1`] = `
+"import { template as _template, fragment as _fragment, createIf as _createIf, append as _append } from 'vue/vapor';
+
+export function render(_ctx) {
+  const t0 = _template("<div>hello</div>")
+  const t1 = _fragment()
+  const n0 = t1()
+  const n1 = _createIf(() => (_ctx.ok), () => {
+    const n2 = t0()
+    return n2
+  })
+  const n3 = _createIf(() => (_ctx.ok), () => {
+    const n4 = t0()
+    return n4
+  })
+  _append(n0, n1, n3)
+  return n0
+}"
+`;
+
+exports[`compiler: v-if > template v-if 1`] = `
+"import { template as _template, fragment as _fragment, createIf as _createIf, children as _children, renderEffect as _renderEffect, setText as _setText, append as _append } from 'vue/vapor';
+
+export function render(_ctx) {
+  const t0 = _template("<div></div>hello<p></p>")
+  const t1 = _fragment()
+  const n0 = t1()
+  const n1 = _createIf(() => (_ctx.ok), () => {
+    const n2 = t0()
+    const { 2: [n3],} = _children(n2)
+    _renderEffect(() => {
+      _setText(n3, _ctx.msg)
+    })
+    return n2
+  })
+  _append(n0, n1)
+  return n0
+}"
+`;
diff --git a/packages/compiler-vapor/__tests__/transforms/vIf.spec.ts b/packages/compiler-vapor/__tests__/transforms/vIf.spec.ts
new file mode 100644 (file)
index 0000000..b7f7b18
--- /dev/null
@@ -0,0 +1,52 @@
+import { makeCompile } from './_utils'
+import {
+  transformElement,
+  transformInterpolation,
+  transformOnce,
+  transformVIf,
+  transformVText,
+} from '../../src'
+
+const compileWithVIf = makeCompile({
+  nodeTransforms: [
+    transformOnce,
+    transformInterpolation,
+    transformVIf,
+    transformElement,
+  ],
+  directiveTransforms: {
+    text: transformVText,
+  },
+})
+
+describe('compiler: v-if', () => {
+  test('basic v-if', () => {
+    const { code } = compileWithVIf(`<div v-if="ok">{{msg}}</div>`)
+    expect(code).matchSnapshot()
+  })
+
+  test('template v-if', () => {
+    const { code } = compileWithVIf(
+      `<template v-if="ok"><div/>hello<p v-text="msg"/></template>`,
+    )
+    expect(code).matchSnapshot()
+  })
+
+  test('dedupe same template', () => {
+    const { code, ir } = compileWithVIf(
+      `<div v-if="ok">hello</div><div v-if="ok">hello</div>`,
+    )
+    expect(code).matchSnapshot()
+    expect(ir.template).lengthOf(2)
+  })
+
+  test.todo('v-if with v-once')
+  test.todo('component v-if')
+  test.todo('v-if + v-else')
+  test.todo('v-if + v-else-if')
+  test.todo('v-if + v-else-if + v-else')
+  test.todo('comment between branches')
+  describe.todo('errors')
+  describe.todo('codegen')
+  test.todo('v-on with v-if')
+})
index f25e6f5a4661cad8117f800afa708c5b0742f18b..0c7b139456fd9689cc275965a12c2128d097d56f 100644 (file)
@@ -24,6 +24,7 @@ import { transformRef } from './transforms/transformRef'
 import { transformInterpolation } from './transforms/transformInterpolation'
 import type { HackOptions } from './ir'
 import { transformVModel } from './transforms/vModel'
+import { transformVIf } from './transforms/vIf'
 
 export type CompilerOptions = HackOptions<BaseCompilerOptions>
 
@@ -96,7 +97,13 @@ export function getBaseTransformPreset(
   prefixIdentifiers?: boolean,
 ): TransformPreset {
   return [
-    [transformOnce, transformRef, transformInterpolation, transformElement],
+    [
+      transformOnce,
+      transformRef,
+      transformInterpolation,
+      transformVIf,
+      transformElement,
+    ],
     {
       bind: transformVBind,
       on: transformVOn,
index 1352fba7d70e7a654f51ca642ac86b92be1aeba6..9824925db098af6f3e86de726ba69f7fbc4aedda 100644 (file)
@@ -8,6 +8,7 @@ import {
   locStub,
 } from '@vue/compiler-dom'
 import {
+  type BlockFunctionIRNode,
   type IRDynamicChildren,
   IRNodeTypes,
   type OperationNode,
@@ -26,6 +27,7 @@ import { genSetRef } from './generators/ref'
 import { genSetModelValue } from './generators/modelValue'
 import { genAppendNode, genInsertNode, genPrependNode } from './generators/dom'
 import { genWithDirective } from './generators/directive'
+import { genIf } from './generators/if'
 
 interface CodegenOptions extends BaseCodegenOptions {
   expressionPlugins?: ParserPlugin[]
@@ -271,49 +273,11 @@ export function generate(
         )
       } else {
         // fragment
-        pushNewline(
-          `const t0 = ${vaporHelper('fragment')}()\n`,
-          NewlineType.End,
-        )
+        pushNewline(`const t${i} = ${vaporHelper('fragment')}()`)
       }
     })
 
-    {
-      pushNewline(`const n${ir.dynamic.id} = t0()`)
-
-      const children = genChildren(ir.dynamic.children)
-      if (children) {
-        pushNewline(
-          `const ${children} = ${vaporHelper('children')}(n${ir.dynamic.id})`,
-        )
-      }
-
-      const directiveOps = ir.operation.filter(
-        (oper): oper is WithDirectiveIRNode =>
-          oper.type === IRNodeTypes.WITH_DIRECTIVE,
-      )
-      for (const directives of groupDirective(directiveOps)) {
-        genWithDirective(directives, ctx)
-      }
-
-      for (const operation of ir.operation) {
-        genOperation(operation, ctx)
-      }
-
-      for (const { operations } of ir.effect) {
-        pushNewline(`${vaporHelper('renderEffect')}(() => {`)
-        withIndent(() => {
-          for (const operation of operations) {
-            genOperation(operation, ctx)
-          }
-        })
-        pushNewline('})')
-      }
-
-      // TODO multiple-template
-      // TODO return statement in IR
-      pushNewline(`return n${ir.dynamic.id}`)
-    }
+    genBlockFunctionContent(ir, ctx)
   })
 
   newline()
@@ -396,6 +360,8 @@ function genOperation(oper: OperationNode, context: CodegenContext) {
       return genPrependNode(oper, context)
     case IRNodeTypes.APPEND_NODE:
       return genAppendNode(oper, context)
+    case IRNodeTypes.IF:
+      return genIf(oper, context)
     case IRNodeTypes.WITH_DIRECTIVE:
       // generated, skip
       return
@@ -404,6 +370,45 @@ function genOperation(oper: OperationNode, context: CodegenContext) {
   }
 }
 
+export function genBlockFunctionContent(
+  ir: BlockFunctionIRNode | RootIRNode,
+  ctx: CodegenContext,
+) {
+  const { pushNewline, withIndent, vaporHelper } = ctx
+  pushNewline(`const n${ir.dynamic.id} = t${ir.templateIndex}()`)
+
+  const children = genChildren(ir.dynamic.children)
+  if (children) {
+    pushNewline(
+      `const ${children} = ${vaporHelper('children')}(n${ir.dynamic.id})`,
+    )
+  }
+
+  const directiveOps = ir.operation.filter(
+    (oper): oper is WithDirectiveIRNode =>
+      oper.type === IRNodeTypes.WITH_DIRECTIVE,
+  )
+  for (const directives of groupDirective(directiveOps)) {
+    genWithDirective(directives, ctx)
+  }
+
+  for (const operation of ir.operation) {
+    genOperation(operation, ctx)
+  }
+
+  for (const { operations } of ir.effect) {
+    pushNewline(`${vaporHelper('renderEffect')}(() => {`)
+    withIndent(() => {
+      for (const operation of operations) {
+        genOperation(operation, ctx)
+      }
+    })
+    pushNewline('})')
+  }
+
+  pushNewline(`return n${ir.dynamic.id}`)
+}
+
 function groupDirective(ops: WithDirectiveIRNode[]): WithDirectiveIRNode[][] {
   const directiveMap: Record<number, WithDirectiveIRNode[]> = {}
   for (const oper of ops) {
diff --git a/packages/compiler-vapor/src/generators/if.ts b/packages/compiler-vapor/src/generators/if.ts
new file mode 100644 (file)
index 0000000..69ef737
--- /dev/null
@@ -0,0 +1,28 @@
+import { type CodegenContext, genBlockFunctionContent } from '../generate'
+import type { BlockFunctionIRNode, IfIRNode } from '../ir'
+import { genExpression } from './expression'
+
+export function genIf(oper: IfIRNode, context: CodegenContext) {
+  const { pushFnCall, vaporHelper, pushNewline, push, withIndent } = context
+  const { condition, positive, negative } = oper
+
+  pushNewline(`const n${oper.id} = `)
+  pushFnCall(
+    vaporHelper('createIf'),
+    () => {
+      push('() => (')
+      genExpression(condition, context)
+      push(')')
+    },
+    () => genBlockFunction(positive),
+    !!negative && (() => genBlockFunction(negative!)),
+  )
+
+  function genBlockFunction(oper: BlockFunctionIRNode) {
+    push('() => {')
+    withIndent(() => {
+      genBlockFunctionContent(oper, context)
+    })
+    pushNewline('}')
+  }
+}
index 3b1c3d429872d7a56e92774bccc474d8e0dac962..2402d410ad99576067b10cda7369cb73ddec6c57 100644 (file)
@@ -12,3 +12,4 @@ export { transformVOn } from './transforms/vOn'
 export { transformOnce } from './transforms/vOnce'
 export { transformVShow } from './transforms/vShow'
 export { transformVText } from './transforms/vText'
+export { transformVIf } from './transforms/vIf'
index 708262d7228718dd7b1015204d6b47fddf433652..09ca59fff6b95db1bba6da1c889221c78acbd882 100644 (file)
@@ -5,6 +5,7 @@ import type {
   RootNode,
   SimpleExpressionNode,
   SourceLocation,
+  TemplateChildNode,
 } from '@vue/compiler-dom'
 import type { Prettify } from '@vue/shared'
 import type { DirectiveTransform, NodeTransform } from './transform'
@@ -27,6 +28,9 @@ export enum IRNodeTypes {
   CREATE_TEXT_NODE,
 
   WITH_DIRECTIVE,
+
+  IF,
+  BLOCK_FUNCTION,
 }
 
 export interface BaseIRNode {
@@ -37,16 +41,30 @@ export interface BaseIRNode {
 // TODO refactor
 export type VaporHelper = keyof typeof import('../../runtime-vapor/src')
 
-export interface RootIRNode extends BaseIRNode {
-  type: IRNodeTypes.ROOT
-  source: string
-  node: RootNode
-  template: Array<TemplateFactoryIRNode | FragmentFactoryIRNode>
+export interface BlockFunctionIRNode extends BaseIRNode {
+  type: IRNodeTypes.BLOCK_FUNCTION
+  node: RootNode | TemplateChildNode
+  templateIndex: number
   dynamic: IRDynamicInfo
   effect: IREffect[]
   operation: OperationNode[]
 }
 
+export interface RootIRNode extends Omit<BlockFunctionIRNode, 'type'> {
+  type: IRNodeTypes.ROOT
+  node: RootNode
+  source: string
+  template: Array<TemplateFactoryIRNode | FragmentFactoryIRNode>
+}
+
+export interface IfIRNode extends BaseIRNode {
+  type: IRNodeTypes.IF
+  id: number
+  condition: IRExpression
+  positive: BlockFunctionIRNode
+  negative?: BlockFunctionIRNode
+}
+
 export interface TemplateFactoryIRNode extends BaseIRNode {
   type: IRNodeTypes.TEMPLATE_FACTORY
   template: string
@@ -158,6 +176,9 @@ export type OperationNode =
   | PrependNodeIRNode
   | AppendNodeIRNode
   | WithDirectiveIRNode
+  | IfIRNode
+
+export type BlockIRNode = RootIRNode | BlockFunctionIRNode
 
 export interface IRDynamicInfo {
   id: number | null
index f0f311a7d8a23d47c7940041ea34b2254fe4182f..f3b66da6d29adfd8846253fd7026c1efe965c194 100644 (file)
@@ -3,14 +3,16 @@ import {
   type TransformOptions as BaseTransformOptions,
   type CompilerCompatOptions,
   type ElementNode,
+  ElementTypes,
   NodeTypes,
   type ParentNode,
   type RootNode,
   type TemplateChildNode,
   defaultOnError,
   defaultOnWarn,
+  isVSlot,
 } from '@vue/compiler-dom'
-import { EMPTY_OBJ, NOOP, extend, isArray } from '@vue/shared'
+import { EMPTY_OBJ, NOOP, extend, isArray, isString } from '@vue/shared'
 import {
   type IRDynamicInfo,
   type IRExpression,
@@ -18,7 +20,13 @@ import {
   type OperationNode,
   type RootIRNode,
 } from './ir'
-import type { HackOptions, VaporDirectiveNode } from './ir'
+import type {
+  BlockIRNode,
+  FragmentFactoryIRNode,
+  HackOptions,
+  TemplateFactoryIRNode,
+  VaporDirectiveNode,
+} from './ir'
 
 export type NodeTransform = (
   node: RootNode | TemplateChildNode,
@@ -31,6 +39,14 @@ export type DirectiveTransform = (
   context: TransformContext<ElementNode>,
 ) => void
 
+// A structural directive transform is technically also a NodeTransform;
+// Only v-if and v-for fall into this category.
+export type StructuralDirectiveTransform = (
+  node: RootNode | TemplateChildNode,
+  dir: VaporDirectiveNode,
+  context: TransformContext<RootNode | TemplateChildNode>,
+) => void | (() => void)
+
 export type TransformOptions = HackOptions<BaseTransformOptions>
 
 export interface TransformContext<T extends AllNode = AllNode> {
@@ -38,6 +54,7 @@ export interface TransformContext<T extends AllNode = AllNode> {
   parent: TransformContext<ParentNode> | null
   root: TransformContext<RootNode>
   index: number
+  block: BlockIRNode
   options: Required<
     Omit<TransformOptions, 'filename' | keyof CompilerCompatOptions>
   >
@@ -48,6 +65,7 @@ export interface TransformContext<T extends AllNode = AllNode> {
 
   inVOnce: boolean
 
+  enterBlock(ir: TransformContext['block']): () => void
   reference(): number
   increaseId(): number
   registerTemplate(): number
@@ -84,20 +102,34 @@ const defaultOptions = {
 
 // TODO use class for better perf
 function createRootContext(
-  ir: RootIRNode,
+  root: RootIRNode,
   node: RootNode,
   options: TransformOptions = {},
 ): TransformContext<RootNode> {
   let globalId = 0
-  const { effect, operation: operation } = ir
 
   const ctx: TransformContext<RootNode> = {
     node,
     parent: null,
     index: 0,
     root: null!, // set later
+    block: root,
+    enterBlock(ir) {
+      const { block, template, dynamic, childrenTemplate } = this
+      this.block = ir
+      this.dynamic = ir.dynamic
+      this.template = ''
+      this.childrenTemplate = []
+      return () => {
+        // exit
+        this.block = block
+        this.template = template
+        this.dynamic = dynamic
+        this.childrenTemplate = childrenTemplate
+      }
+    },
     options: extend({}, defaultOptions, options),
-    dynamic: ir.dynamic,
+    dynamic: root.dynamic,
     inVOnce: false,
 
     increaseId: () => globalId++,
@@ -113,13 +145,13 @@ function createRootContext(
       ) {
         return this.registerOperation(...operations)
       }
-      const existing = effect.find((e) =>
+      const existing = this.block.effect.find((e) =>
         isSameExpression(e.expressions, expressions as IRExpression[]),
       )
       if (existing) {
         existing.operations.push(...operations)
       } else {
-        effect.push({
+        this.block.effect.push({
           expressions: expressions as IRExpression[],
           operations,
         })
@@ -140,24 +172,34 @@ function createRootContext(
     template: '',
     childrenTemplate: [],
     registerTemplate() {
-      if (!ctx.template) return -1
+      let templateNode: TemplateFactoryIRNode | FragmentFactoryIRNode
 
-      const idx = ir.template.findIndex(
-        (t) =>
-          t.type === IRNodeTypes.TEMPLATE_FACTORY &&
-          t.template === ctx.template,
-      )
-      if (idx !== -1) return idx
+      if (this.template) {
+        const idx = root.template.findIndex(
+          (t) =>
+            t.type === IRNodeTypes.TEMPLATE_FACTORY &&
+            t.template === this.template,
+        )
+        if (idx !== -1) {
+          return (this.block.templateIndex = idx)
+        }
 
-      ir.template.push({
-        type: IRNodeTypes.TEMPLATE_FACTORY,
-        template: ctx.template,
-        loc: node.loc,
-      })
-      return ir.template.length - 1
+        templateNode = {
+          type: IRNodeTypes.TEMPLATE_FACTORY,
+          template: this.template,
+          loc: node.loc,
+        }
+      } else {
+        templateNode = {
+          type: IRNodeTypes.FRAGMENT_FACTORY,
+          loc: node.loc,
+        }
+      }
+      root.template.push(templateNode)
+      return (this.block.templateIndex = root.template.length - 1)
     },
     registerOperation(...node) {
-      operation.push(...node)
+      this.block.operation.push(...node)
     },
   }
   ctx.root = ctx
@@ -199,6 +241,7 @@ export function transform(
     source: root.source,
     loc: root.loc,
     template: [],
+    templateIndex: -1,
     dynamic: {
       id: null,
       referenced: true,
@@ -211,17 +254,9 @@ export function transform(
   }
 
   const ctx = createRootContext(ir, root, options)
-  transformNode(ctx)
 
-  if (ctx.node.type === NodeTypes.ROOT) {
-    ctx.registerTemplate()
-  }
-  if (ir.template.length === 0) {
-    ir.template.push({
-      type: IRNodeTypes.FRAGMENT_FACTORY,
-      loc: root.loc,
-    })
-  }
+  transformNode(ctx)
+  ctx.registerTemplate()
 
   return ir
 }
@@ -251,7 +286,6 @@ function transformNode(
       node = context.node
     }
   }
-
   switch (node.type) {
     case NodeTypes.ROOT:
     case NodeTypes.ELEMENT: {
@@ -351,3 +385,37 @@ function processDynamicChildren(ctx: TransformContext<RootNode | ElementNode>) {
     }
   }
 }
+
+export function createStructuralDirectiveTransform(
+  name: string | RegExp,
+  fn: StructuralDirectiveTransform,
+): NodeTransform {
+  const matches = isString(name)
+    ? (n: string) => n === name
+    : (n: string) => name.test(n)
+
+  return (node, context) => {
+    if (node.type === NodeTypes.ELEMENT) {
+      const { props } = node
+      // structural directive transforms are not concerned with slots
+      // as they are handled separately in vSlot.ts
+      if (node.tagType === ElementTypes.TEMPLATE && props.some(isVSlot)) {
+        return
+      }
+      const exitFns = []
+      for (let i = 0; i < props.length; i++) {
+        const prop = props[i]
+        if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
+          // structural directives are removed to avoid infinite recursion
+          // also we remove them *before* applying so that it can further
+          // traverse itself in case it moves the node around
+          props.splice(i, 1)
+          i--
+          const onExit = fn(node, prop as VaporDirectiveNode, context)
+          if (onExit) exitFns.push(onExit)
+        }
+      }
+      return exitFns
+    }
+  }
+}
diff --git a/packages/compiler-vapor/src/transforms/vIf.ts b/packages/compiler-vapor/src/transforms/vIf.ts
new file mode 100644 (file)
index 0000000..c8473ba
--- /dev/null
@@ -0,0 +1,91 @@
+import {
+  ElementTypes,
+  NodeTypes,
+  type RootNode,
+  type TemplateChildNode,
+  type TemplateNode,
+} from '@vue/compiler-dom'
+import {
+  type TransformContext,
+  createStructuralDirectiveTransform,
+} from '../transform'
+import {
+  type BlockFunctionIRNode,
+  IRNodeTypes,
+  type IfIRNode,
+  type VaporDirectiveNode,
+} from '../ir'
+import { extend } from '@vue/shared'
+
+export const transformVIf = createStructuralDirectiveTransform(
+  /^(if|else|else-if)$/,
+  processIf,
+)
+
+export function processIf(
+  node: RootNode | TemplateChildNode,
+  dir: VaporDirectiveNode,
+  context: TransformContext<RootNode | TemplateChildNode>,
+) {
+  // TODO refactor this
+  const parentContext = extend({}, context, {
+    currentScopeIR: context.block,
+  })
+
+  if (dir.name === 'if') {
+    const id = context.reference()
+    context.dynamic.ghost = true
+    const [branch, onExit] = createIfBranch(node, dir, context)
+    const operation: IfIRNode = {
+      type: IRNodeTypes.IF,
+      id,
+      loc: dir.loc,
+      condition: dir.exp!,
+      positive: branch,
+    }
+    parentContext.registerOperation(operation)
+    return onExit
+  }
+}
+
+export function createIfBranch(
+  node: RootNode | TemplateChildNode,
+  dir: VaporDirectiveNode,
+  context: TransformContext<RootNode | TemplateChildNode>,
+): [BlockFunctionIRNode, () => void] {
+  if (
+    node.type === NodeTypes.ELEMENT &&
+    node.tagType !== ElementTypes.TEMPLATE
+  ) {
+    node = extend({}, node, {
+      tagType: ElementTypes.TEMPLATE,
+      children: [node],
+    } as TemplateNode)
+    context.node = node
+  }
+
+  const branch: BlockFunctionIRNode = {
+    type: IRNodeTypes.BLOCK_FUNCTION,
+    loc: dir.loc,
+    node: node,
+    templateIndex: -1,
+    dynamic: {
+      id: null,
+      referenced: true,
+      ghost: true,
+      placeholder: null,
+      children: {},
+    },
+    effect: [],
+    operation: [],
+  }
+
+  const exitBlock = context.enterBlock(branch)
+  context.reference()
+  const onExit = () => {
+    context.template += context.childrenTemplate.join('')
+    context.registerTemplate()
+    exitBlock()
+  }
+  return [branch, onExit]
+}
index 7f8a76e2fea5c91f1e9624dc09035bc67ea8814e..37b518e5260cd5516d9b8670a1bcdbff761bbd1d 100644 (file)
@@ -18,7 +18,22 @@ export function insert(block: Block, parent: Node, anchor: Node | null = null) {
   // }
 }
 
-export function prepend(parent: ParentBlock, ...nodes: Node[]) {
+export function prepend(parent: ParentBlock, ...blocks: Block[]) {
+  const nodes: Node[] = []
+
+  for (const block of blocks) {
+    if (block instanceof Node) {
+      nodes.push(block)
+    } else if (isArray(block)) {
+      prepend(parent, ...block)
+    } else {
+      prepend(parent, block.nodes)
+      block.anchor && prepend(parent, block.anchor)
+    }
+  }
+
+  if (!nodes.length) return
+
   if (parent instanceof Node) {
     // TODO use insertBefore for better performance https://jsbench.me/rolpg250hh/1
     parent.prepend(...nodes)
@@ -27,7 +42,22 @@ export function prepend(parent: ParentBlock, ...nodes: Node[]) {
   }
 }
 
-export function append(parent: ParentBlock, ...nodes: Node[]) {
+export function append(parent: ParentBlock, ...blocks: Block[]) {
+  const nodes: Node[] = []
+
+  for (const block of blocks) {
+    if (block instanceof Node) {
+      nodes.push(block)
+    } else if (isArray(block)) {
+      append(parent, ...block)
+    } else {
+      append(parent, block.nodes)
+      block.anchor && append(parent, block.anchor)
+    }
+  }
+
+  if (!nodes.length) return
+
   if (parent instanceof Node) {
     // TODO use insertBefore for better performance
     parent.append(...nodes)