vite-plugin-html-entry
v0.1.0
Published
Import an .html file with ?html to get its built URL - Vite registers it as a real build entry (added to rollupOptions.input automatically) so its <link> and <script> tags are processed like any other page, not copied as a static asset. Useful for multi-p
Maintainers
Readme
vite-plugin-html-entry
A Vite plugin that lets you import an .html file with a ?html suffix and get back its built URL - without listing that file in build.rollupOptions.input. The file is registered as a real Vite/Rollup build entry, so its <link> and <script> tags are processed and rewritten exactly like any other page - not just copied as a static asset.
import checkoutUrl from './checkout/index.html?html'
document.querySelector('iframe').src = checkoutUrlVite's bundler only knows about HTML files it's told about upfront via build.rollupOptions.input. This plugin discovers ?html imports in your source and wires them in automatically, so you can add a new page - and get a URL for it - without touching vite.config.ts.
It also works during vite dev: the import resolves to the dev server URL for that page, so you get HMR on it like any other page.
Use cases
- Extra HTML entry points for a multi-page app (MPA) - add a page by importing it, not by editing
vite.config.ts. - Embedding a page in an
<iframe>(checkout flow, widget, live preview) that isn't part of your app's normal routes. - Standalone pages outside your SPA's router - a changelog, a print view, a static legal page - that still need to go through Vite's build pipeline.
- Browser extensions (Manifest V3 in particular), where you often need to open a small HTML page - a permission prompt, an OAuth landing page, an ad-hoc popup - that isn't one of the extension's declared entry points (side panel, action popup, options page). Point
toUrlat your platform's own URL resolver (see Options) and the import gives you a ready-to-use URL forchrome.windows.create,browser.windows.create, or whatever your runtime expects.
Install
npm install -D vite-plugin-html-entryUsage
// vite.config.ts
import { defineConfig } from 'vite'
import { htmlEntry } from 'vite-plugin-html-entry'
export default defineConfig({
plugins: [htmlEntry()],
})// anywhere in your source
import changelogUrl from './changelog/index.html?html'
window.open(changelogUrl, '_blank')changelog/index.html is picked up automatically (see How it works) and built as its own entry, with its own JS/CSS output - even though it's never listed in vite.config.ts and isn't linked from anywhere else in the app. By default, changelogUrl resolves to the root-absolute URL path of the built file (e.g. "/changelog/index.html") - see Options to resolve it differently.
TypeScript
Without a type declaration, tsc won't know what import x from './page.html?html' resolves to. Add the plugin's client types to your tsconfig.json:
{
"compilerOptions": {
"types": ["vite-plugin-html-entry/client"]
}
}Options
htmlEntry(options?: {
/**
* Turns the built HTML path (relative to Vite's root, e.g.
* "src/changelog/index.html") into the expression the import resolves to.
* Defaults to the root-absolute URL path (e.g. "/src/changelog/index.html").
*/
toUrl?: (relativePath: string) => string
/** Extra directory names to skip while scanning for `?html` imports. */
skipDirs?: string[]
})For a runtime that resolves its own asset URLs - a browser extension, for instance - point toUrl at that API instead of the default absolute path:
htmlEntry({ toUrl: (relativePath) => `myRuntime.resolveAssetUrl(${JSON.stringify(relativePath)})` })How it works
Rollup/Vite decide which files are build entries before any module resolution happens, so a plugin can't add one lazily from inside resolveId/load the way it can for most virtual modules. To get around that, this plugin's config hook walks your source tree once at build startup, looking for '...html?html' string literals, and adds every HTML file it finds to build.rollupOptions.input. From there, resolveId/load just resolve the import to a virtual module exporting the emitted file's URL.
This means:
- The import specifier must be a plain relative string literal (
'./page.html?html'), not a variable or template expression - it has to be discoverable by scanning source text. - Adding a new
?htmlimport requires restarting the dev server or rebuilding, since the scan only runs once at startup.
FAQ
How do I add an HTML file as a Vite build entry without editing vite.config.ts?
Install this plugin, add htmlEntry() to plugins, and import the file with a ?html suffix (import url from './page.html?html'). The plugin finds the import and adds the file to build.rollupOptions.input for you.
How do I get a URL for an HTML page in Vite, for use in an <iframe>, window.open, or a link?
That's exactly what the default import resolves to - a root-absolute URL path to the built (or dev-server-served) HTML file, ready to assign to iframe.src, pass to window.open, or use as an href.
How do I open a browser extension page (Manifest V3) that isn't declared in manifest.json?
Import the page's .html file with ?html and pass a toUrl option that returns your runtime's own URL-resolving call, e.g. toUrl: (p) => `chrome.runtime.getURL(${JSON.stringify(p)})` . See Options.
Does this work with vite dev and HMR?
Yes - during dev, the import resolves to the dev server URL for that page, and the page gets HMR like any other Vite entry.
How is this different from just listing the file in build.rollupOptions.input directly?
rollupOptions.input requires knowing every entry page upfront and wiring its output path back into your code by hand. This plugin discovers the entries from your ?html imports and gives you the built URL directly at the import site, so adding a page is a one-line change in your source, not a config edit.
License
MIT
