@@ -326,9 +326,21 @@ function holePunch(buf: Buffer, scan: IcuScanResult): HolePunchStats {
326326 let bytesZeroed = 0 ;
327327 let bytesKept = 0 ;
328328
329- for ( const entry of scan . entries ) {
329+ const lastIndex = scan . entries . length - 1 ;
330+
331+ for ( let i = 0 ; i < scan . entries . length ; i += 1 ) {
332+ const entry = scan . entries [ i ] ;
330333 entry . shouldRemove = shouldRemoveEntry ( entry . name ) ;
331334
335+ // Skip the last entry: its size is estimated (no successor to measure
336+ // against) and zeroing it could overwrite bytes outside the ICU blob.
337+ // One skipped entry has negligible impact on compression savings.
338+ if ( i === lastIndex ) {
339+ keptEntries += 1 ;
340+ bytesKept += entry . dataSize ;
341+ continue ;
342+ }
343+
332344 // Clamp data size to not exceed buffer bounds
333345 const safeSize = Math . min ( entry . dataSize , buf . length - entry . dataOffset ) ;
334346 if ( safeSize <= 0 ) {
@@ -394,52 +406,107 @@ export {
394406 shouldRemoveEntry ,
395407 holePunch ,
396408 processBinary ,
409+ formatSize ,
410+ estimateLastEntrySize ,
411+ runCli ,
397412} ;
398- export type { IcuScanResult , IcuEntry , HolePunchStats } ;
413+ export type { IcuScanResult , IcuEntry , HolePunchStats , CliFileResult } ;
399414
400415// --- CLI Entry Point ---
401416
402- function main ( ) : void {
403- const cliArgs = process . argv . slice ( 2 ) ;
404- const isVerbose = cliArgs . includes ( "--verbose" ) || cliArgs . includes ( "-v" ) ;
405- const filePaths = cliArgs . filter ( ( a ) => ! a . startsWith ( "-" ) ) ;
417+ /** Result from a single file processed by the CLI */
418+ type CliFileResult = {
419+ filePath : string ;
420+ status : "no_icu" | "no_removable" | "success" ;
421+ stats ?: HolePunchStats ;
422+ originalSize ?: number ;
423+ } ;
424+
425+ /**
426+ * Run the hole-punch CLI logic.
427+ *
428+ * Extracted from main() so it can be tested in-process without mocking
429+ * process.exit or console output.
430+ *
431+ * @returns Error message string if validation fails, or array of results
432+ */
433+ function runCli (
434+ args : string [ ]
435+ ) : { error : string } | { results : CliFileResult [ ] } {
436+ const filePaths = args . filter ( ( a ) => ! a . startsWith ( "-" ) ) ;
406437
407438 if ( filePaths . length === 0 ) {
408- console . error (
409- "Usage: bun run script/hole-punch.ts [--verbose] <binary-path> ..."
410- ) ;
411- console . error ( "" ) ;
412- console . error (
413- "Reduces compressed binary size by ~24% by zeroing unused ICU data."
414- ) ;
415- console . error ( "Modifies binaries in-place." ) ;
416- process . exit ( 1 ) ;
439+ return {
440+ error :
441+ "Usage: bun run script/hole-punch.ts [--verbose] <binary-path> ..." ,
442+ } ;
417443 }
418444
419445 // Validate all files exist before processing
420446 for ( const filePath of filePaths ) {
421447 if ( ! existsSync ( filePath ) ) {
422- console . error ( `Error: File not found: ${ filePath } ` ) ;
423- process . exit ( 1 ) ;
448+ return { error : `Error: File not found: ${ filePath } ` } ;
424449 }
425450 const stat = statSync ( filePath ) ;
426451 if ( ! stat . isFile ( ) ) {
427- console . error ( `Error: Not a file: ${ filePath } ` ) ;
428- process . exit ( 1 ) ;
452+ return { error : `Error: Not a file: ${ filePath } ` } ;
429453 }
430454 }
431455
456+ const results : CliFileResult [ ] = [ ] ;
457+
432458 for ( const filePath of filePaths ) {
433459 const originalSize = statSync ( filePath ) . size ;
434460 const stats = processBinary ( filePath ) ;
435461
436462 if ( ! stats ) {
437- console . error ( ` Warning: No ICU data found in ${ filePath } , skipping` ) ;
463+ results . push ( { filePath , status : "no_icu" } ) ;
438464 continue ;
439465 }
440466
441467 if ( stats . removedEntries === 0 ) {
442- console . log ( ` ${ filePath } : no removable entries found` ) ;
468+ results . push ( { filePath, status : "no_removable" , stats, originalSize } ) ;
469+ continue ;
470+ }
471+
472+ results . push ( { filePath, status : "success" , stats, originalSize } ) ;
473+ }
474+
475+ return { results } ;
476+ }
477+
478+ function main ( ) : void {
479+ const cliArgs = process . argv . slice ( 2 ) ;
480+ const isVerbose = cliArgs . includes ( "--verbose" ) || cliArgs . includes ( "-v" ) ;
481+ const result = runCli ( cliArgs ) ;
482+
483+ if ( "error" in result ) {
484+ console . error ( result . error ) ;
485+ if ( result . error . startsWith ( "Usage:" ) ) {
486+ console . error ( "" ) ;
487+ console . error (
488+ "Reduces compressed binary size by ~24% by zeroing unused ICU data."
489+ ) ;
490+ console . error ( "Modifies binaries in-place." ) ;
491+ }
492+ process . exit ( 1 ) ;
493+ }
494+
495+ for ( const fileResult of result . results ) {
496+ if ( fileResult . status === "no_icu" ) {
497+ console . error (
498+ ` Warning: No ICU data found in ${ fileResult . filePath } , skipping`
499+ ) ;
500+ continue ;
501+ }
502+
503+ if ( fileResult . status === "no_removable" ) {
504+ console . log ( ` ${ fileResult . filePath } : no removable entries found` ) ;
505+ continue ;
506+ }
507+
508+ const { stats, originalSize, filePath } = fileResult ;
509+ if ( ! stats ) {
443510 continue ;
444511 }
445512
@@ -452,7 +519,7 @@ function main(): void {
452519 ` ${ filePath } : zeroed ${ stats . removedEntries } /${ stats . totalEntries } ICU entries (${ formatSize ( stats . bytesZeroed ) } , ${ pct } % of ICU data)`
453520 ) ;
454521
455- if ( isVerbose ) {
522+ if ( isVerbose && originalSize !== undefined ) {
456523 console . log ( ` Raw size: ${ formatSize ( originalSize ) } (unchanged)` ) ;
457524 console . log ( ` ICU entries kept: ${ stats . keptEntries } ` ) ;
458525 console . log ( ` ICU data kept: ${ formatSize ( stats . bytesKept ) } ` ) ;
0 commit comments