npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@struggler/vite-plugin-mpa

v1.2.2

Published

Vite MPA: input keys map to dist HTML paths; virtual resolve, dev middleware (pre), relative script rewrite, optional HTML minify.

Readme

@struggler/vite-plugin-mpa

中文 · README_ZH.md

npm version License: MIT Vite

When to use it

  • Multiple independent frontends in one repo — e.g. several folders each with index.html + app entry, without merging them into one SPA bundle: landing pages, admin vs marketing, or several “mini sites” in a single Vite project.
  • Shared components, separate “projects” — e.g. components/, shared/, or utils/ imported from more than one entry folder. Each product stays its own HTML entry and JS bundle, but you reuse the same UI kit, theme, or hooks. Good for one design system with multiple deployable surfaces (not the same as one SPA with many routes — here each entry is built separately; this plugin keeps dev/build URL rules consistent for all of them).
  • URLs you control — deployment paths like /main/, /login/ come from the input key, while the value is still the real *.html on disk; no need to mirror the source tree (see input → where files land).
  • Same rules in dev and build — virtual paths, config.base, and optional startup log of key → page URL on pnpm run dev (logInputMap).

This plugin augments Vite’s multi-page flow so dev and build stay aligned, relative scripts keep working, and the special index key maps to a single root index.html. Details: What it does.

When you might skip it

Compared to a single-page app, plain MPA, or a multi-package monorepo:

| Situation | Notes | |-----------|--------| | Classic SPA | One index.html, one entry, client-side routes — you usually don’t need this plugin. | | Plain Vite MPA | If you don’t care about dev URL vs dist layout matching the “key rules” below, try without the plugin first; add it if relative assets or path mismatch bite you. | | pnpm / npm monorepo | One Vite app per package, each with its own index.html — that’s a multi-package setup; you often don’t need this plugin unless you want one project, several HTML entries with virtual paths. | | This plugin | Several input entries in one Vite app, each its own main bundle; you can still share components/, but it is not the same as “one big SPA, many routes”. |

npm: @struggler/vite-plugin-mpa · GitHub: strugglerx/vite-mpa-plugin · CHANGELOG.md

| | | |--|--| | Key → output | Key rules: only the literal key index maps to a root index.html (not index/index.html); any other key like main or login uses key/index.html. writeBundle relayouts when Vite first emits under the source tree. | | Same file, many keys | Multiple input keys can target one *.html; each key gets its own output path. | | Dev | configureServer with order: 'pre', URL shape close to production; base stripping. | | ./main.js | Optional rewrite of relative script / link URLs to root-based paths (rewriteHtmlRelativeToRoot, on by default). | | More | inject, styleInline, createMpaPlugin + htmlMinify. |

Example example/: two Vue 3 MPA apps under app/page1/ and app/page2/; shows multiple keys to one page (index and main → same HTML) and login to the other, plus a root public/index.html for navigation. See example/dist and the input section.

Contents

What it does

With several HTML entry points, Rollup and the Vite dev server can disagree on how virtual entries map to files. This plugin:

  • Rewrites options.input to prefixed virtual IDs, then uses resolveId + load to read the real *.html files;
  • In dev, maps request URLs to the correct input entry, stripping config.base from the path when needed.

You can add build-only HTML minification via html-minifier-terser (transformIndexHtml, enforce: "post").

Requirements

  • Vite >=3 (peer dependency — install in your app)
  • Node.js 18+ recommended (uses structuredClone, etc.)

Install

npm install @struggler/vite-plugin-mpa
# or
pnpm add @struggler/vite-plugin-mpa
# or
yarn add @struggler/vite-plugin-mpa

input → where files land in dist

(build.outDir defaults to dist; paths below are relative to it.)

| | Meaning | |--|---------| | input value | The real *.html on disk — Vite bundles from that file; script/asset resolution uses that file’s directory. | | input key | Chooses the output path for that entry’s built HTML in dist (the virtual path, which can differ from the value’s folder). |

Key → built HTML (with default indexHtml: "index.html"; keys that end in .html are listed below):

| Key | Output (relative to outDir) | |-----|------------------------------| | index | index.html only at the root (not index/index.html) | | Other one-segment keys, e.g. main, login | key/index.html |

Example (see example/vite.config.js for output and optional multi-key / same file):

// build.rollupOptions.input
{
  index: "app/page1/index.html",
  main:  "app/page1/index.html", // two keys → two URLs, same file on disk
  login: "app/page2/index.html",
}

Key index → root index.html; main / loginmain/index.html, login/index.html (see virtual-path rules). A root public/index.html is also copied to index.html — that can clash with an index key; resolve by renaming the key, moving public, or similar (see Dev server and base).

Sample tree after vite build in example/: chunk layout (assets/ vs static/js/, etc.) is controlled by build.rollupOptions.output (the example uses static/…); file names with hashes can change every build.

example/dist/
├── index.html          # often from public; conflicts with `index` key if both set
├── main/index.html     # key main
├── login/index.html    # key login
└── static/             # example’s custom `output`; default is often `assets/`
    ├── js/…
    └── css/…

Vite may emit HTML under the source path first; this plugin then moves it in writeBundle to match the table. Several keys for the same *.html produce one output per key. If you change Rollup output options, verify with a real build. Plain Vite (no plugin) often uses key.html at the root of dist — different from the table. See Vite — MPA and the virtual-path table.

Usage

MpaPlugin — MPA only

// vite.config.js
import { MpaPlugin } from "@struggler/vite-plugin-mpa"

export default {
  plugins: [MpaPlugin()],
  build: {
    rollupOptions: {
      input: {
        index: "index.html",
        about: "src/pages/about/index.html",
      },
    },
  },
}
  • The key in input controls the virtual path rule (table below). The value is the real HTML path relative to project root.

createMpaPlugin — MPA + build-time HTML minify

import { createMpaPlugin } from "@struggler/vite-plugin-mpa"

export default {
  plugins: createMpaPlugin({ htmlMinify: true }),
  build: {
    rollupOptions: {
      input: {
        index: "index.html",
        about: "src/pages/about/index.html",
      },
    },
  },
}

createMpaPlugin is [MpaPlugin(rest)] when htmlMinify is off, or [MpaPlugin(rest), htmlMinifyPlugin(opts)] when on. The htmlMinify key is not passed to MpaPlugin.

Options

| Option | Where | Description | |--------|--------|-------------| | indexHtml | MpaPlugin / createMpaPlugin | Main entry filename (default index.html). You can pass main or main.html; it joins with keys as key/indexHtml. | | inject | same | Object for global <%=%> replacement, or a function ({ key }) => object merged with injectPages. | | injectPages | same | e.g. { about: { title: 'a' } } — per-entry key overrides on top of inject. | | styleInline | same | For “directory” load paths (e.g. about/index.htmlabout key), whether to inject <style> via a script. Default true; set false to skip. | | transformHtml | same | (html, { key, phase: 'load' \| 'serve' }) => HTML. See pipeline below. | | debug | same | true logs resolution under [vite-plugin-mpa]. | | logInputMap | same | On dev (vite, pnpm run dev), logs each input key → page URL (with base) ← source *.html. Default true; set false to silence. No print on build. | | rewriteHtmlRelativeToRoot | same | Default true: rewrites ./ / ../ in script[src] and link[href] to a path from project root (with config.base), so ./main.js still works when input keys do not mirror the source folder and dist HTML is moved to the key path. Set false if you only use fixed or root-absolute URLs. | | htmlMinify | createMpaPlugin only | true (defaults), an options object, or unset (no minify plugin). |

load pipeline (directory / nested entry): read disk → transformHtml (phase: 'load') → optional rewriteHtmlRelativeToRoot → if styleInline !== false, replaceIndexStylereplaceInject.
“Direct” entry (key is *.html-style as resolved): read → transformHtmlreplaceInject (no replaceIndexStyle).
Dev server: read → transformHtml (phase: 'serve') → replaceInject (no replaceIndexStyle).

Inject example

index.html:

<title><%= pageTitle %></title>
MpaPlugin({
  inject: { pageTitle: "My site" },
  injectPages: {
    about: { pageTitle: "About" },
  },
})

TypeScript

Types are in index.d.ts. vite is a peer; add npm i -D vite if your IDE does not resolve import type { Plugin } from "vite".

Rollup input keys and virtual paths

(Assuming the plugin’s indexHtml option matches your entries—default is index.html. If you set MpaPlugin({ indexHtml: "main.html" }), replace index.html below with that filename.)

| Key | Virtual path (relative to root) | |-----|-----------------------------------| | Ends with .html | Same as the key, e.g. entry.html | | Exactly the string index | A single file at the project root: <indexHtml> (default index.html). Not index/index.html. | | Anything else | ${key}/<indexHtml> (e.g. main/index.html, login/index.html by default) |

Why index is special: Only when the rollup input key is the literal string index does the virtual path point to one HTML file directly under root (e.g. index.html). For any other short key like main or login, the path is <key>/<indexHtml> (a “folder + file” under root). So the main MPA at key index lines up with /index.html at the site root, while other apps use /main/index.html, /login/index.html, etc. If you need the “main” app to also live at myapp/index.html, don’t use the key index—use e.g. myapp or app as the key.

public/index.html: The root index.html is often used for a static landing page. If the MPA also uses the key index, both target the same path—use another key (e.g. main for the first app) or change public, as in the example.

Key index vs key index.html: A key literally named index.html (first table row) also resolves to a path ending in index.html, but the key passed to inject / transformHtml is the string index.html, not index. Pick one convention and stay consistent.

On load, besides matching a key that equals a relative path, the plugin can strip a trailing /<indexHtml> and match by key; replaceIndexStyle runs only on that “directory-style” path (unless styleInline: false).

Dev server and base

After ignoring paths that contain @ (Vite internals), the middleware strips config.base from the URL (e.g. base: '/app/' and request /app/about → match input key about). Path shape rules (empty, .html, or extensionless) align with the input → output section above; the new part here is base handling.

Dev URLs match the virtual paths (same rules as the virtual-path table and the built dist layout, e.g. key index/index.html, key login/login/index.html). The middleware is registered with configureServer + order: 'pre' so it usually runs before static public files, and you can open those paths without relying on a deep source-only URL. If both public/index.html and an MPA index key exist, they compete for /index.html—use another key (e.g. main) for the app, or adjust public.

Inline styles (replaceIndexStyle)

When a load only matches by stripping the trailing index.html segment, <style> blocks are moved into a runtime <script> that creates a <style> element, using textContent and JSON.stringify so CSS with backticks / ${ does not break the script. Set styleInline: false to skip, or use a “direct” match so this branch is not used.

Exports

| Name | Description | |------|-------------| | MpaPlugin | Returns a Vite plugin. Options in the table. | | createMpaPlugin | Returns an array of plugins; can append minify. htmlMinify is not passed to MpaPlugin. |

Changelog

CHANGELOG.md (notable changes between versions).

Links

Contributing

Issues and pull requests are welcome. For larger changes, please open an issue first.

Tests: npm test (Vitest).

License

MIT — see LICENSE.

Copyright (c) 2026 moqi ([email protected]). The full text of the MIT License is in the LICENSE file; it must be included in redistributions. This short note is not legal advice.