UNPKG

vite-plugin-html-template-mpa

Version:
460 lines (456 loc) 14.9 kB
// ../../vite-plugin/vite-plugin-html-template-mpa/src/index.ts import { createHash } from "crypto"; import fs2 from "fs"; import path from "path"; import shell from "shelljs"; import { normalizePath } from "vite"; // ../../vite-plugin/vite-plugin-html-template-mpa/package.json var name = "vite-plugin-html-template-mpa"; // ../../vite-plugin/vite-plugin-html-template-mpa/src/utils/index.ts import { render } from "ejs"; import { promises as fs } from "fs"; import { minify as minifyFn } from "html-minifier-terser"; async function readHtmlTemplate(templatePath) { return await fs.readFile(templatePath, { encoding: "utf8" }); } async function getHtmlContent(payload) { const { pagesDir, templatePath, pageName: pageName2, pageTitle, pageEntry, isMPA, entry, extraData, input, pages, jumpTarget, injectOptions, addEntryScript, mpaAutoAddMainTs, onlyUseEjsAndMinify } = payload; let content = ""; const entryJsPath = (() => { if (isMPA) { if (pageEntry.includes("src")) { return `/${pageEntry.replace("/./", "/").replace("//", "/")}`; } return ["/", "/index.html"].includes(extraData.url) ? `/${pagesDir}/index/${pageEntry}` : `/${pagesDir}/${pageName2}/${pageEntry}`; } return entry; })(); try { content = await readHtmlTemplate(templatePath); } catch (e) { console.error(e); } const inputKeys = typeof input === "string" ? [] : Object.keys(input || {}); const pagesKeys = Object.keys(pages || {}); function getHref(item, params) { const _params = params ? "?" + params : ""; return !isMPA ? `/${pagesDir}/${item}/index.html${_params}` : `/${item}/index.html${_params}`; } const links = inputKeys?.length > 1 ? inputKeys.map((item) => { if (pagesKeys.includes(item)) { const href = getHref(item, pages[item].urlParams); return `<a target="${jumpTarget}" href="${href}">${pages[item].title || ""} ${item}</a><br />`; } return `<a target="${jumpTarget}" href="${getHref( item )}">${item}</a><br />`; }) : []; if (!onlyUseEjsAndMinify) { if (pageName2 === "index" && links?.length) { content = content.replace( "</body>", `${links.join("").replace(/,/g, " ")} </body>` ); } else if (isMPA && mpaAutoAddMainTs || addEntryScript) { content = content.replace( "</body>", `<script type="module" src="${entryJsPath}"></script></body>` ); } } const { data, ejsOptions } = injectOptions || { data: {}, ejsOptions: {} }; return await render( content, { title: pageTitle || "", ...data }, ejsOptions ); } function isMpa(viteConfig) { const input = viteConfig?.build?.rollupOptions?.input ?? void 0; return typeof input !== "string" && Object.keys(input || {}).length > 1; } function getOptions(minify) { return { collapseWhitespace: minify, keepClosingSlash: minify, removeComments: minify, removeRedundantAttributes: minify, removeScriptTypeAttributes: minify, removeStyleLinkTypeAttributes: minify, useShortDoctype: minify, minifyCSS: minify }; } async function minifyHtml(html, minify) { if (typeof minify === "boolean" && !minify) { return html; } let minifyOptions = minify; if (typeof minify === "boolean" && minify) { minifyOptions = getOptions(minify); } return await minifyFn(html, minifyOptions); } function isPlainObject(value) { if (Object.prototype.toString.call(value) !== "[object Object]") { return false; } const prototype = Object.getPrototypeOf(value); return prototype === null || prototype === Object.prototype; } function pick(obj, keys) { return keys.reduce((acc, key) => { if (obj.hasOwnProperty(key)) { acc[key] = obj[key]; } return acc; }, {}); } function last(array) { if (!Array.isArray(array) || array.length === 0) { return void 0; } return array[array.length - 1]; } // ../../vite-plugin/vite-plugin-html-template-mpa/src/index.ts var resolve = (p) => path.resolve(process.cwd(), p); var PREFIX = "src"; var uniqueHash = createHash("sha256").update(String((/* @__PURE__ */ new Date()).getTime())).digest("hex").substring(0, 16); var isEmptyObject = (val) => isPlainObject(val) && Object.getOwnPropertyNames(val).length === 0; var getPageData = (options, pageName2) => { let page = {}; const commonOptions = pick(options, [ "template", "title", "entry", "filename", "urlParams", "inject" ]); const isSpa = !options.pages || isEmptyObject(options.pages); if (isSpa) { return commonOptions; } else { page = { ...commonOptions, ...options.pages?.[pageName2] }; return page; } }; var pageName; var isBuild = false; function htmlTemplate(userOptions = {}) { const options = { pagesDir: "src/views", pages: {}, jumpTarget: "_self", buildCfg: { moveHtmlTop: true, moveHtmlDirTop: false, buildPrefixName: "", htmlHash: false, buildAssetDirName: "", buildChunkDirName: "", buildEntryDirName: "", htmlPrefixSearchValue: "", htmlPrefixReplaceValue: "" }, minify: true, mpaAutoAddMainTs: true, ...userOptions }; let config; return { name, config(config2, env) { isBuild = env.command === "build"; }, configResolved(resolvedConfig) { const { buildPrefixName, htmlHash, buildAssetDirName, buildChunkDirName, buildEntryDirName } = options.buildCfg; const assetDir = resolvedConfig.build.assetsDir || "assets"; if (!options.onlyUseEjsAndMinify && isMpa(resolvedConfig)) { const _output = resolvedConfig.build.rollupOptions.output; if (buildPrefixName) { const _input = {}; const rollupInput = resolvedConfig.build.rollupOptions.input; Object.keys(rollupInput).map((key) => { _input[((isBuild ? buildPrefixName : "") || "") + key] = rollupInput[key]; }); resolvedConfig.build.rollupOptions.input = _input; } if (htmlHash) { const buildAssets = { entryFileNames: `${assetDir}/[name].js`, chunkFileNames: `${assetDir}/[name].js`, assetFileNames: `${assetDir}/[name].[ext]` }; const buildOutput = resolvedConfig.build.rollupOptions.output; if (buildOutput) { resolvedConfig.build.rollupOptions.output = { ...buildOutput, ...buildAssets }; } else { resolvedConfig.build.rollupOptions.output = buildAssets; } } if (buildAssetDirName) { if (htmlHash || !String(_output.assetFileNames)?.includes("[hash]")) { _output.assetFileNames = `${assetDir}/${buildAssetDirName}/[name].[ext]`; } else { _output.assetFileNames = `${assetDir}/${buildAssetDirName}/[name]-[hash].[ext]`; } } if (buildChunkDirName) { if (htmlHash || !String(_output.chunkFileNames)?.includes("[hash]")) { _output.chunkFileNames = `${assetDir}/${buildChunkDirName}/[name].js`; } else { _output.chunkFileNames = `${assetDir}/${buildChunkDirName}/[name]-[hash].js`; } } if (buildEntryDirName) { if (htmlHash || !String(_output.entryFileNames)?.includes("[hash]")) { _output.entryFileNames = `${assetDir}/${buildEntryDirName}/[name].js`; } else { _output.entryFileNames = `${assetDir}/${buildEntryDirName}/[name]-[hash].js`; } } resolvedConfig.build.rollupOptions.output = { ...resolvedConfig.build.rollupOptions.output, ..._output }; } else if (!isBuild && options.template && !resolvedConfig.build.rollupOptions.input) { resolvedConfig.build.rollupOptions.input = path.resolve(resolvedConfig.root, options.template); } config = resolvedConfig; }, configureServer(server) { return () => { server.middlewares.use(async (req, res, next) => { if (!req.url?.endsWith(".html") && req.url !== "/") { return next(); } const url = options.pagesDir + req.originalUrl; pageName = (() => { if (url === "/") { return "index"; } return url.match(new RegExp(`${options.pagesDir}/(.*)/`))?.[1] || "index"; })(); const page = getPageData(options, pageName); const templateOption = page.template; const _input = config.build?.rollupOptions?.input; const templatePath = options.onlyUseEjsAndMinify ? typeof _input === "string" ? _input : config.build?.rollupOptions?.input?.[pageName] : templateOption ? resolve(templateOption) : isMpa(config) ? resolve("public/index.html") : resolve("index.html"); let content = await getHtmlContent({ pagesDir: options.pagesDir, pageName, templatePath, pageEntry: page.entry || "main", pageTitle: page.title || "", injectOptions: page.inject, isMPA: isMpa(config), entry: options.entry || "/src/main", extraData: { base: config.base, url }, addEntryScript: options.addEntryScript || false, mpaAutoAddMainTs: options.mpaAutoAddMainTs, input: config.build.rollupOptions.input, pages: options.pages || {}, jumpTarget: options.jumpTarget, onlyUseEjsAndMinify: options.onlyUseEjsAndMinify }); content = await server.transformIndexHtml?.( url, content, req.originalUrl ); res.end(content); }); }; }, resolveId(id) { if (!options.onlyUseEjsAndMinify && id.endsWith(".html")) { id = normalizePath(id); if (!isMpa(config)) { return `${PREFIX}/${path.basename(id)}`; } else { pageName = last(path.dirname(id).split("/")) || ""; const inputPages = config.build.rollupOptions.input; for (const key in inputPages) { const value = normalizePath(inputPages?.[key]); if (value === id) { return `${PREFIX}/${options.pagesDir.replace( "src/", "" )}/${pageName}/index.html`; } } } } return null; }, load(id) { if (id.endsWith(".html")) { id = normalizePath(id); const idNoPrefix = id.slice(PREFIX.length); pageName = last(path.dirname(id).split(options.pagesDir)).replace( /\//g, "" ); const page = getPageData(options, pageName); const publicIndexHtml = resolve("public/index.html"); const indexHtml = resolve("index.html"); const templateOption = page.template; const templatePath = templateOption ? resolve(templateOption) : fs2.existsSync(publicIndexHtml) ? publicIndexHtml : indexHtml; return getHtmlContent({ pagesDir: options.pagesDir, pageName, templatePath, pageEntry: page.entry || "main", entry: options.entry || "/src/main", pageTitle: page.title || "", isMPA: isMpa(config), /** * { base: '/', url: '/views/test-one/index.html' } * { base: '/', url: '/views/test-two/index.html' } * { base: '/', url: '/views/test-twos/index.html' } */ extraData: { base: config.base, url: isMpa(config) ? idNoPrefix : "/" }, injectOptions: page.inject, addEntryScript: options.addEntryScript || false, mpaAutoAddMainTs: options.mpaAutoAddMainTs, input: config.build.rollupOptions.input, pages: options.pages }); } return null; }, transformIndexHtml(data) { const page = getPageData(options, pageName); return { html: data, tags: page.inject?.tags || [] }; }, closeBundle() { if (isMpa(config)) { shell.rm( "-rf", resolve(`${config.build?.outDir || "dist"}/index.html`) ); } } }; } function createMinifyHtmlPlugin(userOptions = {}) { const options = { pagesDir: "src/views", pages: {}, jumpTarget: "_self", buildCfg: { moveHtmlTop: true, moveHtmlDirTop: false, buildPrefixName: "", htmlHash: false, buildAssetDirName: "", buildChunkDirName: "", buildEntryDirName: "", htmlPrefixSearchValue: "", htmlPrefixReplaceValue: "" }, minify: true, mpaAutoAddMainTs: true, ...userOptions }; let config; return { name: "vite:minify-html", enforce: "post", configResolved(resolvedConfig) { config = resolvedConfig; }, async generateBundle(_, bundle) { const htmlFiles = Object.keys(bundle).filter((i) => i.endsWith(".html")); for (const item of htmlFiles) { const htmlChunk = bundle[item]; const { moveHtmlTop, moveHtmlDirTop, buildPrefixName, htmlHash } = options.buildCfg; const _pageName = htmlChunk.fileName.replace(/\\/g, "/").split("/"); const htmlName = (buildPrefixName || "") + _pageName[_pageName.length - 2]; if (htmlChunk) { let _source = htmlChunk.source; if (htmlHash) { _source = htmlChunk.source.replace(/\.js/g, `.js?${uniqueHash}`).replace(/.css/g, `.css?${uniqueHash}`); } if (options.minify) { htmlChunk.source = await minifyHtml(_source, options.minify); } else { htmlChunk.source = _source; } if (options?.buildCfg?.htmlPrefixSearchValue) { htmlChunk.source = htmlChunk.source.replace( new RegExp(options.buildCfg.htmlPrefixSearchValue, "g"), options?.buildCfg?.htmlPrefixReplaceValue || "" ); } } if (isMpa(config)) { if (moveHtmlTop) { htmlChunk.fileName = htmlName + ".html"; } else if (moveHtmlDirTop) { htmlChunk.fileName = htmlName + "/index.html"; } } else { htmlChunk.fileName = "index.html"; } } } }; } function createHtmlPlugin(userOptions = {}) { if (userOptions.onlyMinify) { return [ createMinifyHtmlPlugin(userOptions) ]; } return [ htmlTemplate(userOptions), createMinifyHtmlPlugin(userOptions) ]; } export { createMinifyHtmlPlugin, createHtmlPlugin as default, htmlTemplate };