跪拜 Guibai
← Back to the summary

A Vite Plugin That Renames v-model to v-mortal — and Why That's a Compile-Time Win


theme: devui-blue highlight: atom-one-dark

Yesterday, while reviewing code, I suddenly spotted several v-mortal entries in the diff. Huh? Completely unfamiliar, never seen it before. Search engines turned up nothing, and AI said it didn't know either, asking if it was a typo — maybe v-model?

So I found the author of that code, my colleague Xiao Shuang, and asked: "Is this a typo? Shouldn't it be v-model?"

Xiao Shuang chuckled: "Brother, this is a custom directive I built myself. It's pretty advanced — it does two-way data binding just like v-model!"

Turns out Xiao Shuang's English name is Mortal, so he wrote a Vite plugin that replaces every v-mortal with v-model at compile time.

This is a clever approach: the Vue compiler never needs to handle v-mortal, so there's zero extra runtime overhead. Let's see how he implemented it!

Writing the plugin

Using Vite 8 as an example, consulting the Plugin API simple examples docs, first create a VMortalPlugin plugin function:

import type { Plugin } from "vite";

export function VMortalPlugin(): Plugin {
  return {
    name: "v-mortal",
    enforce: "pre",
    transform: {
      filter: {
        id: /\.vue$/
      },
      handler(code) {
        // ...
      }
    }
  };
}

Next, inside transform.handler, use vue/compiler-sfc's parse() to parse the Vue code and extract the template block:

import { parse } from "vue/compiler-sfc";

const { template } = parse(code).descriptor;

Then traverse template.ast to find every v-mortal directive and replace it with v-model using MagicString:

const visit = (node: TemplateChildNode) => {
  // Only process element nodes (e.g. <input>, <CustomComponent>, etc.)
  if (node.type === NodeTypes.ELEMENT) {
    for (const prop of node.props) {
      // Look for the custom directive v-mortal
      if (prop.type === NodeTypes.DIRECTIVE && prop.name === MORTAL) {
        const start = prop.loc.start.offset;
        // Replace v-mortal with v-model
        // Only replace the directive name part, preserving arguments and values
        ms.overwrite(start, start + V_MORTAL.length, "v-model");
      }
    }
  }

  // If the current node has children, continue recursively traversing child nodes
  if ("children" in node) {
    for (const child of node.children) {
      // Filter out simple expression nodes, only process nodes that may contain template structures
      if (
        typeof child === "object" &&
        child.type !== NodeTypes.SIMPLE_EXPRESSION
      ) {
        visit(child);
      }
    }
  }
};

// Start traversing all nodes from the root children of the template AST
for (const child of template.ast.children) {
  visit(child);
}

Once processing is done, finally return code and map (source-map):

return {
  code: ms.toString(),
  map: ms.generateMap({ hires: true })
};

At this point the plugin is complete. Now every v-mortal directive in Vue code will be automatically replaced with v-model during the compilation phase.

The complete code is as follows:

// v-mortal.ts

import type { TemplateChildNode } from "@vue/compiler-core";
import { NodeTypes } from "@vue/compiler-core";
import type { Plugin } from "vite";
import { MagicString, parse } from "vue/compiler-sfc";

const MORTAL = "mortal";
const V_MORTAL = `v-${MORTAL}`;

export function VMortalPlugin(): Plugin {
  return {
    name: "v-mortal",
    enforce: "pre",
    transform: {
      filter: {
        id: /\.vue$/
      },
      handler(code) {
        const { template } = parse(code).descriptor;

        if (template == null || template.ast == null) {
          return;
        }

        const ms = new MagicString(code);

        const visit = (node: TemplateChildNode) => {
          if (node.type === NodeTypes.ELEMENT) {
            for (const prop of node.props) {
              if (prop.type === NodeTypes.DIRECTIVE && prop.name === MORTAL) {
                const start = prop.loc.start.offset;
                ms.overwrite(start, start + V_MORTAL.length, "v-model");
              }
            }
          }

          if ("children" in node) {
            for (const child of node.children) {
              if (
                typeof child === "object" &&
                child.type !== NodeTypes.SIMPLE_EXPRESSION
              ) {
                visit(child);
              }
            }
          }
        };

        for (const child of template.ast.children) {
          visit(child);
        }

        return {
          code: ms.toString(),
          map: ms.generateMap({ hires: true })
        };
      }
    }
  };
}

IntelliSense

When writing v-model, typing v-m usually triggers IDE IntelliSense to suggest v-model. So how can v-mortal get the same treatment?

Xiao Shuang, being clever, consulted the Vue official docs again and found the section on adding types for custom global directives. He followed the pattern and provided a v-mortal.d.ts.

// v-mortal.d.ts

import type { Directive } from "vue";

export type MortalDirective = Directive<HTMLElement, any>;

declare module "vue" {
  export interface GlobalDirectives {
    vMortal: MortalDirective;
  }
}

Now v-mortal also gets IntelliSense — the developer experience is maxed out!

Summary and review

Through this small case, we've learned the basic principles of Vite plugin development and the engineering practice of using TypeScript to improve the development experience.

Extending Vue's syntax capabilities through compile-time transformations gives developers a way to write code that better fits team habits, without introducing extra runtime cost.

v-mortal, though just an inappropriate simple example, demonstrates a very important idea in frontend engineering: push complex logic as early as possible into the build phase, shifting the runtime burden onto the compiler and toolchain.

Note: this case is only for demonstrating plugin development. It's not recommended to do this in daily work, lest you receive "warm greetings" from enthusiastic colleagues~ 😁

Final words

I'm Xiao He (xiaohe0601), passionate about code, currently focused on AI and frontend development.

Feel free to follow my WeChat official account 「小何不会写代码」, where I share development insights, best practices, and technical explorations from time to time. Hope it helps you!

Comments

Top 2 of 4 from juejin.cn, machine-translated. The original thread is authoritative.

striveLei 1 likes

Since v-mortal gets replaced with v-model at build time, why not just use v-model directly?

xiaohe0601

Good question [look]

KieranYin 1 likes

Hehe, brother~~~ keep up the high-quality sharing [heart]

xiaohe0601

Hehe, thanks Kieran for the support [heart]