vite-plugin-html-template-mpa
Version:
html template map for vite
460 lines (456 loc) • 14.9 kB
JavaScript
// ../../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
};