Overview of Transformer
Implementation
Next, let's look at the Transformer
that converts AST
to IR
.
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:
- NodeTransform
- DirectiveTransform
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.
<template>
<p>Hello, Vapor!</p>
</template>
The obtained AST is as follows:
{
"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,
{
"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:
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.