Skip to content

Overview of Transformer

Implementation

Next, let's look at the Transformer that converts AST to IR.

compiler vapor transformer

As we discussed in the compiler overview, the concept of a transformer has existed in compiler-core since vuejs/core. The implementation is around here.

Since it is not related to Vapor Mode, we will skip it this time, but the transformer in Vapor Mode is designed with reference to the original transformer (not used).
The transformer for Vapor Mode that we will read this time is implemented around here.

We are calling a function called transform implemented in transform.ts in the compiler.

209// AST -> IR
210export function transform(
211  node: RootNode,
212  options: TransformOptions = {},
213): RootIRNode {

Call sequence (compile: parse -> transform -> generate):

36// code/AST -> IR (transform) -> JS (generate)
37export function compile(
38  source: string | RootNode,
39  options: CompilerOptions = {},
40): VaporCodegenResult {
62  const ast = isString(source) ? parse(source, resolvedOptions) : source
76  const ir = transform(
77    ast,
78    extend({}, resolvedOptions, {
79      nodeTransforms: [
80        ...nodeTransforms,
81        ...(options.nodeTransforms || []), // user transforms
82      ],
83      directiveTransforms: extend(
84        {},
85        directiveTransforms,
86        options.directiveTransforms || {}, // user transforms
87      ),
88    }),
89  )
91  return generate(ir, resolvedOptions)

Design of Transformer

There are two types of interfaces in the Transformer. NodeTransform and DirectiveTransform.

31export type NodeTransform = (
32  node: RootNode | TemplateChildNode,
33  context: TransformContext<RootNode | TemplateChildNode>,
34) => void | (() => void) | (() => void)[]
36export type DirectiveTransform = (
37  dir: VaporDirectiveNode,
38  node: ElementNode,
39  context: TransformContext<ElementNode>,
40) => DirectiveTransformResult | void

Various transformers are implemented in /transforms/, and they are either one of these two.

To quickly summarize which is which:

As you might guess from the names. These transformers convert the AST into IR.

As you can see from

76  const ir = transform(
77    ast,
78    extend({}, resolvedOptions, {
79      nodeTransforms: [
80        ...nodeTransforms,
81        ...(options.nodeTransforms || []), // user transforms
82      ],
83      directiveTransforms: extend(
84        {},
85        directiveTransforms,
86        options.directiveTransforms || {}, // user transforms
87      ),
88    }),
89  )

these transformers are passed as options to the transform function.

nodeTransforms and directiveTransforms come from the following:

63  const [nodeTransforms, directiveTransforms] =
64    getBaseTransformPreset(prefixIdentifiers)
100export function getBaseTransformPreset(
101  prefixIdentifiers?: boolean,
102): TransformPreset {
103  return [
104    [
105      transformVOnce,
106      transformVIf,
107      transformVFor,
108      transformSlotOutlet,
109      transformTemplateRef,
110      transformText,
111      transformElement,
112      transformVSlot,
113      transformComment,
114      transformChildren,
115    ],
116    {
117      bind: transformVBind,
118      on: transformVOn,
119      html: transformVHtml,
120      text: transformVText,
121      show: transformVShow,
122      model: transformVModel,
123    },
124  ]
125}
16import { transformChildren } from './transforms/transformChildren'
17import { transformVOnce } from './transforms/vOnce'
18import { transformElement } from './transforms/transformElement'
19import { transformVHtml } from './transforms/vHtml'
20import { transformVText } from './transforms/vText'
21import { transformVBind } from './transforms/vBind'
22import { transformVOn } from './transforms/vOn'
23import { transformVShow } from './transforms/vShow'
24import { transformTemplateRef } from './transforms/transformTemplateRef'
25import { transformText } from './transforms/transformText'
26import { transformVModel } from './transforms/vModel'
27import { transformVIf } from './transforms/vIf'
28import { transformVFor } from './transforms/vFor'
29import { transformComment } from './transforms/transformComment'
30import { transformSlotOutlet } from './transforms/transformSlotOutlet'
31import { transformVSlot } from './transforms/vSlot'

Reading the transform Function

Let's read the transform function right away.

210export function transform(
211  node: RootNode,
212  options: TransformOptions = {},
213): RootIRNode {
214  const ir: RootIRNode = {
215    type: IRNodeTypes.ROOT,
216    node,
217    source: node.source,
218    template: [],
219    component: new Set(),
220    directive: new Set(),
221    block: newBlock(node),
222  }
223
224  const context = new TransformContext(ir, node, options)
225
226  transformNode(context)
227
228  return ir
229}

The transform function holds a single object called TransformContext.

In short, it's an object that holds options and state necessary for the transformation.

62export class TransformContext<T extends AllNode = AllNode> {

We'll read the implementation in this context as we follow the actual transformation process.

For now, we start the transformation process by passing this context to a function called transformNode.

224  const context = new TransformContext(ir, node, options)
225
226  transformNode(context)
231export function transformNode(
232  context: TransformContext<RootNode | TemplateChildNode>,
233): void {

This time, we will follow the process of transforming the AST obtained from the small component we are currently reading.

vue
<template>
  <p>Hello, Vapor!</p>
</template>

The obtained AST is as follows:

json
{
  "type": "RootNode",
  "source": "\n  <p>Hello, Vapor!</p>n",
  "children": [
    {
      "type": "ElementNode",
      "tag": "p",
      "ns": 0,
      "tagType": "Element",
      "props": [],
      "children": [
        {
          "type": "TextNode",
          "content": "Hello, Vapor!"
        }
      ]
    }
  ],
  "helpers": {},
  "components": [],
  "directives": [],
  "hoists": [],
  "imports": [],
  "cached": [],
  "temps": 0
}

First, this Node goes into transformNode, and transformNode sequentially executes the nodeTransforms passed as options.

237  const { nodeTransforms } = context.options
238  const exitFns = []
239  for (const nodeTransform of nodeTransforms) {
240    const onExit = nodeTransform(node, context)

By design, after applying a transform, any functions to be executed at the end are received as onExit. These are stored to be executed later,

241    if (onExit) {
242      if (isArray(onExit)) {
243        exitFns.push(...onExit)
244      } else {
245        exitFns.push(onExit)
246      }
247    }

and are executed at the end of transformNode.

259  let i = exitFns.length
260  while (i--) {
261    exitFns[i]()
262  }

Let's look at the execution of nodeTransforms right away. The order is as follows:

105      transformVOnce,
106      transformVIf,
107      transformVFor,
108      transformSlotOutlet,
109      transformTemplateRef,
110      transformText,
111      transformElement,
112      transformVSlot,
113      transformComment,
114      transformChildren,

Since we are not yet using directives or slots this time, we will read transformText -> transformElement -> transformChildren in order.

transformText

The implementation is here.

packages/compiler-vapor/src/transforms/transformText.ts

If the type of the node we are looking at is ELEMENT, and all its child nodes are text-like and contain interpolations, we treat that node as a "text container" and process it (processTextLikeContainer). A text-like node is either text or interpolation.

63function processTextLikeContainer(
64  children: TextLike[],
65  context: TransformContext<ElementNode>,
66) {
67  const values = children.map(child => createTextLikeExpression(child, context))
68  const literals = values.map(getLiteralExpressionValue)
69  if (literals.every(l => l != null)) {
70    context.childrenTemplate = literals.map(l => String(l))
71  } else {
72    context.registerEffect(values, {
73      type: IRNodeTypes.SET_TEXT,
74      element: context.reference(),
75      values,
76    })
77  }
78}

This time, as you can see from the AST,

json
{
  "type": "ElementNode",
  "tag": "p",
  "ns": 0,
  "tagType": "Element",
  "props": [],
  "children": [
    {
      "type": "TextNode",
      "content": "Hello, Vapor!"
    }
  ]
}

Since we do not include interpolations this time, we do not enter this branch.

Although the order is a bit out of sequence, we proceed to read the nodes one by one, and when we enter the TextNode, we pass through the following branch further down.

40  } else if (node.type === NodeTypes.TEXT) {
41    context.template += node.content
42  }

We add the content of the text node to the template property of the context and finish. The template becomes "Hello, Vapor!".

transformElement

The implementation is here.

packages/compiler-vapor/src/transforms/transformElement.ts

First of all, this transform operates entirely within the onExit lifecycle.
Note that it returns a function.

43  return function postTransformElement() {

Since it is not a Component this time, transformNativeElement will be executed (assuming we are reading the p tag now).

55    const isComponent = tagType === ElementTypes.COMPONENT
62    ;(isComponent ? transformComponentElement : transformNativeElement)(
63      tag,
64      propsResult,
65      context as TransformContext<ElementNode>,
66    )
130function transformNativeElement(
131  tag: string,
132  propsResult: PropsResult,
133  context: TransformContext<ElementNode>,
134) {

In transformNativeElement, we generate a string to pass as an argument to the template function.

First, we extract the tag name from the AST and concatenate it with <.

137  let template = ''
138
139  template += `<${tag}`

If there are props, we generate those as well, but since there are none this time, we'll skip it.

Finally, we insert the childrenTemplate held in the context and generate the closing tag to finish.

165  template += `>` + context.childrenTemplate.join('')
166  // TODO remove unnecessary close tag, e.g. if it's the last element of the template
167  if (!isVoidTag(tag)) {
168    template += `</${tag}>`
169  }

childrenTemplate is created in transformChildren.

In terms of the execution order of the transforms, it's transformText -> transformElement -> transformChildren, but since the transformElement processing we just saw is executed in onExit, transformChildren is executed first, so the childrenTemplate has already been generated.

Now let's look at where childrenTemplate is actually created.

transformChildren

The implementation is here.

packages/compiler-vapor/src/transforms/transformChildren.ts

What it does is simple: it sequentially executes transformNode on each of the children of the incoming node.

18  for (const [i, child] of node.children.entries()) {
19    const childContext = context.create(child, i)
20    transformNode(childContext)

What's interesting here is that when it enters a child node, it first creates a new context (childContext) specifically for the child node.
Then, after transformNode is done, it retrieves the template held in that childContext and pushes it into the parent context.
(The push is just Array.prototype.push)

32    } else {
33      context.childrenTemplate.push(childContext.template)
34    }

We have been able to create the string "<p>Hello, Vapor!</p>" in context.template.

Not Finished Yet

Although we were able to generate the string, we actually need to generate code like:

js
const t0 = _template("<p>Hello, Vapor!</p>");
function _sfc_render(_ctx) {
  const n0 = t0();
  return n0;
}

We still lack some information to achieve this.
We haven't yet seen the implementation that makes this template into t0, assigns the result to n0, and returns it in the render function.
We'll see where this is done on the next page.