vite-plugin-stable-sri
v1.0.1
Published
Vite plugin for stable, reproducible Subresource Integrity manifests.
Maintainers
Readme
vite-plugin-stable-sri
A Vite plugin that produces stable, reproducible Subresource Integrity (SRI) hashes — a deterministic, byte-for-byte build outcome similar in spirit to webpack's reproducible output.
Why
Vite (and Rollup) chunk names are content-hashed, but the contents of those chunks can shift between builds because of non-deterministic module order, vendor grouping, or internal export minification. That means an SRI hash computed from a build today may not match the same source rebuilt tomorrow — which defeats the point of pinning integrity hashes for caching, CSP, or supply-chain verification.
This plugin makes the bundle layout deterministic so the SRI hash is too. It:
- Forces stable
entryFileNames,chunkFileNames, andassetFileNamespatterns with hexhashCharacters. - Applies a deterministic
manualChunksstrategy (default: one chunk pernode_modulespackage). - Computes SRI hashes after the bundle is finalized, in sorted order, and emits a stable JSON manifest.
- Optionally injects
integrityandcrossoriginattributes into emitted HTML and augments Vite's ownmanifest.json.
Why use this over vite-plugin-sri?
Existing SRI plugins (such as vite-plugin-sri) do one thing: after the build, they parse the emitted HTML and inject integrity/crossorigin attributes for the assets it references. If that's all you need, they're simpler and completely non-invasive — reach for one of those.
This plugin targets a larger problem: integrity hashes you can pin, distribute, and reproduce, not just attributes baked into HTML. Choose it when you want:
- Reproducible hashes. Vite/Rollup chunk contents can drift between builds (module order, vendor grouping, internal export minification), so a hash computed today may not match the same source rebuilt tomorrow. This plugin pins the bundle layout (
manualChunks, stable file names, hex hashes, no internal-export minification) so the SRI hash is stable too. Plain SRI plugins hash whatever the build happens to produce and don't address drift. - An out-of-band manifest. It emits a sorted, byte-for-byte stable
sri-manifest.jsonand writesintegrityinto Vite's ownmanifest.json. That lets a CDN, proxy, CSP pipeline, or separate verification step consume integrity values without scraping HTML. HTML-only plugins leave nothing to consume. - Correct link handling. Only
rel="stylesheet"andrel="modulepreload"links get integrity. Plugins that target every<link href>can attachcrossoriginto icons,preconnect, ormanifestlinks and break those requests. - Lossless HTML edits. It inserts attributes via targeted offset-based edits, leaving the rest of the document byte-identical, rather than re-serializing the whole HTML through an HTML parser.
- Configurability. Hash algorithm,
crossoriginmode,hashLength,includefilter, and chunk strategy are all options, instead of being hardcoded.
The trade-off: this plugin owns part of your build config (see Side effects on your Vite config) and has no special handling for @vitejs/plugin-legacy. If you don't consume the manifest and don't need reproducibility, that's more machinery than you need — use a plain SRI plugin instead.
Requirements
- Node.js ≥ 18
- Vite 5, 6, or 7 (peer dependency)
- ESM (this package is ESM-only)
Install
npm install --save-dev vite-plugin-stable-sriUsage
// vite.config.ts
import { defineConfig } from "vite";
import { stableSri } from "vite-plugin-stable-sri";
export default defineConfig({
plugins: [
stableSri({
algorithm: "sha384",
crossorigin: "anonymous",
chunkStrategy: "per-package",
injectHtml: true,
manifest: true,
}),
],
});The plugin only runs during vite build (apply: "build").
What gets emitted
.vite/sri-manifest.json
{
"assets/index-3f1b1d6a7c9e8f02.js": {
"integrity": "sha384-XzqMnIuKgL...==",
"bytes": 12843
},
"assets/vendor-react-1ab2cd34.js": {
"integrity": "sha384-aR4mZ8fE9k...==",
"bytes": 132901
}
}Keys are sorted; bytes are the exact source size used to compute the hash.
Injected HTML
<script type="module" crossorigin="anonymous"
integrity="sha384-XzqMnIuKgL...=="
src="/assets/index-3f1b1d6a7c9e8f02.js"></script>
<link rel="stylesheet" crossorigin="anonymous"
integrity="sha384-pL2..." href="/assets/index-9c8d7e6f5a4b3c2d.css">Augmented .vite/manifest.json
Each entry gets an integrity field alongside Vite's existing file, imports, etc.
Options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| algorithm | "sha256" \| "sha384" \| "sha512" | "sha384" | Hash algorithm used for SRI digests. |
| crossorigin | "anonymous" \| "use-credentials" \| false | "anonymous" | Value for the injected crossorigin attribute (or false to skip). |
| injectHtml | boolean | true | Inject integrity (and crossorigin) attributes into emitted HTML. |
| manifest | boolean | true | Emit .vite/sri-manifest.json mapping bundle file → integrity hash. |
| manifestFile | string | ".vite/sri-manifest.json" | Path of the emitted SRI manifest. |
| include | RegExp | /\.(js\|css)$/ | Files included in the manifest / HTML rewriting. |
| chunkStrategy | "none" \| "single-vendor" \| "per-package" \| (id) => string \| undefined | "per-package" | How vendor code is grouped into chunks for stable file names. See below. |
| hashLength | number | 16 | Length of the content-hash segment in output file names. |
| augmentViteManifest | boolean | true | Add integrity to entries in Vite's own .vite/manifest.json. |
chunkStrategy
"none"— nomanualChunks; Rollup's default grouping (less stable across dependency upgrades)."single-vendor"— everything innode_moduleslands in a singlevendorchunk."per-package"(default) — one chunk per top-levelnode_modulespackage (vendor-react,vendor-react-dom,vendor-@scope-pkg, …). Best stability/cacheability trade-off, but note it can emit many small chunks (one request per package) for dependency-heavy apps; switch to"single-vendor"if you'd rather trade granular caching for fewer requests.(id) => string | undefined— supply your own function; return a chunk name for IDs you want to group, orundefinedto leave the module ungrouped.
Side effects on your Vite config
This plugin sets the following inside build.rollupOptions.output, overriding any user-supplied values:
entryFileNames,chunkFileNames,assetFileNameshashCharacters: "hex"manualChunks(according tochunkStrategy)minifyInternalExports: false
It also sets build.manifest: true so the Vite manifest is available for augmentation.
If you need fine-grained control over chunk file names, pass a custom chunkStrategy function rather than overriding chunkFileNames yourself.
Limitations
- SSR builds are left untouched. The plugin only acts on client builds. During an SSR build (
build.ssr) it makes no changes — server chunks aren't loaded with SRI, and forcingmanualChunksthere can conflict with theinlineDynamicImportsthat SSR/library builds use. - Browser-enforced SRI covers static references only. Injected
integrityprotects the entry<script>, the emitted stylesheet, and the staticmodulepreload/stylesheetlinks in your HTML. Chunks pulled in at runtime via Vite's preload helper (import()) are not givenintegrityby the browser, and bare ESMimporthas no SRI mechanism of its own (it relies onmodulepreloadlinks covering the graph). Every chunk's hash is still recorded insri-manifest.jsonfor out-of-band verification (CDN, proxy, CSP pipeline), but the browser does not enforce integrity on those runtime fetches. If complete browser-enforced integrity is your goal, prefer fewer, statically-referenced chunks. - No special handling for
@vitejs/plugin-legacy.
License
MIT © Justin Formentin
