Skip to content

Commit b696c24

Browse files
authored
Fix pre-compressed assets not loading (#5386)
2 parents 1404d2c + c615b5a commit b696c24

File tree

2 files changed

+72
-76
lines changed

2 files changed

+72
-76
lines changed

crates/templates/src/functions.rs

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,7 @@ impl Object for IncludeAsset {
505505
if tracker.mark_included(&src) {
506506
writeln!(
507507
output,
508-
r#"<script type="module" src="{src}" crossorigin{integrity}></script>"#
508+
r#"<script type="module" src="{src}" crossorigin="anonymous"{integrity}></script>"#
509509
)
510510
.unwrap();
511511
}
@@ -516,22 +516,17 @@ impl Object for IncludeAsset {
516516
if tracker.mark_included(&src) {
517517
writeln!(
518518
output,
519-
r#"<link rel="stylesheet" href="{src}" crossorigin{integrity} />"#
519+
r#"<link rel="stylesheet" href="{src}" crossorigin="anonymous"{integrity} />"#
520520
)
521521
.unwrap();
522522
}
523523
}
524524

525525
mas_spa::FileType::Json => {
526526
// When a JSON is included at the top level (a translation), we preload it
527-
let integrity = main.integrity_attr();
528527
let src = main.src(assets_base);
529528
if tracker.mark_preloaded(&src) {
530-
writeln!(
531-
output,
532-
r#"<link rel="preload" href="{src}" as="fetch" crossorigin{integrity} />"#,
533-
)
534-
.unwrap();
529+
writeln!(output, r#"<link rel="preload" href="{src}" as="fetch" />"#,).unwrap();
535530
}
536531
}
537532

@@ -546,24 +541,25 @@ impl Object for IncludeAsset {
546541
}
547542

548543
for asset in imported {
549-
let integrity = asset.integrity_attr();
550544
let src = asset.src(assets_base);
551545
match asset.file_type() {
552546
mas_spa::FileType::Stylesheet => {
553547
// Imported stylesheets are inserted directly, not just preloaded
554548
if tracker.mark_included(&src) {
549+
let integrity = asset.integrity_attr();
555550
writeln!(
556551
output,
557-
r#"<link rel="stylesheet" href="{src}" crossorigin{integrity} />"#
552+
r#"<link rel="stylesheet" href="{src}" crossorigin="anonymous"{integrity} />"#
558553
)
559554
.unwrap();
560555
}
561556
}
562557
mas_spa::FileType::Script => {
563558
if tracker.mark_preloaded(&src) {
559+
let integrity = asset.integrity_attr();
564560
writeln!(
565561
output,
566-
r#"<link rel="modulepreload" href="{src}" crossorigin{integrity} />"#,
562+
r#"<link rel="modulepreload" href="{src}" crossorigin="anonymous"{integrity} />"#,
567563
)
568564
.unwrap();
569565
}
@@ -572,7 +568,7 @@ impl Object for IncludeAsset {
572568
if tracker.mark_preloaded(&src) {
573569
writeln!(
574570
output,
575-
r#"<link rel="preload" href="{src}" as="image" fetchpriority="low" crossorigin{integrity} />"#,
571+
r#"<link rel="preload" href="{src}" as="image" crossorigin="anonymous" fetchpriority="low" />"#,
576572
)
577573
.unwrap();
578574
}

frontend/vite.config.ts

Lines changed: 64 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@
44
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
55
// Please see LICENSE files in the repository root for full details.
66

7+
import { createWriteStream } from "node:fs";
78
import { type FileHandle, open } from "node:fs/promises";
8-
import { resolve } from "node:path";
9-
import { promisify } from "node:util";
9+
import path, { resolve } from "node:path";
10+
import { Readable } from "node:stream";
11+
import { pipeline } from "node:stream/promises";
1012
import zlib from "node:zlib";
1113
import { tanstackRouter } from "@tanstack/router-plugin/vite";
1214
import react from "@vitejs/plugin-react";
1315
import browserslistToEsbuild from "browserslist-to-esbuild";
1416
import { globSync } from "tinyglobby";
15-
import type { Environment, Manifest, PluginOption } from "vite";
17+
import type { Manifest, PluginOption } from "vite";
1618
import codegen from "vite-plugin-graphql-codegen";
1719
import { defineConfig } from "vitest/config";
1820

@@ -33,54 +35,66 @@ function i18nHotReload(): PluginOption {
3335

3436
// Pre-compress the assets, so that the server can serve them directly
3537
function compression(): PluginOption {
36-
const gzip = promisify(zlib.gzip);
37-
const brotliCompress = promisify(zlib.brotliCompress);
38-
3938
return {
4039
name: "asset-compression",
4140
apply: "build",
4241
enforce: "post",
4342

44-
async generateBundle(_outputOptions, bundle) {
45-
const promises = Object.entries(bundle).flatMap(
46-
([fileName, assetOrChunk]) => {
47-
const source =
48-
assetOrChunk.type === "asset"
49-
? assetOrChunk.source
50-
: assetOrChunk.code;
51-
52-
// Don't compress empty files, only compress CSS, JS and JSON files
53-
if (
54-
!source ||
55-
!(
56-
fileName.endsWith(".js") ||
57-
fileName.endsWith(".css") ||
58-
fileName.endsWith(".json")
59-
)
60-
) {
61-
return [];
62-
}
63-
64-
const uncompressed = Buffer.from(source);
65-
66-
// We pre-compress assets with brotli as it offers the best
67-
// compression ratios compared to even zstd, and gzip as a fallback
68-
return [
69-
{ compress: gzip, ext: "gz" },
70-
{ compress: brotliCompress, ext: "br" },
71-
].map(async ({ compress, ext }) => {
72-
const compressed = await compress(uncompressed);
73-
74-
this.emitFile({
75-
type: "asset",
76-
fileName: `${fileName}.${ext}`,
77-
source: compressed,
43+
writeBundle: {
44+
// We need to run after Vite's plugins, as it will do some final touches
45+
// to the files in this phase
46+
order: "post",
47+
async handler({ dir }, bundle) {
48+
const promises = Object.entries(bundle).flatMap(
49+
([fileName, assetOrChunk]) => {
50+
const source =
51+
assetOrChunk.type === "asset"
52+
? assetOrChunk.source
53+
: assetOrChunk.code;
54+
55+
// Don't compress empty files, only compress CSS, JS and JSON files
56+
if (
57+
!source ||
58+
!(
59+
fileName.endsWith(".js") ||
60+
fileName.endsWith(".css") ||
61+
fileName.endsWith(".json")
62+
)
63+
) {
64+
return [];
65+
}
66+
67+
const uncompressed = Buffer.from(source);
68+
69+
// We pre-compress assets with brotli as it offers the best
70+
// compression ratios compared to even zstd, and gzip as a fallback
71+
return [
72+
{ compressor: zlib.createGzip(), ext: "gz" },
73+
{
74+
compressor: zlib.createBrotliCompress({
75+
params: {
76+
[zlib.constants.BROTLI_PARAM_MODE]:
77+
zlib.constants.BROTLI_MODE_TEXT,
78+
// 10 yields better results and is quicker than 11
79+
[zlib.constants.BROTLI_PARAM_QUALITY]: 10,
80+
[zlib.constants.BROTLI_PARAM_SIZE_HINT]:
81+
uncompressed.length,
82+
},
83+
}),
84+
ext: "br",
85+
},
86+
].map(async ({ compressor, ext }) => {
87+
const output = path.join(dir, `${fileName}.${ext}`);
88+
const readStream = Readable.from(uncompressed);
89+
const writeStream = createWriteStream(output);
90+
91+
await pipeline(readStream, compressor, writeStream);
7892
});
79-
});
80-
},
81-
);
93+
},
94+
);
8295

83-
await Promise.all(promises);
96+
await Promise.all(promises);
97+
},
8498
},
8599
};
86100
}
@@ -95,22 +109,13 @@ declare module "vite" {
95109
// This is needed so that the preloading & asset integrity generation works
96110
// It also calculates integrity hashes for the assets
97111
function augmentManifest(): PluginOption {
98-
// Store a per-environment state, in case the build is run multiple times, like in watch mode
99-
const state = new Map<Environment, Record<string, Promise<string>>>();
100112
return {
101113
name: "augment-manifest",
102114
apply: "build",
103115
enforce: "post",
104116

105-
perEnvironmentStartEndDuringDev: true,
106-
buildStart() {
107-
state.set(this.environment, {});
108-
},
109-
110-
generateBundle(_outputOptions, bundle) {
111-
const envState = state.get(this.environment);
112-
if (!envState) throw new Error("No state for environment");
113-
117+
async writeBundle({ dir }, bundle): Promise<void> {
118+
const hashes: Record<string, Promise<string>> = {};
114119
for (const [fileName, assetOrChunk] of Object.entries(bundle)) {
115120
// Start calculating hash of the asset. We can let that run in the
116121
// background
@@ -119,20 +124,14 @@ function augmentManifest(): PluginOption {
119124
? assetOrChunk.source
120125
: assetOrChunk.code;
121126

122-
envState[fileName] = (async (): Promise<string> => {
127+
hashes[fileName] = (async (): Promise<string> => {
123128
const digest = await crypto.subtle.digest(
124129
"SHA-384",
125130
Buffer.from(source),
126131
);
127132
return `sha384-${Buffer.from(digest).toString("base64")}`;
128133
})();
129134
}
130-
},
131-
132-
async writeBundle({ dir }): Promise<void> {
133-
const envState = state.get(this.environment);
134-
if (!envState) throw new Error("No state for environment");
135-
state.delete(this.environment);
136135

137136
const manifestPath = resolve(dir, "manifest.json");
138137

@@ -152,7 +151,7 @@ function augmentManifest(): PluginOption {
152151

153152
for (const chunk of Object.values(manifest)) {
154153
existing.add(chunk.file);
155-
chunk.integrity = await envState[chunk.file];
154+
chunk.integrity = await hashes[chunk.file];
156155
for (const css of chunk.css ?? []) needs.add(css);
157156
for (const sub of chunk.assets ?? []) needs.add(sub);
158157
}
@@ -162,7 +161,7 @@ function augmentManifest(): PluginOption {
162161
for (const asset of missing) {
163162
manifest[asset] = {
164163
file: asset,
165-
integrity: await envState[asset],
164+
integrity: await hashes[asset],
166165
};
167166
}
168167

@@ -199,6 +198,7 @@ export default defineConfig((env) => ({
199198
sourcemap: true,
200199
target: browserslistToEsbuild(),
201200
cssCodeSplit: true,
201+
reportCompressedSize: false,
202202

203203
rollupOptions: {
204204
// This uses all the files in the src/entrypoints directory as inputs

0 commit comments

Comments
 (0)