vite-plugin-page-html
Version:
A simple and flexible Vite plugin for processing HTML pages, integrating multi-page application (MPA) configuration, EJS template support, and HTML compression.
290 lines (282 loc) • 8.39 kB
JavaScript
// src/pagePlugin.ts
import historyFallback from "connect-history-api-fallback";
// src/utils/util.ts
import * as vite from "vite";
// src/const.ts
var PLUGIN_NAME = "vite-plugin-page-html";
var bodyInjectRE = /<\/body>/;
var scriptRE = /<script(?=\s)(?=[^>]*type=["']module["'])(?=[^>]*src=["'][^"']*["'])[^>]*>([\s\S]*?)<\/script>/gi;
// src/utils/util.ts
import { error as errorLog, colors } from "diy-log";
function errlog(...args) {
errorLog(`[${colors.gray(PLUGIN_NAME)}] `, ...args);
}
function getViteVersion() {
return vite?.version ? Number(vite.version.split(".")[0]) : 2;
}
function cleanUrl(url) {
if (!url) return "/";
const queryRE = /\?.*$/s;
const hashRE = /#.*$/s;
return url.replace(hashRE, "").replace(queryRE, "");
}
function cleanPageUrl(path) {
return path.replace(/(^\/)|(\/$)/g, "").replace(/\.htm(l)?$/i, "");
}
// src/utils/core.ts
import ejs from "ejs";
import { resolve } from "pathe";
import { normalizePath } from "vite";
async function compileHtml(ejsOptions = {}, extendData = {}, viteConfig) {
return async function(html, data = {}) {
try {
const ejsData = {
...extendData,
pageHtmlVitePlugin: {
title: data?.title,
entry: data?.entry,
data: data?.inject.data
},
...data
};
let result = await ejs.render(html, ejsData, ejsOptions);
if (data?.entry) {
result = result.replace(scriptRE, "").replace(
bodyInjectRE,
`<script type="module" src="${normalizePath(data.entry)}"></script>
</body>`
);
}
return result;
} catch (e) {
errlog(e.message);
return "";
}
};
}
function createPage(options = {}) {
const {
entry,
template = "index.html",
title = "Vite App",
data = {},
ejsOptions = {},
inject = {}
} = options;
const defaults = {
entry,
template,
title,
ejsOptions,
inject: {
data: inject.data ?? data,
tags: inject.tags ?? []
}
};
const page = options.page || "index";
const pages = {};
if (typeof page === "string") {
const pageUrl = cleanPageUrl(page);
pages[pageUrl] = {
...defaults,
path: pageUrl
};
} else {
Object.entries(page).forEach(([name, pageItem]) => {
const pageUrl = cleanPageUrl(name);
if (!pageItem || typeof pageItem !== "string" && !pageItem.entry) {
errlog(`page ${name} is invalid`);
return;
}
if (typeof pageItem === "string") {
pageItem = { entry: pageItem };
}
pages[pageUrl] = {
...defaults,
...pageItem,
inject: {
...defaults.inject,
...pageItem.inject ?? {}
},
path: pageUrl
};
});
}
return pages;
}
function createRewire(reg, page, baseUrl, proxyKeys, whitelist) {
const from = typeof reg === "string" ? new RegExp(`^/${reg}*`) : reg;
return {
from,
to: ({ parsedUrl }) => {
const pathname = parsedUrl.path;
const excludeBaseUrl = pathname.replace(baseUrl, "/");
const template = resolve(baseUrl, page.template);
if (excludeBaseUrl.startsWith("/static")) {
return excludeBaseUrl;
}
if (excludeBaseUrl === "/") {
return template;
}
if (whitelist?.some((reg2) => reg2.test(excludeBaseUrl))) {
return pathname;
}
const isProxyPath = proxyKeys.some((key) => pathname.startsWith(resolve(baseUrl, key)));
return isProxyPath ? pathname : template;
}
};
}
function createWhitelist(rewrites) {
const result = [/^\/__\w+\/$/];
if (rewrites) {
rewrites = Array.isArray(rewrites) ? rewrites : [rewrites];
for (const reg of rewrites) {
result.push(typeof reg === "string" ? new RegExp(reg) : reg);
}
}
return result;
}
function createRewrites(pages, viteConfig, options = {}) {
const rewrites = [];
const baseUrl = viteConfig.base ?? "/";
const proxyKeys = Object.keys(viteConfig.server?.proxy ?? {});
const whitelist = createWhitelist(options.rewriteWhitelist);
Object.entries(pages).forEach(([_, page]) => {
const reg = new RegExp(`${page.path}(\\/|\\.html|\\/index\\.html)?$`, "i");
rewrites.push(createRewire(reg, page, baseUrl, proxyKeys, whitelist));
});
rewrites.push(createRewire("", pages["index"] ?? {}, baseUrl, proxyKeys, whitelist));
return rewrites;
}
// src/utils/file.ts
import { resolve as resolve2 } from "pathe";
import fs from "fs/promises";
import fse from "fs-extra";
async function checkExistOfPath(p, root) {
if (!p || p === "." || p === "./") return "";
const paths = p.replace(root, "").split("/");
if (paths[0] === "") paths.shift();
if (paths.length === 0) return "";
let result = "";
try {
for (let i = 0; i < paths.length; i++) {
result = resolve2(root, ...paths.slice(0, i + 1));
await fs.access(result, fs.constants.F_OK);
}
return result;
} catch {
return result;
}
}
async function copyOneFile(src, dest, root) {
try {
const result = await checkExistOfPath(dest, root);
await fse.copy(src, dest, { overwrite: false, errorOnExist: true });
return result;
} catch {
return "";
}
}
async function createVirtualHtml(pages, root) {
const _root = root ?? process.cwd();
return Promise.all(
Object.entries(pages).map(
([_, page]) => copyOneFile(resolve2(_root, page.template), resolve2(_root, `${page.path}.html`), _root)
)
);
}
async function removeVirtualHtml(files) {
if (!files?.length) return;
try {
const uniqueFiles = Array.from(new Set(files.filter(Boolean)));
await Promise.all(uniqueFiles.map((file) => fse.remove(file)));
} catch (e) {
errlog(e.message);
}
}
// src/pagePlugin.ts
function createPagePlugin(pluginOptions = {}) {
let viteConfig;
let renderHtml;
const pageInput = {};
let needRemoveVirtualHtml = [];
const pages = createPage(pluginOptions);
const transformIndexHtmlHandler = async (html, ctx) => {
let pageUrl = cleanUrl(ctx.originalUrl ?? ctx.path);
if (pageUrl.startsWith(viteConfig.base)) {
pageUrl = pageUrl.replace(viteConfig.base, "");
}
pageUrl = cleanPageUrl(pageUrl) || "index";
const pageData = pages[pageUrl] || pages[`${pageUrl}/index`];
if (pageData) {
html = await renderHtml(html, pageData);
return {
html,
tags: pageData.inject.tags
};
} else {
errlog(`${ctx.originalUrl ?? ctx.path} not found!`);
return html;
}
};
return {
name: PLUGIN_NAME,
enforce: "pre",
async config(config, { command }) {
Object.entries(pages).forEach(([name, current]) => {
const template = command === "build" ? `${current.path}.html` : current.template;
pageInput[name] = template;
});
if (!config.build?.rollupOptions?.input) {
return { build: { rollupOptions: { input: pageInput } } };
}
config.build.rollupOptions.input = pageInput;
},
async configResolved(resolvedConfig) {
viteConfig = resolvedConfig;
if (resolvedConfig.command === "build") {
needRemoveVirtualHtml = await createVirtualHtml(pages, resolvedConfig.root);
}
renderHtml = await compileHtml(
pluginOptions.ejsOptions,
{ ...resolvedConfig.env },
resolvedConfig
);
},
configureServer(server) {
server.middlewares.use(
historyFallback({
verbose: !!process.env.DEBUG && process.env.DEBUG !== "false",
disableDotRule: void 0,
htmlAcceptHeaders: ["text/html", "application/xhtml+xml"],
rewrites: createRewrites(pages, viteConfig, pluginOptions)
})
);
},
transformIndexHtml: getViteVersion() < 5 ? {
enforce: "pre",
transform: transformIndexHtmlHandler
} : {
order: "pre",
handler: transformIndexHtmlHandler
},
closeBundle() {
if (needRemoveVirtualHtml.length) {
removeVirtualHtml(needRemoveVirtualHtml);
}
}
};
}
// src/index.ts
import createMinifyPlugin from "vite-plugin-minify-html";
function createPlugin(pluginOptions = {}) {
const opts = Object.assign({ minify: true }, pluginOptions);
const plugins = [createPagePlugin(opts)];
if (opts.minify) {
plugins.push(createMinifyPlugin(opts.minify));
}
return plugins;
}
export {
createPlugin as default
};