mirror of
https://github.com/element-hq/matrix-authentication-service.git
synced 2026-09-26 22:29:08 +00:00
Handle pre-compressed assets better (#5370)
This commit is contained in:
@@ -248,9 +248,9 @@ pub fn build_router(
|
||||
mas_config::HttpResource::Assets { path } => {
|
||||
let static_service = ServeDir::new(path)
|
||||
.append_index_html_on_directories(false)
|
||||
// The vite build pre-compresses assets with brotli and gzip
|
||||
.precompressed_br()
|
||||
.precompressed_gzip()
|
||||
.precompressed_deflate();
|
||||
.precompressed_gzip();
|
||||
|
||||
let add_cache_headers = axum::middleware::map_response(
|
||||
async |mut res: Response<ServeFileSystemResponseBody>| {
|
||||
|
||||
Generated
-69
@@ -67,9 +67,7 @@
|
||||
"tinyglobby": "^0.2.15",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "7.3.0",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-graphql-codegen": "^3.7.0",
|
||||
"vite-plugin-manifest-sri": "^0.2.0",
|
||||
"vitest": "^4.0.15"
|
||||
}
|
||||
},
|
||||
@@ -8088,21 +8086,6 @@
|
||||
"url": "https://github.com/sponsors/rawify"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@@ -8275,13 +8258,6 @@
|
||||
"csstype": "^3.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/graphql": {
|
||||
"version": "16.12.0",
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz",
|
||||
@@ -9886,19 +9862,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
|
||||
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/knip": {
|
||||
"version": "5.74.0",
|
||||
"resolved": "https://registry.npmjs.org/knip/-/knip-5.74.0.tgz",
|
||||
@@ -13062,16 +13025,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unixify": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unixify/-/unixify-1.0.0.tgz",
|
||||
@@ -13344,21 +13297,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-compression": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-compression/-/vite-plugin-compression-0.5.1.tgz",
|
||||
"integrity": "sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.2",
|
||||
"debug": "^4.3.3",
|
||||
"fs-extra": "^10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": ">=2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-graphql-codegen": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-graphql-codegen/-/vite-plugin-graphql-codegen-3.7.0.tgz",
|
||||
@@ -13371,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",
|
||||
|
||||
@@ -77,9 +77,7 @@
|
||||
"tinyglobby": "^0.2.15",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "7.3.0",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-graphql-codegen": "^3.7.0",
|
||||
"vite-plugin-manifest-sri": "^0.2.0",
|
||||
"vitest": "^4.0.15"
|
||||
},
|
||||
"msw": {
|
||||
|
||||
+154
-64
@@ -4,16 +4,16 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { type FileHandle, open } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import zlib from "node:zlib";
|
||||
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 compression from "vite-plugin-compression";
|
||||
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 {
|
||||
@@ -31,6 +31,154 @@ function i18nHotReload(): PluginOption {
|
||||
};
|
||||
}
|
||||
|
||||
// Pre-compress the assets, so that the server can serve them directly
|
||||
function compression(): PluginOption {
|
||||
const gzip = promisify(zlib.gzip);
|
||||
const brotliCompress = promisify(zlib.brotliCompress);
|
||||
|
||||
return {
|
||||
name: "asset-compression",
|
||||
apply: "build",
|
||||
enforce: "post",
|
||||
|
||||
async generateBundle(_outputOptions, bundle) {
|
||||
const promises = Object.entries(bundle).flatMap(
|
||||
([fileName, assetOrChunk]) => {
|
||||
const source =
|
||||
assetOrChunk.type === "asset"
|
||||
? assetOrChunk.source
|
||||
: assetOrChunk.code;
|
||||
|
||||
// Don't compress empty files, only compress CSS, JS and JSON files
|
||||
if (
|
||||
!source ||
|
||||
!(
|
||||
fileName.endsWith(".js") ||
|
||||
fileName.endsWith(".css") ||
|
||||
fileName.endsWith(".json")
|
||||
)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const uncompressed = Buffer.from(source);
|
||||
|
||||
// We pre-compress assets with brotli as it offers the best
|
||||
// compression ratios compared to even zstd, and gzip as a fallback
|
||||
return [
|
||||
{ compress: gzip, ext: "gz" },
|
||||
{ compress: brotliCompress, ext: "br" },
|
||||
].map(async ({ compress, ext }) => {
|
||||
const compressed = await compress(uncompressed);
|
||||
|
||||
this.emitFile({
|
||||
type: "asset",
|
||||
fileName: `${fileName}.${ext}`,
|
||||
source: compressed,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
await Promise.all(promises);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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: "./",
|
||||
|
||||
@@ -69,67 +217,9 @@ 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",
|
||||
augmentManifest(),
|
||||
|
||||
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");
|
||||
|
||||
const manifest: Manifest | undefined = await readFile(
|
||||
manifestPath,
|
||||
"utf-8",
|
||||
).then(JSON.parse, () => undefined);
|
||||
|
||||
if (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: "",
|
||||
};
|
||||
}
|
||||
|
||||
await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
manifestSRI(),
|
||||
|
||||
// Pre-compress the assets, so that the server can serve them directly
|
||||
compression({
|
||||
algorithm: "gzip",
|
||||
ext: ".gz",
|
||||
}),
|
||||
compression({
|
||||
algorithm: "brotliCompress",
|
||||
ext: ".br",
|
||||
}),
|
||||
compression({
|
||||
algorithm: "deflate",
|
||||
ext: ".zz",
|
||||
}),
|
||||
compression(),
|
||||
|
||||
i18nHotReload(),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user