This blog walks through an example of creating a custom Vite plugin that automatically zips build artifacts at build time. It also includes TypeScript support for full type safety.
Implementation
We first initialize a Node.js project and install TypeScript along with the Node type definitions.
mkdir vite-plugin-zip
cd vite-plugin-zip
npm init -y
npm i -D typescript @types/node
Next, we need to install archiver, a package used to generate archives in the plugin.
We’ll save it as a devDependency because the plugin is only used during the build process, either on developers’ machines or in CI pipelines, and it should not be included in production builds.
npm i -D archiver
We also need a tsconfig.json file to inform the tsc compiler of the input and output dirs for the project, and to specify whether we are building the project as an ES module or a CJS module.
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext", // building for esm, enabling import/export syntax
"lib": ["ESNext"],
"moduleResolution": "Node",
"esModuleInterop": true,
"outDir": "./plugins-dist", // output
"rootDir": "./plugins", // input
"declaration": true,
"skipLibCheck": true
},
"include": ["plugins"]
}
Next, we write our plugin code in vite-plugin-zip.ts.
We use the closeBundle hook from Rollup (which is used by Vite under the hood for its plugin system) to run the ZIP logic during the output generation stage.
//vite-plugin-zip.ts
import fs from "fs";
import path from "path";
import archiver from "archiver";
interface ZipPluginOptions {
outputName?: string;
input?: string;
}
export default function vitePluginZip(options: ZipPluginOptions = {}) {
const { outputName = "dist.zip", input = "dist" } = options;
return {
name: "vite-plugin-zip",
closeBundle: async () => {
const outputPath = path.resolve(process.cwd(), outputName);
const inputPath = path.resolve(process.cwd(), input);
if (!fs.existsSync(inputPath)) {
console.warn(
`[vite-plugin-zip] Input folder "${input}" does not exist.`
);
return;
}
const output = fs.createWriteStream(outputPath);
const archive = archiver("zip", { zlib: { level: 9 } });
return new Promise<void>((resolve, reject) => {
output.on("close", () => {
console.log(
`[vite-plugin-zip] Created ${outputName} (${archive.pointer()} total bytes)`
);
resolve();
});
archive.on("error", (err) => reject(err));
archive.pipe(output);
archive.directory(inputPath, false);
archive.finalize();
});
},
};
}
We may also add some log info.
output.on("close", () => {
const sizeInKB = (archive.pointer() / 1024).toFixed(2);
console.log(
`\x1b[32m[vite-plugin-zip]\x1b[0m Created "${outputName}" (${sizeInKB} KB)`
);
console.log(
`\x1b[32m[vite-plugin-zip]\x1b[0m Zip file is ready at: ${outputPath}`
);
resolve();
});
Finally, we can build the plugin by running
npx tsc
Which compiles(transpiles) plugins/vite-plugin-zip.ts into plugins-dist/vite-plugin-zip.js and plugins-dist/vite-plugin.d.ts.
Testing
Start with a freshly created Vite project, created using
pnpm create vite
We then edit vite.config.ts, do a local relative import of our newly created plugin, and optionally pass needed params into the plugin.
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import vitePluginZip from "../vite-plugin-zip/plugins-dist/vite-plugin-zip.js";
export default defineConfig({
plugins: [
react(),
vitePluginZip({
outputName: "build.zip", // optional
input: "dist", // optional
}),
],
});
To build the test project, run
pnpm run build
We can then see the log info of our custom Vite plugin
and the generated ZIP file.