UNPKG

vite-plugin-htmls

Version:

Vite Plugin that simplifies creation of HTML files to serve your bundles

234 lines (197 loc) 5.63 kB
import { extname, resolve } from 'path'; import { readFileSync, pathExists } from 'fs-extra'; import type { Plugin, ResolvedConfig, HtmlTagDescriptor, ViteDevServer } from 'vite'; import type { OutputBundle, OutputAsset, OutputChunk } from 'rollup'; import { isAbsoluteUrl, addTrailingSlash } from './utils'; // import { assert } from 'console'; export interface HtmlPluginOptions { /** * Adds the given favicon path to the output html * Default: `false` */ favicon?: false | string; /** * The file to write the html to. Also you can specify a subdirectory here. * Default: `index.html` */ filename?: string; /** * Accept `.[j|t]s?[x]` as input, which works like webpack input. * Default: `src/main.[j|t]s?[x]` */ input?: string; /** * Pass a html-minifier options object to minify the output. * Default: `false` */ meta?: false; /** * The `webpack` require path to the template */ template?: string ; /** * Allow to use a html string instead of reading from a file. * Default: `false` */ templateContent?: false | string; /** * The title to use for the generated HTML document. * Default: `Vite App` */ title?: string; /** * The publicPath use for the generates assets. * Default: `auto`. */ publicPath?: 'auto' | string; } export type OutputBundleExt = | (OutputAsset & { ext: string }) | (OutputChunk & { ext: string }); export default function htmlPlugin(userOptions?: HtmlPluginOptions): Plugin { let viteConfig: ResolvedConfig = null; if (!userOptions.template && !userOptions.templateContent) { if (pathExists(resolve('index.html'))) { userOptions.template = 'index.html' } else { return; } } return { name: 'vite-plugin-html', config(cfg) { // eslint-disable-next-line no-param-reassign cfg.build = { ...cfg.build, rollupOptions: { ...cfg?.build?.rollupOptions, preserveEntrySignatures: 'exports-only', output: { ...cfg?.build?.rollupOptions?.output, format: 'es', }, input: userOptions.input, }, }; return cfg; }, configResolved(resolvedConfig) { viteConfig = resolvedConfig; }, async generateBundle(_, bundle) { const htmlTags = classifyFiles( getFiles(bundle), viteConfig, ); const html = injectToHtml( parseTemplate(userOptions.template, userOptions.templateContent), htmlTags, ); this.emitFile({ type: 'asset', source: html, name: 'html', fileName: 'index.html', }); }, transformIndexHtml (html) { const entryTag: HtmlTagDescriptor = { tag: 'script', attrs: { type: 'module', src: userOptions.input, }, injectTo: 'body', }; return [ entryTag ] } }; } function parseTemplate(template?: string, templateContent?: string | false): string { if (template) { return readFileSync(resolve(template), 'utf-8'); } if (templateContent) { return templateContent; } } function classifyFiles(files: OutputBundleExt[], config: ResolvedConfig): HtmlTagDescriptor[] { const htmlTags: HtmlTagDescriptor[] = []; for (let i = 0; i < files.length; ++i) { if (files[i].ext === '.css') { htmlTags.push({ tag: 'link', attrs: { rel: 'stylesheet', href: toPublicPath(files[i].fileName, config), }, injectTo: 'head', }); } // Only appends entry file if (files[i].ext === '.js' && (files[i] as OutputChunk).isEntry) { htmlTags.push({ tag: 'script', attrs: { type: 'module', src: toPublicPath(files[i].fileName, config), }, injectTo: 'body', }); } } return htmlTags; } function getFiles(bundle: OutputBundle): OutputBundleExt[] { return Object.values(bundle) .map((file) => ({ ...file, ext: extname(file.fileName), })); } function toPublicPath(filename: string, config: ResolvedConfig) { return isAbsoluteUrl(filename) ? filename : addTrailingSlash(config.base) + filename; } const headInjectRE = /<\/head>/; const bodyInjectRE = /<\/body>/; function injectToHtml(html: string, tags: HtmlTagDescriptor[]) { let _html = html; const hasHeadElement = headInjectRE.test(html); const hasBodyElement = bodyInjectRE.test(html); tags.forEach((tag) => { if (tag.injectTo === 'head' && hasHeadElement) { _html = _html.replace(headInjectRE, `${serializeTag(tag)}\n$&`); } if (tag.injectTo === 'body' && hasBodyElement) { _html = _html.replace(bodyInjectRE, `${serializeTag(tag)}\n$&`); } }); return _html; } const unaryTags = new Set(['link', 'meta', 'base']); function serializeTag({ tag, attrs, children }: HtmlTagDescriptor): string { if (unaryTags.has(tag)) { return `<${tag}${serializeAttrs(attrs)}>`; } else { return `<${tag}${serializeAttrs(attrs)}>${serializeTags(children)}</${tag}>`; } } function serializeTags(tags: HtmlTagDescriptor['children']): string { if (typeof tags === 'string') { return tags; } else if (tags) { return tags.map(serializeTag).join('\n '); } return ''; } function serializeAttrs(attrs: HtmlTagDescriptor['attrs']): string { let res = ''; for (const key in attrs) { if (typeof attrs[key] === 'boolean') { res += attrs[key] ? ` ${key}` : ''; } else { res += ` ${key}=${JSON.stringify(attrs[key])}`; } } return res; }