Following the release of the React project’s Render template merge collision detection plugin, today’s post will focus on implementing Vue’s Merge collision detection WebPack plugin.

Tips: Currently this WebPack plugin only supports vue2. X, version 3.0 has not been tested yet. Later should be free time will support 3.0 = =. ~~ Compatible with VUE2 and 3

The previous React article implemented how to detect legacy merge conflict code in a Render template in packaged mode. How about Vue?

The answer is yes! Just the idea and method of implementation changed from Babel plug-in to WebPack plug-in.

Why the previous Babel plugin implementation didn’t work

As mentioned in the previous article, JSX in React will eventually be Babel compiled into react. CreateElement, so we can use the Babel plugin to analyze the AST syntax tree of react rendering templates.

What about Vue? In the Astexplore website, we select the file type vue to compile and see the AST syntax tree structure of the vUE single file.

Can we also use this form to write a Babel plug-in form to analyze the AST syntax tree of the template in vue? In fact, no, the vUE project packaged via WebPack is similar to vue-cli, webpack does not recognize the single file at the end of the.vue file. So the.vue single file in vUE is first processed by the Vue-Loader, which will use the official vue-template-compilre for compilation and static analysis. The script code is still passed to Babel for compilation, but the Template rendering template is optimized by the Ue-template-compilre to produce the final string template.

The. Vue template will not be passed to Babel for processing, the Vue-template-compilre will do that already. So you can’t get the compiled code from the Template in the form of the Babel plug-in.

Another way of thinking – webPack plug-ins

We know that no matter what we do with the template code, the final rendering template code will be packaged by WebPack into a module, which will be exported to JS files by WebPack.

Therefore, we can use the WebPack plug-in to listen for specific lifecycle hooks and get the module generated at the time of packaging. After some filtering, you will get the code for the rendering template that is compiled by viee-template-compilre.

This article is not to teach you how to write a wenpack plug-in, webpack plug-in related or can move to the Webpack official website to repair.

To get the full module generated by webPack, we listen to the Compilation seal hook:

Find the Template module from the Modules generated by WebPack

class CheckVueConflictPlugin {
  private readonly options: CheckVueConflictPluginOptions;

  constructor(options? : CheckVueConflictPluginOptions) {
    this.options = options || {};
  }

  apply(compiler: Compiler) {
    const { force = false } = this.options;
    const pluginName = this.constructor.name;
    const { options } = compiler;
    const { mode } = options;
    // It is not in production mode and does not need to be forced to exit
    if(mode ! = ='production' && !force) {
      return;
    }
    // Register the instantiated Compilation hook
    compiler.hooks.compilation.tap(pluginName, (compilation) = > {
      compilation.hooks.seal.tap(pluginName, () = > {
        const newModule = Array.from(compilation.modules) as VueTemplateModule[];
        // Filter the module with type template
        const templateModulesArray = newModule.filter(
          (module) = > module.resource && isVueTemplate(module.resource) && module.resource ! = =module.userRequest, ); }); }); }}Copy the code

A.vue single file will be split into three files by type when it is compiled by compile-template-compilre:

  1. source.vue? vue&type=templateRepresents the render function template
  2. source.vue? vue&type=scriptRepresents the JS logic in a script
  3. source.vue? vue&type=stylePresentation style file

Take a look at the WebPack-packed Template rendering function module:

It’s not hard to write code to filter out the Template rendering function:

// Check whether the VUE template is valid. If the MAC path is a relative path, it cannot be instantiated
// Switch to regular
function isVueTemplate (url) {
  if (/\.vue\? vue&type=template/.test(url)) {
    return true}}class CheckVueConflictPlugin {
  private readonly options: CheckVueConflictPluginOptions;

  constructor(options? : CheckVueConflictPluginOptions) {
    this.options = options || {};
  }

  apply(compiler: Compiler) {
    const { force = false } = this.options;
    const pluginName = this.constructor.name;
    const { options } = compiler;
    const { mode } = options;
    // It is not in production mode and does not need to be forced to exit
    if(mode ! = ='production' && !force) {
      return;
    }
    // Register the instantiated Compilation hook
    compiler.hooks.compilation.tap(pluginName, (compilation) = > {
      compilation.hooks.seal.tap(pluginName, () = > {
        const newModule = Array.from(compilation.modules) as VueTemplateModule[];
        const templateModulesArray = newModule.filter(
          (module) = > module.resource && isVueTemplate(module.resource) && module.resource ! = =module.userRequest,
        );
        // There are two types of template. One is the template file processed by the Vue-Loader template. In this case, the request or userRequest reference path is vue-loader
        // One is exported after being compiled by vue-Loader
        // We need the second type, i.e. the request and resource paths are different when processed by the Vue-Loader
        if (templateModulesArray.length) {
          // In this case, the obtained module content is the template string content that has been static analyzed and optimized by Vue-Loader
          for (let i = 0; i < templateModulesArray.length; i++) {
            if(templateModulesArray[i]._source._value) { checkIsConflict(templateModulesArray[i]); }}}}); }); }}Copy the code

Babel is introduced to analyze the AST syntax tree of template rendering function

From the above steps we get the render string optimized by vue-template-compilre, assuming the original template code looks something like this:

<template>
  <div id="app">
    <router-view />= = = = = = = = = = = = = = =<div>2131313</div>
    <Triangle/>
    <div>
      <div>1</div>
      <div>2</div>
      <div>
        <span>span1</span>
        <span>span2</span>
        <span>span3</span>
      </div>
    </div>
  </div>
</template>
Copy the code

This will generate the following render function string (formatted and commented to make the code easier to read) after being optimized for the Ue-tempalte -compilre:

// The template will eventually be compiled into the render function and exported
var render = function() {
  var _vm = this;
  var _h = _vm.$createElement;
  var _c = _vm._self._c || _h;
  // For a component or tag, this is converted to a _c function
  return _c(
    "div",
    { attrs: { id: "app" } },
    [
      _c("router-view"),
      // Convert the HTML tag content to _vm._v function
      _vm._v("\n ===============\n "),
      _c("div", [_vm._v("2131313")]),
      _c("Triangle"),
      _vm._m(0)].1
  );
};
// Static nodes in vue will be converted to static nodes that will not change. Diff will skip these nodes for performance optimization.
var staticRenderFns = [
  function() {
    var _vm = this;
    var _h = _vm.$createElement;
    var _c = _vm._self._c || _h;
    return _c("div", [
      _c("div", [_vm._v("1")]),
      _c("div", [_vm._v("2")]),
      _c("div", [
        _c("span", [_vm._v("span1")]),
        _c("span", [_vm._v("span2")]),
        _c("span", [_vm._v("span3")]]]]); }];export {render, staticRenderFns}
Copy the code

As you can see, in order to verify that there is no legacy merge conflict code in the template, we only need the strings in _vm._v to be regular matches. Let’s throw this code into Astexplore.

After obtaining the corresponding AST syntax tree, use the Babel plug-in to convert the template source file obtained above into the AST syntax tree, and determine the specific node by the visitor pattern.

import { Compiler, Module } from 'webpack';
import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
import * as babelTypes from '@babel/types';
import { StringLiteral } from '@babel/types';

interface VueTemplateModule extendsModule { resource? :string;
  _source: {
    _value: string; }; userRequest? :string;
}
const newVueToken = ['_vm'];
const vuePropertyKey = ['_v'];

function checkIsConflict(module: VueTemplateModule) {
  const { _source, resource } = module;
  const vueTemplateAst = parse(_source._value, {
    sourceType: 'module'}); traverse(vueTemplateAst, {CallExpression(path) {
      const { callee } = path.node;
      if (
        !(
          babelTypes.isMemberExpression(callee) &&
          babelTypes.isIdentifier(callee.object) &&
          newVueToken.includes(callee.object.name) &&
          babelTypes.isIdentifier(callee.property) &&
          vuePropertyKey.includes(callee.property.name)
        )
      ) {
        return;
      }
      // get the component type name and it's extra props options
      const childrenArray = path.node.arguments;
      const stringLiteralChildArray = childrenArray.filter((children) = >
        babelTypes.isStringLiteral(children),
      ) as StringLiteral[];

      const stringLiteralValArray = stringLiteralChildArray.map((child) = > child.value);

      const conflictText = stringLiteralValArray.find((strText) = > strText.match(/ (= {7}) | (> {7}) | (< / {7})));
      if (conflictText) {
        // Throw an error when a merge conflict is detected
        throw new Error(
          In [`${resource}A possible merge conflict has been detected in the file. Please resubmit the merge conflict content as${conflictText}
          `,); }}}); }Copy the code

At this point, a webPack plug-in is completed to detect whether there is a merge conflict code in the VUE rendering template. The complete implementation source code is as follows:

import { URL } from 'url';
import { Compiler, Module } from 'webpack';
import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
import * as babelTypes from '@babel/types';
import { StringLiteral } from '@babel/types';

interface VueTemplateModule extendsModule { resource? :string;
  _source: {
    _value: string; }; userRequest? :string;
}

interface CheckVueConflictPluginOptions {
  // Whether to forcibly enable template conflict detectionforce? :boolean;
}
const newVueToken = ['_vm'];
const vuePropertyKey = ['_v'];

// Check whether the VUE template is valid. If the MAC path is a relative path, it cannot be instantiated
// Switch to regular
function isVueTemplate (url) {
  if (/\.vue\? vue&type=template/.test(url)) {
    return true}}function checkIsConflict(module: VueTemplateModule) {
  const { _source, resource } = module;
  const vueTemplateAst = parse(_source._value, {
    sourceType: 'module'}); traverse(vueTemplateAst, {CallExpression(path) {
      const { callee } = path.node;
      if (
        !(
          babelTypes.isMemberExpression(callee) &&
          babelTypes.isIdentifier(callee.object) &&
          newVueToken.includes(callee.object.name) &&
          babelTypes.isIdentifier(callee.property) &&
          vuePropertyKey.includes(callee.property.name)
        )
      ) {
        return;
      }
      // get the component type name and it's extra props options
      const childrenArray = path.node.arguments;
      const stringLiteralChildArray = childrenArray.filter((children) = >
        babelTypes.isStringLiteral(children),
      ) as StringLiteral[];

      const stringLiteralValArray = stringLiteralChildArray.map((child) = > child.value);

      const conflictText = stringLiteralValArray.find((strText) = > strText.match(/ (= {7}) | (> {7}) | (< / {7})));
      if (conflictText) {
        // Throw an error when a merge conflict is detected
        throw new Error(
          In [`${resource}A possible merge conflict has been detected in the file. Please resubmit the merge conflict content as${conflictText}
          `,); }}}); }class CheckVueConflictPlugin {
  private readonly options: CheckVueConflictPluginOptions;

  constructor(options? : CheckVueConflictPluginOptions) {
    this.options = options || {};
  }

  apply(compiler: Compiler) {
    const { force = false } = this.options;
    const pluginName = this.constructor.name;
    const { options } = compiler;
    const { mode } = options;
    // It is not in production mode and does not need to be forced to exit
    if(mode ! = ='production' && !force) {
      return;
    }
    // Register the instantiated Compilation hook
    compiler.hooks.compilation.tap(pluginName, (compilation) = > {
      compilation.hooks.seal.tap(pluginName, () = > {
        const newModule = Array.from(compilation.modules) as VueTemplateModule[];
        const templateModulesArray = newModule.filter(
          (module) = > module.resource && isVueTemplate(module.resource) && module.resource ! = =module.userRequest,
        );
        // There are two types of template. One is the template file processed by the Vue-Loader template. In this case, the request or userRequest reference path is vue-loader
        // One is exported after being compiled by vue-Loader
        // We need the second type, i.e. the request and resource paths are different when processed by the Vue-Loader
        if (templateModulesArray.length) {
          // In this case, the obtained module content is the template string content that has been static analyzed and optimized by Vue-Loader
          for (let i = 0; i < templateModulesArray.length; i++) {
            if(templateModulesArray[i]._source._value) { checkIsConflict(templateModulesArray[i]); }}}}); }); }}export default CheckVueConflictPlugin;

Copy the code

Extra meal: VuE3 compatibility

Vue3’s single-file template is still split into three files, identified by type (just like vuE2).

  1. source.vue? vue&type=templateRepresents the render function template
  2. source.vue? vue&type=scriptRepresents the JS logic in a script
  3. source.vue? vue&type=stylePresentation style file

Only the compiled render template is changed, as shown in the following template:

<template>
  <header class="header">
    <div class="logo">
      <i class="back" />
    </div>
    <span>123456</span>
    <div class="tabs">
      <div class="tab-item" v-for="item in tabList" :key="item.name">
        <RouterLink :to="item.link">{{ item.name }}</RouterLink>
      </div>
    </div>= = = = = = = = = = = = = = = = = =<RouterLink class="user" to="/user">
      <i class="iconfont icon-user" />
      >>>>>>>>>>>>sss
      <span>Test environment data</span>
    </RouterLink>
  </header>
</template>

Copy the code

Will compile and optimize the js code as follows:


import { createVNode as _createVNode, renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock, toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, withScopeId as _withScopeId, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from "vue"
const _withId = /*#__PURE__*/_withScopeId("data-v-77d8155b")

_pushScopeId("data-v-77d8155b")
const _hoisted_1 = { class: "header" }
const _hoisted_2 = /*#__PURE__*/_createVNode("div", { class: "logo"},/*#__PURE__*/_createVNode("i", { class: "back"-})]1)
const _hoisted_3 = /*#__PURE__*/_createVNode("span".null."123456", -1)
const _hoisted_4 = { class: "tabs" }
const _hoisted_5 = /*#__PURE__*/_createTextVNode("= = = = = = = = = = = = = = = = = =")
const _hoisted_6 = /*#__PURE__*/_createVNode("i", { class: "iconfont icon-user" }, null, -1)
const _hoisted_7 = /*#__PURE__*/_createTextVNode(" >>>>>>>>>>>>sss ")
const _hoisted_8 = /*#__PURE__*/_createVNode("span".null."Test Environment Data", -1)
_popScopeId()

export const render = /*#__PURE__*/_withId(function render(_ctx, _cache) {
  const _component_RouterLink = _resolveComponent("RouterLink")

  return (_openBlock(), _createBlock("header", _hoisted_1, [
    _hoisted_2,
    _hoisted_3,
    _createVNode("div", _hoisted_4, [
      (_openBlock(true), _createBlock(_Fragment, null, _renderList(_ctx.tabList, (item) = > {
        return (_openBlock(), _createBlock("div", {
          class: "tab-item".key: item.name
        }, [
          _createVNode(_component_RouterLink, {
            to: item.link
          }, {
            default: _withId(() = > [
              _createTextVNode(_toDisplayString(item.name), 1 /* TEXT */)),_: 2
          }, 1032["to"]]))}),128 /* KEYED_FRAGMENT */))
    ]),
    _hoisted_5,
    _createVNode(_component_RouterLink, {
      class: "user".to: "/user"
    }, {
      default: _withId(() = > [
        _hoisted_6,
        _hoisted_7,
        _hoisted_8
      ]),
      _: 1}}))))Copy the code

As you can see, the methods for getting static text are concentrated in the two core methods of _createVNode and _createTextVNode. Judge according to the corresponding AST syntax tree:

export function checkVue3IsConflict(module: VueTemplateModule) {
  const { _source, resource } = module;
  const vueTemplateAst = parse(_source._value, {
    sourceType: 'module'}); traverse(vueTemplateAst, {CallExpression(path) {
      // @ts-ignore
      const { callee } = path.node;
      const nodeArguments = path.node.arguments;
      const isCreateVNode =
        babelTypes.isIdentifier(callee) &&
        callee.name === '_createVNode' &&
        babelTypes.isStringLiteral(nodeArguments[2]);
      const isCreateTextVNode =
        babelTypes.isIdentifier(callee) &&
        callee.name === '_createTextVNode' &&
        babelTypes.isStringLiteral(nodeArguments[0]);
      if(! (isCreateVNode || isCreateTextVNode)) {return;
      }
      const pendingCheckStr = nodeArguments[isCreateVNode ? 2 : 0] as StringLiteral;

      const conflictText = pendingCheckStr.value.match(/ (= {7}) | (> {7}) | (< / {7}));
      if (conflictText) {
        // Throw an error when a merge conflict is detected
        throw new Error(
          In [`${resource}A possible merge conflict has been detected in the file. Please resubmit the merge conflict content as${pendingCheckStr.value}
          `,); }}}); }Copy the code

The effect

use

The compiled library has been uploaded to NPM. If you want to use it, NPM can search @carrotwu/check-vue-conflict-webpack-plugin

How to implement a Babel plugin that detects if a render template has legacy merge conflicts

The articles I write are all first published on blog sites, and in general, nuggets are less likely to move here. If you want to learn more about my technical articles, you can click on my blog.