Replace vite-plugin-sri with our own plugin

This avoids rewriting the manifest file multiple times, and it's also
slightly more efficient, as it doesn't re-read the assets from disk
This commit is contained in:
Quentin Gliech committed 2025-12-18 16:05:25 +01:00
1 parent b298c89984
commit d574851537
3 files changed
+96 -70

No files matched your search

-8
View File
@@ -68,7 +68,6 @@
"typescript": "^5.9.3",
"vite": "7.3.0",
"vite-plugin-graphql-codegen": "^3.7.0",
"vite-plugin-manifest-sri": "^0.2.0",
"vitest": "^4.0.15"
}
},
@@ -13310,13 +13309,6 @@
"vite": ">=2.7.0 <8.0.0"
}
},
"node_modules/vite-plugin-manifest-sri": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/vite-plugin-manifest-sri/-/vite-plugin-manifest-sri-0.2.0.tgz",
"integrity": "sha512-Zt5jt19xTIJ91LOuQTCtNG7rTFc5OziAjBz2H5NdCGqaOD1nxrWExLhcKW+W4/q8/jOPCg/n5ncYEQmqCxiGQQ==",
"dev": true,
"license": "MIT"
},
"node_modules/vitest": {
"version": "4.0.15",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.15.tgz",
-1
View File
@@ -78,7 +78,6 @@
"typescript": "^5.9.3",
"vite": "7.3.0",
"vite-plugin-graphql-codegen": "^3.7.0",
"vite-plugin-manifest-sri": "^0.2.0",
"vitest": "^4.0.15"
},
"msw": {
+96 -61
View File
@@ -12,9 +12,8 @@ import { tanstackRouter } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react";
import browserslistToEsbuild from "browserslist-to-esbuild";
import { globSync } from "tinyglobby";
import type { Manifest, PluginOption } from "vite";
import type { Environment, Manifest, PluginOption } from "vite";
import codegen from "vite-plugin-graphql-codegen";
import manifestSRI from "vite-plugin-manifest-sri";
import { defineConfig } from "vitest/config";
function i18nHotReload(): PluginOption {
@@ -86,6 +85,100 @@ function compression(): PluginOption {
};
}
declare module "vite" {
interface ManifestChunk {
integrity: string;
}
}
// Custom plugin to make sure that each asset has an entry in the manifest
// This is needed so that the preloading & asset integrity generation works
// It also calculates integrity hashes for the assets
function augmentManifest(): PluginOption {
// Store a per-environment state, in case the build is run multiple times, like in watch mode
const state = new Map<Environment, Record<string, Promise<string>>>();
return {
name: "augment-manifest",
apply: "build",
enforce: "post",
perEnvironmentStartEndDuringDev: true,
buildStart() {
state.set(this.environment, {});
},
generateBundle(_outputOptions, bundle) {
const envState = state.get(this.environment);
if (!envState) throw new Error("No state for environment");
for (const [fileName, assetOrChunk] of Object.entries(bundle)) {
// Start calculating hash of the asset. We can let that run in the
// background
const source =
assetOrChunk.type === "asset"
? assetOrChunk.source
: assetOrChunk.code;
envState[fileName] = (async (): Promise<string> => {
const digest = await crypto.subtle.digest(
"SHA-384",
Buffer.from(source),
);
return `sha384-${Buffer.from(digest).toString("base64")}`;
})();
}
},
async writeBundle({ dir }): Promise<void> {
const envState = state.get(this.environment);
if (!envState) throw new Error("No state for environment");
state.delete(this.environment);
const manifestPath = resolve(dir, "manifest.json");
let manifestHandle: FileHandle;
try {
manifestHandle = await open(manifestPath, "r+");
} catch (error) {
// Manifest does not exist, nothing to do but still warn about
this.warn(`Failed to open manifest at ${manifestPath}: ${error}`);
return;
}
const rawManifest = await manifestHandle.readFile("utf-8");
const manifest = JSON.parse(rawManifest) as Manifest;
const existing: Set<string> = new Set();
const needs: Set<string> = new Set();
for (const chunk of Object.values(manifest)) {
existing.add(chunk.file);
chunk.integrity = await envState[chunk.file];
for (const css of chunk.css ?? []) needs.add(css);
for (const sub of chunk.assets ?? []) needs.add(sub);
}
const missing = Array.from(needs).filter((a) => !existing.has(a));
for (const asset of missing) {
manifest[asset] = {
file: asset,
integrity: await envState[asset],
};
}
// Overwrite the manifest with the augmented entries
// XXX: you'd think that doing `manifestHandle.writeFile` would work, as
// the docs says that it 'overwrites the file if it exists'. Turns out, it
// reuses the previous position from `readFile`, so that would append on
// the existing, so we have to use `write` with an explicit position.
// Truncating the file just in case the output is smaller than before.
await manifestHandle.truncate(0);
await manifestHandle.write(JSON.stringify(manifest, null, 2), 0, "utf-8");
await manifestHandle.close();
},
};
}
export default defineConfig((env) => ({
base: "./",
@@ -124,65 +217,7 @@ export default defineConfig((env) => ({
react(),
// Custom plugin to make sure that each asset has an entry in the manifest
// This is needed so that the preloading & asset integrity generation works
{
name: "manifest-missing-assets",
apply: "build",
enforce: "post",
writeBundle: {
// This needs to be executed sequentially before the manifestSRI plugin
sequential: true,
order: "pre",
async handler({ dir }): Promise<void> {
const manifestPath = resolve(dir, "manifest.json");
let manifestHandle: FileHandle;
try {
manifestHandle = await open(manifestPath, "r+");
} catch (error) {
// Manifest does not exist, nothing to do but still warn about
this.warn(`Failed to open manifest at ${manifestPath}: ${error}`);
return;
}
const rawManifest = await manifestHandle.readFile("utf-8");
const manifest = JSON.parse(rawManifest) as Manifest;
const existing: Set<string> = new Set();
const needs: Set<string> = new Set();
for (const chunk of Object.values(manifest)) {
existing.add(chunk.file);
for (const css of chunk.css ?? []) needs.add(css);
for (const sub of chunk.assets ?? []) needs.add(sub);
}
const missing = Array.from(needs).filter((a) => !existing.has(a));
if (missing.length > 0) {
for (const asset of missing) {
manifest[asset] = {
file: asset,
integrity: "",
};
}
// Overwrite the manifest with the new entries
await manifestHandle.truncate(0);
await manifestHandle.write(
JSON.stringify(manifest, null, 2),
0,
"utf-8",
);
}
await manifestHandle.close();
},
},
},
manifestSRI(),
augmentManifest(),
compression(),