UNPKG

vite-plugin-virtual-mpa

Version:

Out-of-box MPA plugin for Vite, with html template engine and virtual files support.

384 lines (376 loc) 13.1 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name2 in all) __defProp(target, name2, { get: all[name2], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { createMpaPlugin: () => createMpaPlugin2, createPages: () => createPages }); module.exports = __toCommonJS(src_exports); // src/plugin.ts var import_ejs = __toESM(require("ejs")); var import_picocolors = __toESM(require("picocolors")); var import_fs2 = __toESM(require("fs")); var import_path2 = __toESM(require("path")); var import_connect_history_api_fallback = __toESM(require("connect-history-api-fallback")); // package.json var name = "vite-plugin-virtual-mpa"; // src/utils.ts var import_path = __toESM(require("path")); var import_fs = __toESM(require("fs")); function replaceSlash(str) { return str?.replaceAll(/[\\/]+/g, "/"); } function createPages(pages) { return Array.isArray(pages) ? pages : [pages]; } function scanPages(scanOptions) { const { filename, entryFile, scanDirs, template } = scanOptions || {}; const pages = []; for (const entryDir of [scanDirs].flat().filter(Boolean)) { for (const name2 of import_fs.default.readdirSync(entryDir)) { const dir = import_path.default.join(entryDir, name2); if (!import_fs.default.statSync(dir).isDirectory()) continue; const entryPath = entryFile ? import_path.default.join(dir, entryFile) : ""; const tplPath = template ? import_path.default.join(dir, template) : ""; pages.push({ name: name2, template: replaceSlash( import_fs.default.existsSync(tplPath) ? tplPath : void 0 ), entry: replaceSlash( import_fs.default.existsSync(entryPath) ? import_path.default.join("/", entryPath) : void 0 ), filename: replaceSlash( typeof filename === "function" ? filename(name2) : void 0 ) }); } } return pages; } function resolvePageById(id, root, pageMap) { return pageMap[replaceSlash(import_path.default.relative(root, id))]; } // src/plugin.ts var import_vite = require("vite"); var PREFIX = "\0virtual-page:"; var bodyInject = /<\/body>/; var pluginName = import_picocolors.default.cyan(name); function createMpaPlugin(config) { const { template = "index.html", verbose = true, pages = [], rewrites, previewRewrites, watchOptions, scanOptions, transformHtml } = config; let resolvedConfig; let inputMap = {}; let virtualPageMap = {}; let tplSet = /* @__PURE__ */ new Set(); function configInit(pages2) { const tempInputMap = {}; const tempVirtualPageMap = {}; const tempTplSet = /* @__PURE__ */ new Set([template]); for (const page of [...pages2, ...scanPages(scanOptions)]) { const { name: name2, filename, template: template2, entry } = page; for (const item of [name2, filename, template2, entry]) { if (item && item.includes("\\")) { throwError( `'\\' is not allowed, please use '/' instead, received ${item}` ); } } const virtualFilename = filename || `${name2}.html`; if (virtualFilename.startsWith("/")) throwError( `Make sure the path relative, received '${virtualFilename}'` ); if (name2.includes("/")) throwError(`Page name shouldn't include '/', received '${name2}'`); if (entry && !entry.startsWith("/")) { throwError( `Entry must be an absolute path relative to the project root, received '${entry}'` ); } if (tempInputMap[name2]) continue; tempInputMap[name2] = virtualFilename; tempVirtualPageMap[virtualFilename] = page; template2 && tempTplSet.add(template2); } inputMap = tempInputMap; virtualPageMap = tempVirtualPageMap; tplSet = tempTplSet; } function useHistoryFallbackMiddleware(middlewares, rewrites2 = []) { const { base } = resolvedConfig; if (rewrites2 === false) return; middlewares.use( // @ts-ignore (0, import_connect_history_api_fallback.default)({ // Override the index (default /index.html). index: (0, import_vite.normalizePath)(`/${base}/index.html`), htmlAcceptHeaders: ["text/html", "application/xhtml+xml"], rewrites: rewrites2.concat([ { /** * Put built-in matching rules in order of length so that to preferentially match longer paths. * Closed #52. */ from: new RegExp( (0, import_vite.normalizePath)( `/${base}/(${Object.keys(inputMap).sort((a, b) => b.length - a.length).join("|")})` ) ), to: (ctx) => { return (0, import_vite.normalizePath)(`/${base}/${inputMap[ctx.match[1]]}`); } }, { from: /\/$/, /** * Support /dir/ without explicit index.html * @see https://github.com/vitejs/vite/blob/main/packages/vite/src/node/server/middlewares/htmlFallback.ts#L13 */ to({ parsedUrl, request }) { const rewritten = decodeURIComponent(parsedUrl.pathname) + "index.html"; if (import_fs2.default.existsSync(rewritten.replace(base, ""))) { return rewritten; } return request.url; } } ]) }) ); if (verbose) { middlewares.use((req, res, next) => { const { url, originalUrl } = req; if (originalUrl !== url) { console.log( `[${pluginName}]: Rewriting ${import_picocolors.default.blue(originalUrl)} to ${import_picocolors.default.blue(url)}` ); } next(); }); } } return { name: pluginName, config(config2) { configInit(pages); return { appType: "mpa", clearScreen: config2.clearScreen ?? false, optimizeDeps: { entries: pages.map((v) => v.entry).filter((v) => !!v) }, build: { rollupOptions: { input: Object.values(inputMap).map((v) => PREFIX + v) // Use PREFIX to distinguish these files from others. } } }; }, configResolved(config2) { resolvedConfig = config2; if (verbose) { const colorProcess = (path3) => (0, import_vite.normalizePath)( `${import_picocolors.default.blue(`<${config2.build.outDir}>/`)}${import_picocolors.default.green(path3)}` ); const inputFiles = Object.values(inputMap).map(colorProcess); console.log( `[${pluginName}]: Generated virtual files: ${inputFiles.join("\n")}` ); } }, /** * Intercept virtual html requests. */ resolveId(id) { return id.startsWith(PREFIX) ? ( /** * Entry paths here must be absolute, otherwise it may cause problem on Windows. Closes #43 * @see https://github.com/vitejs/vite/issues/9771 */ import_path2.default.resolve(resolvedConfig.root, id.slice(PREFIX.length)) ) : void 0; }, /** * Get html according to page configurations. */ load(id) { const page = resolvePageById(id, resolvedConfig.root, virtualPageMap); if (!page) return null; const templateContent = import_fs2.default.readFileSync( page.template || template, "utf-8" ); return import_ejs.default.render( !page.entry ? templateContent : templateContent.replace( bodyInject, `<script type="module" src="${(0, import_vite.normalizePath)( `${page.entry}` )}"></script> </body>` ), // Variables injection { ...resolvedConfig.env, ...page.data }, // For error report { filename: id, root: resolvedConfig.root } ); }, transformIndexHtml(html, ctx) { const page = resolvePageById( ctx.filename, resolvedConfig.root, virtualPageMap ); return page && transformHtml?.(html, { ...ctx, page }); }, configureServer(server) { const { config: config2, watcher, middlewares, pluginContainer, transformIndexHtml } = server; const base = (0, import_vite.normalizePath)(`/${config2.base || "/"}/`); if (watchOptions) { const { events, handler, include, excluded } = typeof watchOptions === "function" ? { handler: watchOptions } : watchOptions; const isMatch = (0, import_vite.createFilter)(include || /.*/, excluded); watcher.on("all", (type, filename) => { if (events && !events.includes(type)) return; if (!isMatch(filename)) return; const file = replaceSlash(import_path2.default.relative(config2.root, filename)); verbose && console.log( `[${pluginName}]: ${import_picocolors.default.green(`file ${type}`)} - ${import_picocolors.default.dim(file)}` ); handler({ type, file, server, reloadPages: configInit }); }); } watcher.on("change", (file) => { if (file.endsWith(".html") && tplSet.has(replaceSlash(import_path2.default.relative(config2.root, file)))) { (server.ws || server.hot).send({ type: "full-reload", path: "*" }); } }); useHistoryFallbackMiddleware(middlewares, rewrites); middlewares.use(async (req, res, next) => { const url = req.url; const fileName = url.replace(base, "").replace(/[?#].*$/s, ""); if (res.writableEnded || !fileName.endsWith(".html") || // HTML Fallback Middleware appends '.html' to URLs !virtualPageMap[fileName]) { return next(); } Object.entries(config2?.server?.headers || {}).forEach( ([key, value]) => { res.setHeader(key, value); } ); res.setHeader("Content-Type", "text/html"); res.statusCode = 200; try { const loadResult = await pluginContainer.load( import_path2.default.resolve(config2.root, fileName) ); if (!loadResult) { return next(new Error(`Failed to load url ${fileName}`)); } res.end( await transformIndexHtml( url, typeof loadResult === "string" ? loadResult : loadResult.code, req.originalUrl ) ); } catch (e) { next(e); } }); }, configurePreviewServer(server) { useHistoryFallbackMiddleware(server.middlewares, previewRewrites); } }; } function throwError(message) { throw new Error(`[${pluginName}]: ${import_picocolors.default.red(message)}`); } // src/html-minify.ts var import_html_minifier_terser = require("html-minifier-terser"); function htmlMinifyPlugin(options) { return { name: "vite:html-minify", enforce: "post", apply: "build", transformIndexHtml: (html) => { return (0, import_html_minifier_terser.minify)(html, { removeComments: true, collapseWhitespace: true, collapseBooleanAttributes: true, removeEmptyAttributes: true, minifyCSS: true, minifyJS: true, minifyURLs: true, ...options }); } }; } // src/index.ts function createMpaPlugin2(config) { const { htmlMinify } = config; return !htmlMinify ? [createMpaPlugin(config)] : [ createMpaPlugin(config), htmlMinifyPlugin(htmlMinify === true ? {} : htmlMinify) ]; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { createMpaPlugin, createPages });