Created
February 16, 2012 21:06
-
-
Save cowboy/1847848 to your computer and use it in GitHub Desktop.
Packify grunt task (in a gist just for now)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * grunt | |
| * https://github.com/cowboy/grunt | |
| * | |
| * Copyright (c) 2012 "Cowboy" Ben Alman | |
| * Licensed under the MIT license. | |
| * http://benalman.com/about/license/ | |
| */ | |
| /*global config:true, task:true*/ | |
| config.init({ | |
| lint: { | |
| all: ['grunt.js'] | |
| }, | |
| min: { | |
| 'dist/project.min.js': ['src/project.js'] | |
| }, | |
| packify: { | |
| 'dist/project.pck.js': ['dist/project.min.js'] | |
| }, | |
| jshint: { | |
| options: { | |
| curly: true, | |
| eqeqeq: true, | |
| immed: true, | |
| latedef: true, | |
| newcap: true, | |
| noarg: true, | |
| sub: true, | |
| undef: true, | |
| boss: true, | |
| eqnull: true, | |
| evil: true | |
| }, | |
| globals: { | |
| file: true, | |
| log: true, | |
| option: true | |
| } | |
| } | |
| }); | |
| // Default task. | |
| task.registerTask('default', 'lint min packify'); | |
| task.registerBasicTask('packify', 'Pack some JavaScript nice and small', function(data, name) { | |
| var files = file.expand(data); | |
| // Concat specified files. | |
| var max = task.helper('concat', files); | |
| // Packify source. | |
| var packified = task.helper('packify', max); | |
| // Write packified source. | |
| file.write(name, packified); | |
| // Fail task if errors were logged. | |
| if (task.hadErrors()) { return false; } | |
| // Otherwise, print a success message.... | |
| log.writeln('File "' + name + '" created.'); | |
| // ...and report some size information. | |
| task.helper('packify_info', packified, max); | |
| }); | |
| task.registerHelper('packify', function(src) { | |
| // Single quotes need to be escaped, so use double-quotes in your input | |
| // source whenever possible. | |
| var script = src.replace(/'/g, "\\'"); | |
| // Replace any non-space whitespace with spaces (shouldn't be necessary). | |
| script = script.replace(/\s+/g, ' '); | |
| // Return number of chars saved by replacing `count` occurences of `string`. | |
| function getSavings(string, count) { | |
| return (string.length - 1) * (count - 1) - 2; | |
| } | |
| // Just trying to keep things DRY here... Let's match some patterns! | |
| function getReMatch(pattern, text) { | |
| var re = new RegExp(pattern.replace(/(\W)/g, '\\$1'), 'g'); | |
| return [text.match(re) || [], re]; | |
| } | |
| var potentials = {}; | |
| var potentialsList = []; | |
| var map = ''; | |
| var i, chunk, matches, savings, re, potential, char; | |
| // Look for recurring patterns between 2 and 20 characters in length (could | |
| // have been between 2 and len / 2, but that gets REALLY slow). | |
| for (var chunkSize = 2, len = script.length; chunkSize <= 20; chunkSize++) { | |
| // Start at the beginning of the input string, go to the end. | |
| for (i = 0; i < len - chunkSize; i++) { | |
| // Grab the "chunk" at the current position. | |
| chunk = script.substr(i, chunkSize); | |
| if (!potentials[chunk]) { | |
| // Find the number of chunk matches in the input script. | |
| matches = getReMatch(chunk, script)[0]; | |
| // If any matches, save this chunk as a potential pattern. By using an | |
| // object instead of an array, we don't have to worry about uniquing | |
| // the array as new potentials will just overwrite previous potentials. | |
| if (getSavings(chunk, matches.length) >= 0) { | |
| potentials[chunk] = matches.length; | |
| } | |
| } | |
| } | |
| } | |
| // Since we'll need to sort the potentials, create an array from the object. | |
| for (i in potentials) { | |
| if (potentials.hasOwnProperty(i)) { | |
| potentialsList.push({pattern: i, count: potentials[i]}); | |
| } | |
| } | |
| // Potentials get sorted first by byte savings, then by # of occurrences | |
| // (favoring smaller count, longer patterns), then lexicographically. | |
| function sortPotentials(a, b) { | |
| return getSavings(b.pattern, b.count) - getSavings(a.pattern, a.count) || | |
| a.count - b.count || | |
| (a.pattern < b.pattern ? -1 : a.pattern > b.pattern ? 1 : 0); | |
| } | |
| // Loop over all the potential patterns, unless we run out of replacement | |
| // chars first. Dealing with 7-bit ASCII, valid replacement chars are 1-31 | |
| // & 127 (excluding ASCII 10 & 13). | |
| for (var charCode = 0; potentialsList.length && charCode < 127; ) { | |
| // Re-calculate match counts. | |
| for (i = 0, len = potentialsList.length; i < len; i++) { | |
| potential = potentialsList[i]; | |
| matches = getReMatch(potential.pattern, script)[0]; | |
| potential.count = matches.length; | |
| } | |
| // Sort the array of potentials such that replacements that will yield the | |
| // highest byte savings come first. | |
| potentialsList.sort(sortPotentials); | |
| // Get the current best potential replacement. | |
| potential = potentialsList.shift(); | |
| // Find all chunk matches in the input string. | |
| chunk = potential.pattern; | |
| matches = getReMatch( chunk, script ); | |
| re = matches[1]; | |
| matches = matches[0]; | |
| // Ensure that replacing this potential pattern still actually saves bytes. | |
| savings = getSavings(chunk, matches.length); | |
| if (savings >= 0) { | |
| // Increment the current replacement character. | |
| charCode = ++charCode === 10 ? 11 : | |
| charCode === 13 ? 14 : | |
| charCode === 32 ? 127 : | |
| charCode; | |
| // Get the replacement char. | |
| char = String.fromCharCode(charCode); | |
| if (option('debug')) { | |
| log.debug(charCode, char, matches.length, chunk, savings); | |
| } | |
| // Replace the pattern with the replacement character. | |
| script = script.replace(re, char); | |
| // Add the char + pattern combo into the map of replacements. | |
| map += char + chunk; | |
| } | |
| } | |
| // For each group of 1 low ASCII char / 1+ regular ASCII chars combo in the | |
| // map string, replace the low ASCII char in the script string with the | |
| // remaining regular ASCII chars, then eval the script string. Using with in | |
| // this manner ensures that the temporary _ var won't be leaked. | |
| var result = "" + | |
| "with({_:'" + script + "'})" + | |
| "'" + map + "'.replace(/.([ -~]+)/g,function(x,y){" + | |
| "_=_.replace(RegExp(x[0],'g'),y)" + | |
| "})," + | |
| "eval(_)"; | |
| // Return the result if successful, otherwise false on failure. | |
| return eval(result.replace('eval(_)', '_')) === src || option('debug') ? | |
| result : false; | |
| }); | |
| // Output some size info about the packified source. | |
| task.registerHelper('packify_info', function(packified, max) { | |
| var color = packified.length < max.length ? 'green' : 'red'; | |
| log.writeln('Uncompressed size: ' + String(max.length)[color] + ' bytes.'); | |
| log.writeln('Packified size: ' + String(packified.length)[color] + ' bytes.'); | |
| }); |
Author
Author
Also, FWIW, I might put this in grunt. If I don't, I'll make it readily available to people. Either way, have fun!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This will use grunt and my packify code to make a small file even smaller (maybe). For example, given this
grunt.jsfile and asrc/project.jsfile: