1
1
import { createHash } from "node:crypto" ;
2
2
import { existsSync } from "node:fs" ;
3
- import { access , constants , copyFile , readFile , writeFile } from "node:fs/promises" ;
3
+ import { access , constants , copyFile , readFile , stat , writeFile } from "node:fs/promises" ;
4
4
import { basename , dirname , extname , join } from "node:path/posix" ;
5
5
import type { Config } from "./config.js" ;
6
6
import { CliError , isEnoent } from "./error.js" ;
@@ -12,15 +12,16 @@ import type {MarkdownPage} from "./markdown.js";
12
12
import { parseMarkdown } from "./markdown.js" ;
13
13
import { extractNodeSpecifier } from "./node.js" ;
14
14
import { extractNpmSpecifier , populateNpmCache , resolveNpmImport } from "./npm.js" ;
15
- import { isPathImport , relativePath , resolvePath } from "./path.js" ;
15
+ import { isAssetPath , isPathImport , relativePath , resolvePath } from "./path.js" ;
16
16
import { renderPage } from "./render.js" ;
17
17
import type { Resolvers } from "./resolvers.js" ;
18
18
import { getModuleResolver , getResolvers } from "./resolvers.js" ;
19
19
import { resolveImportPath , resolveStylesheetPath } from "./resolvers.js" ;
20
20
import { bundleStyles , rollupClient } from "./rollup.js" ;
21
21
import { searchIndex } from "./search.js" ;
22
22
import { Telemetry } from "./telemetry.js" ;
23
- import { faint , yellow } from "./tty.js" ;
23
+ import { tree } from "./tree.js" ;
24
+ import { faint , green , magenta , red , yellow } from "./tty.js" ;
24
25
25
26
export interface BuildOptions {
26
27
config : Config ;
@@ -218,15 +219,12 @@ export async function build(
218
219
await effects . writeFile ( alias , contents ) ;
219
220
}
220
221
221
- // Render pages, resolving against content-hashed file names!
222
- for ( const [ sourceFile , { page, resolvers} ] of pages ) {
223
- const sourcePath = join ( root , sourceFile ) ;
224
- const outputPath = join ( dirname ( sourceFile ) , basename ( sourceFile , ".md" ) + ".html" ) ;
222
+ // Wrap the resolvers to apply content-hashed file names.
223
+ for ( const [ sourceFile , page ] of pages ) {
225
224
const path = join ( "/" , dirname ( sourceFile ) , basename ( sourceFile , ".md" ) ) ;
226
- const options = { path, ...config } ;
227
- effects . output . write ( `${ faint ( "render" ) } ${ sourcePath } ${ faint ( "→" ) } ` ) ;
228
- const html = await renderPage ( page , {
229
- ...options ,
225
+ const { resolvers} = page ;
226
+ pages . set ( sourceFile , {
227
+ ...page ,
230
228
resolvers : {
231
229
...resolvers ,
232
230
resolveFile ( specifier ) {
@@ -251,12 +249,86 @@ export async function build(
251
249
}
252
250
}
253
251
} ) ;
252
+ }
253
+
254
+ // Render pages!
255
+ for ( const [ sourceFile , { page, resolvers} ] of pages ) {
256
+ const sourcePath = join ( root , sourceFile ) ;
257
+ const outputPath = join ( dirname ( sourceFile ) , basename ( sourceFile , ".md" ) + ".html" ) ;
258
+ const path = join ( "/" , dirname ( sourceFile ) , basename ( sourceFile , ".md" ) ) ;
259
+ effects . output . write ( `${ faint ( "render" ) } ${ sourcePath } ${ faint ( "→" ) } ` ) ;
260
+ const html = await renderPage ( page , { ...config , path, resolvers} ) ;
254
261
await effects . writeFile ( outputPath , html ) ;
255
262
}
256
263
264
+ // Log page sizes.
265
+ const columnWidth = 12 ;
266
+ effects . logger . log ( "" ) ;
267
+ for ( const [ indent , name , description , node ] of tree ( pages ) ) {
268
+ if ( node . children ) {
269
+ effects . logger . log (
270
+ `${ faint ( indent ) } ${ name } ${ faint ( description ) } ${
271
+ node . depth ? "" : [ "Page" , "Imports" , "Files" ] . map ( ( name ) => name . padStart ( columnWidth ) ) . join ( " " )
272
+ } `
273
+ ) ;
274
+ } else {
275
+ const [ sourceFile , { resolvers} ] = node . data ! ;
276
+ const outputPath = join ( dirname ( sourceFile ) , basename ( sourceFile , ".md" ) + ".html" ) ;
277
+ const path = join ( "/" , dirname ( sourceFile ) , basename ( sourceFile , ".md" ) ) ;
278
+ const resolveOutput = ( name : string ) => join ( config . output , resolvePath ( path , name ) ) ;
279
+ const pageSize = ( await stat ( join ( config . output , outputPath ) ) ) . size ;
280
+ const importSize = await accumulateSize ( resolvers . staticImports , resolvers . resolveImport , resolveOutput ) ;
281
+ const fileSize =
282
+ ( await accumulateSize ( resolvers . files , resolvers . resolveFile , resolveOutput ) ) +
283
+ ( await accumulateSize ( resolvers . assets , resolvers . resolveFile , resolveOutput ) ) +
284
+ ( await accumulateSize ( resolvers . stylesheets , resolvers . resolveStylesheet , resolveOutput ) ) ;
285
+ effects . logger . log (
286
+ `${ faint ( indent ) } ${ name } ${ description } ${ [ pageSize , importSize , fileSize ]
287
+ . map ( ( size ) => formatBytes ( size , columnWidth ) )
288
+ . join ( " " ) } `
289
+ ) ;
290
+ }
291
+ }
292
+ effects . logger . log ( "" ) ;
293
+
257
294
Telemetry . record ( { event : "build" , step : "finish" , pageCount} ) ;
258
295
}
259
296
297
+ async function accumulateSize (
298
+ files : Iterable < string > ,
299
+ resolveFile : ( path : string ) => string ,
300
+ resolveOutput : ( path : string ) => string
301
+ ) : Promise < number > {
302
+ let size = 0 ;
303
+ for ( const file of files ) {
304
+ const fileResolution = resolveFile ( file ) ;
305
+ if ( isAssetPath ( fileResolution ) ) {
306
+ try {
307
+ size += ( await stat ( resolveOutput ( fileResolution ) ) ) . size ;
308
+ } catch {
309
+ // ignore missing file
310
+ }
311
+ }
312
+ }
313
+ return size ;
314
+ }
315
+
316
+ function formatBytes ( size : number , length : number , locale : Intl . LocalesArgument = "en-US" ) : string {
317
+ let color : ( text : string ) => string ;
318
+ let text : string ;
319
+ if ( size < 1e3 ) {
320
+ text = "<1 kB" ;
321
+ color = faint ;
322
+ } else if ( size < 1e6 ) {
323
+ text = ( size / 1e3 ) . toLocaleString ( locale , { maximumFractionDigits : 0 } ) + " kB" ;
324
+ color = green ;
325
+ } else {
326
+ text = ( size / 1e6 ) . toLocaleString ( locale , { minimumFractionDigits : 3 , maximumFractionDigits : 3 } ) + " MB" ;
327
+ color = size < 10e6 ? yellow : size < 50e6 ? magenta : red ;
328
+ }
329
+ return color ( text . padStart ( length ) ) ;
330
+ }
331
+
260
332
export class FileBuildEffects implements BuildEffects {
261
333
private readonly outputRoot : string ;
262
334
readonly logger : Logger ;
0 commit comments