@@ -4,71 +4,58 @@ import { promisify } from 'node:util'
4
4
import colors from 'picocolors'
5
5
import type { Plugin } from 'rollup'
6
6
import type { ResolvedConfig } from '../config'
7
- import { normalizePath } from '../utils'
7
+ import { isDefined , normalizePath } from '../utils'
8
8
import { LogLevels } from '../logger'
9
9
10
- const enum WriteType {
11
- JS ,
12
- CSS ,
13
- ASSET ,
14
- HTML ,
15
- SOURCE_MAP
16
- }
17
-
18
- const writeColors = {
19
- [ WriteType . JS ] : colors . cyan ,
20
- [ WriteType . CSS ] : colors . magenta ,
21
- [ WriteType . ASSET ] : colors . green ,
22
- [ WriteType . HTML ] : colors . blue ,
23
- [ WriteType . SOURCE_MAP ] : colors . gray
10
+ const groups = [
11
+ { name : 'Assets' , color : colors . green } ,
12
+ { name : 'CSS' , color : colors . magenta } ,
13
+ { name : 'JS' , color : colors . cyan }
14
+ ]
15
+ type LogEntry = {
16
+ name : string
17
+ group : typeof groups [ number ] [ 'name' ]
18
+ size : number
19
+ compressedSize : number | null
20
+ mapSize : number | null
24
21
}
25
22
26
23
export function buildReporterPlugin ( config : ResolvedConfig ) : Plugin {
27
24
const compress = promisify ( gzip )
28
25
const chunkLimit = config . build . chunkSizeWarningLimit
29
26
30
- function isLarge ( code : string | Uint8Array ) : boolean {
31
- // bail out on particularly large chunks
32
- return code . length / 1000 > chunkLimit
33
- }
34
-
35
- async function getCompressedSize ( code : string | Uint8Array ) : Promise < string > {
36
- if ( config . build . ssr || ! config . build . reportCompressedSize ) {
37
- return ''
38
- }
39
- return ` / gzip: ${ displaySize (
40
- ( await compress ( typeof code === 'string' ? code : Buffer . from ( code ) ) )
41
- . length / 1000
42
- ) } `
43
- }
44
-
45
- function printFileInfo (
46
- filePath : string ,
47
- content : string | Uint8Array ,
48
- type : WriteType ,
49
- maxLength : number ,
50
- outDir = config . build . outDir ,
51
- compressedSize = ''
52
- ) {
53
- outDir =
54
- normalizePath (
55
- path . relative ( config . root , path . resolve ( config . root , outDir ) )
56
- ) + '/'
57
- const kB = content . length / 1000
58
- const sizeColor = kB > chunkLimit ? colors . yellow : colors . dim
59
- config . logger . info (
60
- `${ colors . gray ( colors . white ( colors . dim ( outDir ) ) ) } ${ writeColors [ type ] (
61
- filePath . padEnd ( maxLength + 2 )
62
- ) } ${ sizeColor ( `${ displaySize ( kB ) } ${ compressedSize } ` ) } `
63
- )
64
- }
65
-
66
27
const tty = process . stdout . isTTY && ! process . env . CI
67
28
const shouldLogInfo = LogLevels [ config . logLevel || 'info' ] >= LogLevels . info
68
29
let hasTransformed = false
69
30
let hasRenderedChunk = false
31
+ let hasCompressChunk = false
70
32
let transformedCount = 0
71
33
let chunkCount = 0
34
+ let compressedCount = 0
35
+
36
+ async function getCompressedSize (
37
+ code : string | Uint8Array
38
+ ) : Promise < number | null > {
39
+ if ( config . build . ssr || ! config . build . reportCompressedSize ) {
40
+ return null
41
+ }
42
+ if ( shouldLogInfo && ! hasCompressChunk ) {
43
+ if ( ! tty ) {
44
+ config . logger . info ( 'computing gzip size...' )
45
+ } else {
46
+ writeLine ( 'computing gzip size (0)...' )
47
+ }
48
+ hasCompressChunk = true
49
+ }
50
+ const compressed = await compress (
51
+ typeof code === 'string' ? code : Buffer . from ( code )
52
+ )
53
+ compressedCount ++
54
+ if ( shouldLogInfo && tty ) {
55
+ writeLine ( `computing gzip size (${ compressedCount } )...` )
56
+ }
57
+ return compressed . length
58
+ }
72
59
73
60
const logTransform = throttle ( ( id : string ) => {
74
61
writeLine (
@@ -111,6 +98,7 @@ export function buildReporterPlugin(config: ResolvedConfig): Plugin {
111
98
112
99
renderStart ( ) {
113
100
chunkCount = 0
101
+ compressedCount = 0
114
102
} ,
115
103
116
104
renderChunk ( ) {
@@ -129,78 +117,111 @@ export function buildReporterPlugin(config: ResolvedConfig): Plugin {
129
117
} ,
130
118
131
119
generateBundle ( ) {
132
- if ( shouldLogInfo && tty ) {
133
- process . stdout . clearLine ( 0 )
134
- process . stdout . cursorTo ( 0 )
135
- }
120
+ if ( shouldLogInfo && tty ) clearLine ( )
136
121
} ,
137
122
138
123
async writeBundle ( { dir : outDir } , output ) {
139
124
let hasLargeChunks = false
140
125
141
126
if ( shouldLogInfo ) {
127
+ const entries = (
128
+ await Promise . all (
129
+ Object . values ( output ) . map (
130
+ async ( chunk ) : Promise < LogEntry | null > => {
131
+ if ( chunk . type === 'chunk' ) {
132
+ return {
133
+ name : chunk . fileName ,
134
+ group : 'JS' ,
135
+ size : chunk . code . length ,
136
+ compressedSize : await getCompressedSize ( chunk . code ) ,
137
+ mapSize : chunk . map ? chunk . map . toString ( ) . length : null
138
+ }
139
+ } else {
140
+ if ( chunk . fileName . endsWith ( '.map' ) ) return null
141
+ const isCSS = chunk . fileName . endsWith ( '.css' )
142
+ return {
143
+ name : chunk . fileName ,
144
+ group : isCSS ? 'CSS' : 'Assets' ,
145
+ size : chunk . source . length ,
146
+ mapSize : null , // Rollup doesn't support CSS maps?
147
+ compressedSize : isCSS
148
+ ? await getCompressedSize ( chunk . source )
149
+ : null
150
+ }
151
+ }
152
+ }
153
+ )
154
+ )
155
+ ) . filter ( isDefined )
156
+ if ( tty ) clearLine ( )
157
+
142
158
let longest = 0
143
- for ( const file in output ) {
144
- const l = output [ file ] . fileName . length
145
- if ( l > longest ) longest = l
159
+ let biggestSize = 0
160
+ let biggestMap = 0
161
+ let biggestCompressSize = 0
162
+ for ( const entry of entries ) {
163
+ if ( entry . name . length > longest ) longest = entry . name . length
164
+ if ( entry . size > biggestSize ) biggestSize = entry . size
165
+ if ( entry . mapSize && entry . mapSize > biggestMap ) {
166
+ biggestMap = entry . mapSize
167
+ }
168
+ if (
169
+ entry . compressedSize &&
170
+ entry . compressedSize > biggestCompressSize
171
+ ) {
172
+ biggestCompressSize = entry . compressedSize
173
+ }
146
174
}
147
175
148
- // large chunks are deferred to be logged at the end so they are more
149
- // visible.
150
- const deferredLogs : ( ( ) => void ) [ ] = [ ]
151
-
152
- await Promise . all (
153
- Object . keys ( output ) . map ( async ( file ) => {
154
- const chunk = output [ file ]
155
- if ( chunk . type === 'chunk' ) {
156
- const log = async ( ) => {
157
- printFileInfo (
158
- chunk . fileName ,
159
- chunk . code ,
160
- WriteType . JS ,
161
- longest ,
162
- outDir ,
163
- await getCompressedSize ( chunk . code )
176
+ const sizePad = displaySize ( biggestSize ) . length
177
+ const mapPad = displaySize ( biggestMap ) . length
178
+ const compressPad = displaySize ( biggestCompressSize ) . length
179
+
180
+ const relativeOutDir = normalizePath (
181
+ path . relative (
182
+ config . root ,
183
+ path . resolve ( config . root , outDir ?? config . build . outDir )
184
+ )
185
+ )
186
+ const assetsDir = `${ config . build . assetsDir } /`
187
+
188
+ for ( const group of groups ) {
189
+ const filtered = entries . filter ( ( e ) => e . group === group . name )
190
+ if ( ! filtered . length ) continue
191
+ for ( const entry of filtered . sort ( ( a , z ) => a . size - z . size ) ) {
192
+ const isLarge =
193
+ group . name === 'JS' && entry . size / 1000 > chunkLimit
194
+ if ( isLarge ) hasLargeChunks = true
195
+ const sizeColor = isLarge ? colors . yellow : colors . dim
196
+ let log = colors . dim ( relativeOutDir + '/' )
197
+ log += entry . name . startsWith ( assetsDir )
198
+ ? colors . dim ( assetsDir ) +
199
+ group . color (
200
+ entry . name
201
+ . slice ( assetsDir . length )
202
+ . padEnd ( longest + 2 - assetsDir . length )
164
203
)
165
- if ( chunk . map ) {
166
- printFileInfo (
167
- chunk . fileName + '.map' ,
168
- chunk . map . toString ( ) ,
169
- WriteType . SOURCE_MAP ,
170
- longest ,
171
- outDir
172
- )
173
- }
174
- }
175
- if ( isLarge ( chunk . code ) ) {
176
- hasLargeChunks = true
177
- deferredLogs . push ( log )
178
- } else {
179
- await log ( )
180
- }
181
- } else if ( chunk . source ) {
182
- const isCSS = chunk . fileName . endsWith ( '.css' )
183
- const isMap = chunk . fileName . endsWith ( '.js.map' )
184
- printFileInfo (
185
- chunk . fileName ,
186
- chunk . source ,
187
- isCSS
188
- ? WriteType . CSS
189
- : isMap
190
- ? WriteType . SOURCE_MAP
191
- : WriteType . ASSET ,
192
- longest ,
193
- outDir ,
194
- isCSS ? await getCompressedSize ( chunk . source ) : undefined
204
+ : group . color ( entry . name . padEnd ( longest + 2 ) )
205
+ log += colors . bold (
206
+ sizeColor ( displaySize ( entry . size ) . padStart ( sizePad ) )
207
+ )
208
+ if ( entry . compressedSize ) {
209
+ log += colors . dim (
210
+ ` │ gzip: ${ displaySize ( entry . compressedSize ) . padStart (
211
+ compressPad
212
+ ) } `
195
213
)
196
214
}
197
- } )
198
- )
199
-
200
- await Promise . all ( deferredLogs . map ( ( l ) => l ( ) ) )
215
+ if ( entry . mapSize ) {
216
+ log += colors . dim (
217
+ ` │ map: ${ displaySize ( entry . mapSize ) . padStart ( mapPad ) } `
218
+ )
219
+ }
220
+ config . logger . info ( log )
221
+ }
222
+ }
201
223
} else {
202
- hasLargeChunks = Object . keys ( output ) . some ( ( file ) => {
203
- const chunk = output [ file ]
224
+ hasLargeChunks = Object . values ( output ) . some ( ( chunk ) => {
204
225
return chunk . type === 'chunk' && chunk . code . length / 1000 > chunkLimit
205
226
} )
206
227
}
@@ -225,15 +246,19 @@ export function buildReporterPlugin(config: ResolvedConfig): Plugin {
225
246
}
226
247
227
248
function writeLine ( output : string ) {
228
- process . stdout . clearLine ( 0 )
229
- process . stdout . cursorTo ( 0 )
249
+ clearLine ( )
230
250
if ( output . length < process . stdout . columns ) {
231
251
process . stdout . write ( output )
232
252
} else {
233
253
process . stdout . write ( output . substring ( 0 , process . stdout . columns - 1 ) )
234
254
}
235
255
}
236
256
257
+ function clearLine ( ) {
258
+ process . stdout . clearLine ( 0 )
259
+ process . stdout . cursorTo ( 0 )
260
+ }
261
+
237
262
function throttle ( fn : Function ) {
238
263
let timerHandle : NodeJS . Timeout | null = null
239
264
return ( ...args : any [ ] ) => {
@@ -245,8 +270,8 @@ function throttle(fn: Function) {
245
270
}
246
271
}
247
272
248
- function displaySize ( kB : number ) {
249
- return `${ kB . toLocaleString ( 'en' , {
273
+ function displaySize ( bytes : number ) {
274
+ return `${ ( bytes / 1000 ) . toLocaleString ( 'en' , {
250
275
maximumFractionDigits : 2 ,
251
276
minimumFractionDigits : 2
252
277
} ) } kB`
0 commit comments