import { render } from 'ejs'; import { expand } from 'dotenv-expand'; import dotenv from 'dotenv'; import path, { join, dirname } from 'pathe'; import fse from 'fs-extra'; import { normalizePath } from 'vite'; import { parse } from 'node-html-parser'; import fg from 'fast-glob'; import consola from 'consola'; import { dim } from 'colorette'; import history from 'connect-history-api-fallback'; import { minify } from 'html-minifier-terser'; import { createFilter } from '@rollup/pluginutils'; function loadEnv(mode, envDir, prefix = "") { if (mode === "local") { throw new Error(`"local" cannot be used as a mode name because it conflicts with the .local postfix for .env files.`); } const env = {}; const envFiles = [ `.env.${mode}.local`, `.env.${mode}`, `.env.local`, `.env` ]; for (const file of envFiles) { const path = lookupFile(envDir, [file], true); if (path) { const parsed = dotenv.parse(fse.readFileSync(path)); expand({ parsed, ignoreProcessEnv: true }); for (const [key, value] of Object.entries(parsed)) { if (key.startsWith(prefix) && env[key] === void 0) { env[key] = value; } else if (key === "NODE_ENV") { process.env.VITE_USER_NODE_ENV = value; } } } } return env; } function lookupFile(dir, formats, pathOnly = false) { for (const format of formats) { const fullPath = join(dir, format); if (fse.pathExistsSync(fullPath) && fse.statSync(fullPath).isFile()) { return pathOnly ? fullPath : fse.readFileSync(fullPath, "utf-8"); } } const parentDir = dirname(dir); if (parentDir !== dir) { return lookupFile(parentDir, formats, pathOnly); } } async function isDirEmpty(dir) { return fse.readdir(dir).then((files) => { return files.length === 0; }); } const DEFAULT_TEMPLATE = "index.html"; const ignoreDirs = [".", "", "/"]; const bodyInjectRE = /<\/body>/; function createPlugin(userOptions = {}) { const { entry, template = DEFAULT_TEMPLATE, pages = [], verbose = false } = userOptions; let viteConfig; let env = {}; return { name: "vite:html", enforce: "pre", configResolved(resolvedConfig) { viteConfig = resolvedConfig; env = loadEnv(viteConfig.mode, viteConfig.root, ""); }, config(conf) { const input = createInput(userOptions, conf); if (input) { return { build: { rollupOptions: { input } } }; } }, configureServer(server) { let _pages = []; const rewrites = []; if (!isMpa(viteConfig)) { const template2 = userOptions.template || DEFAULT_TEMPLATE; const filename = DEFAULT_TEMPLATE; _pages.push({ filename, template: template2 }); } else { _pages = pages.map((page) => { return { filename: page.filename || DEFAULT_TEMPLATE, template: page.template || DEFAULT_TEMPLATE }; }); } const proxy = viteConfig.server?.proxy ?? {}; const baseUrl = viteConfig.base ?? "/"; const keys = Object.keys(proxy); let indexPage = null; for (const page of _pages) { if (page.filename !== "index.html") { rewrites.push(createRewire(page.template, page, baseUrl, keys)); } else { indexPage = page; } } if (indexPage) { rewrites.push(createRewire("", indexPage, baseUrl, keys)); } server.middlewares.use(history({ disableDotRule: void 0, htmlAcceptHeaders: ["text/html", "application/xhtml+xml"], rewrites })); }, transformIndexHtml: { enforce: "pre", async transform(html, ctx) { const url = ctx.filename; const base = viteConfig.base; const excludeBaseUrl = url.replace(base, "/"); const htmlName = path.relative(process.cwd(), excludeBaseUrl); const page = getPage(userOptions, htmlName, viteConfig); const { injectOptions = {} } = page; const _html = await renderHtml(html, { injectOptions, viteConfig, env, entry: page.entry || entry, verbose }); const { tags = [] } = injectOptions; return { html: _html, tags }; } }, async closeBundle() { const outputDirs = []; if (isMpa(viteConfig) || pages.length) { for (const page of pages) { const dir = path.dirname(page.template); if (!ignoreDirs.includes(dir)) { outputDirs.push(dir); } } } else { const dir = path.dirname(template); if (!ignoreDirs.includes(dir)) { outputDirs.push(dir); } } const cwd = path.resolve(viteConfig.root, viteConfig.build.outDir); const htmlFiles = await fg(outputDirs.map((dir) => `${dir}/*.html`), { cwd: path.resolve(cwd), absolute: true }); await Promise.all(htmlFiles.map((file) => fse.move(file, path.resolve(cwd, path.basename(file)), { overwrite: true }))); const htmlDirs = await fg(outputDirs.map((dir) => dir), { cwd: path.resolve(cwd), onlyDirectories: true, absolute: true }); await Promise.all(htmlDirs.map(async (item) => { const isEmpty = await isDirEmpty(item); if (isEmpty) { return fse.remove(item); } })); } }; } function createInput({ pages = [], template = DEFAULT_TEMPLATE }, viteConfig) { const input = {}; if (isMpa(viteConfig) || pages?.length) { const templates = pages.map((page) => page.template); templates.forEach((temp) => { let dirName = path.dirname(temp); const file = path.basename(temp); dirName = dirName.replace(/\s+/g, "").replace(/\//g, "-"); const key = dirName === "." || dirName === "public" || !dirName ? file.replace(/\.html/, "") : dirName; input[key] = path.resolve(viteConfig.root, temp); }); return input; } else { const dir = path.dirname(template); if (ignoreDirs.includes(dir)) { return void 0; } else { const file = path.basename(template); const key = file.replace(/\.html/, ""); return { [key]: path.resolve(viteConfig.root, template) }; } } } async function renderHtml(html, config) { const { injectOptions, viteConfig, env, entry, verbose } = config; const { data, ejsOptions } = injectOptions; const ejsData = { ...viteConfig?.env ?? {}, ...viteConfig?.define ?? {}, ...env || {}, ...data }; let result = await render(html, ejsData, ejsOptions); if (entry) { result = removeEntryScript(result, verbose); result = result.replace(bodyInjectRE, `