Hello, I’m glad you can click on this blog, this blog is for the Vite experience series of actual combat article, after reading carefully I believe you can also write a Vite plug-in of your own.

Vite is a new front-end build tool that can significantly improve the front-end development experience.

From zero to one, I will complete a Vite: MarkDown plug-in that reads markdown files in the project directory, parses them into HTML, and eventually renders them into a page.

If you haven’t used Vite yet, you can check out my first two articles. I’ve only been playing it for two days. (below)

  • First experience of Vite + Vue3 — Vite chapter
  • First experience of Vite + Vue3 — Chapter Vue3

This series also takes a look at the Vite source code. Previous articles can be found here:

  • Vite source code interpretation series (graphic combination) – local development server
  • Vite source interpretation series (graphic and text combination) – construction
  • Vite source code interpretation series (graphic combination) – plug-in

Implementation approach

In fact, the implementation of vite plug-in is the Loader + Plugin for WebPack, we will implement markdown plug-in is actually more like the loader part, but will also use some vite plug-in hook functions (such as hot overload).

I need to prepare a plug-in to convert markdown files to HTML. In this case, I’m using Markdown-it, a popular Markdown parser.

Second, I need to recognize the Markdown tag in the code and read the markdown file specified in the tag. This can be done using the fs module of Node plus the re.

Ok, now that we have the implementation ideas sorted out, we are ready to implement the plugin.

Initialize the plug-in directory

We use the NPM init command to initialize the plug-in, named @vitejs/plugin-markdown.

For easy debugging, I created the plugin directory directly in my Vite Demo project.

The repository address of this plugin is @vitejs/ plugin-Markdown. If you are interested, you can download the code directly.

In package.json, we don’t have to worry about setting up the entry file, we can implement our functionality first.

Create test files

Here, we create test files testmd. vue and readme. md in our test project, with the contents and final effect shown below.

Now that we have created the test file, we are ready to explore how to implement it.

Create plug-in entry file —index.ts

Let’s create the plugin entry file — index.ts.

Vite’s plugins support TS, so we’ll write the plugin directly in typescript.

The file contains three attributes: name, Enforce and transform.

  • Name: plug-in name;
  • Enforce: This plug-in is executed before the plugin-vue plug-in so that it can be parsed directly to the original template file;
  • Transform: Code translation, this function is similar towebpackloader.
export default function markdownPlugin() :Plugin {
  return {
    // Plug-in name
    name: 'vite:markdown'.// This plug-in is executed before the plugin-vue plug-in so that it can be parsed directly to the original template file
    enforce: 'pre'.// Code translation, this function is similar to the 'loader' of 'webpack'
    transform(code, id, opt){}}}module.exports = markdownPlugin
markdownPlugin['default'] = markdownPlugin
Copy the code

Filter non-target files

Next, we will filter the files, without conversion, the vUE files that are not Vue files and do not use the G-Markdown tag.

Add the following regular code at the beginning of the transform function.

const vueRE = /\.vue$/;
const markdownRE = /\<g-markdown.*\/\>/g;
if(! vueRE.test(id) || ! markdownRE.test(code))return code;
Copy the code

willmarkdownTag is replaced byhtmlThe text

Next, we will take three steps:

  1. matchingvueAll in the fileg-markdownThe label
  2. Load the correspondingmarkdownThe contents of the file willmarkdownConvert text to browser-recognizedhtmlThe text
  3. willmarkdownTag is replaced byhtmlText, introductionstyleFile, output file content

Let’s start by matching all the G-markdown tags in the vue file, using the same re as above:

const mdList = code.match(markdownRE);
Copy the code

Then a traversal is performed on the list of matched tags, and the markdown text inside each tag is read out:

const filePathRE = / (? <=file=("|')).*(? ) / = (" | "); mdList? .forEach(md= > {
  // Matches the markdown file directory
  const fileRelativePaths = md.match(filePathRE);
  if(! fileRelativePaths? .length)return;

  // Markdown file relative path
  constfileRelativePath = fileRelativePaths! [0];
  // Find the current vue directory
  const fileDir = path.dirname(id);
  // Concatenate the absolute path of the MD file according to the current vue file directory and the relative path of the imported Markdown file
  const filePath = path.resolve(fileDir, fileRelativePath);
  // Read the contents of the markdown file
  const mdText = file.readFileSync(filePath, 'utf-8');

  / /...
});
Copy the code

MdText is the markdown text that we read (figure below)

Next, we need to implement a function to transform this text. We use the markdown-it plug-in mentioned earlier. We create a transformMarkdown function to do this:

const MarkdownIt = require('markdown-it');

const md = new MarkdownIt();
export const transformMarkdown = (mdText: string) :string= > {
  // Add a wrapper class named article-content so we can add styles later
  return `
    <section class='article-content'>
      ${md.render(mdText)}
    </section>
  `;
}
Copy the code

Then, we add the conversion function in the above traversal process, and then replace the original label with the converted text, as follows:

mdList? .forEach(md= > {
  / /...
  // Read the contents of the markdown file
  const mdText = file.readFileSync(filePath, 'utf-8');

  // Replace the G-markdown tag with the converted HTML text
  transformCode = transformCode.replace(md, transformMarkdown(mdText));
});
Copy the code

Once we have the transformed text, and the page is ready to display, we finally add a gold-digging style file to the transform function, as follows:

transform(code, id, opt) {
  / /...
  // Style is a type of text, which is very long
  transformCode = `
    ${transformCode}
    <style scoped>
      ${style}
    </style>
  `

  // Return the converted code
  return transformCode;
}
Copy the code

@vitejs/plugin- Markdown combat plugin address

The plug-in

We need to introduce the plug-in in the test project, we can configure it in vite.config.ts, the code implementation is as follows:

In real development, this step should be done early because the plug-in is introduced early so that changes to the plug-in code can be seen in real time.

Some dependencies may be reported missing after the plug-in is introduced, and you need to install them in your test project for debugging (not in production), such as Markdown-it.

import { defineConfig } from 'vite'
import path from 'path';
import vue from '@vitejs/plugin-vue'
import markdown from './plugin-markdown/src/index';

// https://vitejs.dev/config/
export default defineConfig({
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "src"),}},plugins: [
    vue(),
    // use the @vitejs/plugin-markdown plugin
    markdown()
  ]
});
Copy the code

Then, using the vite command, start our project (don’t forget to introduce the test file testmd. vue in app.vue) and you’ll see something like this. (As shown below)

Configure hot overload

At this point, our plug-in is missing a hot reload feature. Without this feature, modifying the MD file will not trigger hot reloading, requiring a project restart each time.

We need to listen for our MD file in the plugin’s handleHotUpdate hook function, and hot-reload the vue file that depends on that MD file.

Before we do that, we need to store the vue file that introduced the MD file in the transform traversal loop.

Create a map at the top of the plug-in to store dependencies as follows

const mdRelationMap = new Map<string.string> ();Copy the code

The dependencies are then stored in the transform.

mdList? .forEach(md= > {
  / /...
  // Concatenate the absolute path of the MD file according to the current vue file directory and the relative path of the imported Markdown file
  const mdFilePath = path.resolve(fileDir, fileRelativePath);
  // Record the vue file ID to import the current MD file
  mdRelationMap.set(mdFilePath, id);
});
Copy the code

HandleHotUpdate = handleHotUpdate = handleHotUpdate;

handleHotUpdate(ctx) {
  const { file, server, modules } = ctx;
  
  // Filter non-MD files
  if(path.extname(file) ! = ='.md') return;

  // Find the vue file importing the MD file
  const relationId = mdRelationMap.get(file) as string;
  // Find the moduleNode for the vue file
  constrelationModule = [...server.moduleGraph.getModulesByFile(relationId)!] [0];
  // Send a websocket message for a single file hot reload
  server.ws.send({
    type: 'update'.updates: [{type: 'js-update'.path: relationModule.file! , acceptedPath: relationModule.file! , timestamp:new Date().getTime()
      }
    ]
  });

  // Specify the module to be recompiled
  return [...modules, relationModule]
},
Copy the code

At this point, we can modify our MD file and see the page updated in real time. (As shown below)

Fun: by the way, about handleHotUpdate processing document content rarely, for server moduleGraph. GetModulesByFile the API or in vite issue found in the code snippet in the inside, If you find relevant document resources, please also share with me, thank you.

At this point, our plug-in development is complete.

Release the plugin

In all of the above steps, we used local debug mode, which can be cumbersome to share.

Next, we build our package and upload it to NPM for everyone to install and experience.

Let’s add the following lines to package.json.

  "main": "dist/index.js".// Import file
  "scripts": {
    // Empty the dist directory and build the files into it
    "build": "rimraf dist && run-s build-bundle"."build-bundle": "esbuild src/index.ts --bundle --platform=node --target=node12 --external:@vue/compiler-sfc --external:vue/compiler-sfc --external:vite --outfile=dist/index.js"
  },
Copy the code

Then, don’t forget to install rimraf, run-s, and esbuild dependencies. After installing the dependencies, run NPM run build and see our code compiled into the dist directory.

When everything is ready, we can use the NPM publish command to publish our package. (As shown below)

We can then replace the dependencies in vue.config.ts with our built version as follows:

// Because of the local network problem, I can not upload this package, here I directly import the local package, the same as the reference line NPM package
import markdown from './plugin-markdown';
Copy the code

Then we run the project and successfully parse the Markdown file! (As shown below)

summary

That concludes this tutorial.

To get a better grasp of vite plug-in development, it is important to have a clear understanding of the roles and responsibilities of the following lifecycle hooks.

field instructions Belongs to
name The plug-in name viterollupShared
handleHotUpdate Perform custom HMR (module hot replacement) update processing viteexclusive
config Called before parsing the Vite configuration. Can be customized configuration, will be withviteBasic configurations are merged viteexclusive
configResolved Called after the Vite configuration is parsed. You can readviteTo perform some operations viteexclusive
configureServer Is the hook used to configure the development server. The most common use case is to add custom middleware to an internal CONNECT application. viteexclusive
transformIndexHtml conversionindex.htmlDedicated hooks for. viteexclusive
options In the collectionrollupBefore the configuration,viteCalled when the (local) service is started, and can berollupMerge configurations viterollupShared
buildStart inrollupIn the building,viteCalled when the (local) service is started and accessible in this functionrollupThe configuration of the viterollupShared
resolveId Called when parsing a module, and can return a specialresolveIdTo specify a certainimportStatement loads a specific module viterollupShared
load Called when a module is parsed and can return a code block to specify aimportStatement loads a specific module viterollupShared
transform Called when the module is parsed to convert the source code and output the converted result, similar towebpackloader viterollupShared
buildEnd inviteBefore local services are closed,rollupCalled before output file to directory viterollupShared
closeBundle inviteBefore local services are closed,rollupCalled before output file to directory viterollupShared

If you find a good article or documentation that covers these hook functions in more detail, feel free to share it.

Until this post, the Vite series of 6 issues has come to a successful end. Thank you for your support.

One last thing

If you’ve already seen it, please give it a thumbs up

Your likes are the greatest encouragement to the author, and can also let more people see this article!

If you find this article helpful, please help to light up the star on Github.