1 import fs from "node:fs/promises";
2 import { promisify } from "node:util";
3 import zlib from "node:zlib";
4 import { exec as nodeExec } from "node:child_process";
5 import chalk from "chalk";
6 import isCleanWorkingDir from "./isCleanWorkingDir.js";
9 const lastRunBranch = " last run";
11 const gzip = promisify( zlib.gzip );
12 const brotli = promisify( zlib.brotliCompress );
13 const exec = promisify( nodeExec );
15 async function getBranchName() {
16 const { stdout } = await exec( "git rev-parse --abbrev-ref HEAD" );
20 async function getCommitHash() {
21 const { stdout } = await exec( "git rev-parse HEAD" );
25 function getBranchHeader( branch, commit ) {
26 let branchHeader = branch.trim();
28 branchHeader = chalk.bold( branchHeader ) + chalk.gray( ` @${ commit }` );
30 branchHeader = chalk.italic( branchHeader );
35 async function getCache( loc ) {
38 const contents = await fs.readFile( loc, "utf8" );
39 cache = JSON.parse( contents );
44 const lastRun = cache[ lastRunBranch ];
45 if ( !lastRun || !lastRun.meta || lastRun.meta.version !== VERSION ) {
46 console.log( "Compare cache version mismatch. Rewriting..." );
52 function cacheResults( results ) {
53 const files = Object.create( null );
54 results.forEach( function( result ) {
55 files[ result.filename ] = {
64 function saveCache( loc, cache ) {
66 // Keep cache readable for manual edits
67 return fs.writeFile( loc, JSON.stringify( cache, null, " " ) + "\n" );
70 function compareSizes( existing, current, padLength ) {
71 if ( typeof current !== "number" ) {
72 return chalk.grey( `${ existing }`.padStart( padLength ) );
74 const delta = current - existing;
76 return chalk.red( `+${ delta }`.padStart( padLength ) );
78 return chalk.green( `${ delta }`.padStart( padLength ) );
81 function sortBranches( a, b ) {
82 if ( a === lastRunBranch ) {
85 if ( b === lastRunBranch ) {
97 export async function compareSize( { cache = ".sizecache.json", files } = {} ) {
98 if ( !files || !files.length ) {
99 throw new Error( "No files specified" );
102 const branch = await getBranchName();
103 const commit = await getCommitHash();
104 const sizeCache = await getCache( cache );
106 let rawPadLength = 0;
109 const results = await Promise.all(
110 files.map( async function( filename ) {
112 let contents = await fs.readFile( filename, "utf8" );
114 // Remove the short SHA and .dirty from comparisons.
115 // The short SHA so commits can be compared against each other
116 // and .dirty to compare with the existing branch during development.
117 const sha = /jQuery v\d+.\d+.\d+(?:-[\w\.]+)?(?:\+slim\.|\+)?(\w+(?:\.dirty)?)?/.exec( contents )[ 1 ];
118 contents = contents.replace( new RegExp( sha, "g" ), "" );
120 const size = Buffer.byteLength( contents, "utf8" );
121 const gzippedSize = ( await gzip( contents ) ).length;
122 const brotlifiedSize = ( await brotli( contents ) ).length;
124 // Add one to give space for the `+` or `-` in the comparison
125 rawPadLength = Math.max( rawPadLength, size.toString().length + 1 );
126 gzPadLength = Math.max( gzPadLength, gzippedSize.toString().length + 1 );
127 brPadLength = Math.max( brPadLength, brotlifiedSize.toString().length + 1 );
129 return { filename, raw: size, gz: gzippedSize, br: brotlifiedSize };
133 const sizeHeader = "raw".padStart( rawPadLength ) +
134 "gz".padStart( gzPadLength + 1 ) +
135 "br".padStart( brPadLength + 1 ) +
138 const sizes = results.map( function( result ) {
139 const rawSize = result.raw.toString().padStart( rawPadLength );
140 const gzSize = result.gz.toString().padStart( gzPadLength );
141 const brSize = result.br.toString().padStart( brPadLength );
142 return `${ rawSize } ${ gzSize } ${ brSize } ${ result.filename }`;
145 const comparisons = Object.keys( sizeCache ).sort( sortBranches ).map( function( branch ) {
146 const meta = sizeCache[ branch ].meta || {};
147 const commit = meta.commit;
149 const files = sizeCache[ branch ].files;
150 const branchSizes = Object.keys( files ).map( function( filename ) {
151 const branchResult = files[ filename ];
152 const compareResult = results.find( function( result ) {
153 return result.filename === filename;
156 const compareRaw = compareSizes( branchResult.raw, compareResult.raw, rawPadLength );
157 const compareGz = compareSizes( branchResult.gz, compareResult.gz, gzPadLength );
158 const compareBr = compareSizes( branchResult.br, compareResult.br, brPadLength );
159 return `${ compareRaw } ${ compareGz } ${ compareBr } ${ filename }`;
163 "", // New line before each branch
164 getBranchHeader( branch, commit ),
171 "", // Opening new line
172 chalk.bold( "Sizes" ),
176 "" // Closing new line
179 console.log( output );
181 // Always save the last run
182 // Save version under last run
183 sizeCache[ lastRunBranch ] = {
184 meta: { version: VERSION },
185 files: cacheResults( results )
188 // Only save cache for the current branch
189 // if the working directory is clean.
190 if ( await isCleanWorkingDir() ) {
191 sizeCache[ branch ] = {
193 files: cacheResults( results )
195 console.log( `Saved cache for ${ branch }.` );
198 await saveCache( cache, sizeCache );