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" ;
78import { 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" ;
1012import zlib from "node:zlib" ;
1113import { tanstackRouter } from "@tanstack/router-plugin/vite" ;
1214import react from "@vitejs/plugin-react" ;
1315import browserslistToEsbuild from "browserslist-to-esbuild" ;
1416import { globSync } from "tinyglobby" ;
15- import type { Environment , Manifest , PluginOption } from "vite" ;
17+ import type { Manifest , PluginOption } from "vite" ;
1618import codegen from "vite-plugin-graphql-codegen" ;
1719import { 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
3537function 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
97111function 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