UNPKG

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
// 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 };