]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor(compiler-vapor): CodeFragment for codegen
author三咲智子 Kevin Deng <sxzz@sxzz.moe>
Tue, 30 Jan 2024 14:08:28 +0000 (22:08 +0800)
committer三咲智子 Kevin Deng <sxzz@sxzz.moe>
Tue, 30 Jan 2024 14:08:28 +0000 (22:08 +0800)
15 files changed:
packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap
packages/compiler-vapor/src/generate.ts
packages/compiler-vapor/src/generators/directive.ts
packages/compiler-vapor/src/generators/dom.ts
packages/compiler-vapor/src/generators/event.ts
packages/compiler-vapor/src/generators/expression.ts
packages/compiler-vapor/src/generators/html.ts
packages/compiler-vapor/src/generators/if.ts
packages/compiler-vapor/src/generators/modelValue.ts
packages/compiler-vapor/src/generators/prop.ts
packages/compiler-vapor/src/generators/ref.ts
packages/compiler-vapor/src/generators/template.ts [new file with mode: 0644]
packages/compiler-vapor/src/generators/text.ts

index 35bc588ce168dbf2832b79abe48154246bd2630f..dfde624753fc3b1f9c4f592d7ffa376e53c1cd09 100644 (file)
@@ -17,7 +17,7 @@ export function render(_ctx) {
 `;
 
 exports[`compile > custom directive > basic 1`] = `
-"import { template as _template, children as _children, withDirectives as _withDirectives, resolveDirective("vTest") as _resolveDirective("vTest"), resolveDirective("vHello") as _resolveDirective("vHello") } from 'vue/vapor';
+"import { template as _template, children as _children, resolveDirective("vTest") as _resolveDirective("vTest"), resolveDirective("vHello") as _resolveDirective("vHello"), withDirectives as _withDirectives } from 'vue/vapor';
 
 export function render(_ctx) {
   const t0 = _template("<div></div>")
index bc726ca1261f9811fba2cfea1e252ba0b8c274c8..791e2c0852c00510834a718a6f7f8783e87f5fa3 100644 (file)
@@ -1,7 +1,7 @@
 // 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';
+"import { template as _template, fragment as _fragment, children as _children, renderEffect as _renderEffect, setText as _setText, createIf as _createIf, append as _append } from 'vue/vapor';
 
 export function render(_ctx) {
   const t0 = _template("<div></div>")
@@ -69,7 +69,7 @@ export function render(_ctx) {
 `;
 
 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';
+"import { template as _template, fragment as _fragment, children as _children, renderEffect as _renderEffect, setText as _setText, createIf as _createIf, append as _append } from 'vue/vapor';
 
 export function render(_ctx) {
   const t0 = _template("<div></div>hello<p></p>")
index a469d61bd47e4796a2dcd7d8068e7e30d3e15afe..ba5cd77b010904e7819190b24f9c7bd46928162b 100644 (file)
@@ -67,7 +67,7 @@ export function render(_ctx) {
 `;
 
 exports[`v-on > event modifier 1`] = `
-"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers, withKeys as _withKeys } from 'vue/vapor';
+"import { template as _template, children as _children, withModifiers as _withModifiers, on as _on, withKeys as _withKeys } from 'vue/vapor';
 
 export function render(_ctx) {
   const t0 = _template("<a></a><form></form><a></a><div></div><div></div><a></a><div></div><input><input><input><input><input><input><input><input><input><input><input><input><input><input><input>")
@@ -239,7 +239,7 @@ export function render(_ctx) {
 `;
 
 exports[`v-on > should not wrap keys guard if no key modifier is present 1`] = `
-"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers } from 'vue/vapor';
+"import { template as _template, children as _children, withModifiers as _withModifiers, on as _on } from 'vue/vapor';
 
 export function render(_ctx) {
   const t0 = _template("<div></div>")
@@ -251,7 +251,7 @@ export function render(_ctx) {
 `;
 
 exports[`v-on > should support multiple events and modifiers options w/ prefixIdentifiers: true 1`] = `
-"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers, withKeys as _withKeys } from 'vue/vapor';
+"import { template as _template, children as _children, withModifiers as _withModifiers, on as _on, withKeys as _withKeys } from 'vue/vapor';
 
 export function render(_ctx) {
   const t0 = _template("<div></div>")
@@ -264,7 +264,7 @@ export function render(_ctx) {
 `;
 
 exports[`v-on > should support multiple modifiers and event options w/ prefixIdentifiers: true 1`] = `
-"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers } from 'vue/vapor';
+"import { template as _template, children as _children, withModifiers as _withModifiers, on as _on } from 'vue/vapor';
 
 export function render(_ctx) {
   const t0 = _template("<div></div>")
@@ -276,7 +276,7 @@ export function render(_ctx) {
 `;
 
 exports[`v-on > should transform click.middle 1`] = `
-"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers } from 'vue/vapor';
+"import { template as _template, children as _children, withModifiers as _withModifiers, on as _on } from 'vue/vapor';
 
 export function render(_ctx) {
   const t0 = _template("<div></div>")
@@ -288,7 +288,7 @@ export function render(_ctx) {
 `;
 
 exports[`v-on > should transform click.middle 2`] = `
-"import { template as _template, children as _children, renderEffect as _renderEffect, on as _on, withModifiers as _withModifiers } from 'vue/vapor';
+"import { template as _template, children as _children, renderEffect as _renderEffect, withModifiers as _withModifiers, on as _on } from 'vue/vapor';
 
 export function render(_ctx) {
   const t0 = _template("<div></div>")
@@ -302,7 +302,7 @@ export function render(_ctx) {
 `;
 
 exports[`v-on > should transform click.right 1`] = `
-"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers } from 'vue/vapor';
+"import { template as _template, children as _children, withModifiers as _withModifiers, on as _on } from 'vue/vapor';
 
 export function render(_ctx) {
   const t0 = _template("<div></div>")
@@ -314,7 +314,7 @@ export function render(_ctx) {
 `;
 
 exports[`v-on > should transform click.right 2`] = `
-"import { template as _template, children as _children, renderEffect as _renderEffect, on as _on, withKeys as _withKeys, withModifiers as _withModifiers } from 'vue/vapor';
+"import { template as _template, children as _children, renderEffect as _renderEffect, withModifiers as _withModifiers, withKeys as _withKeys, on as _on } from 'vue/vapor';
 
 export function render(_ctx) {
   const t0 = _template("<div></div>")
@@ -340,7 +340,7 @@ export function render(_ctx) {
 `;
 
 exports[`v-on > should wrap both for dynamic key event w/ left/right modifiers 1`] = `
-"import { template as _template, children as _children, renderEffect as _renderEffect, on as _on, withKeys as _withKeys, withModifiers as _withModifiers } from 'vue/vapor';
+"import { template as _template, children as _children, renderEffect as _renderEffect, withModifiers as _withModifiers, withKeys as _withKeys, on as _on } from 'vue/vapor';
 
 export function render(_ctx) {
   const t0 = _template("<div></div>")
@@ -354,7 +354,7 @@ export function render(_ctx) {
 `;
 
 exports[`v-on > should wrap keys guard for keyboard events or dynamic events 1`] = `
-"import { template as _template, children as _children, on as _on, withKeys as _withKeys, withModifiers as _withModifiers } from 'vue/vapor';
+"import { template as _template, children as _children, withModifiers as _withModifiers, withKeys as _withKeys, on as _on } from 'vue/vapor';
 
 export function render(_ctx) {
   const t0 = _template("<div></div>")
@@ -366,7 +366,7 @@ export function render(_ctx) {
 `;
 
 exports[`v-on > should wrap keys guard for static key event w/ left/right modifiers 1`] = `
-"import { template as _template, children as _children, on as _on, withKeys as _withKeys } from 'vue/vapor';
+"import { template as _template, children as _children, withKeys as _withKeys, on as _on } from 'vue/vapor';
 
 export function render(_ctx) {
   const t0 = _template("<div></div>")
index 240cc73474f3646d1d3d4dba0ce2ee9f0c6f9184..b24e231a253506ffb9e5f40fe163c18a9b87b693 100644 (file)
@@ -29,6 +29,7 @@ import { genSetModelValue } from './generators/modelValue'
 import { genAppendNode, genInsertNode, genPrependNode } from './generators/dom'
 import { genWithDirective } from './generators/directive'
 import { genIf } from './generators/if'
+import { genTemplate } from './generators/template'
 
 interface CodegenOptions extends BaseCodegenOptions {
   expressionPlugins?: ParserPlugin[]
@@ -38,35 +39,27 @@ interface CodegenOptions extends BaseCodegenOptions {
 // @ts-expect-error
 function checkNever(x: never): never {}
 
+export type CodeFragment =
+  | string
+  | [code: string, newlineIndex?: number, loc?: SourceLocation, name?: string]
+  | undefined
+
 export interface CodegenContext {
   options: Required<CodegenOptions>
 
   source: string
-  code: string
-  line: number
-  column: number
-  offset: number
+  code: CodeFragment[]
   indentLevel: number
   map?: SourceMapGenerator
 
-  push(
-    code: string,
-    newlineIndex?: number,
-    loc?: SourceLocation,
-    name?: string,
-  ): void
-  newline(
-    code?: string,
-    newlineIndex?: number,
-    loc?: SourceLocation,
-    name?: string,
-  ): void
-  pushMulti(
-    codes: [left: string, right: string, segment?: string],
-    ...fn: Array<false | string | (() => void)>
-  ): void
-  pushCall(name: string, ...args: Array<false | string | (() => void)>): void
-  withIndent(fn: () => void): void
+  push(...args: CodeFragment[]): void
+  newline(): CodeFragment
+  multi(
+    codes: [left: string, right: string, segment: string],
+    ...fn: Array<false | CodeFragment[]>
+  ): CodeFragment[]
+  call(name: string, ...args: Array<false | CodeFragment[]>): CodeFragment[]
+  withIndent<T>(fn: () => T): T
 
   helpers: Set<string>
   vaporHelpers: Set<string>
@@ -77,6 +70,7 @@ export interface CodegenContext {
 function createCodegenContext(ir: RootIRNode, options: CodegenOptions) {
   const helpers = new Set<string>([])
   const vaporHelpers = new Set<string>([])
+  const [code, push] = buildCodeFragment()
   const context: CodegenContext = {
     options: extend(
       {
@@ -100,10 +94,7 @@ function createCodegenContext(ir: RootIRNode, options: CodegenOptions) {
     ),
 
     source: ir.source,
-    code: '',
-    column: 1,
-    line: 1,
-    offset: 0,
+    code,
     indentLevel: 0,
 
     helpers,
@@ -117,95 +108,35 @@ function createCodegenContext(ir: RootIRNode, options: CodegenOptions) {
       return `_${name}`
     },
 
-    push(code, newlineIndex = NewlineType.None, loc, name) {
-      context.code += code
-      if (!__BROWSER__ && context.map) {
-        if (loc) addMapping(loc.start, name)
-
-        if (newlineIndex === NewlineType.Unknown) {
-          // multiple newlines, full iteration
-          advancePositionWithMutation(context, code)
-        } else {
-          // fast paths
-          context.offset += code.length
-          if (newlineIndex === NewlineType.None) {
-            // no newlines; fast path to avoid newline detection
-            if (__TEST__ && code.includes('\n')) {
-              throw new Error(
-                `CodegenContext.push() called newlineIndex: none, but contains` +
-                  `newlines: ${code.replace(/\n/g, '\\n')}`,
-              )
-            }
-            context.column += code.length
-          } else {
-            // single newline at known index
-            if (newlineIndex === NewlineType.End) {
-              newlineIndex = code.length - 1
-            }
-            if (
-              __TEST__ &&
-              (code.charAt(newlineIndex) !== '\n' ||
-                code.slice(0, newlineIndex).includes('\n') ||
-                code.slice(newlineIndex + 1).includes('\n'))
-            ) {
-              throw new Error(
-                `CodegenContext.push() called with newlineIndex: ${newlineIndex} ` +
-                  `but does not conform: ${code.replace(/\n/g, '\\n')}`,
-              )
-            }
-            context.line++
-            context.column = code.length - newlineIndex
-          }
-        }
-        if (loc && loc !== locStub) {
-          addMapping(loc.end)
-        }
-      }
+    push,
+    newline() {
+      return [`\n${`  `.repeat(context.indentLevel)}`, NewlineType.Start]
     },
-    newline(code, newlineIndex, node) {
-      context.push(`\n${`  `.repeat(context.indentLevel)}`, NewlineType.Start)
-      code && context.push(code, newlineIndex, node)
-    },
-    pushMulti([left, right, seg], ...fns) {
+    multi([left, right, seg], ...fns) {
+      const frag: CodeFragment[] = []
       fns = fns.filter(Boolean)
-      context.push(left)
+      frag.push(left)
       for (const [i, fn] of fns.entries()) {
-        if (isString(fn)) context.push(fn)
-        else (fn as () => void)()
-        if (seg && i < fns.length - 1) context.push(seg)
+        if (fn) {
+          frag.push(...fn)
+          if (i < fns.length - 1) frag.push(seg)
+        }
       }
-      context.push(right)
+      frag.push(right)
+      return frag
     },
-    pushCall(name, ...args) {
-      context.push(name)
-      context.pushMulti(['(', ')', ', '], ...args)
+    call(name, ...args) {
+      return [name, ...context.multi(['(', ')', ', '], ...args)]
     },
     withIndent(fn) {
       ++context.indentLevel
-      fn()
+      const ret = fn()
       --context.indentLevel
+      return ret
     },
   }
 
   const filename = context.options.filename
-
-  function addMapping(loc: Position, name: string | null = null) {
-    // we use the private property to directly add the mapping
-    // because the addMapping() implementation in source-map-js has a bunch of
-    // unnecessary arg and validation checks that are pure overhead in our case.
-    const { _names, _mappings } = context.map!
-    if (name !== null && !_names.has(name)) _names.add(name)
-    _mappings.add({
-      originalLine: loc.line,
-      originalColumn: loc.column - 1, // source-map column is 0 based
-      generatedLine: context.line,
-      generatedColumn: context.column - 1,
-      source: filename,
-      // @ts-expect-error it is possible to be null
-      name,
-    })
-  }
-
   if (!__BROWSER__ && context.options.sourceMap) {
     // lazy require source-map implementation, only in non-browser builds
     context.map = new SourceMapGenerator()
@@ -228,37 +159,27 @@ export function generate(
   options: CodegenOptions = {},
 ): VaporCodegenResult {
   const ctx = createCodegenContext(ir, options)
-  const { push, withIndent, newline, helpers, vaporHelper, vaporHelpers } = ctx
+  const { push, withIndent, newline, helpers, vaporHelpers } = ctx
 
   const functionName = 'render'
   const isSetupInlined = !!options.inline
   if (isSetupInlined) {
     push(`(() => {`)
   } else {
-    // placeholder for preamble
-    newline()
-    newline(`export function ${functionName}(_ctx) {`)
+    push(
+      // placeholder for preamble
+      newline(),
+      newline(),
+      `export function ${functionName}(_ctx) {`,
+    )
   }
 
   withIndent(() => {
-    ir.template.forEach((template, i) => {
-      if (template.type === IRNodeTypes.TEMPLATE_FACTORY) {
-        // TODO source map?
-        newline(
-          `const t${i} = ${vaporHelper('template')}(${JSON.stringify(
-            template.template,
-          )})`,
-        )
-      } else {
-        // fragment
-        newline(`const t${i} = ${vaporHelper('fragment')}()`)
-      }
-    })
-
-    genBlockFunctionContent(ir, ctx)
+    ir.template.forEach((template, i) => push(...genTemplate(template, i, ctx)))
+    push(...genBlockFunctionContent(ir, ctx))
   })
 
-  newline()
+  push(newline())
   if (isSetupInlined) {
     push('})()')
   } else {
@@ -276,12 +197,13 @@ export function generate(
       .map(h => `${h} as _${h}`)
       .join(', ')} } from 'vue';`
 
+  let codegen = genCodeFragment(ctx)
   if (!isSetupInlined) {
-    ctx.code = preamble + ctx.code
+    codegen = preamble + codegen
   }
 
   return {
-    code: ctx.code,
+    code: codegen,
     ast: ir,
     preamble,
     map: ctx.map ? ctx.map.toJSON() : undefined,
@@ -290,6 +212,82 @@ export function generate(
   }
 }
 
+function genCodeFragment(context: CodegenContext) {
+  let codegen = ''
+  let line = 1
+  let column = 1
+  let offset = 0
+
+  for (let frag of context.code) {
+    if (!frag) continue
+    if (isString(frag)) frag = [frag]
+
+    let [code, newlineIndex = NewlineType.None, loc, name] = frag
+    codegen += code
+
+    if (!__BROWSER__ && context.map) {
+      if (loc) addMapping(loc.start, name)
+      if (newlineIndex === NewlineType.Unknown) {
+        // multiple newlines, full iteration
+        advancePositionWithMutation({ line, column, offset }, code)
+      } else {
+        // fast paths
+        offset += code.length
+        if (newlineIndex === NewlineType.None) {
+          // no newlines; fast path to avoid newline detection
+          if (__TEST__ && code.includes('\n')) {
+            throw new Error(
+              `CodegenContext.push() called newlineIndex: none, but contains` +
+                `newlines: ${code.replace(/\n/g, '\\n')}`,
+            )
+          }
+          column += code.length
+        } else {
+          // single newline at known index
+          if (newlineIndex === NewlineType.End) {
+            newlineIndex = code.length - 1
+          }
+          if (
+            __TEST__ &&
+            (code.charAt(newlineIndex) !== '\n' ||
+              code.slice(0, newlineIndex).includes('\n') ||
+              code.slice(newlineIndex + 1).includes('\n'))
+          ) {
+            throw new Error(
+              `CodegenContext.push() called with newlineIndex: ${newlineIndex} ` +
+                `but does not conform: ${code.replace(/\n/g, '\\n')}`,
+            )
+          }
+          line++
+          column = code.length - newlineIndex
+        }
+      }
+      if (loc && loc !== locStub) {
+        addMapping(loc.end)
+      }
+    }
+  }
+
+  return codegen
+
+  function addMapping(loc: Position, name: string | null = null) {
+    // we use the private property to directly add the mapping
+    // because the addMapping() implementation in source-map-js has a bunch of
+    // unnecessary arg and validation checks that are pure overhead in our case.
+    const { _names, _mappings } = context.map!
+    if (name !== null && !_names.has(name)) _names.add(name)
+    _mappings.add({
+      originalLine: loc.line,
+      originalColumn: loc.column - 1, // source-map column is 0 based
+      generatedLine: line,
+      generatedColumn: column - 1,
+      source: context.options.filename,
+      // @ts-expect-error it is possible to be null
+      name,
+    })
+  }
+}
+
 function genChildren(children: IRDynamicInfo[]) {
   let code = ''
   let offset = 0
@@ -320,7 +318,10 @@ function genChildren(children: IRDynamicInfo[]) {
   return `{${code}}`
 }
 
-function genOperation(oper: OperationNode, context: CodegenContext) {
+function genOperation(
+  oper: OperationNode,
+  context: CodegenContext,
+): CodeFragment[] {
   // TODO: cache old value
   switch (oper.type) {
     case IRNodeTypes.SET_PROP:
@@ -347,22 +348,35 @@ function genOperation(oper: OperationNode, context: CodegenContext) {
       return genIf(oper, context)
     case IRNodeTypes.WITH_DIRECTIVE:
       // generated, skip
-      return
+      break
     default:
       return checkNever(oper)
   }
+
+  return []
+}
+
+export function buildCodeFragment() {
+  const frag: CodeFragment[] = []
+  const push = frag.push.bind(frag)
+  return [frag, push] as const
 }
 
 export function genBlockFunctionContent(
   ir: BlockFunctionIRNode | RootIRNode,
   ctx: CodegenContext,
-) {
+): CodeFragment[] {
   const { newline, withIndent, vaporHelper } = ctx
-  newline(`const n${ir.dynamic.id} = t${ir.templateIndex}()`)
+  const [frag, push] = buildCodeFragment()
+
+  push(newline(), `const n${ir.dynamic.id} = t${ir.templateIndex}()`)
 
   const children = genChildren(ir.dynamic.children)
   if (children) {
-    newline(`const ${children} = ${vaporHelper('children')}(n${ir.dynamic.id})`)
+    push(
+      newline(),
+      `const ${children} = ${vaporHelper('children')}(n${ir.dynamic.id})`,
+    )
   }
 
   const directiveOps = ir.operation.filter(
@@ -370,24 +384,24 @@ export function genBlockFunctionContent(
       oper.type === IRNodeTypes.WITH_DIRECTIVE,
   )
   for (const directives of groupDirective(directiveOps)) {
-    genWithDirective(directives, ctx)
+    push(...genWithDirective(directives, ctx))
   }
 
   for (const operation of ir.operation) {
-    genOperation(operation, ctx)
+    push(...genOperation(operation, ctx))
   }
 
   for (const { operations } of ir.effect) {
-    newline(`${vaporHelper('renderEffect')}(() => {`)
+    push(newline(), `${vaporHelper('renderEffect')}(() => {`)
     withIndent(() => {
-      for (const operation of operations) {
-        genOperation(operation, ctx)
-      }
+      operations.forEach(op => push(...genOperation(op, ctx)))
     })
-    newline('})')
+    push(newline(), '})')
   }
 
-  newline(`return n${ir.dynamic.id}`)
+  push(newline(), `return n${ir.dynamic.id}`)
+
+  return frag
 }
 
 function groupDirective(ops: WithDirectiveIRNode[]): WithDirectiveIRNode[][] {
index 2c9d9143c7dec7b3ed2c05d602a404800eb945c7..2367bb8d9ea6eb9df7cf97428c246c9793c30fd2 100644 (file)
@@ -1,86 +1,73 @@
 import { createSimpleExpression, isSimpleIdentifier } from '@vue/compiler-dom'
 import { camelize } from '@vue/shared'
 import { genExpression } from './expression'
-import type { CodegenContext } from '../generate'
+import type { CodeFragment, CodegenContext } from '../generate'
 import type { WithDirectiveIRNode } from '../ir'
 
 export function genWithDirective(
   opers: WithDirectiveIRNode[],
   context: CodegenContext,
 ) {
-  const {
-    push,
-    newline,
-    pushCall,
-    pushMulti,
-    vaporHelper,
-    options: { bindingMetadata },
-  } = context
+  const { newline, call, multi, vaporHelper } = context
 
-  newline()
-  pushCall(
-    vaporHelper('withDirectives'),
-    // 1st arg: node
-    `n${opers[0].element}`,
-    // 2nd arg: directives
-    () => {
-      // directive
-      pushMulti(
-        ['[', ']', ', '],
-        ...opers.map(oper => () => {
-          push('[')
+  const element = `n${opers[0].element}`
+  const directiveItems = opers.map(genDirective)
+  const directives = multi(['[', ']', ', '], ...directiveItems)
 
-          const { dir, builtin } = oper
-          if (dir.name === 'show') {
-            push(vaporHelper('vShow'))
-          } else if (builtin) {
-            push(vaporHelper(builtin))
-          } else {
-            const directiveReference = camelize(`v-${dir.name}`)
-            // TODO resolve directive
-            if (bindingMetadata[directiveReference]) {
-              const directiveExpression =
-                createSimpleExpression(directiveReference)
-              directiveExpression.ast = null
-              genExpression(directiveExpression, context)
-            } else {
-              push(vaporHelper(`resolveDirective("${directiveReference}")`))
-            }
-          }
+  return [
+    newline(),
+    ...call(vaporHelper('withDirectives'), [element], directives),
+  ]
 
-          if (dir.exp) {
-            push(', () => ')
-            genExpression(dir.exp, context)
-          } else if (dir.arg || dir.modifiers.length) {
-            push(', void 0')
-          }
+  function genDirective({ dir, builtin }: WithDirectiveIRNode): CodeFragment[] {
+    const NULL = ['void 0']
 
-          if (dir.arg) {
-            push(', ')
-            genExpression(dir.arg, context)
-          } else if (dir.modifiers.length) {
-            push(', void 0')
-          }
+    const directive = genDirective()
+    const value = dir.exp
+      ? ['() => ', ...genExpression(dir.exp, context)]
+      : dir.arg || dir.modifiers.length
+        ? NULL
+        : false
+    const argument = dir.arg
+      ? genExpression(dir.arg, context)
+      : dir.modifiers.length
+        ? NULL
+        : false
+    const modifiers = dir.modifiers.length
+      ? ['{ ', genDirectiveModifiers(), ' }']
+      : false
 
-          if (dir.modifiers.length) {
-            push(', ')
-            push('{ ')
-            push(genDirectiveModifiers(dir.modifiers))
-            push(' }')
-          }
+    return multi(['[', ']', ', '], directive, value, argument, modifiers)
 
-          push(']')
-        }),
-      )
-    },
-  )
-}
+    function genDirective(): CodeFragment[] {
+      const {
+        vaporHelper,
+        options: { bindingMetadata },
+      } = context
+      if (dir.name === 'show') {
+        return [vaporHelper('vShow')]
+      } else if (builtin) {
+        return [vaporHelper(builtin)]
+      } else {
+        const directiveReference = camelize(`v-${dir.name}`)
+        // TODO resolve directive
+        if (bindingMetadata[directiveReference]) {
+          const directiveExpression = createSimpleExpression(directiveReference)
+          directiveExpression.ast = null
+          return genExpression(directiveExpression, context)
+        } else {
+          return [vaporHelper(`resolveDirective("${directiveReference}")`)]
+        }
+      }
+    }
 
-function genDirectiveModifiers(modifiers: string[]) {
-  return modifiers
-    .map(
-      value =>
-        `${isSimpleIdentifier(value) ? value : JSON.stringify(value)}: true`,
-    )
-    .join(', ')
+    function genDirectiveModifiers() {
+      return dir.modifiers
+        .map(
+          value =>
+            `${isSimpleIdentifier(value) ? value : JSON.stringify(value)}: true`,
+        )
+        .join(', ')
+    }
+  }
 }
index 17a3e27cc742cf3265ed3ca8caa4d3aba9a717c7..f30b8c6d8fb6e9e561ea0b7fb7e3fe38ac5514a1 100644 (file)
@@ -1,4 +1,4 @@
-import type { CodegenContext } from '../generate'
+import type { CodeFragment, CodegenContext } from '../generate'
 import type {
   AppendNodeIRNode,
   InsertNodeIRNode,
@@ -7,35 +7,47 @@ import type {
 
 export function genInsertNode(
   oper: InsertNodeIRNode,
-  { newline, pushCall, vaporHelper }: CodegenContext,
-) {
+  { newline, call, vaporHelper }: CodegenContext,
+): CodeFragment[] {
   const elements = ([] as number[]).concat(oper.element)
   let element = elements.map(el => `n${el}`).join(', ')
   if (elements.length > 1) element = `[${element}]`
-  newline()
-  pushCall(vaporHelper('insert'), element, `n${oper.parent}`, `n${oper.anchor}`)
+  return [
+    newline(),
+    ...call(
+      vaporHelper('insert'),
+      [element],
+      [`n${oper.parent}`],
+      [`n${oper.anchor}`],
+    ),
+  ]
 }
 
 export function genPrependNode(
   oper: PrependNodeIRNode,
-  { newline, pushCall, vaporHelper }: CodegenContext,
-) {
-  newline()
-  pushCall(
-    vaporHelper('prepend'),
-    `n${oper.parent}`,
-    oper.elements.map(el => `n${el}`).join(', '),
-  )
+  { newline, call, vaporHelper }: CodegenContext,
+): CodeFragment[] {
+  return [
+    newline(),
+    ...call(
+      vaporHelper('prepend'),
+      [`n${oper.parent}`],
+      ...oper.elements.map(el => [`n${el}`]),
+    ),
+  ]
 }
 
 export function genAppendNode(
   oper: AppendNodeIRNode,
-  { newline, pushCall, vaporHelper }: CodegenContext,
-) {
+  { newline, call, vaporHelper }: CodegenContext,
+): CodeFragment[] {
   newline()
-  pushCall(
-    vaporHelper('append'),
-    `n${oper.parent}`,
-    oper.elements.map(el => `n${el}`).join(', '),
-  )
+  return [
+    newline(),
+    ...call(
+      vaporHelper('append'),
+      [`n${oper.parent}`],
+      ...oper.elements.map(el => [`n${el}`]),
+    ),
+  ]
 }
index f369315d9982e1a2e5c0c8d6546c6af44381dbe0..7d79b579ec6b88bc8709f2ac7cc6dffa87074fc7 100644 (file)
@@ -1,5 +1,5 @@
 import { isMemberExpression } from '@vue/compiler-dom'
-import type { CodegenContext } from '../generate'
+import type { CodeFragment, CodegenContext } from '../generate'
 import type { SetEventIRNode } from '../ir'
 import { genExpression } from './expression'
 
@@ -7,60 +7,38 @@ import { genExpression } from './expression'
 const fnExpRE =
   /^\s*([\w$_]+|(async\s*)?\([^)]*?\))\s*(:[^=]+)?=>|^\s*(async\s+)?function(?:\s+[\w$]+)?\s*\(/
 
-export function genSetEvent(oper: SetEventIRNode, context: CodegenContext) {
-  const {
-    vaporHelper,
-    push,
-    newline,
-    pushMulti,
-    pushCall,
-    options: ctxOptions,
-  } = context
+export function genSetEvent(
+  oper: SetEventIRNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  const { vaporHelper, newline, call, options: ctxOptions } = context
   const { keys, nonKeys, options } = oper.modifiers
 
-  newline()
-  pushCall(
-    vaporHelper('on'),
-    // 1st arg: event name
-    () => push(`n${oper.element}`),
-    // 2nd arg: event name
-    () => {
-      if (oper.keyOverride) {
-        const find = JSON.stringify(oper.keyOverride[0])
-        const replacement = JSON.stringify(oper.keyOverride[1])
-        pushMulti(['(', ')'], () => genExpression(oper.key, context))
-        push(` === ${find} ? ${replacement} : `)
-        pushMulti(['(', ')'], () => genExpression(oper.key, context))
-      } else {
-        genExpression(oper.key, context)
-      }
-    },
-    // 3rd arg: event handler
-    () => {
-      const pushWithKeys = (fn: () => void) => {
-        push(`${vaporHelper('withKeys')}(`)
-        fn()
-        push(`, ${genArrayExpression(keys)})`)
-      }
-      const pushWithModifiers = (fn: () => void) => {
-        push(`${vaporHelper('withModifiers')}(`)
-        fn()
-        push(`, ${genArrayExpression(nonKeys)})`)
-      }
-      const pushNoop = (fn: () => void) => fn()
+  const name = genName()
+  const handler = genFinalizedHandler()
+  const opt = !!options.length && [
+    `{ ${options.map(v => `${v}: true`).join(', ')} }`,
+  ]
 
-      ;(keys.length ? pushWithKeys : pushNoop)(() =>
-        (nonKeys.length ? pushWithModifiers : pushNoop)(() => {
-          genEventHandler()
-        }),
-      )
-    },
-    // 4th arg, gen options
-    !!options.length &&
-      (() => push(`{ ${options.map(v => `${v}: true`).join(', ')} }`)),
-  )
+  return [
+    newline(),
+    ...call(vaporHelper('on'), [`n${oper.element}`], name, handler, opt),
+  ]
+
+  function genName(): CodeFragment[] {
+    const expr = genExpression(oper.key, context)
+    // TODO unit test
+    if (oper.keyOverride) {
+      const find = JSON.stringify(oper.keyOverride[0])
+      const replacement = JSON.stringify(oper.keyOverride[1])
+      const wrapped: CodeFragment[] = ['(', ...expr, ')']
+      return [...wrapped, ` === ${find} ? ${replacement} : `, ...wrapped]
+    } else {
+      return genExpression(oper.key, context)
+    }
+  }
 
-  function genEventHandler() {
+  function genEventHandler(): CodeFragment[] {
     const exp = oper.value
     if (exp && exp.content.trim()) {
       const isMemberExp = isMemberExpression(exp.content, ctxOptions)
@@ -68,24 +46,52 @@ export function genSetEvent(oper: SetEventIRNode, context: CodegenContext) {
       const hasMultipleStatements = exp.content.includes(`;`)
 
       if (isInlineStatement) {
-        push('$event => ')
-        push(hasMultipleStatements ? '{' : '(')
         const knownIds = Object.create(null)
         knownIds['$event'] = 1
-        genExpression(exp, context, knownIds)
-        push(hasMultipleStatements ? '}' : ')')
-      } else if (isMemberExp) {
-        push('(...args) => (')
-        genExpression(exp, context)
-        push(' && ')
-        genExpression(exp, context)
-        push('(...args))')
+
+        return [
+          '$event => ',
+          hasMultipleStatements ? '{' : '(',
+          ...genExpression(exp, context, knownIds),
+          hasMultipleStatements ? '}' : ')',
+        ]
       } else {
-        genExpression(exp, context)
+        const expr = genExpression(exp, context)
+        if (isMemberExp) {
+          return ['(...args) => (', ...expr, ' && ', ...expr, '(...args))']
+        } else {
+          return expr
+        }
       }
-    } else {
-      push('() => {}')
     }
+    return ['() => {}']
+  }
+
+  function genFinalizedHandler(): CodeFragment[] {
+    let expr = genEventHandler()
+
+    if (nonKeys.length) {
+      expr = [
+        vaporHelper('withModifiers'),
+        '(',
+        ...expr,
+        ', ',
+        genArrayExpression(nonKeys),
+        ')',
+      ]
+    }
+    if (keys.length) {
+      expr = [
+        vaporHelper('withKeys'),
+        '(',
+        ...expr,
+        ', ',
+        genArrayExpression(keys),
+        ')',
+      ]
+    }
+
+    return expr
   }
 }
 
index 90019290ec842b86a4f7d62281a49d7dc8a34870..ba8c1d71f355d73e06a5c57044c4ce2c1cfd234a 100644 (file)
@@ -8,23 +8,27 @@ import {
 import { isGloballyAllowed, isString, makeMap } from '@vue/shared'
 import type { Identifier } from '@babel/types'
 import type { IRExpression } from '../ir'
-import type { CodegenContext } from '../generate'
+import {
+  type CodeFragment,
+  type CodegenContext,
+  buildCodeFragment,
+} from '../generate'
 
 export function genExpression(
   node: IRExpression,
   context: CodegenContext,
   knownIds: Record<string, number> = Object.create(null),
-): void {
+): CodeFragment[] {
   const {
-    push,
     options: { prefixIdentifiers },
   } = context
-  if (isString(node)) return push(node)
+  if (isString(node)) return [node]
 
   const { content: rawExpr, ast, isStatic, loc } = node
   if (isStatic) {
-    return push(JSON.stringify(rawExpr), NewlineType.None, loc)
+    return [[JSON.stringify(rawExpr), NewlineType.None, loc]]
   }
+
   if (
     __BROWSER__ ||
     !prefixIdentifiers ||
@@ -34,12 +38,12 @@ export function genExpression(
     isGloballyAllowed(rawExpr) ||
     isLiteralWhitelisted(rawExpr)
   ) {
-    return push(rawExpr, NewlineType.None, loc)
+    return [[rawExpr, NewlineType.None, loc]]
   }
 
   if (ast === null) {
     // the expression is a simple identifier
-    return genIdentifier(rawExpr, context, loc)
+    return [genIdentifier(rawExpr, context, loc)]
   }
 
   const ids: Identifier[] = []
@@ -55,6 +59,7 @@ export function genExpression(
   )
   if (ids.length) {
     ids.sort((a, b) => a.start! - b.start!)
+    const [frag, push] = buildCodeFragment()
     ids.forEach((id, i) => {
       // range is offset by -1 due to the wrapping parens when parsed
       const start = id.start! - 1
@@ -62,21 +67,24 @@ export function genExpression(
       const last = ids[i - 1]
 
       const leadingText = rawExpr.slice(last ? last.end! - 1 : 0, start)
-      if (leadingText.length) push(leadingText, NewlineType.Unknown)
+      if (leadingText.length) push([leadingText, NewlineType.Unknown])
 
       const source = rawExpr.slice(start, end)
-      genIdentifier(source, context, {
-        start: advancePositionWithClone(node.loc.start, source, start),
-        end: advancePositionWithClone(node.loc.start, source, end),
-        source,
-      })
+      push(
+        genIdentifier(source, context, {
+          start: advancePositionWithClone(node.loc.start, source, start),
+          end: advancePositionWithClone(node.loc.start, source, end),
+          source,
+        }),
+      )
 
       if (i === ids.length - 1 && end < rawExpr.length) {
-        push(rawExpr.slice(end), NewlineType.Unknown)
+        push([rawExpr.slice(end), NewlineType.Unknown])
       }
     })
+    return frag
   } else {
-    push(rawExpr, NewlineType.Unknown)
+    return [[rawExpr, NewlineType.Unknown]]
   }
 }
 
@@ -84,9 +92,9 @@ const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
 
 function genIdentifier(
   id: string,
-  { options, vaporHelper, push }: CodegenContext,
+  { options, vaporHelper }: CodegenContext,
   loc?: SourceLocation,
-): void {
+): CodeFragment {
   const { inline, bindingMetadata } = options
   let name: string | undefined = id
   if (inline) {
@@ -102,5 +110,5 @@ function genIdentifier(
   } else {
     id = `_ctx.${id}`
   }
-  push(id, NewlineType.None, loc, name)
+  return [id, NewlineType.None, loc, name]
 }
index 3ca7c569562dd00f65afab6279012558664afcbd..f6188fd72c40a7d3b59e4410e2229fab126b6f1f 100644 (file)
@@ -1,11 +1,18 @@
-import type { CodegenContext } from '../generate'
+import type { CodeFragment, CodegenContext } from '../generate'
 import type { SetHtmlIRNode } from '../ir'
 import { genExpression } from './expression'
 
-export function genSetHtml(oper: SetHtmlIRNode, context: CodegenContext) {
-  const { newline, pushCall, vaporHelper } = context
-  newline()
-  pushCall(vaporHelper('setHtml'), `n${oper.element}`, () =>
-    genExpression(oper.value, context),
-  )
+export function genSetHtml(
+  oper: SetHtmlIRNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  const { newline, call, vaporHelper } = context
+  return [
+    newline(),
+    ...call(
+      vaporHelper('setHtml'),
+      [`n${oper.element}`],
+      genExpression(oper.value, context),
+    ),
+  ]
 }
index 72b2b099199771ccbe7b6a557f29d6146a8eb270..69848b424f359e5e3ef5fdee529c0a069f64400f 100644 (file)
@@ -1,4 +1,9 @@
-import { type CodegenContext, genBlockFunctionContent } from '../generate'
+import {
+  type CodeFragment,
+  type CodegenContext,
+  buildCodeFragment,
+  genBlockFunctionContent,
+} from '../generate'
 import { type BlockFunctionIRNode, IRNodeTypes, type IfIRNode } from '../ir'
 import { genExpression } from './expression'
 
@@ -6,43 +11,45 @@ export function genIf(
   oper: IfIRNode,
   context: CodegenContext,
   isNested = false,
-) {
-  const { pushCall, vaporHelper, newline, push } = context
+): CodeFragment[] {
+  const { call, vaporHelper, newline } = context
   const { condition, positive, negative } = oper
+  const [frag, push] = buildCodeFragment()
 
-  let positiveArg = () => genBlockFunction(positive, context)
-  let negativeArg: false | (() => void) = false
+  const conditionExpr: CodeFragment[] = [
+    '() => (',
+    ...genExpression(condition, context),
+    ')',
+  ]
+
+  let positiveArg = genBlockFunction(positive, context)
+  let negativeArg: false | CodeFragment[] = false
 
   if (negative) {
     if (negative.type === IRNodeTypes.BLOCK_FUNCTION) {
-      negativeArg = () => genBlockFunction(negative, context)
+      negativeArg = genBlockFunction(negative, context)
     } else {
-      negativeArg = () => {
-        push('() => ')
-        genIf(negative!, context, true)
-      }
+      negativeArg = ['() => ', ...genIf(negative!, context, true)]
     }
   }
 
-  if (!isNested) newline(`const n${oper.id} = `)
-  pushCall(
-    vaporHelper('createIf'),
-    () => {
-      push('() => (')
-      genExpression(condition, context)
-      push(')')
-    },
-    positiveArg,
-    negativeArg,
+  if (!isNested) push(newline(), `const n${oper.id} = `)
+  push(
+    ...call(vaporHelper('createIf'), conditionExpr, positiveArg, negativeArg),
   )
-}
 
-function genBlockFunction(oper: BlockFunctionIRNode, context: CodegenContext) {
-  const { newline, push, withIndent } = context
+  return frag
+}
 
-  push('() => {')
-  withIndent(() => {
-    genBlockFunctionContent(oper, context)
-  })
-  newline('}')
+function genBlockFunction(
+  oper: BlockFunctionIRNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  const { newline, withIndent } = context
+  return [
+    '() => {',
+    ...withIndent(() => genBlockFunctionContent(oper, context)),
+    newline(),
+    '}',
+  ]
 }
index f596cb2c9aa250676e69529c2e93912ef8294035..b50d2233489df5ff96eed6ab0bb106d2847a737b 100644 (file)
@@ -1,41 +1,41 @@
 import { camelize, isString } from '@vue/shared'
 import { genExpression } from './expression'
 import type { SetModelValueIRNode } from '../ir'
-import type { CodegenContext } from '../generate'
+import type { CodeFragment, CodegenContext } from '../generate'
 
 export function genSetModelValue(
   oper: SetModelValueIRNode,
   context: CodegenContext,
-) {
+): CodeFragment[] {
   const {
     vaporHelper,
-    push,
     newline,
-    pushCall,
+    call,
     options: { isTS },
   } = context
 
-  newline()
-  pushCall(
-    vaporHelper('on'),
-    // 1st arg: event name
-    () => push(`n${oper.element}`),
-    // 2nd arg: event name
-    () => {
-      if (isString(oper.key)) {
-        push(JSON.stringify(`update:${camelize(oper.key)}`))
-      } else {
-        push('`update:${')
-        genExpression(oper.key, context)
-        push('}`')
-      }
-    },
-    // 3rd arg: event handler
-    () => {
-      push((isTS ? `($event: any)` : `$event`) + ' => ((')
+  const name = genName()
+  const handler = genHandler()
+
+  return [
+    newline(),
+    ...call(vaporHelper('on'), [`n${oper.element}`], name, handler),
+  ]
+
+  function genName(): CodeFragment[] {
+    if (isString(oper.key)) {
+      return [JSON.stringify(`update:${camelize(oper.key)}`)]
+    } else {
+      return ['`update:${', ...genExpression(oper.key, context), '}`']
+    }
+  }
+
+  function genHandler(): CodeFragment[] {
+    return [
+      (isTS ? `($event: any)` : `$event`) + ' => ((',
       // TODO handle not a ref
-      genExpression(oper.value, context)
-      push(') = $event)')
-    },
-  )
+      ...genExpression(oper.value, context),
+      ') = $event)',
+    ]
+  }
 }
index 204c2f2c5b64a6aa8ee5e7968e170851e509f7d3..e8f17e934598d6aaad2ed46b9f09fb44e5fa350f 100644 (file)
@@ -1,14 +1,16 @@
-import type { CodegenContext } from '../generate'
+import type { CodeFragment, CodegenContext } from '../generate'
 import type { SetPropIRNode } from '../ir'
 import { genExpression } from './expression'
 import { isString } from '@vue/shared'
 
-export function genSetProp(oper: SetPropIRNode, context: CodegenContext) {
-  const { pushCall, pushMulti, newline, vaporHelper, helper } = context
+export function genSetProp(
+  oper: SetPropIRNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  const { call, newline, vaporHelper, helper } = context
 
-  newline()
-
-  const element = `n${oper.element}`
+  const element = [`n${oper.element}`]
+  const expr = genExpression(oper.key, context)
 
   // fast path for static props
   if (isString(oper.key) || oper.key.isStatic) {
@@ -27,40 +29,35 @@ export function genSetProp(oper: SetPropIRNode, context: CodegenContext) {
     }
 
     if (helperName) {
-      pushCall(
-        vaporHelper(helperName),
-        element,
-        omitKey
-          ? false
-          : () => {
-              const expr = () => genExpression(oper.key, context)
-              if (oper.runtimeCamelize) {
-                pushCall(helper('camelize'), expr)
-              } else {
-                expr()
-              }
-            },
-        () => genExpression(oper.value, context),
-      )
-      return
+      return [
+        newline(),
+        ...call(
+          vaporHelper(helperName),
+          element,
+          omitKey ? false : expr,
+          genExpression(oper.value, context),
+        ),
+      ]
     }
   }
 
-  pushCall(
-    vaporHelper('setDynamicProp'),
-    element,
-    // 2. key name
-    () => {
-      if (oper.runtimeCamelize) {
-        pushCall(helper('camelize'), () => genExpression(oper.key, context))
-      } else if (oper.modifier) {
-        pushMulti([`\`${oper.modifier}\${`, `}\``], () =>
-          genExpression(oper.key, context),
-        )
-      } else {
-        genExpression(oper.key, context)
-      }
-    },
-    () => genExpression(oper.value, context),
-  )
+  return [
+    newline(),
+    ...call(
+      vaporHelper('setDynamicProp'),
+      element,
+      genDynamicKey(),
+      genExpression(oper.value, context),
+    ),
+  ]
+
+  function genDynamicKey(): CodeFragment[] {
+    if (oper.runtimeCamelize) {
+      return call(helper('camelize'), expr)
+    } else if (oper.modifier) {
+      return [`\`${oper.modifier}\${`, ...expr, `}\``]
+    } else {
+      return expr
+    }
+  }
 }
index bcd0c0880d1e8f0a15729c8bc56d867609c89ca3..896443037fc65257910265a653f07c5bc7acdedf 100644 (file)
@@ -1,11 +1,18 @@
-import type { CodegenContext } from '../generate'
-import type { SetRefIRNode } from '../ir'
 import { genExpression } from './expression'
+import type { CodeFragment, CodegenContext } from '../generate'
+import type { SetRefIRNode } from '../ir'
 
-export function genSetRef(oper: SetRefIRNode, context: CodegenContext) {
-  const { newline, pushCall, vaporHelper } = context
-  newline()
-  pushCall(vaporHelper('setRef'), `n${oper.element}`, () =>
-    genExpression(oper.value, context),
-  )
+export function genSetRef(
+  oper: SetRefIRNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  const { newline, call, vaporHelper } = context
+  return [
+    newline(),
+    ...call(
+      vaporHelper('setRef'),
+      [`n${oper.element}`],
+      genExpression(oper.value, context),
+    ),
+  ]
 }
diff --git a/packages/compiler-vapor/src/generators/template.ts b/packages/compiler-vapor/src/generators/template.ts
new file mode 100644 (file)
index 0000000..bbf79c0
--- /dev/null
@@ -0,0 +1,25 @@
+import type { CodeFragment, CodegenContext } from '../generate'
+import {
+  type FragmentFactoryIRNode,
+  IRNodeTypes,
+  type TemplateFactoryIRNode,
+} from '../ir'
+
+export function genTemplate(
+  node: TemplateFactoryIRNode | FragmentFactoryIRNode,
+  index: number,
+  { newline, vaporHelper }: CodegenContext,
+): CodeFragment[] {
+  if (node.type === IRNodeTypes.TEMPLATE_FACTORY) {
+    // TODO source map?
+    return [
+      newline(),
+      `const t${index} = ${vaporHelper('template')}(${JSON.stringify(
+        node.template,
+      )})`,
+    ]
+  } else {
+    // fragment
+    return [newline(), `const t${index} = ${vaporHelper('fragment')}()`]
+  }
+}
index 7752acd97110809fc76058a63bd0108307e160d7..4ece2111951b7f9d327d7e856a0d51f1d909c56b 100644 (file)
@@ -1,22 +1,30 @@
-import type { CodegenContext } from '../generate'
+import type { CodeFragment, CodegenContext } from '../generate'
 import type { CreateTextNodeIRNode, SetTextIRNode } from '../ir'
 import { genExpression } from './expression'
 
-export function genSetText(oper: SetTextIRNode, context: CodegenContext) {
-  const { pushCall, newline, vaporHelper } = context
-  newline()
-  pushCall(vaporHelper('setText'), `n${oper.element}`, () =>
-    genExpression(oper.value, context),
-  )
+export function genSetText(
+  oper: SetTextIRNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  const { call, newline, vaporHelper } = context
+  return [
+    newline(),
+    ...call(
+      vaporHelper('setText'),
+      [`n${oper.element}`],
+      genExpression(oper.value, context),
+    ),
+  ]
 }
 
 export function genCreateTextNode(
   oper: CreateTextNodeIRNode,
   context: CodegenContext,
-) {
-  const { newline, pushCall, vaporHelper } = context
-  newline(`const n${oper.id} = `)
-  pushCall(vaporHelper('createTextNode'), () =>
-    genExpression(oper.value, context),
-  )
+): CodeFragment[] {
+  const { newline, call, vaporHelper } = context
+  return [
+    newline(),
+    `const n${oper.id} = `,
+    ...call(vaporHelper('createTextNode'), genExpression(oper.value, context)),
+  ]
 }