Написание Vite плагина
- пятница, 11 августа 2023 г. в 00:00:16
Сборщик Vite предоставляет не только хороший функционал, но и удобный API для создания плагинов, позволяющих кастомизировать его практически под любую задачу. То есть, плагины можно писать не только для публикации их в npmjs.com
репозитории, но и для автоматизации исключительно своих задач.
Сложность написания плагина сравнима со сложностью написания сценария для Gulp или GitHub Actions. Для примера напишем плагин, который будет вставлять фрагменты кода в файл index.html. В зависимости от проекта в данный файл необходимо помещать код Google Analytics, метатэги Open Graph и Twitter, подключение Service worker-a, виджета чата поддержки, сплэш скрин и многое другое. В результате index.html становится очень большим и ориентироваться в нем и блоках кода довольно сложно.
Наш плагин позволить держать фрагменты кода в отдельных файлах, а при сборке все будет помещаться в index.html
. Причем это будет происходить не только при непосредственно сборке ( npm build ), но и при запуске Vite dev сервера с поддержкой HRM (Hot Module Replacement).
Репозиторий плагина - https://github.com/altrusl/vite-plugin-html-injection
NPM - https://www.npmjs.com/package/vite-plugin-html-injection
Плагины Vite являются расширением плагинов сборщика Rollup, который используется для сборки Vite проекта под капотом. За небольшим исключением плагины Rollup работают в Vite, в то же время последний добавляет несколько хуков в API для плагинов, которые мы и будет использовать.
Написание плагина по большому счету является написанием кода для данных хуков. Мы задействуем два из них.
Первый используемый хук - configResolved.
Он нужен для того, чтобы получить конфиг Vite, из которого мы позже получим абсолютный путь к директории проекта - config.root
Второй хук - transformIndexHtml
. В нем непосредственно нужно произвести изменение содержимого index.html
. Аргументом мы получим строку с оригинальным содержимым index.html
, вернуть надо модифицированный контент.
import { Plugin, ResolvedConfig } from "vite";
import path from "path";
import fs from "fs";
import { IHtmlInjectionConfig, IHtmlInjectionConfigInjection } from "./types";
export function htmlInjectionPlugin(
htmlInjectionConfig: IHtmlInjectionConfig
): Plugin {
let config: undefined | ResolvedConfig;
return {
name: "html-injection",
configResolved(resolvedConfig) {
config = resolvedConfig;
},
transformIndexHtml(html: string) {
let out = html;
for (let i = 0; i < htmlInjectionConfig.injections.length; i++) {
const injection: IHtmlInjectionConfigInjection =
htmlInjectionConfig.injections[i];
let root = (config as ResolvedConfig).root;
const filePath = path.resolve(root, injection.path);
let data = fs.readFileSync(filePath, "utf8");
if (injection.type === "js") {
data = `<script>\n${data}\n</script>\n`;
} else if (injection.type === "css") {
data = `<style>\n${data}\n</style>\n`;
}
switch (injection.injectTo) {
case "head":
out = out.replace("</head>", `${data}\n</head>`);
break;
case "head-prepend":
out = out.replace(/<head(.*)>/i, `$&\n${data}`);
break;
case "body":
out = out.replace("</body>", `${data}\n</body>`);
break;
case "body-prepend":
out = out.replace(/<body(.*)>/i, `$&\n${data}`);
break;
default:
break;
}
}
return out;
},
};
}
В массиве htmlInjectionConfig.injections
содержатся описания вставляемых фрагментов кода - конфигурация плагина, передаваемая ему как аргумент в vite.config.js
проекта, использующего наш плагин.
// vite.config.js
// example for Vue project
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { htmlInjectionPlugin } from "vite-plugin-html-injection";
export default defineConfig({
plugins: [
vue(),
htmlInjectionPlugin({
// Example configuration. Can be stored in a separate json file.
injections: [
{
name: "Open Graph",
path: "./src/injections/open-graph.html",
type: "raw",
injectTo: "head",
},
{
name: "Splash screen",
path: "./src/injections/splash-screen.html",
type: "raw",
injectTo: "body-prepend",
},
{
name: "Service worker",
path: "./src/injections/sw.js",
type: "js",
injectTo: "head",
},
],
}),
]
});
В случае написания "локального" плагина для себя, вместо добавления его в devDependencies
и импорта из node_modules
:
import { htmlInjectionPlugin } from "vite-plugin-html-injection";
можно импортить его локально:
import { htmlInjectionPlugin } from "./src/plugins/vite-plugin-html-injection";
Существует три типа фрагментов кода, с которыми работает плагин — raw
, js
и css
.
Raw
фрагменты вставляются как есть, js
и css
оборачиваются в теги <script>
и <style>
соответственно.
Также есть четыре места, куда можно вставить фрагмент кода: начало и конец тега head
и начало и конец body
. Соответствующие значения свойства injectTo
: head-prepend
, head
, body-prepend
и body
.
Файл ./src/injections/ga.html
может выглядеть как-то так:
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-8W4X32XXXX" />
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "G-8W4X32XXXX");
</script>
В коде плагина мы используем библиотеки fs
и path
, которые помещать с сборку плагина не нужно, потому что они будут предоставлены самым Vite во время выполнения плагина. В vite.config.js
проекта плагина это нужно указать явно.
// vite.config.js
import { defineConfig } from "vite";
import { resolve } from "path";
export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, "./index.ts"),
name: "HtmlInjection",
fileName: "index",
formats: ["es", "cjs"],
},
rollupOptions: {
external: ["fs", "path"],
},
},
});
В итоге размер плагина получается меньше 1КБ.
Ну и package.json
плагина:
{
"name": "vite-plugin-html-injection",
"version": "1.1.9",
"description": "Vite plugin for injecting html, js, css code snippets into index.html",
"license": "MIT",
"homepage": "https://github.com/altrusl/vite-plugin-html-injection",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist"
],
"scripts": {
"build": "vite build && copy types.d.ts dist\\index.d.ts"
},
"devDependencies": {
"@antfu/eslint-config": "^0.39.8",
"@types/node": "^20.4.5",
"eslint": "^8.46.0",
"typescript": "^5.1.6",
"vite": "^4.4.7"
},
"peerDependencies": {
"vite": ">= 2.0.0"
}
}
После сборки плагина публикуем его на npmjs.com:
npm publish --access public