]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor: codegen indent
author三咲智子 Kevin Deng <sxzz@sxzz.moe>
Fri, 1 Dec 2023 14:45:08 +0000 (22:45 +0800)
committer三咲智子 Kevin Deng <sxzz@sxzz.moe>
Fri, 1 Dec 2023 14:45:08 +0000 (22:45 +0800)
packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap
packages/compiler-vapor/__tests__/__snapshots__/fixtures.test.ts.snap
packages/compiler-vapor/__tests__/compile.test.ts
packages/compiler-vapor/src/generate.ts

index 22883caa72ddab4036f13278f5784f479a1b2e67..4babbb9fd076bb1a08314af5deba66b8ed5dbfbc 100644 (file)
 
 exports[`compile > bindings 1`] = `
 "export function render(_ctx) {
-  const t0 = _template('<div>count is <!>.</div>');
-  const n0 = t0();
-  const {
-    0: [
-      n3,
-      {
-        1: [n2],
-      },
-    ],
-  } = _children(n0);
-  const n1 = _createTextNode(count.value);
-  _insert(n1, n3, n2);
+  const t0 = _template(\\"<div>count is <!>.</div>\\")
+  const n0 = t0()
+  const { 0: [n3, { 1: [n2],}],} = _children(n0)
+  const n1 = _createTextNode(count.value)
+  _insert(n1, n3, n2)
   _effect(() => {
-    _setText(n1, undefined, count.value);
-  });
-  return n0;
+    _setText(n1, undefined, count.value)
+  })
+  return n0
 }
-import { template as _template, children as _children, createTextNode as _createTextNode, insert as _insert, effect as _effect, setText as _setText } from 'vue/vapor';
+
+import { template as _template, children as _children, createTextNode as _createTextNode, insert as _insert, effect as _effect, setText as _setText } from 'vue/vapor'
 "
 `;
 
 exports[`compile > directives > v-bind > simple expression 1`] = `
 "export function render(_ctx) {
-  const t0 = _template('<div></div>');
-  const n0 = t0();
-  const {
-    0: [n1],
-  } = _children(n0);
+  const t0 = _template(\\"<div></div>\\")
+  const n0 = t0()
+  const { 0: [n1],} = _children(n0)
   _effect(() => {
-    _setAttr(n1, 'id', undefined, id.value);
-  });
-  return n0;
+    _setAttr(n1, \\"id\\", undefined, id.value)
+  })
+  return n0
 }
-import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor';
+
+import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor'
 "
 `;
 
 exports[`compile > directives > v-html > no expression 1`] = `
 "export function render(_ctx) {
-  const t0 = _template('<div></div>');
-  const n0 = t0();
-  const {
-    0: [n1],
-  } = _children(n0);
+  const t0 = _template(\\"<div></div>\\")
+  const n0 = t0()
+  const { 0: [n1],} = _children(n0)
   _effect(() => {
-    _setHtml(n1, undefined, '');
-  });
-  return n0;
+    _setHtml(n1, undefined, \\"\\")
+  })
+  return n0
 }
-import { template as _template, children as _children, effect as _effect, setHtml as _setHtml } from 'vue/vapor';
+
+import { template as _template, children as _children, effect as _effect, setHtml as _setHtml } from 'vue/vapor'
 "
 `;
 
 exports[`compile > directives > v-html > simple expression 1`] = `
 "export function render(_ctx) {
-  const t0 = _template('<div></div>');
-  const n0 = t0();
-  const {
-    0: [n1],
-  } = _children(n0);
+  const t0 = _template(\\"<div></div>\\")
+  const n0 = t0()
+  const { 0: [n1],} = _children(n0)
   _effect(() => {
-    _setHtml(n1, undefined, code.value);
-  });
-  return n0;
+    _setHtml(n1, undefined, code.value)
+  })
+  return n0
 }
-import { template as _template, children as _children, effect as _effect, setHtml as _setHtml } from 'vue/vapor';
+
+import { template as _template, children as _children, effect as _effect, setHtml as _setHtml } from 'vue/vapor'
 "
 `;
 
 exports[`compile > directives > v-on > event modifier 1`] = `
 "export function render(_ctx) {
-  const t0 = _template('<div></div>');
-  const n0 = t0();
-  const {
-    0: [n1],
-  } = _children(n0);
+  const t0 = _template(\\"<div></div>\\")
+  const n0 = t0()
+  const { 0: [n1],} = _children(n0)
   _effect(() => {
-    _on(n1, 'click', _withModifiers(handleClick, ['prevent', 'stop']));
-  });
-  return n0;
+    _on(n1, \\"click\\", _withModifiers(handleClick, [\\"prevent\\", \\"stop\\"]))
+  })
+  return n0
 }
-import { template as _template, children as _children, effect as _effect, withModifiers as _withModifiers, on as _on } from 'vue/vapor';
+
+import { template as _template, children as _children, effect as _effect, withModifiers as _withModifiers, on as _on } from 'vue/vapor'
 "
 `;
 
 exports[`compile > directives > v-on > simple expression 1`] = `
 "export function render(_ctx) {
-  const t0 = _template('<div></div>');
-  const n0 = t0();
-  const {
-    0: [n1],
-  } = _children(n0);
+  const t0 = _template(\\"<div></div>\\")
+  const n0 = t0()
+  const { 0: [n1],} = _children(n0)
   _effect(() => {
-    _on(n1, 'click', handleClick);
-  });
-  return n0;
+    _on(n1, \\"click\\", handleClick)
+  })
+  return n0
 }
-import { template as _template, children as _children, effect as _effect, on as _on } from 'vue/vapor';
+
+import { template as _template, children as _children, effect as _effect, on as _on } from 'vue/vapor'
 "
 `;
 
 exports[`compile > directives > v-once > as root node 1`] = `
 "export function render(_ctx) {
-  const t0 = _template('<div></div>');
-  const n0 = t0();
-  const {
-    0: [n1],
-  } = _children(n0);
-  _setAttr(n1, 'id', undefined, foo);
-  return n0;
+  const t0 = _template(\\"<div></div>\\")
+  const n0 = t0()
+  const { 0: [n1],} = _children(n0)
+  _setAttr(n1, \\"id\\", undefined, foo)
+  return n0
 }
-import { template as _template, children as _children, setAttr as _setAttr } from 'vue/vapor';
+
+import { template as _template, children as _children, setAttr as _setAttr } from 'vue/vapor'
 "
 `;
 
 exports[`compile > directives > v-once > basic 1`] = `
 "export function render(_ctx) {
-  const t0 = _template('<div> <span></span></div>');
-  const n0 = t0();
-  const {
-    0: [
-      n3,
-      {
-        1: [n2],
-      },
-    ],
-  } = _children(n0);
-  const n1 = _createTextNode(msg.value);
-  _setText(n1, undefined, msg.value);
-  _setAttr(n2, 'class', undefined, clz.value);
-  _prepend(n3, n1);
-  return n0;
+  const t0 = _template(\\"<div> <span></span></div>\\")
+  const n0 = t0()
+  const { 0: [n3, { 1: [n2],}],} = _children(n0)
+  const n1 = _createTextNode(msg.value)
+  _setText(n1, undefined, msg.value)
+  _setAttr(n2, \\"class\\", undefined, clz.value)
+  _prepend(n3, n1)
+  return n0
 }
-import { template as _template, children as _children, createTextNode as _createTextNode, setText as _setText, setAttr as _setAttr, prepend as _prepend } from 'vue/vapor';
+
+import { template as _template, children as _children, createTextNode as _createTextNode, setText as _setText, setAttr as _setAttr, prepend as _prepend } from 'vue/vapor'
 "
 `;
 
 exports[`compile > directives > v-pre > basic 1`] = `
 "export function render(_ctx) {
-  const t0 = _template('<div :id=\\"foo\\"><Comp></Comp>{{ bar }}</div>');
-  const n0 = t0();
-  return n0;
+  const t0 = _template(\\"<div :id=\\\\\\"foo\\\\\\"><Comp></Comp>{{ bar }}</div>\\")
+  const n0 = t0()
+  return n0
 }
-import { template as _template } from 'vue/vapor';
+
+import { template as _template } from 'vue/vapor'
 "
 `;
 
 exports[`compile > directives > v-pre > self-closing v-pre 1`] = `
 "export function render(_ctx) {
-  const t0 = _template('<div></div><div><Comp></Comp></div>');
-  const n0 = t0();
-  const {
-    1: [n1],
-  } = _children(n0);
-  const n2 = _createTextNode(bar);
-  _append(n1, n2);
+  const t0 = _template(\\"<div></div><div><Comp></Comp></div>\\")
+  const n0 = t0()
+  const { 1: [n1],} = _children(n0)
+  const n2 = _createTextNode(bar)
+  _append(n1, n2)
   _effect(() => {
-    _setAttr(n1, 'id', undefined, foo);
-  });
+    _setAttr(n1, \\"id\\", undefined, foo)
+  })
   _effect(() => {
-    _setText(n2, undefined, bar);
-  });
-  return n0;
+    _setText(n2, undefined, bar)
+  })
+  return n0
 }
-import { template as _template, children as _children, createTextNode as _createTextNode, append as _append, effect as _effect, setAttr as _setAttr, setText as _setText } from 'vue/vapor';
+
+import { template as _template, children as _children, createTextNode as _createTextNode, append as _append, effect as _effect, setAttr as _setAttr, setText as _setText } from 'vue/vapor'
 "
 `;
 
 exports[`compile > directives > v-pre > should not affect siblings after it 1`] = `
 "export function render(_ctx) {
-  const t0 = _template('<div :id=\\"foo\\"><Comp></Comp>{{ bar }}</div><div><Comp></Comp></div>');
-  const n0 = t0();
-  const {
-    1: [n1],
-  } = _children(n0);
-  const n2 = _createTextNode(bar.value);
-  _append(n1, n2);
+  const t0 = _template(\\"<div :id=\\\\\\"foo\\\\\\"><Comp></Comp>{{ bar }}</div><div><Comp></Comp></div>\\")
+  const n0 = t0()
+  const { 1: [n1],} = _children(n0)
+  const n2 = _createTextNode(bar.value)
+  _append(n1, n2)
   _effect(() => {
-    _setAttr(n1, 'id', undefined, foo.value);
-  });
+    _setAttr(n1, \\"id\\", undefined, foo.value)
+  })
   _effect(() => {
-    _setText(n2, undefined, bar.value);
-  });
-  return n0;
+    _setText(n2, undefined, bar.value)
+  })
+  return n0
 }
-import { template as _template, children as _children, createTextNode as _createTextNode, append as _append, effect as _effect, setAttr as _setAttr, setText as _setText } from 'vue/vapor';
+
+import { template as _template, children as _children, createTextNode as _createTextNode, append as _append, effect as _effect, setAttr as _setAttr, setText as _setText } from 'vue/vapor'
 "
 `;
 
 exports[`compile > directives > v-text > no expression 1`] = `
 "export function render(_ctx) {
-  const t0 = _template('<div></div>');
-  const n0 = t0();
-  const {
-    0: [n1],
-  } = _children(n0);
+  const t0 = _template(\\"<div></div>\\")
+  const n0 = t0()
+  const { 0: [n1],} = _children(n0)
   _effect(() => {
-    _setText(n1, undefined, '');
-  });
-  return n0;
+    _setText(n1, undefined, \\"\\")
+  })
+  return n0
 }
-import { template as _template, children as _children, effect as _effect, setText as _setText } from 'vue/vapor';
+
+import { template as _template, children as _children, effect as _effect, setText as _setText } from 'vue/vapor'
 "
 `;
 
 exports[`compile > directives > v-text > simple expression 1`] = `
 "export function render(_ctx) {
-  const t0 = _template('<div></div>');
-  const n0 = t0();
-  const {
-    0: [n1],
-  } = _children(n0);
+  const t0 = _template(\\"<div></div>\\")
+  const n0 = t0()
+  const { 0: [n1],} = _children(n0)
   _effect(() => {
-    _setText(n1, undefined, str.value);
-  });
-  return n0;
+    _setText(n1, undefined, str.value)
+  })
+  return n0
 }
-import { template as _template, children as _children, effect as _effect, setText as _setText } from 'vue/vapor';
+
+import { template as _template, children as _children, effect as _effect, setText as _setText } from 'vue/vapor'
 "
 `;
 
 exports[`compile > dynamic root 1`] = `
 "export function render(_ctx) {
-  const t0 = _fragment();
-  const n0 = t0();
-  const n1 = _createTextNode(1);
-  const n2 = _createTextNode(2);
-  _append(n0, n1, n2);
+  const t0 = _fragment()
+
+  const n0 = t0()
+  const n1 = _createTextNode(1)
+  const n2 = _createTextNode(2)
+  _append(n0, n1, n2)
   _effect(() => {
-    _setText(n1, undefined, 1);
-  });
+    _setText(n1, undefined, 1)
+  })
   _effect(() => {
-    _setText(n2, undefined, 2);
-  });
-  return n0;
+    _setText(n2, undefined, 2)
+  })
+  return n0
 }
-import { fragment as _fragment, createTextNode as _createTextNode, append as _append, effect as _effect, setText as _setText } from 'vue/vapor';
+
+import { fragment as _fragment, createTextNode as _createTextNode, append as _append, effect as _effect, setText as _setText } from 'vue/vapor'
 "
 `;
 
 exports[`compile > dynamic root nodes and interpolation 1`] = `
 "export function render(_ctx) {
-  const t0 = _template('<button>foo<!>foo</button>');
-  const n0 = t0();
-  const {
-    0: [
-      n1,
-      {
-        1: [n5],
-      },
-    ],
-  } = _children(n0);
-  const n2 = _createTextNode(count);
-  const n3 = _createTextNode(count);
-  const n4 = _createTextNode(count);
-  _prepend(n1, n2);
-  _insert(n3, n1, n5);
-  _append(n1, n4);
+  const t0 = _template(\\"<button>foo<!>foo</button>\\")
+  const n0 = t0()
+  const { 0: [n1, { 1: [n5],}],} = _children(n0)
+  const n2 = _createTextNode(count)
+  const n3 = _createTextNode(count)
+  const n4 = _createTextNode(count)
+  _prepend(n1, n2)
+  _insert(n3, n1, n5)
+  _append(n1, n4)
   _effect(() => {
-    _on(n1, 'click', handleClick);
-  });
+    _on(n1, \\"click\\", handleClick)
+  })
   _effect(() => {
-    _setAttr(n1, 'id', undefined, count);
-    _setText(n2, undefined, count);
-    _setText(n3, undefined, count);
-    _setText(n4, undefined, count);
-  });
-  return n0;
+    _setAttr(n1, \\"id\\", undefined, count)
+    _setText(n2, undefined, count)
+    _setText(n3, undefined, count)
+    _setText(n4, undefined, count)
+  })
+  return n0
 }
-import { template as _template, children as _children, createTextNode as _createTextNode, prepend as _prepend, insert as _insert, append as _append, effect as _effect, on as _on, setAttr as _setAttr, setText as _setText } from 'vue/vapor';
+
+import { template as _template, children as _children, createTextNode as _createTextNode, prepend as _prepend, insert as _insert, append as _append, effect as _effect, on as _on, setAttr as _setAttr, setText as _setText } from 'vue/vapor'
 "
 `;
 
 exports[`compile > fragment 1`] = `
 "export function render(_ctx) {
-  const t0 = _template('<p></p><span></span><div></div>');
-  const n0 = t0();
-  return n0;
+  const t0 = _template(\\"<p></p><span></span><div></div>\\")
+  const n0 = t0()
+  return n0
 }
-import { template as _template } from 'vue/vapor';
+
+import { template as _template } from 'vue/vapor'
 "
 `;
 
 exports[`compile > static + dynamic root 1`] = `
 "export function render(_ctx) {
-  const t0 = _template('3<!>6<!>9');
-  const n0 = t0();
-  const {
-    1: [n9],
-    3: [n10],
-  } = _children(n0);
-  const n1 = _createTextNode(1);
-  const n2 = _createTextNode(2);
-  const n3 = _createTextNode(4);
-  const n4 = _createTextNode(5);
-  const n5 = _createTextNode(7);
-  const n6 = _createTextNode(8);
-  const n7 = _createTextNode('A');
-  const n8 = _createTextNode('B');
-  _prepend(n0, n1, n2);
-  _insert([n3, n4], n0, n9);
-  _insert([n5, n6], n0, n10);
-  _append(n0, n7, n8);
+  const t0 = _template(\\"3<!>6<!>9\\")
+  const n0 = t0()
+  const { 1: [n9], 3: [n10],} = _children(n0)
+  const n1 = _createTextNode(1)
+  const n2 = _createTextNode(2)
+  const n3 = _createTextNode(4)
+  const n4 = _createTextNode(5)
+  const n5 = _createTextNode(7)
+  const n6 = _createTextNode(8)
+  const n7 = _createTextNode('A')
+  const n8 = _createTextNode('B')
+  _prepend(n0, n1, n2)
+  _insert([n3, n4], n0, n9)
+  _insert([n5, n6], n0, n10)
+  _append(n0, n7, n8)
   _effect(() => {
-    _setText(n1, undefined, 1);
-  });
+    _setText(n1, undefined, 1)
+  })
   _effect(() => {
-    _setText(n2, undefined, 2);
-  });
+    _setText(n2, undefined, 2)
+  })
   _effect(() => {
-    _setText(n3, undefined, 4);
-  });
+    _setText(n3, undefined, 4)
+  })
   _effect(() => {
-    _setText(n4, undefined, 5);
-  });
+    _setText(n4, undefined, 5)
+  })
   _effect(() => {
-    _setText(n5, undefined, 7);
-  });
+    _setText(n5, undefined, 7)
+  })
   _effect(() => {
-    _setText(n6, undefined, 8);
-  });
+    _setText(n6, undefined, 8)
+  })
   _effect(() => {
-    _setText(n7, undefined, 'A');
-  });
+    _setText(n7, undefined, 'A')
+  })
   _effect(() => {
-    _setText(n8, undefined, 'B');
-  });
-  return n0;
+    _setText(n8, undefined, 'B')
+  })
+  return n0
 }
-import { template as _template, children as _children, createTextNode as _createTextNode, prepend as _prepend, insert as _insert, append as _append, effect as _effect, setText as _setText } from 'vue/vapor';
+
+import { template as _template, children as _children, createTextNode as _createTextNode, prepend as _prepend, insert as _insert, append as _append, effect as _effect, setText as _setText } from 'vue/vapor'
 "
 `;
 
 exports[`compile > static template 1`] = `
 "export function render(_ctx) {
-  const t0 = _template('<div><p>hello</p><input><span></span></div>');
-  const n0 = t0();
-  return n0;
+  const t0 = _template(\\"<div><p>hello</p><input><span></span></div>\\")
+  const n0 = t0()
+  return n0
 }
-import { template as _template } from 'vue/vapor';
+
+import { template as _template } from 'vue/vapor'
 "
 `;
index 99a13914585c264662a5e964236897f9a5a37457..665784958b2307b88c1a93969ea9847051f95720 100644 (file)
@@ -16,30 +16,31 @@ const increment = () => count.value++
 
 
 return (() => {
-const t0 = _template(\\"<h1 id=\\\\\\"title\\\\\\">Counter</h1><p>Count: </p><p>Double: </p><button>Increment</button><div></div><input type=\\\\\\"text\\\\\\"><p>once: </p><p>{{ count }}</p>\\")
-const n0 = t0()
-const { 1: [n2], 2: [n4], 3: [n5], 4: [n6], 6: [n8],} = _children(n0)
-const n1 = _createTextNode(count.value)
-_append(n2, n1)
-const n3 = _createTextNode(double.value)
-_append(n4, n3)
-const n7 = _createTextNode(count.value)
-_setText(n7, undefined, count.value)
-_append(n8, n7)
-_effect(() => {
-_setText(n1, undefined, count.value)
-})
-_effect(() => {
-_setText(n3, undefined, double.value)
-})
-_effect(() => {
-_on(n5, \\"click\\", increment)
-})
-_effect(() => {
-_setHtml(n6, undefined, html)
-})
-return n0
+  const t0 = _template(\\"<h1 id=\\\\\\"title\\\\\\">Counter</h1><p>Count: </p><p>Double: </p><button>Increment</button><div></div><input type=\\\\\\"text\\\\\\"><p>once: </p><p>{{ count }}</p>\\")
+  const n0 = t0()
+  const { 1: [n2], 2: [n4], 3: [n5], 4: [n6], 6: [n8],} = _children(n0)
+  const n1 = _createTextNode(count.value)
+  _append(n2, n1)
+  const n3 = _createTextNode(double.value)
+  _append(n4, n3)
+  const n7 = _createTextNode(count.value)
+  _setText(n7, undefined, count.value)
+  _append(n8, n7)
+  _effect(() => {
+    _setText(n1, undefined, count.value)
+  })
+  _effect(() => {
+    _setText(n3, undefined, double.value)
+  })
+  _effect(() => {
+    _on(n5, \\"click\\", increment)
+  })
+  _effect(() => {
+    _setHtml(n6, undefined, html)
+  })
+  return n0
 })()
+
 import { template as _template, children as _children, createTextNode as _createTextNode, append as _append, setText as _setText, effect as _effect, on as _on, setHtml as _setHtml } from 'vue/vapor'
 
 }
index e4b8e295fbea3e0bfc29c0c5ab1e008ee88d36e4..e718947758172ed7749ab6c256d01a398fe10f3c 100644 (file)
@@ -1,19 +1,8 @@
 import { type RootNode, BindingTypes, ErrorCodes } from '@vue/compiler-dom'
 import { type CompilerOptions, compile as _compile } from '../src'
 
-// TODO remove it
-import { format } from 'prettier'
-
-async function compile(
-  template: string | RootNode,
-  options: CompilerOptions = {},
-) {
+function compile(template: string | RootNode, options: CompilerOptions = {}) {
   let { code } = _compile(template, options)
-  code = await format(code, {
-    parser: 'babel',
-    printWidth: 999999,
-    singleQuote: true,
-  })
   return code
 }
 
@@ -203,7 +192,9 @@ describe('compile', () => {
         )
 
         expect(code).toMatchSnapshot()
-        expect(code).contains('<div :id="foo"><Comp></Comp>{{ bar }}</div>')
+        expect(code).contains(
+          JSON.stringify('<div :id="foo"><Comp></Comp>{{ bar }}</div>'),
+        )
         expect(code).not.contains('effect')
       })
 
index 5eff9a9404b71bf06cd838241370e2cd2619af64..69bef1b0c53f42079a9125935e69d2fca3a0749a 100644 (file)
@@ -30,9 +30,10 @@ export interface CodegenContext
   indentLevel: number
   map?: SourceMapGenerator
 
-  push(code: string, newlineIndex?: NewlineType, node?: IRNode): void
+  push(code: string, newlineIndex?: number, node?: IRNode): void
+  pushWithNewline(code: string, newlineIndex?: number, node?: IRNode): void
   indent(): void
-  deindent(withoutNewLine?: boolean): void
+  deindent(): void
   newline(): void
 
   helpers: Set<string>
@@ -90,7 +91,7 @@ function createCodegenContext(
       vaporHelpers.add(name)
       return `_${name}`
     },
-    push(code, newlineIndex: NewlineType = NewlineType.None, node) {
+    push(code, newlineIndex = NewlineType.None, node) {
       context.code += code
       if (!__BROWSER__ && context.map) {
         if (node) {
@@ -144,15 +145,15 @@ function createCodegenContext(
         }
       }
     },
+    pushWithNewline(code, newlineIndex, node) {
+      context.newline()
+      context.push(code, newlineIndex, node)
+    },
     indent() {
-      newline(++context.indentLevel)
+      ++context.indentLevel
     },
-    deindent(withoutNewLine = false) {
-      if (withoutNewLine) {
-        --context.indentLevel
-      } else {
-        newline(--context.indentLevel)
-      }
+    deindent() {
+      --context.indentLevel
     },
     newline() {
       newline(context.indentLevel)
@@ -196,74 +197,82 @@ export function generate(
   options: CodegenOptions = {},
 ): CodegenResult {
   const ctx = createCodegenContext(ir, options)
+  const { push, pushWithNewline, indent, deindent, newline } = ctx
   const { vaporHelper, helpers, vaporHelpers } = ctx
 
   const functionName = 'render'
   const isSetupInlined = !!options.inline
   if (isSetupInlined) {
-    ctx.push(`(() => {\n`, NewlineType.End)
+    push(`(() => {`)
   } else {
-    ctx.push(`export function ${functionName}(_ctx) {\n`, NewlineType.End)
+    push(`export function ${functionName}(_ctx) {`)
   }
+  indent()
 
   ir.template.forEach((template, i) => {
     if (template.type === IRNodeTypes.TEMPLATE_FACTORY) {
       // TODO source map?
-      ctx.push(
+      pushWithNewline(
         `const t${i} = ${vaporHelper('template')}(${JSON.stringify(
           template.template,
-        )})\n`,
-        NewlineType.End,
+        )})`,
       )
     } else {
       // fragment
-      ctx.push(`const t0 = ${vaporHelper('fragment')}()\n`, NewlineType.End)
+      pushWithNewline(
+        `const t0 = ${vaporHelper('fragment')}()\n`,
+        NewlineType.End,
+      )
     }
   })
 
   {
-    ctx.push(`const n${ir.dynamic.id} = t0()\n`, NewlineType.End, ir)
+    pushWithNewline(`const n${ir.dynamic.id} = t0()`)
+
     const children = genChildren(ir.dynamic.children)
     if (children) {
-      ctx.push(
-        `const ${children} = ${vaporHelper('children')}(n${ir.dynamic.id})\n`,
-        NewlineType.End,
+      pushWithNewline(
+        `const ${children} = ${vaporHelper('children')}(n${ir.dynamic.id})`,
       )
     }
 
     for (const operation of ir.operation) {
-      genOperation(operation, ctx).forEach((args) => ctx.push(...args))
+      genOperation(operation, ctx)
     }
     for (const [_expr, operations] of Object.entries(ir.effect)) {
-      ctx.push(`${vaporHelper('effect')}(() => {\n`, NewlineType.End)
+      pushWithNewline(`${vaporHelper('effect')}(() => {`)
+      indent()
       for (const operation of operations) {
-        genOperation(operation, ctx).forEach((args) => ctx.push(...args))
+        genOperation(operation, ctx)
       }
-      ctx.push('})\n', NewlineType.End)
+      deindent()
+      pushWithNewline('})')
     }
     // TODO multiple-template
     // TODO return statement in IR
-    ctx.push(`return n${ir.dynamic.id}\n`, NewlineType.End)
+    pushWithNewline(`return n${ir.dynamic.id}`)
   }
 
+  deindent()
+  newline()
   if (isSetupInlined) {
-    ctx.push('})()')
+    push('})()')
   } else {
-    ctx.push('}')
+    push('}')
   }
 
   ctx.newline()
 
   if (vaporHelpers.size)
     // TODO: extract
-    ctx.push(
+    pushWithNewline(
       `import { ${[...vaporHelpers]
         .map((h) => `${h} as _${h}`)
         .join(', ')} } from 'vue/vapor'\n`,
       NewlineType.End,
     )
   if (helpers.size)
-    ctx.push(
+    pushWithNewline(
       `import { ${[...helpers]
         .map((h) => `${h} as _${h}`)
         .join(', ')} } from 'vue'\n`,
@@ -280,30 +289,24 @@ export function generate(
 
 function genOperation(
   oper: OperationNode,
-  { vaporHelper }: CodegenContext,
-): Parameters<CodegenContext['push']>[] {
+  { vaporHelper, pushWithNewline }: CodegenContext,
+) {
   // TODO: cache old value
   switch (oper.type) {
     case IRNodeTypes.SET_PROP: {
-      return [
-        [
-          `${vaporHelper('setAttr')}(n${oper.element}, ${JSON.stringify(
-            oper.name,
-          )}, undefined, ${oper.value})\n`,
-          NewlineType.End,
-        ],
-      ]
+      pushWithNewline(
+        `${vaporHelper('setAttr')}(n${oper.element}, ${JSON.stringify(
+          oper.name,
+        )}, undefined, ${oper.value})`,
+      )
+      return
     }
 
     case IRNodeTypes.SET_TEXT: {
-      return [
-        [
-          `${vaporHelper('setText')}(n${oper.element}, undefined, ${
-            oper.value
-          })\n`,
-          NewlineType.End,
-        ],
-      ]
+      pushWithNewline(
+        `${vaporHelper('setText')}(n${oper.element}, undefined, ${oper.value})`,
+      )
+      return
     }
 
     case IRNodeTypes.SET_EVENT: {
@@ -313,70 +316,54 @@ function genOperation(
           oper.modifiers,
         )})`
       }
-      return [
-        [
-          `${vaporHelper('on')}(n${oper.element}, ${JSON.stringify(
-            oper.name,
-          )}, ${value})\n`,
-          NewlineType.End,
-        ],
-      ]
+      pushWithNewline(
+        `${vaporHelper('on')}(n${oper.element}, ${JSON.stringify(
+          oper.name,
+        )}, ${value})`,
+      )
+      return
     }
 
     case IRNodeTypes.SET_HTML: {
-      return [
-        [
-          `${vaporHelper('setHtml')}(n${oper.element}, undefined, ${
-            oper.value
-          })\n`,
-          NewlineType.End,
-        ],
-      ]
+      pushWithNewline(
+        `${vaporHelper('setHtml')}(n${oper.element}, undefined, ${oper.value})`,
+      )
+      return
     }
 
     case IRNodeTypes.CREATE_TEXT_NODE: {
-      return [
-        [
-          `const n${oper.id} = ${vaporHelper('createTextNode')}(${
-            oper.value
-          })\n`,
-          NewlineType.End,
-        ],
-      ]
+      pushWithNewline(
+        `const n${oper.id} = ${vaporHelper('createTextNode')}(${oper.value})`,
+      )
+      return
     }
 
     case IRNodeTypes.INSERT_NODE: {
       const elements = ([] as number[]).concat(oper.element)
       let element = elements.map((el) => `n${el}`).join(', ')
       if (elements.length > 1) element = `[${element}]`
-      return [
-        [
-          `${vaporHelper('insert')}(${element}, n${
-            oper.parent
-          }${`, n${oper.anchor}`})\n`,
-          NewlineType.End,
-        ],
-      ]
+      pushWithNewline(
+        `${vaporHelper('insert')}(${element}, n${
+          oper.parent
+        }${`, n${oper.anchor}`})`,
+      )
+      return
     }
     case IRNodeTypes.PREPEND_NODE: {
-      return [
-        [
-          `${vaporHelper('prepend')}(n${oper.parent}, ${oper.elements
-            .map((el) => `n${el}`)
-            .join(', ')})\n`,
-          NewlineType.End,
-        ],
-      ]
+      pushWithNewline(
+        `${vaporHelper('prepend')}(n${oper.parent}, ${oper.elements
+          .map((el) => `n${el}`)
+          .join(', ')})`,
+      )
+      return
     }
     case IRNodeTypes.APPEND_NODE: {
-      return [
-        [
-          `${vaporHelper('append')}(n${oper.parent}, ${oper.elements
-            .map((el) => `n${el}`)
-            .join(', ')})\n`,
-          NewlineType.End,
-        ],
-      ]
+      pushWithNewline(
+        `${vaporHelper('append')}(n${oper.parent}, ${oper.elements
+          .map((el) => `n${el}`)
+          .join(', ')})`,
+      )
+      return
     }
     default:
       return checkNever(oper)