index.cjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. 'use strict';
  2. Object.defineProperty(exports, '__esModule', { value: true });
  3. const ejs = require('ejs');
  4. const dotenvExpand = require('dotenv-expand');
  5. const dotenv = require('dotenv');
  6. const path = require('pathe');
  7. const fse = require('fs-extra');
  8. const vite = require('vite');
  9. const nodeHtmlParser = require('node-html-parser');
  10. const fg = require('fast-glob');
  11. const consola = require('consola');
  12. const colorette = require('colorette');
  13. const history = require('connect-history-api-fallback');
  14. const htmlMinifierTerser = require('html-minifier-terser');
  15. const pluginutils = require('@rollup/pluginutils');
  16. function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e["default"] : e; }
  17. const dotenv__default = /*#__PURE__*/_interopDefaultLegacy(dotenv);
  18. const path__default = /*#__PURE__*/_interopDefaultLegacy(path);
  19. const fse__default = /*#__PURE__*/_interopDefaultLegacy(fse);
  20. const fg__default = /*#__PURE__*/_interopDefaultLegacy(fg);
  21. const consola__default = /*#__PURE__*/_interopDefaultLegacy(consola);
  22. const history__default = /*#__PURE__*/_interopDefaultLegacy(history);
  23. function loadEnv(mode, envDir, prefix = "") {
  24. if (mode === "local") {
  25. throw new Error(`"local" cannot be used as a mode name because it conflicts with the .local postfix for .env files.`);
  26. }
  27. const env = {};
  28. const envFiles = [
  29. `.env.${mode}.local`,
  30. `.env.${mode}`,
  31. `.env.local`,
  32. `.env`
  33. ];
  34. for (const file of envFiles) {
  35. const path = lookupFile(envDir, [file], true);
  36. if (path) {
  37. const parsed = dotenv__default.parse(fse__default.readFileSync(path));
  38. dotenvExpand.expand({
  39. parsed,
  40. ignoreProcessEnv: true
  41. });
  42. for (const [key, value] of Object.entries(parsed)) {
  43. if (key.startsWith(prefix) && env[key] === void 0) {
  44. env[key] = value;
  45. } else if (key === "NODE_ENV") {
  46. process.env.VITE_USER_NODE_ENV = value;
  47. }
  48. }
  49. }
  50. }
  51. return env;
  52. }
  53. function lookupFile(dir, formats, pathOnly = false) {
  54. for (const format of formats) {
  55. const fullPath = path.join(dir, format);
  56. if (fse__default.pathExistsSync(fullPath) && fse__default.statSync(fullPath).isFile()) {
  57. return pathOnly ? fullPath : fse__default.readFileSync(fullPath, "utf-8");
  58. }
  59. }
  60. const parentDir = path.dirname(dir);
  61. if (parentDir !== dir) {
  62. return lookupFile(parentDir, formats, pathOnly);
  63. }
  64. }
  65. async function isDirEmpty(dir) {
  66. return fse__default.readdir(dir).then((files) => {
  67. return files.length === 0;
  68. });
  69. }
  70. const DEFAULT_TEMPLATE = "index.html";
  71. const ignoreDirs = [".", "", "/"];
  72. const bodyInjectRE = /<\/body>/;
  73. function createPlugin(userOptions = {}) {
  74. const {
  75. entry,
  76. template = DEFAULT_TEMPLATE,
  77. pages = [],
  78. verbose = false
  79. } = userOptions;
  80. let viteConfig;
  81. let env = {};
  82. return {
  83. name: "vite:html",
  84. enforce: "pre",
  85. configResolved(resolvedConfig) {
  86. viteConfig = resolvedConfig;
  87. env = loadEnv(viteConfig.mode, viteConfig.root, "");
  88. },
  89. config(conf) {
  90. const input = createInput(userOptions, conf);
  91. if (input) {
  92. return {
  93. build: {
  94. rollupOptions: {
  95. input
  96. }
  97. }
  98. };
  99. }
  100. },
  101. configureServer(server) {
  102. let _pages = [];
  103. const rewrites = [];
  104. if (!isMpa(viteConfig)) {
  105. const template2 = userOptions.template || DEFAULT_TEMPLATE;
  106. const filename = DEFAULT_TEMPLATE;
  107. _pages.push({
  108. filename,
  109. template: template2
  110. });
  111. } else {
  112. _pages = pages.map((page) => {
  113. return {
  114. filename: page.filename || DEFAULT_TEMPLATE,
  115. template: page.template || DEFAULT_TEMPLATE
  116. };
  117. });
  118. }
  119. const proxy = viteConfig.server?.proxy ?? {};
  120. const baseUrl = viteConfig.base ?? "/";
  121. const keys = Object.keys(proxy);
  122. let indexPage = null;
  123. for (const page of _pages) {
  124. if (page.filename !== "index.html") {
  125. rewrites.push(createRewire(page.template, page, baseUrl, keys));
  126. } else {
  127. indexPage = page;
  128. }
  129. }
  130. if (indexPage) {
  131. rewrites.push(createRewire("", indexPage, baseUrl, keys));
  132. }
  133. server.middlewares.use(history__default({
  134. disableDotRule: void 0,
  135. htmlAcceptHeaders: ["text/html", "application/xhtml+xml"],
  136. rewrites
  137. }));
  138. },
  139. transformIndexHtml: {
  140. enforce: "pre",
  141. async transform(html, ctx) {
  142. const url = ctx.filename;
  143. const base = viteConfig.base;
  144. const excludeBaseUrl = url.replace(base, "/");
  145. const htmlName = path__default.relative(process.cwd(), excludeBaseUrl);
  146. const page = getPage(userOptions, htmlName, viteConfig);
  147. const { injectOptions = {} } = page;
  148. const _html = await renderHtml(html, {
  149. injectOptions,
  150. viteConfig,
  151. env,
  152. entry: page.entry || entry,
  153. verbose
  154. });
  155. const { tags = [] } = injectOptions;
  156. return {
  157. html: _html,
  158. tags
  159. };
  160. }
  161. },
  162. async closeBundle() {
  163. const outputDirs = [];
  164. if (isMpa(viteConfig) || pages.length) {
  165. for (const page of pages) {
  166. const dir = path__default.dirname(page.template);
  167. if (!ignoreDirs.includes(dir)) {
  168. outputDirs.push(dir);
  169. }
  170. }
  171. } else {
  172. const dir = path__default.dirname(template);
  173. if (!ignoreDirs.includes(dir)) {
  174. outputDirs.push(dir);
  175. }
  176. }
  177. const cwd = path__default.resolve(viteConfig.root, viteConfig.build.outDir);
  178. const htmlFiles = await fg__default(outputDirs.map((dir) => `${dir}/*.html`), { cwd: path__default.resolve(cwd), absolute: true });
  179. await Promise.all(htmlFiles.map((file) => fse__default.move(file, path__default.resolve(cwd, path__default.basename(file)), {
  180. overwrite: true
  181. })));
  182. const htmlDirs = await fg__default(outputDirs.map((dir) => dir), { cwd: path__default.resolve(cwd), onlyDirectories: true, absolute: true });
  183. await Promise.all(htmlDirs.map(async (item) => {
  184. const isEmpty = await isDirEmpty(item);
  185. if (isEmpty) {
  186. return fse__default.remove(item);
  187. }
  188. }));
  189. }
  190. };
  191. }
  192. function createInput({ pages = [], template = DEFAULT_TEMPLATE }, viteConfig) {
  193. const input = {};
  194. if (isMpa(viteConfig) || pages?.length) {
  195. const templates = pages.map((page) => page.template);
  196. templates.forEach((temp) => {
  197. let dirName = path__default.dirname(temp);
  198. const file = path__default.basename(temp);
  199. dirName = dirName.replace(/\s+/g, "").replace(/\//g, "-");
  200. const key = dirName === "." || dirName === "public" || !dirName ? file.replace(/\.html/, "") : dirName;
  201. input[key] = path__default.resolve(viteConfig.root, temp);
  202. });
  203. return input;
  204. } else {
  205. const dir = path__default.dirname(template);
  206. if (ignoreDirs.includes(dir)) {
  207. return void 0;
  208. } else {
  209. const file = path__default.basename(template);
  210. const key = file.replace(/\.html/, "");
  211. return {
  212. [key]: path__default.resolve(viteConfig.root, template)
  213. };
  214. }
  215. }
  216. }
  217. async function renderHtml(html, config) {
  218. const { injectOptions, viteConfig, env, entry, verbose } = config;
  219. const { data, ejsOptions } = injectOptions;
  220. const ejsData = {
  221. ...viteConfig?.env ?? {},
  222. ...viteConfig?.define ?? {},
  223. ...env || {},
  224. ...data
  225. };
  226. let result = await ejs.render(html, ejsData, ejsOptions);
  227. if (entry) {
  228. result = removeEntryScript(result, verbose);
  229. result = result.replace(bodyInjectRE, `<script type="module" src="${vite.normalizePath(`${entry}`)}"><\/script>
  230. </body>`);
  231. }
  232. return result;
  233. }
  234. function getPage({ pages = [], entry, template = DEFAULT_TEMPLATE, inject = {} }, name, viteConfig) {
  235. let page;
  236. if (isMpa(viteConfig) || pages?.length) {
  237. page = getPageConfig(name, pages, DEFAULT_TEMPLATE);
  238. } else {
  239. page = createSpaPage(entry, template, inject);
  240. }
  241. return page;
  242. }
  243. function isMpa(viteConfig) {
  244. const input = viteConfig?.build?.rollupOptions?.input ?? void 0;
  245. return typeof input !== "string" && Object.keys(input || {}).length > 1;
  246. }
  247. function removeEntryScript(html, verbose = false) {
  248. if (!html) {
  249. return html;
  250. }
  251. const root = nodeHtmlParser.parse(html);
  252. const scriptNodes = root.querySelectorAll("script[type=module]") || [];
  253. const removedNode = [];
  254. scriptNodes.forEach((item) => {
  255. removedNode.push(item.toString());
  256. item.parentNode.removeChild(item);
  257. });
  258. verbose && removedNode.length && consola__default.warn(`vite-plugin-html: Since you have already configured entry, ${colorette.dim(removedNode.toString())} is deleted. You may also delete it from the index.html.
  259. `);
  260. return root.toString();
  261. }
  262. function createSpaPage(entry, template, inject = {}) {
  263. return {
  264. entry,
  265. filename: "index.html",
  266. template,
  267. injectOptions: inject
  268. };
  269. }
  270. function getPageConfig(htmlName, pages, defaultPage) {
  271. const defaultPageOption = {
  272. filename: defaultPage,
  273. template: `./${defaultPage}`
  274. };
  275. const page = pages.filter((page2) => {
  276. return path__default.resolve("/" + page2.template) === path__default.resolve("/" + htmlName);
  277. })?.[0];
  278. return page ?? defaultPageOption ?? void 0;
  279. }
  280. function createRewire(reg, page, baseUrl, proxyUrlKeys) {
  281. return {
  282. from: new RegExp(`^/${reg}*`),
  283. to({ parsedUrl }) {
  284. const pathname = parsedUrl.pathname;
  285. const excludeBaseUrl = pathname.replace(baseUrl, "/");
  286. const template = path__default.resolve(baseUrl, page.template);
  287. if (excludeBaseUrl === "/") {
  288. return template;
  289. }
  290. const isApiUrl = proxyUrlKeys.some((item) => pathname.startsWith(path__default.resolve(baseUrl, item)));
  291. return isApiUrl ? excludeBaseUrl : template;
  292. }
  293. };
  294. }
  295. const htmlFilter = pluginutils.createFilter(["**/*.html"]);
  296. function getOptions(minify) {
  297. return {
  298. collapseWhitespace: minify,
  299. keepClosingSlash: minify,
  300. removeComments: minify,
  301. removeRedundantAttributes: minify,
  302. removeScriptTypeAttributes: minify,
  303. removeStyleLinkTypeAttributes: minify,
  304. useShortDoctype: minify,
  305. minifyCSS: minify
  306. };
  307. }
  308. async function minifyHtml(html, minify) {
  309. if (typeof minify === "boolean" && !minify) {
  310. return html;
  311. }
  312. let minifyOptions = minify;
  313. if (typeof minify === "boolean" && minify) {
  314. minifyOptions = getOptions(minify);
  315. }
  316. return await htmlMinifierTerser.minify(html, minifyOptions);
  317. }
  318. function createMinifyHtmlPlugin({
  319. minify = true
  320. } = {}) {
  321. return {
  322. name: "vite:minify-html",
  323. enforce: "post",
  324. async generateBundle(_, outBundle) {
  325. if (minify) {
  326. for (const bundle of Object.values(outBundle)) {
  327. if (bundle.type === "asset" && htmlFilter(bundle.fileName) && typeof bundle.source === "string") {
  328. bundle.source = await minifyHtml(bundle.source, minify);
  329. }
  330. }
  331. }
  332. }
  333. };
  334. }
  335. consola__default.wrapConsole();
  336. function createHtmlPlugin(userOptions = {}) {
  337. return [createPlugin(userOptions), createMinifyHtmlPlugin(userOptions)];
  338. }
  339. exports.createHtmlPlugin = createHtmlPlugin;