Files
taimed/node_modules/grunt-compare-size/tasks/compare-size.js
2025-07-24 17:21:45 +08:00

397 lines
12 KiB
JavaScript

/*
* grunt-compare-size
* https://github.com/rwldrn/grunt-compare-size
*
* Copyright (c) 2012 Rick Waldron <waldron.rick@gmail.com> &
* Richard Gibson <richard.gibson@gmail.com> &
* Corey Frang <gnarf@gnarf.net> &
* Mike Sherov <mike.sherov@gmail.com>
* Licensed under the MIT license.
*/
"use strict";
var _ = require("lodash");
var fs = require("fs");
var exec = require("child_process").exec;
module.exports = function(grunt) {
// Grunt utilities & task-wide assignments
var file, log, verbose, defaultCache, lastrun, helpers;
file = grunt.file;
log = grunt.log;
verbose = grunt.verbose;
defaultCache = ".sizecache.json";
lastrun = " last run";
helpers = {
// Label sequence helper
sorted_labels: function( cache ) {
var tips = cache[""].tips;
// Sort labels: metadata, then branch tips by first add,
// then user entries by first add, then last run
// Then return without metadata
return Object.keys( cache ).sort(function( a, b ) {
var keys = Object.keys( cache );
return ( a ? 1 : 0 ) - ( b ? 1 : 0 ) ||
( a in tips ? 0 : 1 ) - ( b in tips ? 0 : 1 ) ||
( a.charAt(0) === " " ? 1 : 0 ) - ( b.charAt(0) === " " ? 1 : 0 ) ||
keys.indexOf( a ) - keys.indexOf( b );
}).slice( 1 );
},
// Label with optional commit
label: function( label, commit ) {
return label.replace( /^ /, "" ) + ( commit ? " " + ( "@ " + commit )[ "grey" ] : "" );
},
// Color-coded size difference
delta: function( delta, width ) {
var color = "green";
if ( delta > 0 ) {
delta = "+" + delta;
color = "red";
} else if ( !delta ) {
delta = delta === 0 ? "=" : "?";
color = "grey";
}
return _.padStart( delta, width )[ color ];
},
// Size cache helper
get_cache: function( src ) {
var cache, tmp;
try {
cache = fs.existsSync( src ) ? file.readJSON( src ) : undefined;
} catch ( e ) {
verbose.error( e );
}
// Progressively upgrade `cache`, which is one of:
// empty
// {}
// { file: size [,...] }
// { "": { tips: { label: SHA1, ... } }, label: { file: size, ... }, ... }
// { "": { version: 0.4, tips: { label: SHA1, ... } },
// label: { file: { "": size, compressor: size, ... }, ... }, ... }
if ( typeof cache !== "object" ) {
cache = undefined;
}
if ( !cache || !cache[""] ) {
// If promoting cache to dictionary, assume that data are for last run
cache = _.zipObject( [ "", lastrun ], [ { version: 0, tips: {} }, cache ] );
}
if ( !cache[""].version ) {
cache[""].version = 0.4;
_.forEach( cache, function( sizes, label ) {
if ( !label || !sizes ) {
return;
}
// If promoting sizes to dictionary, assume that compressed size data are indicated by suffixes
Object.keys( sizes ).sort().forEach(function( file ) {
var parts = file.split("."),
prefix = parts.shift();
// Append compressed size data to a matching prefix
while ( parts.length ) {
if ( typeof sizes[ prefix ] === "object" ) {
sizes[ prefix ][ parts.join(".") ] = sizes[ file ];
delete sizes[ file ];
return;
}
prefix += "." + parts.shift();
}
// Store uncompressed size data
sizes[ file ] = { "": sizes[ file ] };
});
});
}
return cache;
},
// Files helper.
sizes: function( task, compressors ) {
var sizes = {},
files = file.expand(
{ filter: "isFile" },
task.filesSrc
);
files.forEach(function( src, index ) {
var contents = file.read( src ),
fileSizes = sizes[ src ] = { "": contents.length };
if ( compressors ) {
Object.keys( compressors ).forEach(function( compressor ) {
fileSizes[ compressor ] = compressors[ compressor ]( contents );
});
}
});
return sizes;
},
// git helper.
git_status: function( done ) {
verbose.write( "Running `git branch` command..." );
exec("git branch --no-color --verbose --no-abbrev --contains HEAD", function( err, stdout ) {
var status = {},
matches = /^\* (.+?)\s+([0-9a-f]{8,})/im.exec( stdout );
if ( err || !matches ) {
verbose.error();
done( err || "branch not found" );
} else if ( matches[ 1 ].indexOf(" ") >= 0 ) {
done( "not a branch tip: " + matches[ 2 ] );
} else {
status.branch = matches[ 1 ];
status.head = matches[ 2 ];
exec("git diff --quiet HEAD", function( err ) {
status.changed = !!err;
done( null, status );
});
}
});
}
};
// Load test harness, if there is one
// A hack, but we can't drop it into tasks/ because loadTasks might evaluate the harness first
if ( grunt.file.expand("./harness/harness*").length ) {
helpers.git_status = require("../harness/harness");
}
// Compare size to saved sizes
// Derived and adapted from Corey Frang's original `sizer`
grunt.registerMultiTask( "compare_size", "Compare working size to saved sizes", function() {
var done = this.async(),
compressors = ( this.options() || {} ).compress,
newsizes = helpers.sizes( this, compressors ),
files = Object.keys( newsizes ),
explicitFile = Object.keys( this.flags || {} ).join(":"),
sizecache = grunt.config("compare_size.options.cache") || defaultCache,
cache = helpers.get_cache( sizecache ),
tips = cache[""].tips,
labels = helpers.sorted_labels( cache );
// Explicit comparison file flag
if ( explicitFile && files.indexOf( explicitFile ) < 0 ) {
log.error( "Unknown file: " + explicitFile );
return false;
}
// Obtain the current branch and continue...
helpers.git_status( function( err, status ) {
var key,
prefixes = compressors ? [ "" ].concat( Object.keys( compressors ) ) : [ "" ],
availableWidth = 79,
columns = prefixes.map(function( label ) {
// Ensure width for the label and 6-character sizes, plus a padding space
return Math.max( label.length + 1, 7 );
}),
// Right-align headers
commonHeader = prefixes.map(function( label, i ) {
return _.padStart( i === 0 && compressors ? "raw" : label, columns[ i ] - 1 );
});
if ( err ) {
log.warn( err );
status = {};
}
// Remaining space goes to the file path
columns.push( Math.max( 1, availableWidth -
columns.reduce(function( a, b ) { return a + b; }) ) );
// Compressed display for explicit-file comparison
if ( explicitFile ) {
// Header
log.writetableln( columns, commonHeader.concat("") );
// Raw sizes
log.writetableln( columns, prefixes.map(function( prefix, i ) {
return _.padStart( newsizes[ explicitFile ][ prefix ], columns[ i ] - 1 );
}).concat( explicitFile ) );
// Comparisons
labels.forEach(function( label ) {
var old = cache[ label ] && cache[ label ][ explicitFile ];
log.writetableln( columns, prefixes.map(function( prefix, i ) {
return helpers.delta( old && newsizes[ explicitFile ][ prefix ] - old[ prefix ], columns[ i ] - 1 );
}).concat( helpers.label(label, tips[label]) ) );
});
// Detailed display for all-files comparison
} else {
// Raw sizes
log.writetableln( columns, commonHeader.concat("Sizes") );
files.forEach(function( key ) {
log.writetableln( columns,
prefixes.map(function( prefix, i ) {
return _.padStart( newsizes[ key ][ prefix ], columns[ i ] - 1 );
}).concat( key + "" )
);
});
// Comparisons
labels.forEach(function( label, index ) {
var key, diff, color,
oldsizes = cache[ label ];
// Skip metadata key and empty cache entries
if ( label === "" || !cache[ label ] ) {
return;
}
// Header
log.writeln("");
log.writetableln( columns,
commonHeader.concat( "Compared to " + helpers.label( label, tips[label] ) )
);
// Data
files.forEach(function( key ) {
var old = oldsizes && oldsizes[ key ];
log.writetableln( columns,
prefixes.map(function( prefix, i ) {
return helpers.delta( old && newsizes[ key ][ prefix ] - old[ prefix ], columns[ i ] - 1 );
}).concat( key + "" )
);
});
});
}
// Update "last run" sizes
cache[ lastrun ] = newsizes;
// Remember if we're at a branch tip and the branch name is an available key
if ( status.branch && !status.changed && ( status.branch in tips || !cache[ status.branch ] ) ) {
tips[ status.branch ] = status.head;
cache[ status.branch ] = newsizes;
log.writeln( "\nSaved as: " + status.branch );
}
// Write to file
file.write( sizecache, JSON.stringify( cache ) );
done();
});
// Fail task if errors were logged.
if ( this.errorCount ) {
return false;
}
});
// List saved sizes
grunt.registerTask( "compare_size:list", "List saved sizes", function() {
var sizecache = grunt.config("compare_size.options.cache") || defaultCache,
cache = helpers.get_cache( sizecache ),
tips = cache[""].tips;
helpers.sorted_labels( cache ).forEach(function( label ) {
// Skip the special labels
if ( label && label.charAt( 0 ) !== " " ) {
log.write( label );
if ( label in tips ) {
log.write( " " + ( "@ " + tips[ label ] )[ "grey" ] );
}
log.writeln("");
}
});
});
// Add custom label
grunt.registerTask( "compare_size:add", "Add to saved sizes", function() {
var label,
sizecache = grunt.config("compare_size.options.cache") || defaultCache,
cache = helpers.get_cache( sizecache );
if ( !cache[ lastrun ] ) {
log.error("No size data found");
return false;
}
// Store last run sizes under each label, clearing them as branch heads
for ( label in this.flags ) {
if ( label in cache[""].tips ) {
delete cache[""].tips[ label ];
log.write("(removed branch data) ");
}
cache[ label ] = cache[ lastrun ];
log.writeln( "Last run saved as: " + label );
}
file.write( sizecache, JSON.stringify( cache ) );
});
// Remove custom label
grunt.registerTask( "compare_size:remove", "Remove from saved sizes", function() {
var label,
sizecache = grunt.config("compare_size.options.cache") || defaultCache,
cache = helpers.get_cache( sizecache );
for ( label in this.flags ) {
delete cache[""].tips[ label ];
delete cache[ label ];
log.writeln( "Removed: " + label );
}
file.write( sizecache, JSON.stringify( cache ) );
});
// Empty size cache
grunt.registerTask( "compare_size:prune", "Clear all saved sizes except those specified", function() {
var sizecache = grunt.config("compare_size.options.cache") || defaultCache,
cache = helpers.get_cache( sizecache ),
saved = Object.keys( cache ),
keep = Object.keys( this.flags );
// If preserving anything, include last run
if ( keep.length ) {
keep.push( lastrun );
}
saved.forEach(function( label ) {
if ( !label || keep.indexOf( label ) !== -1 ) {
return;
}
delete cache[""].tips[ label ];
delete cache[ label ];
if ( keep.length ) {
log.writeln( "Removed: " + label );
}
});
if ( Object.keys( cache ).length > 1 ) {
file.write( sizecache, JSON.stringify( cache ) );
} else if ( fs.existsSync( sizecache ) ) {
fs.unlinkSync( sizecache );
}
});
// Backwards compatibility aliases
grunt.registerTask( "compare_size:empty", function() {
grunt.task.run( ["compare_size:prune"].concat(
Object.keys( this.flags )
).join(":") );
});
"list add remove empty".split(" ").forEach(function( task ) {
grunt.registerTask( "compare_size_" + task, function() {
grunt.task.run( [ "compare_size:" + task ].concat(
Object.keys( this.flags )
).join(":") );
});
});
};