@@ -2,8 +2,9 @@ import { stat, readdir } from 'node:fs/promises';
22import path from 'node:path' ;
33import { fileURLToPath } from 'node:url' ;
44
5- const BASE = fileURLToPath ( import . meta. resolve ( `../../out/base` ) ) ;
6- const HEAD = fileURLToPath ( import . meta. resolve ( `../../out/head` ) ) ;
5+ const BASE = fileURLToPath ( import . meta. resolve ( '../../out' ) ) ;
6+ const HEAD = fileURLToPath ( import . meta. resolve ( '../../out' ) ) ;
7+ const UNITS = [ 'B' , 'KB' , 'MB' , 'GB' ] ;
78
89/**
910 * Formats bytes into human-readable format
@@ -14,25 +15,21 @@ const formatBytes = bytes => {
1415 if ( ! bytes ) {
1516 return '0 B' ;
1617 }
17-
18- const units = [ 'B' , 'KB' , 'MB' , 'GB' ] ;
1918 const i = Math . floor ( Math . log ( Math . abs ( bytes ) ) / Math . log ( 1024 ) ) ;
20-
21- return `${ ( bytes / 1024 ** i ) . toFixed ( 2 ) } ${ units [ i ] } ` ;
19+ return `${ ( bytes / Math . pow ( 1024 , i ) ) . toFixed ( 2 ) } ${ UNITS [ i ] } ` ;
2220} ;
2321
2422/**
2523 * Formats the difference between base and head sizes
2624 * @param {number } base - Base file size in bytes
2725 * @param {number } head - Head file size in bytes
28- * @returns {string } Formatted diff string (e.g., "+1.50 KB (+10.00%)")
26+ * @returns {string } Formatted diff string (e.g., "+1.50 KB (+10. 00%)")
2927 */
3028const formatDiff = ( base , head ) => {
3129 const diff = head - base ;
32- const pct = base ? `${ ( ( diff / base ) * 100 ) . toFixed ( 2 ) } %` : 'N/A' ;
3330 const sign = diff > 0 ? '+' : '' ;
34-
35- return `${ sign } ${ formatBytes ( diff ) } (${ sign } ${ pct } )` ;
31+ const percent = base ? ` ${ sign } ${ ( ( diff / base ) * 100 ) . toFixed ( 2 ) } %` : 'N/A' ;
32+ return `${ sign } ${ formatBytes ( diff ) } (${ percent } )` ;
3633} ;
3734
3835/**
@@ -41,66 +38,142 @@ const formatDiff = (base, head) => {
4138 * @returns {Promise<Map<string, number>> } Map of filename to size
4239 */
4340const getDirectoryStats = async dir => {
44- const entries = await readdir ( dir ) ;
45- const stats = await Promise . all (
46- entries . map ( async file => [ file , ( await stat ( path . join ( dir , file ) ) ) . size ] )
41+ const files = await readdir ( dir ) ;
42+ const entries = await Promise . all (
43+ files . map ( async file => [ file , ( await stat ( path . join ( dir , file ) ) ) . size ] )
4744 ) ;
48-
49- return new Map ( stats ) ;
45+ return new Map ( entries ) ;
5046} ;
5147
48+ /**
49+ * Gets the extension of a file
50+ * @param {string } file - Filename
51+ * @returns {string } File extension (without dot)
52+ */
53+ const getExtension = file => file . split ( '.' ) . pop ( ) ;
54+
5255/**
5356 * Gets the base name of a file (without extension)
5457 * @param {string } file - Filename
55- * @returns {string } Base name
58+ * @returns {string } Base filename
5659 */
57- const getBaseName = file => file . replace ( / \. [ ^ . ] + $ / , '' ) ;
60+ const getBaseName = file => file . slice ( 0 , file . lastIndexOf ( '.' ) || file . length ) ;
5861
59- const [ baseStats , headStats ] = await Promise . all (
60- [ BASE , HEAD ] . map ( getDirectoryStats )
61- ) ;
62+ /**
63+ * Generates a table row for a file
64+ * @param {string } file - Filename
65+ * @param {number } [baseSize] - Base size in bytes
66+ * @param {number } [headSize] - Head size in bytes
67+ * @returns {string } Markdown table row
68+ */
69+ const generateRow = ( file , baseSize , headSize ) => {
70+ const baseCol = baseSize != null ? formatBytes ( baseSize ) : '-' ;
71+ const headCol = headSize != null ? formatBytes ( headSize ) : '-' ;
72+
73+ let diffCol = 'Added' ;
74+ if ( baseSize != null && headSize != null ) {
75+ diffCol = formatDiff ( baseSize , headSize ) ;
76+ } else if ( baseSize != null ) {
77+ diffCol = 'Removed' ;
78+ }
6279
63- const allFiles = new Set ( [ ...baseStats . keys ( ) , ...headStats . keys ( ) ] ) ;
80+ return `| \`${ file } \` | ${ baseCol } | ${ headCol } | ${ diffCol } |` ;
81+ } ;
6482
65- // Count occurrences of each base name to identify paired files
66- const baseNameCounts = new Map ( ) ;
67- for ( const file of allFiles ) {
68- const name = getBaseName ( file ) ;
69- baseNameCounts . set ( name , ( baseNameCounts . get ( name ) || 0 ) + 1 ) ;
70- }
83+ /**
84+ * Generates a markdown table
85+ * @param {string[] } files - List of files
86+ * @param {Map<string, number> } baseStats - Base stats map
87+ * @param {Map<string, number> } headStats - Head stats map
88+ * @returns {string } Markdown table
89+ */
90+ const generateTable = ( files , baseStats , headStats ) => {
91+ const header = '| File | Base | Head | Diff |\n|------|------|------|------|' ;
92+ const rows = files . map ( f =>
93+ generateRow ( f , baseStats . get ( f ) , headStats . get ( f ) )
94+ ) ;
95+ return `${ header } \n${ rows . join ( '\n' ) } ` ;
96+ } ;
7197
72- // Find and sort changed files: non-paired first (alphabetically), then paired (alphabetically)
73- const changedFiles = [ ...allFiles ]
74- // .filter(file => baseStats.get(file) !== headStats.get(file))
75- . sort ( ( a , b ) => {
76- const aPaired = baseNameCounts . get ( getBaseName ( a ) ) > 1 ;
77- const bPaired = baseNameCounts . get ( getBaseName ( b ) ) > 1 ;
78- return aPaired === bPaired ? a . localeCompare ( b ) : aPaired - bPaired ;
79- } ) ;
80-
81- if ( changedFiles . length ) {
82- const rows = changedFiles . map ( file => {
83- const baseSize = baseStats . get ( file ) ;
84- const headSize = headStats . get ( file ) ;
85-
86- if ( ! baseSize ) {
87- return `| \`${ file } \` | - | ${ formatBytes ( headSize ) } | Added |` ;
98+ /**
99+ * Wraps content in a details/summary element
100+ * @param {string } summary - Summary text
101+ * @param {string } content - Content to wrap
102+ * @returns {string } Markdown details element
103+ */
104+ const details = ( summary , content ) =>
105+ `<details>\n<summary>${ summary } </summary>\n\n${ content } \n\n</details>` ;
106+
107+ async function main ( ) {
108+ const [ baseStats , headStats ] = await Promise . all (
109+ [ BASE , HEAD ] . map ( getDirectoryStats )
110+ ) ;
111+
112+ const allFiles = Array . from (
113+ new Set ( [ ...baseStats . keys ( ) , ...headStats . keys ( ) ] )
114+ ) ;
115+
116+ if ( ! allFiles . length ) {
117+ return ;
118+ }
119+
120+ // Separate HTML/JS pairs from other files
121+ const pairs = [ ] ;
122+ const other = [ ] ;
123+ const processed = new Set ( ) ;
124+
125+ for ( const file of allFiles ) {
126+ if ( processed . has ( file ) ) {
127+ continue ;
88128 }
89129
90- if ( ! headSize ) {
91- return `| \`${ file } \` | ${ formatBytes ( baseSize ) } | - | Removed |` ;
130+ const basename = getBaseName ( file ) ;
131+ const hasHtml = allFiles . some (
132+ f => getBaseName ( f ) === basename && getExtension ( f ) === 'html'
133+ ) ;
134+ const hasJs = allFiles . some (
135+ f => getBaseName ( f ) === basename && getExtension ( f ) === 'js'
136+ ) ;
137+
138+ if ( hasHtml && hasJs ) {
139+ allFiles
140+ . filter ( f => getBaseName ( f ) === basename )
141+ . forEach ( f => {
142+ pairs . push ( f ) ;
143+ processed . add ( f ) ;
144+ } ) ;
145+ } else {
146+ other . push ( file ) ;
147+ processed . add ( file ) ;
92148 }
149+ }
93150
94- return `| \` ${ file } \` | ${ formatBytes ( baseSize ) } | ${ formatBytes ( headSize ) } | ${ formatDiff ( baseSize , headSize ) } |` ;
95- } ) ;
151+ pairs . sort ( ) ;
152+ other . sort ( ) ;
96153
97- console . log (
98- `
99- ## Web Generator
154+ // Generate report sections
155+ const sections = [ ] ;
100156
101- | File | Base | Head | Diff |
102- |------|------|------|------|
103- ${ rows . join ( '\n' ) }
104- ` . trim ( )
105- ) ;
157+ if ( pairs . length ) {
158+ sections . push (
159+ details (
160+ `HTML/JS Pairs (${ pairs . length } )` ,
161+ generateTable ( pairs , baseStats , headStats )
162+ )
163+ ) ;
164+ }
165+
166+ if ( other . length ) {
167+ sections . push (
168+ details (
169+ `Other Files (${ other . length } )` ,
170+ generateTable ( other , baseStats , headStats )
171+ )
172+ ) ;
173+ }
174+
175+ console . log ( '## Web Generator' ) ;
176+ console . log ( sections . join ( '\n\n' ) ) ;
106177}
178+
179+ main ( ) ;
0 commit comments