123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- var debug = require('debug')('ylt:fileMinifier');
- var Q = require('q');
- var UglifyJS = require('uglify-js');
- var CleanCSS = require('clean-css');
- var htmlMinifier = require('html-minifier');
- var FileMinifier = function() {
- function minifyFile(entry) {
- var deferred = Q.defer();
- if (!entry.weightCheck || !entry.weightCheck.bodyBuffer) {
- // No valid file available
- deferred.resolve(entry);
- return deferred.promise;
- }
- var fileSize = entry.weightCheck.uncompressedSize;
- var bodyString = entry.weightCheck.bodyBuffer.toString();
- debug('Let\'s try to optimize %s', entry.url);
- debug('Current file size is %d', fileSize);
- var startTime = Date.now();
- if (entry.isJS && !isKnownAsMinified(entry.url) && !looksAlreadyMinified(bodyString)) {
- debug('File is a JS');
- return minifyJs(bodyString)
- .then(function(newFile) {
- if (!newFile) {
- debug('Optimization didn\'t work');
- return entry;
- }
- var endTime = Date.now();
- var newFileSize = newFile.length;
- debug('JS minification complete for %s', entry.url);
-
- if (gainIsEnough(fileSize, newFileSize)) {
- entry.weightCheck.bodyAfterOptimization = newFile;
- entry.weightCheck.optimized = newFileSize;
- entry.weightCheck.isOptimized = false;
- debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
- }
- return entry;
- })
- .fail(function(err) {
- return entry;
- });
- } else if (entry.isCSS) {
- debug('File is a CSS');
- return minifyCss(entry.weightCheck.bodyBuffer.toString())
- .then(function(newFile) {
- if (!newFile) {
- debug('Optimization didn\'t work');
- return entry;
- }
- var endTime = Date.now();
- debug('CSS minification took %dms', endTime - startTime);
- var newFileSize = newFile.length;
- debug('CSS minification complete for %s', entry.url);
-
- if (gainIsEnough(fileSize, newFileSize)) {
- entry.weightCheck.bodyAfterOptimization = newFile;
- entry.weightCheck.optimized = newFileSize;
- entry.weightCheck.isOptimized = false;
- debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
- }
- return entry;
- })
- .fail(function(err) {
- return entry;
- });
- } else if (entry.isHTML) {
- debug('File is an HTML');
- return minifyHtml(entry.weightCheck.bodyBuffer.toString())
- .then(function(newFile) {
- if (!newFile) {
- debug('Optimization didn\'t work');
- return entry;
- }
- var endTime = Date.now();
- debug('HTML minification took %dms', endTime - startTime);
- var newFileSize = newFile.length;
- debug('HTML minification complete for %s', entry.url);
-
- if (gainIsEnough(fileSize, newFileSize)) {
- entry.weightCheck.bodyAfterOptimization = newFile;
- entry.weightCheck.optimized = newFileSize;
- entry.weightCheck.isOptimized = false;
- debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
- }
- return entry;
- })
- .fail(function(err) {
- return entry;
- });
- } else {
- debug('Not minifiable type or already minified');
- deferred.resolve(entry);
- }
- return deferred.promise;
- }
- // The gain is estimated of enough value if it's over 2KB or over 20%,
- // but it's ignored if is below 400 bytes
- function gainIsEnough(oldWeight, newWeight) {
- var gain = oldWeight - newWeight;
- var ratio = gain / oldWeight;
- return (gain > 2096 || (ratio > 0.2 && gain > 400));
- }
- // Uglify
- function minifyJs(body) {
-
- var deferred = Q.defer();
- var startTime = Date.now();
- var result = UglifyJS.minify(body, {
- // Only do the compression step for smaller files
- // otherwise it can take a very long time compared to the gain
- compress: (body.length < 200*1024)
- });
- var endTime = Date.now();
- debug('Uglify took %dms', endTime - startTime);
- deferred.resolve(result.code);
- return deferred.promise;
- }
- // Clear-css
- function minifyCss(body) {
- var deferred = Q.defer();
- try {
- var result = new CleanCSS({compatibility: 'ie8'}).minify(body);
- deferred.resolve(result.styles);
- } catch(err) {
- deferred.reject(err);
- }
- return deferred.promise;
- }
- // HTMLMinifier
- function minifyHtml(body) {
- var deferred = Q.defer();
- try {
- var result = htmlMinifier.minify(body, {
- collapseWhitespace: true,
- conservativeCollapse: true,
- continueOnParseError: true,
- decodeEntities: true,
- minifyCSS: true,
- minifyJS: true,
- preserveLineBreaks: true,
- removeAttributeQuotes: true,
- removeComments: true,
- removeScriptTypeAttributes: true,
- removeStyleLinkTypeAttributes: true
- });
- deferred.resolve(result);
- } catch(err) {
- deferred.reject(err);
- }
-
- return deferred.promise;
- }
- // Avoid losing time trying to compress some JS libraries known as already compressed
- function isKnownAsMinified(url) {
- var result = false;
- // Twitter
- result = result || /^https?:\/\/platform\.twitter\.com\/widgets\.js/.test(url);
- // Facebook
- result = result || /^https:\/\/connect\.facebook\.net\/[^\/]*\/(sdk|all)\.js/.test(url);
- // Google +1
- result = result || /^https:\/\/apis\.google\.com\/js\/plusone\.js/.test(url);
- // jQuery CDN
- result = result || /^https?:\/\/code\.jquery\.com\/.*\.min.js/.test(url);
- // Google Analytics
- result = result || /^https?:\/\/(www|ssl)\.google-analytics\.com\/(.*)\.js/.test(url);
- if (result === true) {
- debug('This file is known as already minified. Skipping minification: %s', url);
- }
- return result;
- }
- // Avoid losing time trying to compress JS files if they already look minified
- // by counting the number of lines compared to the total size.
- // Less than 2KB per line is suspicious
- function looksAlreadyMinified(code) {
- var linesCount = code.split(/\r\n|\r|\n/).length;
- var linesRatio = code.length / linesCount;
- var looksMinified = linesRatio > 2 * 1024;
-
- debug('Lines ratio is %d bytes per line', Math.round(linesRatio));
- debug(looksMinified ? 'It looks already minified' : 'It doesn\'t look minified');
- return looksMinified;
- }
- function entryTypeCanBeMinified(entry) {
- return entry.isJS || entry.isCSS || entry.isHTML;
- }
- return {
- minifyFile: minifyFile,
- minifyJs: minifyJs,
- minifyCss: minifyCss,
- minifyHtml: minifyHtml,
- gainIsEnough: gainIsEnough,
- entryTypeCanBeMinified: entryTypeCanBeMinified,
- isKnownAsMinified: isKnownAsMinified
- };
- };
- module.exports = new FileMinifier();
|