How to Reconstruct Vue Templates from Compiled JavaScript

This article explains how Vue single‑file components are compiled into render functions and staticRenderFns, and provides a step‑by‑step method to reverse‑engineer the compiled JavaScript back into the original template, style, and script sections using AST parsing, transformation, and code generation techniques.

WecTeam
WecTeam
WecTeam
How to Reconstruct Vue Templates from Compiled JavaScript

Composition of a Vue file

Anyone familiar with Vue knows that a Vue single‑file component typically contains three parts: template, script, and style.

However, after compilation the resulting JavaScript file no longer contains these three sections directly, making it difficult to locate the original template. Vue does not render directly from the template; it first compiles the template into a render function.

new Vue({
    render: function () {},
    staticRenderFns: []
})

If both a template tag and a render function exist in a Vue SFC, the render function takes precedence.

In fact, the compilation tool converts the Vue SFC into this form: the style is extracted and scoped with a unique identifier, while the script part remains largely unchanged except for the added render and staticRenderFns functions.

/* scope identifier: data-v-3fd7f12e */
.effect-mask[data-v-3fd7f12e] {
    opacity: 0;
}
j = i("X/U8")(F.a, W, !1, Y, "data-v-3fd7f12e", null).exports,

Therefore, to restore a compiled SFC, the main work is to convert the render function and staticRenderFns back into a template, then re‑import the original script module and retrieve the scoped styles using the identifier.

This article focuses on converting the JavaScript‑based render functions back into a Vue template.

Processing staticRenderFns

staticRenderFns

contain static HTML fragments without variables or expressions.

By constructing a suitable execution context and evaluating these functions, we can obtain the original HTML.

The format of staticRenderFns is:

staticRenderFns: [function () {
    var t = this.$createElement,
        e = this._self._c || t;
    return e("div", { staticClass: "btn on" }, [e("i", { staticClass: "icon iconfont" }), e("span", [this._v("下载")])])
}]

We can create a StaticRender class that implements $createElement, _v, and _self, then mount each static render function onto an instance so it can be executed.

The signature of $createElement (from vue/types/vue.d.ts) is:

export interface CreateElement {
  (tag?: string | Component<any, any, any, any> | AsyncComponent<any, any, any, any> | (() => Component), children?: VNodeChildren): VNode;
  (tag?: string | Component<any, any, any, any> | AsyncComponent<any, any, any, any> | (() => Component), data?: VNodeData, children?: VNodeChildren): VNode;
}

In the static render function, the first argument of $createElement is the tag name, the second argument is the attribute object, and the third argument is the children array (the latter two are optional). The function returns an element node. _v takes a single argument and returns a text node.

With these two methods we can easily obtain a node tree and then convert it to HTML.

interface TextNode {
    type: 'text'
    text: string
}

interface Element {
    type: 'element'
    tag: string
    attrMap?: {[key: string]: any}
    children: Node[]
}

type Node = Element | TextNode

export class StaticRender {
    _self = {}
    renderFunc: () => Node
    constructor(renderFunc: () => Node) {
        this._self = {}
        this.renderFunc = renderFunc
    }
    render() {
        var root = this.renderFunc()
        var _html = this.toHtml(root)
        return _html
    }
    toHtml(root: Node): string {
        // generate html
    }
    attrToString(obj: {[key: string]: any}) {
        // format attributes
    }
    _v(str: string) {
        return { text: str, type: 'text' }
    }
    $createElement(tag: string, attrMap: {[key: string]: any}, children: Node[]) {
        var _tag = tag
        var _attrMap = {}
        var _children = []
        if (Array.isArray(attrMap)) {
            _children = attrMap
        } else {
            _attrMap = attrMap
        }
        if (Array.isArray(children)) {
            _children = children
        }
        var ret = {
            tag: _tag,
            type: 'element',
            attrMap: _attrMap || {},
            children: _children || []
        }
        return ret
    }
}

Evaluating the above yields:

<div class="btn on">
    <i class="icon iconfont"></i>
    <span> 下载 </span>
</div>

The HTML fragment generated by staticRenderFns will be reused later.

Processing render

The render function contains many variables and expressions (e.g., v‑if, v‑for), making it difficult to obtain the template by simple evaluation.

Overall workflow

Both compilation and restoration essentially parse code into an AST, transform it, and generate new code.

Vue templates retain most of the original information during compilation, allowing fairly accurate reconstruction.

Because Vue templates are mainly declarative XML with only a few JavaScript expressions, restoring them is relatively straightforward.

Thus, for render we use AST transformation to obtain the template.

The workflow requires three components: a parser, a transformer, and a generator.

The parser converts the render function into a JavaScript AST.

The transformer converts the JavaScript AST into a Vue template AST.

The generator converts the Vue template AST into a Vue template string.

Parser

Parsers are common tools used by ESLint, minifiers, syntax highlighters, type checkers, etc. We can use @typescript-eslint/typescript-estree to parse JavaScript into an ESTree‑compatible AST.

The project estree provides node type definitions for all JavaScript versions.

An ESTree node basic type looks like this:

interface BaseNode {
    type: string
    loc: {
        end: { line: number; start: number }
        start: { line: number; start: number }
    }
    range: [number, number]
}

Different node types add their own properties; for example, a function call expression is defined as:

interface CallExpressionBase extends BaseNode {
    callee: LeftHandSideExpression;
    arguments: Expression[];
    typeParameters?: TSTypeParameterInstantiation;
    optional: boolean;
}

interface CallExpression extends CallExpressionBase {
    type: AST_NODE_TYPES.CallExpression;
    optional: false;
}

The full list of node types can be found in ts‑estree.ts .

Getting the AST is as simple as:

import { parse, TSESTreeOptions, AST } from "@typescript-eslint/typescript-estree"

class Render {
    options: TSESTreeOptions = {
        errorOnUnknownASTType: true,
        loc: true,
        range: true,
    }
    ast: AST<TSESTreeOptions>
    constructor(code: string, staticTpls: string[]) {
        this.ast = parse(code, this.options) // obtain AST
    }
}

Transformer

After obtaining the JavaScript AST we need Vue‑template AST node definitions. A simplified version is:

Non‑essential properties omitted; see index.d.ts for the complete version.
type ASTNode = ASTElement | ASTText | ASTExpression;

interface ASTElement {
  type: 1;
  tag: string;
  attrsList: { name: string; value: any }[];
  attrsMap: Record<string, any>;
  parent?: ASTElement;
  children: ASTNode[];
}

interface ASTText {
  type: 3;
  text: string;
}

interface ASTExpression {
  type: 2;
  expression: string;
  text: string;
  tokens: (string | Record<string, any>)[];
}

Render functions use several built‑in helpers prefixed with _: _l: generate v‑for structures _e: generate empty nodes _s: generate interpolation strings _m: generate static HTML fragments (from staticRenderFns) _v: generate text nodes

Other helpers such as _u, _p can be added later.

Full helper list: vue/render‑helpers and generation logic in vue/codegen .

In addition, the this context contains component data and methods that appear in the template.

Conversion basics

Traverse the AST from the root, first locating the identifiers that correspond to this and $createElement. In most render functions these are assigned to local variables (e.g., t and i).

When a variable that represents this appears in an expression, treat it as the component instance; when a variable that represents $createElement is called, treat it as element creation.

Find the ReturnStatement; its argument is the entry expression for the Vue template AST.

The entry expression is usually a call to $createElement, but it may also be a conditional expression (e.g., when v‑if is on the root).

Parsing a call to $createElement yields a Vue AST node where the first argument is the tag name, the second argument (if an object) provides attributes, and the third argument (if an array) provides children.

Conditional expressions are converted into a wrapper node containing a v‑if template and a v‑else template.

<template v-if="testExp"></template>
<template v-else></template>

We later optimise the wrapper to avoid unnecessary nesting.

Handle expressions inside the render function (e.g., directives, bound attributes, interpolations). For binary expressions we output a string preserving operator order; for _s we wrap the result with {{ }}.

Example of converting an event binding:

i("transition", { on: { click: function(e) { t.onClick(); } } })

becomes:

<transition @click="onClick()"></transition>

If the parameter name shadows this (e.g., function(t){ ... }) we must keep the parameter name.

For _l (v‑for) the first argument is the list identifier, the second argument is a function whose parameters become the loop variables, and the function body returns the element to repeat.

t._l(t.list, function(e, s) { return i("Item", { key: s + "_" + e.id, attrs: { data: e, flag: t.playing } }) })

converts to:

<Item v-for="(e,s) in list" :key="s + '_' + e.id" :data="e" :flag="playing"></Item>
_e

generates an empty node; we replace it with a placeholder $$null that will be removed during optimisation.

function nonNode() {
    var element = { tag: '$$null', type: 1, attrsList: [], attrsMap: {}, children: [], parent: undefined };
    return element;
}
_s

and _v together produce text nodes or interpolations:

t._v(t._s(t.title.length) + "/15") // → {{title.length + "/15"}}

Resulting node:

function textNode(text) {
    var re = /_s\((.*?)\)/g;
    if (re.test(text)) {
        text = `{{${text.replace(re, (a,b)=>b)}}}`;
    } else {
        if (text.startsWith('"') && text.endsWith('"')) {
            text = text.slice(1, -1);
        }
    }
    var element = { tag: '$$text', type: 1, attrsList: [], attrsMap: { text: text }, children: [], parent: undefined };
    return element;
}
_m

creates a placeholder node whose tag is $$static__index; later we replace it with the actual HTML fragment from staticRenderFns.

function staticNode(_exp) {
    if (_exp.type == AST_NODE_TYPES.Literal) {
        var index = _exp.raw;
        var tag = `$$static__${index}`;
        var element = { tag: tag, type: 1, attrsList: [], attrsMap: {}, children: [], parent: undefined };
        return element;
    } else {
        throw new Error("static node parse error");
    }
}

Attribute handling

Attributes are key‑value pairs. Special keys: on: event bindings (converted to @event) model:

v-model
directives

: Vue directives (e.g., v‑show) attrs: static attributes (expanded) staticClass / staticStyle: static class / style strings

For other keys, if the value is a quoted string it becomes a static attribute; otherwise it becomes a bound attribute prefixed with :.

Optimisation

After transformation we may have empty nodes ( $$null) and nested template wrappers. We traverse the tree to remove empty nodes and collapse multiple template layers.

function optimizeNode1(_root) {
    _root.children = _root.children.filter(child => {
        if (child.type == 1 && child.tag == '$$null' && !child.attrsMap['v-if']) {
            return false;
        }
        return true;
    }).map(child => {
        if (child.type == 1) {
            optimizeNode1(child);
        }
        return child;
    });
    return _root;
}

Generator

The generator converts the Vue template AST back to a string, taking care of:

Replacing $$static__ nodes with the corresponding HTML from staticRenderFns Handling self‑closing tags

Omitting values for v‑else attributes

Finally we can run the result through js‑beautify for formatting.

Example

The full source code is available here [8] and an online converter is provided at online conversion [9].

To use the tool, locate a compiled Vue component (e.g., the Element‑UI library js file [10]), find the $createElement calls, and paste the render and staticRenderFns objects into the online form.

{
    render: function () {
        var t = this.$createElement
        // ...
    },
    staticRenderFns: [function () { /* ... */ }]
}

Running the conversion yields a Vue template identical to the original source, for example:

<template>
    <transition name="el-zoom-in-top" @after-leave="$emit('dodestroy')">
        <div v-show="visible" class="el-time-panel el-popper" :class="popperClass">
            <div class="el-time-panel__content" :class="{ 'has-seconds': showSeconds }">
                <time-spinner ref="spinner" :arrow-control="useArrow" :show-seconds="showSeconds" :am-pm-mode="amPmMode" :date="date" @change="handleChange" @select-range="setSelectionRange"></time-spinner>
            </div>
            <div class="el-time-panel__footer">
                <button class="el-time-panel__btn cancel" type="button" @click="handleCancel">{{t("el.datepicker.cancel")}}</button>
                <button class="el-time-panel__btn" :class="{ confirm: !disabled }" type="button" @click="handleConfirm()">{{t("el.datepicker.confirm")}}</button>
            </div>
        </div>
    </transition>
</template>

The generated template matches the original Element‑UI source exactly.

References

[1] estree: https://github.com/estree/estree

[2] ts‑estree.ts: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/types/src/ts-estree.ts

[3] index.d.ts: https://github.com/vuejs/vue/blob/dev/packages/vue-template-compiler/types/index.d.ts

[4] vue/render‑helpers: https://github.com/vuejs/vue/tree/dev/src/core/instance/render-helpers/index.js

[5] vue/codegen: https://github.com/vuejs/vue/tree/dev/src/compiler/codegen/index.js

[6] vue/codegen (again): https://github.com/vuejs/vue/tree/dev/src/compiler/codegen/index.js

[7] vue/codegen (additional reference): https://github.com/vuejs/vue/tree/dev/src/compiler/codegen/index.js

[8] Project repository: https://github.com/mk33mk333/vue-template-transform

[9] Online conversion tool: https://mk33mk333.github.io/vue-template-transform/index.html

[10] Element‑UI compiled JS: https://element.eleme.io/element-ui.0216a22.js

frontendASTVuerender functionstaticRenderFnstemplate reconstruction
WecTeam
Written by

WecTeam

WecTeam (维C团) is the front‑end technology team of JD.com’s Jingxi business unit, focusing on front‑end engineering, web performance optimization, mini‑program and app development, serverless, multi‑platform reuse, and visual building.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.