Don't you know how to write a vite plugin yet?

Now that vite tools are becoming popular, can we do something meaningful, such as writing a vite plugin, what do you think?
It just so happens that we can take advantage of the immature stage of the vite plug-in ecosystem, make a plug-in that pleases us, makes leaders happy, and makes the community happy, and work hand in hand with it.
If you are interested in vite, you can go to the column: "Vite from entry to mastery"
From this article you can learn

How to create a vite plugin template
The role of each hook of the vite plugin
Hook execution order of vite plugin
How to write your own plugin

Learn about vite plugins
1. What is vite plugin
vite is actually a new front-end web development tool driven by native ES Module.
The vite plug-in can well extend the things that vite itself cannot do, such as file image compression, support for commonjs, packaging progress bar and so on.
2. Why write a vite plugin
I believe that every classmate here is familiar with the relevant configuration of webpack and common plug-ins by now;
As a new front-end building tool, vite is still young and has a lot of extensibility, so why don't we go hand in hand with it now? Do something more meaningful to you, me, and everyone else?
Quick experience
If you want to write a plug-in, you must start by creating a project. The following general template of vite plug-in can be used directly by clone when you write a plug-in in the future;
Plug-in general template github: experience entry
Plugin github: experience entry

Recommended package manager to use priority: pnpm > yarn > npm > cnpm

Long story short, just start dry~
Create a vite plugin generic template
1. Initialize
1.1 Create a folder and initialize it: Just follow the prompts to initialize
mkdir vite-plugin-progress && cd vite-plugin-progress && pnpm init

1.2 Install typescript
pnpm i typescript @types/node -D

1.3 Configure tsconfig.json
{
"compilerOptions": {
"module": "ESNext",
"target": "esnext",
"moduleResolution": "node",
"strict": true,
"declaration": true,
"noUnusedLocals": true,
"esModuleInterop": true,
"outDir": "dist",
"lib": ["ESNext"],
"sourceMap": false,
"noEmitOnError": true,
"noImplicitAny": false
},
"include": [
"src/*",
"*.d.ts"
],
"exclude": [
"node_modules",
"examples",
"dist"
]
}

1.4 Install vite
// enter package.json
{
...
"devDependencies": {
"vite": "*"
}
...
}

2. Configure eslint and prettier (optional)


install eslint
pnpm i eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev



configure .eslintrc: configure the connection


Install prettier (optional)
pnpm i prettier eslint-config-prettier eslint-plugin-prettier --save-dev



configure .prettierrc : configure the connection


3. Add src/index.ts entry
import type { PluginOption } from 'vite';

export default function vitePluginTemplate(): PluginOption {
return {
// plugin name
name: 'vite-plugin-template',

// pre will be executed before post
enforce: 'pre', // post

// Indicate that they are only called in 'build' or 'serve' mode
apply: 'build', // apply can also be a function

config(config, { command }) {
console.log('here is the config hook');
},

configResolved(resolvedConfig) {
console.log('here is the configResolved hook');
},

configureServer(server) {
console.log('here is the configureServer hook');
},

transformIndexHtml(html) {
console.log('here is the transformIndexHtml hook');
},
}
}

The vite plugin function hook will be explained in detail below~
At this point, our basic template is built, but let's think about it now, how should we run this plugin?
Then we need to create some examples to run this code;
4. Create the examples directory
I have created three sets of project demos here. You can just copy them directly. I will not introduce them in detail here.

vite-react
vite-vue2
vite-vue3

If your plugin needs to run more demos, just create the project yourself;
Then we need to configure the project under examples to do a joint debugging with the plugin in the current root directory (take examples/vite-vue3 as an example below).
5. Configure the examples/vite-vue3 project


Modify examples/vite-vue3/package.json
{
...
"devDependencies": {
...
"vite": "link:../../node_modules/vite",
"vite-plugin-template": "link:../../"
}
}



The above means:


Make the vite version in the examples/vite-vue3 project consistent with the version in the root directory vite-plugin-template;


At the same time, point the vite-plugin-template in the examples/vite-vue3 project to the plugin developed in your current root directory;




Introduce plugin: examples/vite-vue3/vite.config.ts
import template from 'vite-plugin-template';

export default defineConfig({
...
plugins: [vue(), template()],
...
});



Install: cd examples/vite-vue3 && pnpm install
cd examples/vite-vue3 && pnpm install




Note: the configuration of examples/vite-vue2 and examples/vite-react is consistent with this

think:
At this point, let's think about it again. We have configured the project in examples/vite-vue3, but how should we run it?
Go directly to the examples/vite-vue3 directory and run pnpm run build or pnpm run dev ?
This obviously cannot run successfully, because src/index.ts in our root directory cannot be run directly, so we need to escape the .ts file into a .js file;
So how do we deal with it?
Then we have to try to use a small and no configuration tool tsup.
6. Install tsup configuration run command
tsup is a lightweight and configuration-free build tool supported by esbuild;
At the same time, it can directly convert .ts, .tsx into tools of different formats esm, cjs, iife;


install tsup
pnpm i tsup -D



Configured in package.json in the root directory
{
...
"scripts": {
"dev": "pnpm run build -- --watch --ignore-watch examples",
"build": "tsup src/index.ts --dts --format cjs,esm",
"example:react": "cd examples/vite-react && pnpm run build",
"example:vue2": "cd examples/vite-vue2 && pnpm run build",
"example:vue3": "cd examples/vite-vue3 && pnpm run build"
},
...
}



7. Development environment running


Development environment operation: real-time monitoring file modification and repackaging (hot update)
pnpm run dev



Run any project in the examples (take vite-vue3 as an example)
pnpm run example:vue3



Notice:

If your plugin will only run at build time, set "example:vue3": "cd examples/vite-vue3 && pnpm run build" ;
Otherwise run pnpm run dev


output:


From here, you can run while developing. You Yuxi said it was cool when she saw it~
8. Post

Install bumpp to add version control and tags

pnpm i bumpp -D


configure package.json

{
...
"scripts": {
...
"prepublishOnly": "pnpm run build",
"release": "npx bumpp --push --tag --commit && pnpm publish",
},
...
}


Run publish after developing the plugin

# first step
pnpm run prepublishOnly

# second step
pnpm run release

So here, our vite plugin template has been written, you can directly clone the vite-plugin-template template to use;
If you are interested in vite's plugin hooks and implementing a real vite plugin, you can continue to read below;
Plugin hooks for vite
1. Hooks unique to vite

enforce : the value can be pre or post , pre will be executed before post;
apply : the value can be build or serve or a function, specifying that they are only called in build or serve mode;
config(config, env) : You can modify vite-related configuration before vite is parsed. The hook receives the original user configuration config and a variable env describing the configuration environment;
configResolved(resolvedConfig) : Called after vite configuration is resolved. Use this hook to read and store the final resolved configuration. It's useful when a plugin needs to do something different depending on the command run.
configure reServer(server) : mainly used to configure the development server and add custom middleware for dev-server (connect application);
transformIndexHtml(html) : Dedicated hook for transforming index.html. The hook receives the current HTML string and conversion context;
handleHotUpdate(ctx): Execute custom HMR update, you can send custom events to the client through ws;

2. Construction phase of common hooks for vite and rollup

options(options) : called when the server starts: get and manipulate the Rollup options, strictly speaking, it is executed before the build phase;
buildStart(options): called every time a build starts;
resolveId(source, importer, options): called for each incoming module request, create a custom confirmation function, which can be used to locate third-party dependencies;
load(id): Called for each incoming module request, the loader can be customized, and can be used to return customized content;
transform(code, id): called on each incoming module request, mainly used to transform a single module;
buildEnd(): Called after the end of the build phase, where the end of the build just means that all modules are escaped;

3. The output stage of the general hook of vite and rollup

outputOptions(options): accepts output parameters;
renderStart(outputOptions, inputOptions): will be fired every time bundle.generate and bundle.write are called;
augmentChunkHash(chunkInfo): used to add hash to chunk;
renderChunk(code, chunk, options): Triggered when a single chunk is translated. rollup is called when each chunk file is output;
generateBundle(options, bundle, isWrite): trigger this hook immediately before calling bundle.write;
writeBundle(options, bundle): After calling bundle.write, after all chunks are written to the file, writeBundle will be called at the end;
closeBundle(): called when the server is closed

4. Execution order of plugin hook function hooks (as shown below)

5. Execution order of plugins

Alias ​​Handling Alias
User plugin setting enforce: 'pre'
vite core plugin
User plugin not set enforce
vite build plugin
User plugin setting enforce: 'post'
vite build post plugins (minify, manifest, reporting)

Hand a vite plugin
The following takes the vite packaging progress bar plugin as an example;

Plugin address: github If you feel good, welcome to star ⭐️
The plugin has been officially collected by vite to the official documentation: link address
Because the focus of this article is not on the detailed implementation process of this plugin, this article will only paste the source code for your reference. The detailed introduction will be explained in the next article, please wait and see!

inde.ts

import type { PluginOption } from 'vite';
import colors from 'picocolors';
import progress from 'progress';
import rd from 'rd';
import { isExists, getCacheData, setCacheData } from './cache';

type Omit = Pick>;
type Merge = Omit> & N;

type PluginOptions = Merge<
ProgressBar.ProgressBarOptions,
{
/**
* total number of ticks to complete
* @default 100
*/
total?: number;

/**
* The format of the progress bar
*/
format?: string;
}
>;

export default function viteProgressBar(options?: PluginOptions): PluginOption {

const { cacheTransformCount, cacheChunkCount } = getCacheData()

let bar: progress;
const stream = options?.stream || process.stderr;
let outDir: string;
let transformCount = 0
let chunkCount = 0
let transformed = 0
let fileCount = 0
let lastPercent = 0
let percent = 0

return {
name: 'vite-plugin-progress',

enforce: 'pre',

apply: 'build',

config(config, { command }) {
if (command === 'build') {
config.logLevel = 'silent';
outDir = config.build?.outDir || 'dist';

options = {
width: 40,

...options
};
options.total = options?.total || 100;

const transforming = isExists ? `${colors.magenta('Transforms:')} :transformCur/:transformTotal | ` : ''
const chunks = isExists ? `${colors.magenta('Chunks:')} :chunkCur/:chunkTotal | ` : ''
const barText = `${colors.cyan(`[:bar]`)}`

const barFormat =
options.format ||
`${colors.green('Bouilding')} ${barText} :percent | ${transforming}${chunks}Time: :elapseds`

delete options.format;
bar = new progress(barFormat, options as ProgressBar.ProgressBarOptions);



// not cache: Loop files in src directory
if (!isExists) {
const readDir = rd.readSync('src');
readDir.forEach((item) => reg.test(item) && fileCount++);
}
}
},

transform(code, id) {
transformCount++

// not cache
if(!isExists) {
const reg = /node_modules/gi;

if (!reg.test(id) && percent < 0.25) {
transformed++
percent = +(transformed / (fileCount * 2)).toFixed(2)
percent < 0.8 && (lastPercent = percent)
}

if (percent >= 0.25 && lastPercent <= 0.65) {
lastPercent = +(lastPercent + 0.001).toFixed(4)
}
}

// go cache
if (isExists) runCachedData()

bar.update(lastPercent, {
transformTotal: cacheTransformCount,
transformCur: transformCount,
chunkTotal: cacheChunkCount,
chunkCur: 0,
})

return {
code,
map: null
};
},

renderChunk() {
chunkCount++

if (lastPercent <= 0.95)
isExists ? runCachedData() : (lastPercent = +(lastPercent + 0.005).toFixed(4))

bar.update(lastPercent, {
transformTotal: cacheTransformCount,
transformCur: transformCount,
chunkTotal: cacheChunkCount,
chunkCur: chunkCount,
})

return null
},

closeBundle() {
// close progress
bar.update(1)
bar.terminate()

// set cache data
setCacheData({
cacheTransformCount: transformCount,
cacheChunkCount: chunkCount,
})

// out successful message
stream.write(
`${colors.cyan(colors.bold(`Build successful. Please see ${outDir} directory`))}`
);
stream.write(' ');
stream.write(' ');
}
};

/**
* run cache data of progress
*/
function runCachedData() {

if (transformCount === 1) {
stream.write(' ');

bar.tick({
transformTotal: cacheTransformCount,
transformCur: transformCount,
chunkTotal: cacheChunkCount,
chunkCur: 0,
})
}

transformed++
percent = lastPercent = +(transformed / (cacheTransformCount + cacheChunkCount)).toFixed(2)
}
}



cache.ts

import fs from 'fs';
import path from 'path';

const dirPath = path.join(process.cwd(), 'node_modules', '.progress');
const filePath = path.join(dirPath, 'index.json');

export interface ICacheData {
/**
* Transform all count
*/
cacheTransformCount: number;

/**
* chunk all count
*/
cacheChunkCount: number
}

/**
*It has been cached
* @return boolean
*/
export const isExists = fs.existsSync(filePath) || false;

/**
* Get cached data
* @returns ICacheData
*/
export const getCacheData = (): ICacheData => {
if (!isExists) return {
cacheTransformCount: 0,
cacheChunkCount: 0
};

return JSON.parse(fs.readFileSync(filePath, 'utf8'));
};

/**
* Set the data to be cached
* @returns
*/
export const setCacheData = (data: ICacheData) => {
!isExists && fs.mkdirSync(dirPath);
fs.writeFileSync(filePath, JSON.stringify(data));
};


at last
This series will be a series of continuous updates. Regarding the entire "Vite From Beginner to Mastery" column, I will mainly explain it from the following aspects, please wait and see! ! !

Babes, you've seen it all here, why don't you like it👍.markdown-body pre,.markdown-body pre>code.hljs{color:#333;background:#f8f8f8}.hljs-comment,.hljs- quote{color:#998;font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#333;font-weight:700}.hljs-literal,.hljs-number ,.hljs-tag .hljs-attr,.hljs-template-variable,.hljs-variable{color:teal}.hljs-doctag,.hljs-string{color:#d14}.hljs-section,.hljs-selector -id,.hljs-title{color:#900;font-weight:700}.hljs-subst{font-weight:400}.hljs-class .hljs-title,.hljs-type{color:#458;font -weight:700}.hljs-attribute,.hljs-name,.hljs-tag{color:navy;font-weight:400}.hljs-link,.hljs-regexp{color:#009926}.hljs-bullet, .hljs-symbol{color:#990073}.hljs-built_in,.hljs-builtin-name{color:#0086b3}.hljs-meta{color:#999;font-weight:700}.hljs-deletion{background: #fdd}.hljs-addition{background:#dfd}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700} Category: Front-end Tags: Front-end Vite Articles are included in the column:
Vite from entry to mastery

Vite (pronounced similar to [weɪt], light, light meaning) is a web development front-end construction tool driven by native ES Module.
Has the following characteristics:
💡 Extremely fast service startup;
⚡️ Lightweight and fast hot reload;
🛠️ Rich functions;
📦 Optimized build;
🔩 Universal plugin;
🔑 Fully typed API.

Related Articles

Explore More Special Offers

  1. Short Message Service(SMS) & Mail Service

    50,000 email package starts as low as USD 1.99, 120 short messages start at only USD 1.00