vite-plugin-virtual-html
Version:
Vite plugin to load html anywhere
499 lines (492 loc) • 15.2 kB
JavaScript
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined") return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
// src/html/types.ts
var POS = /* @__PURE__ */ ((POS2) => {
POS2[POS2["before"] = 0] = "before";
POS2[POS2["after"] = 1] = "after";
return POS2;
})(POS || {});
// src/html/Base.ts
import { createFilter, normalizePath } from "vite";
import * as path from "path";
import * as fs from "fs";
import glob from "fast-glob";
import debug from "debug";
import { createRequire } from "node:module";
import MagicString from "magic-string";
var _require = import.meta.url !== void 0 ? createRequire(import.meta.url) : __require;
var fsp = fs.promises;
var DEFAULT_GLOB_PATTERN = [
"**/*.html",
"!node_modules/**/*.html",
"!.**/*.html"
];
var VIRTUAL_HTML_CONTENT = new MagicString(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>#TITLE#</title>
<script src="#ENTRY#" type="module"></script>
</head>
<body>
#BODY#
</body>
</html>
`);
var DEFAULT_INJECTCODE_ALL = "*";
var Base = class {
constructor(virtualHtmlOptions) {
this.cwd = normalizePath(process.cwd());
this.logger = debug("vite-plugin-virtual-html");
/**
* load html file
* @param args
*/
this._load = async (...args) => {
const [id] = args;
if (this._filter(id)) {
let newId = this.getHtmlName(id, this._config?.root);
const maybeIndexName1 = (newId + "/").replace("//", "/");
const maybeIndexName2 = (newId + "/index").replace("//", "/");
const maybeIndexName3 = newId.replace("index", "").replace("//", "/");
const pageOption = this._pages[newId] || this._pages[maybeIndexName1] || this._pages[maybeIndexName2] || this._pages[maybeIndexName3];
if (pageOption !== void 0) {
if (typeof pageOption === "string") {
const page = await this.generatePageOptions(
pageOption,
this._globalData,
this._globalRender
);
return await this.readHtml(page);
}
if ("template" in pageOption) {
const page = await this.generatePageOptions(
pageOption,
this._globalData,
this._globalRender
);
return await this.readHtml(page);
}
if ("entry" in pageOption) {
return await this.generateVirtualPage(pageOption);
}
}
}
return void 0;
};
/**
* transform code to inject some code into original code
* @param args
*/
this._transform = async (...args) => {
const [code, id] = args;
if (this._filter(id)) {
const ids = id.split("/");
const key = ids[ids.length - 1];
let _code = code;
if (DEFAULT_INJECTCODE_ALL in this._injectCode) {
_code = this.generateInjectCode(
this._injectCode[DEFAULT_INJECTCODE_ALL],
_code
);
}
if (key in this._injectCode) {
_code = this.generateInjectCode(this._injectCode[key], _code);
}
return _code;
}
return null;
};
/**
* get html file's name
* @param id
* @param root
*/
this.getHtmlName = (id, root) => {
const _root = (root ?? "").replace(this.cwd, "");
const _id = id.replace(this.cwd, "");
const result = _id.replace(".html", "").replace(_root !== "" ? this.addTrailingSlash(_root) : "", "");
return result.startsWith("/") ? result.substring(1, result.length) : result;
};
/**
* add trailing slash on path
* @param {string} path
* @returns {string}
*/
this.addTrailingSlash = (path3) => {
const _path = normalizePath(path3.replace(this.cwd, ""));
return _path.endsWith("/") ? _path : `${_path}/`;
};
/**
* generate URL
* @param url
*/
this.generateUrl = (url) => {
if (!url) {
return "/";
}
if (url.indexOf("?") > 0) {
return url.split("?")[0];
}
return url;
};
/**
* read HTML file from disk and generate code from template system(with render function)
* @param template
* @param data
* @param render
*/
this.readHtml = async ({
template = "",
data = {},
render = this.defaultRender
}) => {
const templatePath = path.resolve(this.cwd, `.${template}`);
if (!fs.existsSync(templatePath)) {
this.logger("[vite-plugin-virtual-html]: template file must exist!");
return "";
}
return await this.renderTemplate(templatePath, render, data);
};
/**
* render template
* @param templatePath
* @param render
* @param data
*/
this.renderTemplate = async (templatePath, render, data) => {
const code = await this.readTemplate(templatePath);
return render(
code,
data,
templatePath.substring(templatePath.lastIndexOf(path.sep) + 1)
);
};
/**
* read html file's content to render with render function
* @param templatePath
*/
this.readTemplate = async (templatePath) => {
const result = await fsp.readFile(templatePath);
return result.toString();
};
/**
* generate page option from string/object to object
* @param page
* @param globalData
* @param globalRender
*/
this.generatePageOptions = async (page, globalData, globalRender) => {
if (typeof page === "string") {
return {
template: page,
data: {
...globalData
},
render: globalRender
};
}
const { data = {}, render, template } = page;
return {
template,
data: {
...globalData,
...data
},
render: render ?? globalRender ?? this.defaultRender
};
};
/**
* directly use find\replacement / replacement\find to replace find
* @param {pos, find, replacement}
* @param code
*/
this.generateInjectCode = ({ pos, find, replacement }, code) => {
if (pos === 1 /* after */) {
return code.replace(find, `${find}
${replacement}`);
}
if (pos === 0 /* before */) {
return code.replace(find, `
${replacement}
${find}`);
}
return code;
};
/**
* generate page from virtual page
* @param vPages
*/
this.generateVirtualPage = async (vPages) => {
const { entry, title = "", body = '<div id="app"></div>' } = vPages;
return VIRTUAL_HTML_CONTENT.replace("#ENTRY#", entry).replace("#TITLE#", title).replace("#BODY#", body).toString();
};
/**
* find all html file in project and return it as Pages
*/
this.findAllHtmlInProject = (extraGlobPattern = []) => {
const pages = {};
let realPattern = [];
if (extraGlobPattern.length === 0) {
realPattern = DEFAULT_GLOB_PATTERN;
} else {
const set = /* @__PURE__ */ new Set();
DEFAULT_GLOB_PATTERN.forEach((dg) => set.add(dg));
extraGlobPattern.forEach((dg) => set.add(dg));
for (let key of set.keys()) {
realPattern.push(key);
}
}
const files = glob.sync(realPattern);
files.forEach((file) => {
const filePathArr = file.split("/");
pages[filePathArr[filePathArr.length - 1].replace(".html", "")] = `/${file}`;
});
return pages;
};
this.defaultRender = (template, data) => {
try {
const resolved = _require.resolve("ejs");
return _require(resolved).render(template, data, {
delimiter: "%",
root: process.cwd()
});
} catch (e) {
}
return template;
};
const {
pages: pagesObj,
indexPage = "index",
render = this.defaultRender,
data = {},
extraGlobPattern = [],
injectCode = {},
cwd = normalizePath(process.cwd())
} = virtualHtmlOptions;
if (pagesObj === true || pagesObj === void 0) {
this._pages = this.findAllHtmlInProject(extraGlobPattern);
} else {
this._pages = pagesObj;
}
this._indexPage = indexPage;
this._globalData = data;
this._globalRender = render;
this._injectCode = injectCode;
this._filter = createFilter(/\.html|\/$/);
this.cwd = cwd;
}
};
// src/history-api/historyApiFallbackPlugin.ts
import history from "connect-history-api-fallback";
var historyApiFallbackPlugin = (historyApiOptions) => {
const {
rewrites,
usePreview
} = historyApiOptions;
const configureServerHookName = usePreview ? "configurePreviewServer" : "configureServer";
return {
name: "vite-plugin-virtual-html:history",
[configureServerHookName](server) {
if (rewrites) {
buildHistoryApiFallback(server, rewrites);
}
}
};
};
function buildHistoryApiFallback(server, rewrites) {
server.middlewares.use(history({
disableDotRule: void 0,
htmlAcceptHeaders: ["text/html", "application/xhtml+xml"],
rewrites
}));
}
// src/html/Serve.ts
import { normalizePath as normalizePath2, createFilter as createFilter2 } from "vite";
var HTML_INCLUDE = [/\.html$/, /\/$/];
var HTML_FILTER = createFilter2(HTML_INCLUDE);
var Serve = class extends Base {
constructor(virtualHtmlOptions) {
super(virtualHtmlOptions);
this._configureServer = (server) => {
if (this._rewrites) {
buildHistoryApiFallback(server, this._rewrites);
}
return () => {
server.middlewares.use(async (req, res, next) => {
const originalUrl = req.originalUrl;
const reqUrl = req.url;
let url = decodeURI(this.generateUrl(originalUrl?.endsWith("/") ? originalUrl : reqUrl));
if (this._urlTransformer) {
url = this._urlTransformer(url, req);
}
if (!HTML_FILTER(url) && url !== "/") {
return next();
}
let htmlCode;
if (url === "/" || url === "/index.html") {
url = `/${this._indexPage}.html`;
}
htmlCode = await this._load(normalizePath2(url));
if (htmlCode === void 0) {
res.statusCode = 404;
res.end();
return next();
}
const transformResult = await this._transform(htmlCode, url);
if (transformResult === null) {
return next();
}
res.end(await server.transformIndexHtml(url, transformResult));
next();
});
};
};
this._rewrites = virtualHtmlOptions.rewrites;
this._urlTransformer = virtualHtmlOptions.urlTransformer;
}
};
// src/html/Build.ts
import { normalizePath as normalizePath3 } from "vite";
import fs2, { promises as fsp2 } from "fs";
import path2 from "path";
var Build = class extends Base {
constructor(virtualHtmlOptions) {
super(virtualHtmlOptions);
this._needRemove = [];
}
/**
* check html file's parent directory
* @param html
* @param needRemove
*/
async checkVirtualPath(html, needRemove, root) {
const cwd = normalizePath3(path2.resolve(this.cwd, root));
const pathArr = html.split("/");
const fileName = pathArr[pathArr.length - 1];
const middlePath = html.replace(fileName, "").replace(cwd, "");
const firstPath = middlePath.split("/")[1];
if (!fs2.existsSync(middlePath)) {
needRemove.push(normalizePath3(path2.resolve(cwd, `./${firstPath}`)));
await fsp2.mkdir(path2.resolve(cwd, `./${middlePath}`), {
recursive: true
});
}
}
async _buildConfig(config) {
this._config = config;
const pagesKey = Object.keys(this._pages);
for (let i = 0; i < pagesKey.length; i++) {
const key = pagesKey[i];
const pageOption = this._pages[key];
const vHtml = normalizePath3(path2.resolve(this.cwd, `./${config.root ? this.addTrailingSlash(config.root) : ""}${this.htmlNameAddIndex(key)}.html`));
if (!fs2.existsSync(vHtml)) {
this._needRemove.push(vHtml);
await this.checkVirtualPath(vHtml, this._needRemove, config.root ?? "");
if (typeof pageOption === "string" || "template" in pageOption) {
const genPageOption = await this.generatePageOptions(pageOption, this._globalData, this._globalRender);
await fsp2.copyFile(path2.resolve(this.cwd, `.${genPageOption.template}`), vHtml);
}
if (typeof pageOption !== "string" && "entry" in pageOption) {
await fsp2.writeFile(path2.resolve(this.cwd, vHtml), await this.generateVirtualPage(pageOption));
}
}
}
this.logger("[vite-plugin-virtual-html]: This plugin cannot use in library mode!");
this._distDir = config.build?.outDir ?? "dist";
config.build = {
...config.build,
rollupOptions: {
...config.build?.rollupOptions,
input: {
...config.build?.rollupOptions?.input,
...this.extractHtmlPath(this._pages)
}
}
};
}
_closeBundle() {
for (let vHtml of this._needRemove) {
if (fs2.existsSync(vHtml)) {
fsp2.rm(vHtml, {
recursive: true
}).catch(() => {
});
}
}
}
/**
* use pages' key as html name
* @param pages
*/
extractHtmlPath(pages) {
const newPages = {};
Object.keys(pages).forEach((key) => {
newPages[key] = `/${this.htmlNameAddIndex(key)}.html`;
});
return newPages;
}
htmlNameAddIndex(htmlName) {
return htmlName.endsWith("/") ? htmlName + "index" : htmlName;
}
};
// src/html/VirtualHtmlPlugin.ts
var VirtualHtmlPlugin = (virtualHtmlOptions) => {
let _htmlOptions = virtualHtmlOptions;
let _config;
let _instance = null;
return {
name: "vite-plugin-virtual-html",
async config(config, { command }) {
_config = config;
if (command === "serve") {
if (_htmlOptions.useCustom ?? true) {
config.appType = "custom";
}
_instance = new Serve(_htmlOptions);
} else if (command === "build") {
_instance = new Build(_htmlOptions);
await _instance._buildConfig.call(_instance, config);
}
},
configureServer(server) {
if (_instance._configureServer) {
return _instance._configureServer(server);
}
},
async load(...args) {
if (_instance?._load) {
return await _instance._load(...args);
}
},
async transform(...args) {
if (_instance?._transform) {
return await _instance._transform(...args);
}
},
closeBundle() {
if (_instance._closeBundle) {
return _instance._closeBundle();
}
}
};
};
// src/index.ts
var index_default = (virtualHtmlOptions) => {
return VirtualHtmlPlugin(virtualHtmlOptions);
};
export {
Build,
POS,
Serve,
VirtualHtmlPlugin,
buildHistoryApiFallback,
index_default as default,
historyApiFallbackPlugin
};
//# sourceMappingURL=index.js.map