浏览代码

Merge pull request #183 from gmetais/develop

v1.11.0
Gaël Métais 9 年之前
父节点
当前提交
c82c1c192f

+ 1 - 1
.travis.yml

@@ -2,7 +2,7 @@ language: node_js
 sudo: false
 node_js:
   - "6.2"
-  - "0.12"
+  - "4.0"
 env:
   - CXX=g++-4.8
 addons:

文件差异内容过多而无法显示
+ 0 - 0
front/src/css/icons.css


文件差异内容过多而无法显示
+ 0 - 0
front/src/css/main.css


+ 4 - 0
front/src/js/app.js

@@ -82,3 +82,7 @@ yltApp.config(['$routeProvider', '$locationProvider',
     }
 ]);
 
+// Disable debugging https://docs.angularjs.org/guide/production
+yltApp.config(['$compileProvider', function ($compileProvider) {
+    $compileProvider.debugInfoEnabled(false);
+}]);

+ 14 - 6
front/src/js/directives/offendersDirectives.js

@@ -641,6 +641,11 @@
                     withFnResult = /^([^\s\(]+) \((.+:\d+:\d+)\)$/.exec(trace);
                 }
 
+                if (withFnResult === null) {
+                    // Yet another PhantomJS 2 format?
+                    withFnResult = /^([^\s\(]+|global code)@(.+:\d+:\d+)$/.exec(trace);
+                }
+
                 if (withFnResult === null) {
                     // Try the PhantomJS 2 ERROR format
                     withFnResult = /^([^\s\(]+) (http.+:\d+)$/.exec(trace);
@@ -664,12 +669,15 @@
                 var line = fileAndLineSplit[2];
                 var column = fileAndLineSplit[3];
 
-                out.push({
-                    fnName: fnName,
-                    filePath: filePath,
-                    line: line,
-                    column: column
-                });
+                // Filter phantomas code
+                if (filePath.indexOf('phantomjs://') === -1) {
+                    out.push({
+                        fnName: fnName,
+                        filePath: filePath,
+                        line: line,
+                        column: column
+                    });
+                }
             });
 
         } catch(e) {

文件差异内容过多而无法显示
+ 0 - 0
front/src/less/icons.less


+ 66 - 38
front/src/views/rule.html

@@ -30,7 +30,7 @@
         <p>This rule reached the abnormality threshold, which means there is a real problem you should care about.</p>
     </div>
     <div class="offenders" ng-if="rule.policy.hasOffenders">
-        <h3><ng-pluralize count="rule.offendersObj.count" when="{'0': 'No offenders', 'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize></h3>
+        <h3 ng-if="rule.offendersObj.count >= 0"><ng-pluralize count="rule.offendersObj.count" when="{'0': 'No offenders', 'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize></h3>
 
         <div ng-if="rule.offendersObj.list" class="offendersTable">
             <div ng-repeat="offender in rule.offendersObj.list track by $index">
@@ -106,20 +106,14 @@
                         <div class="similarColors checker"><div ng-style="{'background-color': offender.color1, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color1}}</div><div ng-style="{'background-color': offender.color2, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color2}}</div></div>
                     </div>
 
-                    <div ng-if="policyName === 'cssMobileFirst'">
-                        <b>{{offender.query}}</b> for <ng-pluralize count="offender.rules" when="{'one':'1 rule','other':'{} rules'}"></ng-pluralize>
-                        <file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
-                    </div>
-
                     <div ng-if="policyName === 'cssParsingErrors'">
                         <b>{{offender.error}}</b>
                         <file-and-line file="offender.file" line="offender.line" column="offender.column"></file-and-line>
                         <span ng-if="offender.file">(<a href="http://jigsaw.w3.org/css-validator/validator?profile=css3&usermedium=all&warning=no&uri={{offender.file | encodeURIComponent}}" target="_blank">Check on the W3C validator</a>)</span>
                     </div>
 
-                    <div ng-if="policyName === 'cssComplexSelectors' || policyName === 'cssComplexSelectorsByAttribute' || policyName === 'cssImports' || policyName === 'cssUniversalSelectors' || policyName === 'cssRedundantBodySelectors' || policyName === 'cssRedundantChildNodesSelectors'">
-                        <span ng-if="offender.bolded" ng-bind-html="offender.bolded"></span>
-                        <b ng-if="!offender.bolded">{{offender.css}}</b>
+                    <div ng-if="policyName === 'cssImports'">
+                        {{offender.css}}
                         <file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
                     </div>
 
@@ -127,32 +121,6 @@
                         {{offender.rule}} (<b>x{{offender.occurrences}}</b>)
                     </div>
 
-                    <div ng-if="policyName === 'cssDuplicatedProperties'">
-                        Property <b>{{offender.property}}</b> duplicated in <b>{{offender.rule}} { }</b>
-                        <file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
-                    </div>
-
-                    <div ng-if="policyName === 'cssEmptyRules'">
-                        <b>{{offender.css}} { }</b>
-                        <file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
-                    </div>
-
-                    <div ng-if="policyName === 'cssExpressions'">
-                        {{offender.rule}} {{ '{' + offender.property}}: <b>expression(</b>{{offender.expression}}<b>)</b>}
-                        <file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
-                    </div>
-
-                    <div ng-if="policyName === 'cssImportants'">
-                        {{offender.rule}} {{ '{' + offender.property}}: {{offender.value}} <b>!important</b>}
-                        <file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
-                    </div>
-
-                    <div ng-if="policyName === 'cssOldIEFixes'">
-                        <span ng-if="offender.browser"><b>{{offender.browser}} fix:</b></span>
-                        <span ng-bind-html="offender.bolded"></span>
-                        <file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
-                    </div>
-
                     <div ng-if="policyName === 'cssOldPropertyPrefixes'">
                         <b>{{offender.property}} {{offender.message}}</b>
                         <div ng-if="offender.rules.length" ng-click="offender.showMore = !offender.showMore" class="offenderButton">
@@ -173,7 +141,7 @@
                         <url-link url="offender" max-length="100"></url-link>
                     </div>
 
-                    <div ng-if="policyName === 'notFound' || policyName === 'closedConnections' || policyName === 'multipleRequests' || policyName === 'cachingDisabled' || policyName === 'cachingNotSpecified'">
+                    <div ng-if="policyName === 'notFound' || policyName === 'emptyRequests' || policyName === 'closedConnections' || policyName === 'multipleRequests' || policyName === 'cachingDisabled' || policyName === 'cachingNotSpecified'">
                         <url-link url="offender" max-length="100"></url-link>
                     </div>
 
@@ -187,15 +155,64 @@
                         (<ng-pluralize count="offender.requests" when="{'one':'1 request','other':'{} requests'}"></ng-pluralize>)
                     </div>
 
-                    <div ng-if="policyName === 'globalVariables' || policyName === 'jQueryVersionsLoaded'">
+                    <div ng-if="policyName === 'globalVariables' || policyName === 'jQueryVersionsLoaded' || policyName === 'synchronousXHR'">
                         {{offender}}
                     </div>
+                </div>
+            </div>
+        </div>
+
+        <div ng-repeat="(file, fileDetails) in rule.offendersObj.byFile track by $index">
+            <h3>
+                <ng-pluralize count="fileDetails.count" when="{'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize>
+                in
+                <url-link ng-if="file !== 'Inline CSS'" url="file" max-length="80"></url-link>
+                <span ng-if="file === 'Inline CSS'">inline CSS</span>
+            </h3>
+
+            <div class="offendersTable">
+                <div ng-repeat="offender in fileDetails.offenders track by $index">
+                    <div ng-if="policyName === 'cssComplexSelectors' || policyName === 'cssComplexSelectorsByAttribute' || policyName === 'cssUniversalSelectors' || policyName === 'cssRedundantBodySelectors' || policyName === 'cssRedundantChildNodesSelectors'">
+                        <span ng-if="offender.bolded" ng-bind-html="offender.bolded"></span>
+                        <b ng-if="!offender.bolded">{{offender.css}}</b>
+                        <span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
+                    </div>
+
+                    <div ng-if="policyName === 'cssMobileFirst'">
+                        <b>{{offender.query}}</b> for <ng-pluralize count="offender.rules" when="{'one':'1 rule','other':'{} rules'}"></ng-pluralize>
+                        <span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
+                    </div>
+
+                    <div ng-if="policyName === 'cssDuplicatedProperties'">
+                        Property <b>{{offender.property}}</b> duplicated in <b>{{offender.rule}} { }</b>
+                        <span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
+                    </div>
+
+                    <div ng-if="policyName === 'cssEmptyRules'">
+                        <b>{{offender.css}} { }</b>
+                        <span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
+                    </div>
 
+                    <div ng-if="policyName === 'cssExpressions'">
+                        {{offender.rule}} {{ '{' + offender.property}}: <b>expression(</b>{{offender.expression}}<b>)</b>}
+                        <span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
+                    </div>
+
+                    <div ng-if="policyName === 'cssImportants'">
+                        {{offender.rule}} {{ '{' + offender.property}}: {{offender.value}} <b>!important</b>}
+                        <span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
+                    </div>
+
+                    <div ng-if="policyName === 'cssOldIEFixes'">
+                        <span ng-if="offender.browser"><b>{{offender.browser}} fix:</b></span>
+                        <span ng-bind-html="offender.bolded"></span>
+                        <span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
+                    </div>
                 </div>
             </div>
         </div>
 
-        <div ng-if="!rule.offendersObj.list" class="offendersHtml">
+        <div ng-if="!rule.offendersObj.list && !rule.offendersObj.byFile" class="offendersHtml">
             
             <div ng-if="policyName === 'DOMelementMaxDepth'">
                 <dom-tree tree="rule.offendersObj.tree"></dom-tree>
@@ -318,6 +335,17 @@
         </div>
     </div>
 
+    <div ng-if="policyName === 'identicalFiles'">
+        <div ng-repeat="offender in rule.offendersObj.list track by $index">
+            <h4>A file of {{offender.weight | bytes}} is loaded {{offender.urls.length}} times:</h4>
+            <div class="offendersTable">
+                <div ng-repeat="url in offender.urls">
+                    <div><url-link url="url" max-length="100"></url-link></div>
+                </div>
+            </div>
+        </div>
+    </div>
+
     <div ng-if="policyName === 'smallRequests'">
         <div ng-repeat="(type, requests) in rule.offendersObj.list.byType">
             <h3><ng-pluralize count="requests.length" when="{'0': 'small ' + type + ' file', 'one': '1 small ' + type + ' file', 'other': '{} small ' + type + ' files'}"></ng-pluralize></h3>

+ 180 - 219
lib/metadata/policies.js

@@ -241,6 +241,15 @@ var policies = {
             };
         }
     },
+    "synchronousXHR": {
+        "tool": "phantomas",
+        "label": "Synchronous Ajax requests",
+        "message": "<p>Making an XMLHttpRequest with the <i>async</i> option set to <i>false</i> is deprecated due to the negative effect to performances. The browser's main thread needs to stop everything until the response is received.</p>",
+        "isOkThreshold": 0,
+        "isBadThreshold": 1,
+        "isAbnormalThreshold": 1,
+        "hasOffenders": true
+    },
     "consoleMessages": {
         "tool": "phantomas",
         "label": "Console messages",
@@ -267,7 +276,7 @@ var policies = {
     },
     "jQueryVersion": {
         "label": "jQuery version",
-        "message": "<p>Current latest versions of jQuery are 1.12 (with support for old IE versions) and 2.2 (without).</p><p>Each new version of jQuery optimizes performances. Do not keep an old version of jQuery. Updating can sometimes break a few things, but it is generally quite easy to fix them up. So don't hesitate.</p>",
+        "message": "<p>The current latest version of jQuery is 3.0</p><p>Each new version of jQuery optimizes performances. Do not keep an old version of jQuery. Updating can sometimes break a few things, but it is generally quite easy to fix them up. So don't hesitate.</p>",
         "hasOffenders": false,
         "scoreFn": function(data) {
             var differentVersions = data.toolsResults.phantomas.metrics.jQueryVersionsLoaded;
@@ -279,34 +288,31 @@ var policies = {
                 var value = data.toolsResults.phantomas.metrics.jQueryVersion;
                 var score;
 
-                if (value.indexOf('1.12.') === 0 ||
-                    value.indexOf('2.2.') === 0 ||
-                    value.indexOf('1.13.') === 0 ||
-                    value.indexOf('2.3.') === 0 ||
-                    value.indexOf('3.0.') === 0 ||
-                    value.indexOf('3.1.') === 0) {
+                if (value.indexOf('3.0.') === 0 ||
+                    value.indexOf('3.1.') === 0 ||
+                    value.indexOf('3.2.') === 0) {
                     score = 100;
+                } else if (value.indexOf('1.12.') === 0 ||
+                           value.indexOf('2.2.') === 0) {
+                    score = 90;
                 } else if (value.indexOf('1.11.') === 0 ||
                            value.indexOf('2.1.') === 0) {
-                    score = 90;
+                    score = 70;
                 } else if (value.indexOf('1.10.') === 0 ||
                            value.indexOf('2.0.') === 0) {
-                    score = 70;
-                } else if (value.indexOf('1.9.') === 0) {
                     score = 50;
-                } else if (value.indexOf('1.8.') === 0) {
+                } else if (value.indexOf('1.9.') === 0) {
                     score = 40;
-                } else if (value.indexOf('1.7') === 0) {
+                } else if (value.indexOf('1.8.') === 0) {
                     score = 30;
-                } else if (value.indexOf('1.6') === 0) {
+                } else if (value.indexOf('1.7') === 0) {
                     score = 20;
-                } else if (value.indexOf('1.5') === 0) {
+                } else if (value.indexOf('1.6') === 0) {
                     score = 10;
-                } else if (value.indexOf('1.4') === 0) {
-                    score = 0;
-                } else if (value.indexOf('1.3') === 0) {
-                    score = 0;
-                } else if (value.indexOf('1.2') === 0) {
+                } else if (value.indexOf('1.5') === 0 ||
+                           value.indexOf('1.4') === 0 ||
+                           value.indexOf('1.3') === 0 ||
+                           value.indexOf('1.2') === 0) {
                     score = 0;
                 } else {
                     debug('Unknown jQuery version "%s"', value);
@@ -462,40 +468,18 @@ var policies = {
     "cssComplexSelectors": {
         "tool": "phantomas",
         "label": "Complex selectors",
-        "message": "<p>Complex selectors are CSS selectors with 4 or more expressions, like \"#header ul li .foo\".</p><p>They are adding more work for the browser, and this could be avoided by simplifying selectors.</p>",
+        "message": "<p>Complex selectors are CSS selectors with 4 or more expressions, like \"#header ul li .foo\".</p><p>They are adding more work for the browser, and this could be avoided by simplifying selectors. The <a href=\"http://getbem.com\" target=\"_blank\">B.E.M. methodology</a> is an useful way to simplify your CSS.</p>",
         "isOkThreshold": 0,
         "isBadThreshold": 600,
         "isAbnormalThreshold": 2000,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
-            return {
-                count: offenders.length,
-                list: offenders.map(function(offender) {
-                    var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
-                    return splittedOffender;
-                })
-            };
-        }
-    },
-    "cssComplexSelectorsByAttribute": {
-        "tool": "phantomas",
-        "label": "Complex attributes selector",
-        "message": "<p>Complex attributes selectors are one of these:<ul><li>.foo[type*=bar] (contains bar)</li><li>.foo[type^=bar] (starts with bar)</li><li>.foo[type|=bar] (starts with bar or bar-)</li><li>.foo[type$=bar] (ends with bar)</li><li>.foo[type~=bar baz] (bar or baz)</li></ul></p><p>Their matching process needs more CPU and it has a cost on performances.</p>",
-        "isOkThreshold": 0,
-        "isBadThreshold": 75,
-        "isAbnormalThreshold": 150,
-        "hasOffenders": true,
-        "offendersTransformFn": function(offenders) {
-            return {
-                count: offenders.length,
-                list: offenders.map(function(offender) {
-                    var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
-
-                    splittedOffender.bolded = splittedOffender.css.replace(/(\[[^ ]+[~\|\^\$\*]="[^"]+"\])/g, '<b>$1</b>');
+            var parsedOffenders = offenders.map(function(offender) {
+                var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
+                return splittedOffender;
+            });
 
-                    return splittedOffender;
-                })
-            };
+            return offendersHelpers.orderByFile(parsedOffenders);
         }
     },
     "cssColors": {
@@ -590,10 +574,7 @@ var policies = {
         "isAbnormalThreshold": 1000,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
-            return {
-                count: offenders.length,
-                list: offenders
-            };
+            return offendersHelpers.orderByFile(offenders);
         }
     },
     "cssImports": {
@@ -652,29 +633,28 @@ var policies = {
         "isAbnormalThreshold": 120,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
-            return {
-                count: offenders.length,
-                list: offenders.map(function(offender) {
-                    var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
+            var parsedOffenders = offenders.map(function(offender) {
+                var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
 
-                    var parts = /^([^{]+) {([^ ]+): (.+)}$/.exec(splittedOffender.css);
-
-                    if (!parts) {
-                        debug('cssDuplicatedProperties offenders transform function error with "%s"', offender);
-                        return {
-                            parseError: offender
-                        };
-                    }
+                var parts = /^([^{]+) {([^ ]+): (.+)}$/.exec(splittedOffender.css);
 
+                if (!parts) {
+                    debug('cssDuplicatedProperties offenders transform function error with "%s"', offender);
                     return {
-                        property: parts[2],
-                        rule: parts[1],
-                        file: splittedOffender.file,
-                        line: splittedOffender.line,
-                        column: splittedOffender.column
+                        parseError: offender
                     };
-                })
-            };
+                }
+
+                return {
+                    property: parts[2],
+                    rule: parts[1],
+                    file: splittedOffender.file,
+                    line: splittedOffender.line,
+                    column: splittedOffender.column
+                };
+            });
+
+            return offendersHelpers.orderByFile(parsedOffenders);
         }
     },
     "cssEmptyRules": {
@@ -686,14 +666,8 @@ var policies = {
         "isAbnormalThreshold": 100,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
-            return {
-                count: offenders.length,
-                list: offenders.map(function(offender) {
-                    var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
-
-                    return splittedOffender;
-                })
-            };
+            var parsedOffenders = offenders.map(offendersHelpers.cssOffenderPattern);
+            return offendersHelpers.orderByFile(parsedOffenders);
         }
     },
     "cssExpressions": {
@@ -705,30 +679,29 @@ var policies = {
         "isAbnormalThreshold": 20,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
-            return {
-                count: offenders.length,
-                list: offenders.map(function(offender) {
-                    var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
+            var parsedOffenders = offenders.map(function(offender) {
+                var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
 
-                    var parts = /^(.*) {([^ ]+): expression\((.*)\)}$/.exec(splittedOffender.css);
-
-                    if (!parts) {
-                        debug('cssExpressions offenders transform function error with "%s"', offender);
-                        return {
-                            parseError: offender
-                        };
-                    }
+                var parts = /^(.*) {([^ ]+): expression\((.*)\)}$/.exec(splittedOffender.css);
 
+                if (!parts) {
+                    debug('cssExpressions offenders transform function error with "%s"', offender);
                     return {
-                        rule: parts[1],
-                        property: parts[2],
-                        expression: parts[3],
-                        file: splittedOffender.file,
-                        line: splittedOffender.line,
-                        column: splittedOffender.column
+                        parseError: offender
                     };
-                })
-            };
+                }
+
+                return {
+                    rule: parts[1],
+                    property: parts[2],
+                    expression: parts[3],
+                    file: splittedOffender.file,
+                    line: splittedOffender.line,
+                    column: splittedOffender.column
+                };
+            });
+
+            return offendersHelpers.orderByFile(parsedOffenders);
         }
     },
     "cssImportants": {
@@ -740,30 +713,29 @@ var policies = {
         "isAbnormalThreshold": 200,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
-            return {
-                count: offenders.length,
-                list: offenders.map(function(offender) {
-                    var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
-
-                    var parts = /^(.*) {([^ ]+): (.*) ?\!important}$/.exec(splittedOffender.css);
+            var parsedOffenders = offenders.map(function(offender) {
+                var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
 
-                    if (!parts) {
-                        debug('cssImportants offenders transform function error with "%s"', offender);
-                        return {
-                            parseError: offender
-                        };
-                    }
+                var parts = /^(.*) {([^ ]+): (.*) ?\!important}$/.exec(splittedOffender.css);
 
+                if (!parts) {
+                    debug('cssImportants offenders transform function error with "%s"', offender);
                     return {
-                        rule: parts[1],
-                        property: parts[2],
-                        value: parts[3],
-                        file: splittedOffender.file,
-                        line: splittedOffender.line,
-                        column: splittedOffender.column
+                        parseError: offender
                     };
-                })
-            };
+                }
+
+                return {
+                    rule: parts[1],
+                    property: parts[2],
+                    value: parts[3],
+                    file: splittedOffender.file,
+                    line: splittedOffender.line,
+                    column: splittedOffender.column
+                };
+            });
+
+            return offendersHelpers.orderByFile(parsedOffenders);
         }
     },
     "cssOldIEFixes": {
@@ -775,52 +747,51 @@ var policies = {
         "isAbnormalThreshold": 300,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
-            return {
-                count: offenders.length,
-                list: offenders.map(function(offender) {
-                    var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
+            var parsedOffenders = offenders.map(function(offender) {
+                var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
 
-                    var parts = /^([^{]*)( {([^ ]+): (.*)})?$/.exec(splittedOffender.css);
+                var parts = /^([^{]*)( {([^ ]+): (.*)})?$/.exec(splittedOffender.css);
 
-                    if (!parts) {
-                        debug('cssOldIEFixes offenders transform function error with "%s"', offender);
-                        return {
-                            parseError: offender
-                        };
-                    }
+                if (!parts) {
+                    debug('cssOldIEFixes offenders transform function error with "%s"', offender);
+                    return {
+                        parseError: offender
+                    };
+                }
 
-                    var rule = parts[1];
-                    var property = parts[3];
-                    var value = parts[4];
-                    var browser = null;
-
-                    if (rule.indexOf('* html') === 0) {
-                        rule = rule.replace(/^\* html/, '<b>* html</b>');
-                        browser = 'IE6';
-                    } else if (rule.indexOf('html>body') === 0) {
-                        rule = rule.replace(/^html>body/, '<b>html>body</b>');
-                        browser = 'IE6';
-                    } else if (property.indexOf('*') === 0) {
-                        property = '<b>' + property + '</b>';
-                        browser = 'IE7';
-                    } else if (value.match(/\!ie$/)) {
-                        value = value.replace(/\!ie$/, '<b>!ie</b>');
-                        browser = 'IE7';
-                    } else if (property === '-ms-filter') {
-                        property = '<b>-ms-filter</b>';
-                        browser = 'IE9';
-                    } else if (value.indexOf('progid:DXImageTransform.Microsoft') >= 0) {
-                        value = value.replace(/progid:DXImageTransform\.Microsoft/, '<b>progid:DXImageTransform.Microsoft</b>');
-                        browser = 'IE9';
-                    }
+                var rule = parts[1];
+                var property = parts[3];
+                var value = parts[4];
+                var browser = null;
+
+                if (rule.indexOf('* html') === 0) {
+                    rule = rule.replace(/^\* html/, '<b>* html</b>');
+                    browser = 'IE6';
+                } else if (rule.indexOf('html>body') === 0) {
+                    rule = rule.replace(/^html>body/, '<b>html>body</b>');
+                    browser = 'IE6';
+                } else if (property.indexOf('*') === 0) {
+                    property = '<b>' + property + '</b>';
+                    browser = 'IE7';
+                } else if (value.match(/\!ie$/)) {
+                    value = value.replace(/\!ie$/, '<b>!ie</b>');
+                    browser = 'IE7';
+                } else if (property === '-ms-filter') {
+                    property = '<b>-ms-filter</b>';
+                    browser = 'IE9';
+                } else if (value.indexOf('progid:DXImageTransform.Microsoft') >= 0) {
+                    value = value.replace(/progid:DXImageTransform\.Microsoft/, '<b>progid:DXImageTransform.Microsoft</b>');
+                    browser = 'IE9';
+                }
 
-                    var propertyAndValue = (property && value) ? ' {' + property + ': ' + value + '}' : '';
-                    splittedOffender.bolded = rule + propertyAndValue;
-                    splittedOffender.browser = browser;
+                var propertyAndValue = (property && value) ? ' {' + property + ': ' + value + '}' : '';
+                splittedOffender.bolded = rule + propertyAndValue;
+                splittedOffender.browser = browser;
 
-                    return splittedOffender;
-                })
-            };
+                return splittedOffender;
+            });
+
+            return offendersHelpers.orderByFile(parsedOffenders);
         }
     },
     "cssOldPropertyPrefixes": {
@@ -876,24 +847,6 @@ var policies = {
             };
         }
     },
-    "cssUniversalSelectors": {
-        "tool": "phantomas",
-        "label": "Universal selectors",
-        "message": "<p>Universal selectors are the most expensive CSS selectors.</p><p>More informations <a href=\"http://perfectionkills.com/profiling-css-for-fun-and-profit-optimization-notes/\" target=\"_blank\">here</a>.</p>",
-        "isOkThreshold": 0,
-        "isBadThreshold": 50,
-        "isAbnormalThreshold": 150,
-        "hasOffenders": true,
-        "offendersTransformFn": function(offenders) {
-            return {
-                count: offenders.length,
-                list: offenders.map(function(offender) {
-                    var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
-                    return splittedOffender;
-                })
-            };
-        }
-    },
     "cssRedundantBodySelectors": {
         "tool": "phantomas",
         "label": "Redundant body selectors",
@@ -903,54 +856,50 @@ var policies = {
         "isAbnormalThreshold": 200,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
-            return {
-                count: offenders.length,
-                list: offenders.map(function(offender) {
-                    var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
-
-                    splittedOffender.bolded = splittedOffender.css.replace(/body/, '<b>body</b>');
+            var parsedOffenders = offenders.map(function(offender) {
+                var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
+                splittedOffender.bolded = splittedOffender.css.replace(/body/, '<b>body</b>');
+                return splittedOffender;
+            });
 
-                    return splittedOffender;
-                })
-            };
+            return offendersHelpers.orderByFile(parsedOffenders);
         }
     },
     "cssRedundantChildNodesSelectors": {
         "tool": "phantomas",
         "label": "Redundant tags selectors",
-        "message": "<p>Some tags included inside other tags are obvious. For example, when \"ul li\" is specified in a rule, \"ul\" can be removed because the \"li\" element is <b>always</b> inside a \"ul\". Same thing for \"tr td\", \"select option\", ...</p><p>Lowering compexity in CSS selectors can make the page load a little faster.</p>",
+        "message": "<p>Some tags included inside other tags are obvious. For example, when \"ul li\" is specified in a rule, \"ul\" can be removed because the \"li\" tag is nearly always inside an \"ul\" container (the \"ol\" container is quite rare). Same thing for \"tr td\", \"select option\", ...</p><p>Lowering compexity in CSS selectors can make the page load a little faster.</p>",
         "isOkThreshold": 0,
         "isBadThreshold": 60,
         "isAbnormalThreshold": 200,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
-            return {
-                count: offenders.length,
-                list: offenders.map(function(offender) {
-                    var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
+            var parsedOffenders = offenders.map(function(offender) {
+                var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
 
-                    var rule = splittedOffender.css || '';
-                    var redundanters = [
-                        ['ul', 'li'],
-                        ['ol', 'li'],
-                        ['select', 'option'],
-                        ['table', 'tr'],
-                        ['table', 'th'],
-                    ];
+                var rule = splittedOffender.css || '';
+                var redundanters = [
+                    ['ul', 'li'],
+                    ['ol', 'li'],
+                    ['select', 'option'],
+                    ['table', 'tr'],
+                    ['table', 'th'],
+                ];
+
+                redundanters.forEach(function(couple) {
+                    rule = rule.replace(new RegExp('(^| |>)' + couple[0] + '([^ >]*)?([ >]| > )' + couple[1] + '([^\\w-]|$)', 'g'), '$1<b>' + couple[0] + '</b>$2$3<b>' + couple[1] + '</b>$4');
+                });
 
-                    redundanters.forEach(function(couple) {
-                        rule = rule.replace(new RegExp('(^| |>)' + couple[0] + '([^ >]*)?([ >]| > )' + couple[1] + '([^\\w-]|$)', 'g'), '$1<b>' + couple[0] + '</b>$2$3<b>' + couple[1] + '</b>$4');
-                    });
+                splittedOffender.bolded = rule;
 
-                    splittedOffender.bolded = rule;
+                return splittedOffender;
+            });
 
-                    return splittedOffender;
-                })
-            };
+            return offendersHelpers.orderByFile(parsedOffenders);
         }
     },
     "totalWeight": {
-        "tool": "weightChecker",
+        "tool": "redownload",
         "label": "Total weight",
         "message": "<p>The weight is of course very important if you want the page to load fast. Try to stay under 1MB, which is alreay very long to download over a slow connection.</p><p>Please note that Yellow Lab Tools' engine (PhantomJS) is not compatible with image srcset (unless you use a polyfill). This can lead to incorrect page weight.</p>",
         "isOkThreshold": 716800,
@@ -960,7 +909,7 @@ var policies = {
         "unit": 'bytes'
     },
     "imageOptimization": {
-        "tool": "weightChecker",
+        "tool": "redownload",
         "label": "Image optimization",
         "message": "<p>This metric measures the number of bytes that could be saved by optimizing images.</p><p>Image optimization is generally one of the easiest way to reduce a page weight, and as a result, the page load time. Don't use Photoshop or other image editing tools, they're not very good for optimization. Use specialized tools such as <a href=\"https://kraken.io/\" target=\"_blank\">Kraken.io</a> or the excellent <a href=\"https://imageoptim.com/\" target=\"_blank\">ImageOptim</a> on Mac. For SVG images, you can use <a href=\"https://jakearchibald.github.io/svgomg/\" target=\"_blank\">SVGOMG</a></p><p>The tools in use in YellowLabTools are not set to their maximum optimization power (JPEG quality 85), so you might be able to compress even more!</p><p>Please note that Yellow Lab Tools' engine (PhantomJS) is not compatible with image srcset (unless you use a polyfill). This can lead to incorrect page weight.</p>",
         "isOkThreshold": 10240,
@@ -970,7 +919,7 @@ var policies = {
         "unit": 'bytes'
     },
     "gzipCompression": {
-        "tool": "weightChecker",
+        "tool": "redownload",
         "label": "Gzip compression",
         "message": "<p>Measures the number of bytes that could be saved by compressing file transfers.</p><p>Gzip is a powerfull weight reducer and should be enabled on text-based assets in your server's configuration. Note that gzipping small files (< 1 KB) is arguable, and that some assets such as images should not be gzipped as they are already compressed. <a href=\"https://gist.github.com/gmetais/971ce13a1fbeebd88445\" target=\"_blank\">Here</a> is a list of Content-Types that should be gzipped.</p>",
         "isOkThreshold": 5125,
@@ -980,7 +929,7 @@ var policies = {
         "unit": 'bytes'
     },
     "fileMinification": {
-        "tool": "weightChecker",
+        "tool": "redownload",
         "label": "File minification",
         "message": "<p>This is the weight that could be saved if all text resources were correctly minified.</p><p>The tools in use here are <b>UglifyJS</b>, <b>clean-css</b> and <b>HTMLMinifier</b>. These tools are so good that some of your minified files can be marked as unminified. Change your tool it this happens :)</p><p>The gains of minification are generally small, but the impact can be high when these text files are loaded on the critical path.</p>",
         "isOkThreshold": 5125,
@@ -990,7 +939,7 @@ var policies = {
         "unit": 'bytes'
     },
     "totalRequests": {
-        "tool": "weightChecker",
+        "tool": "redownload",
         "label": "Requests number",
         "message": "<p>This is one of the most important performance rule. Every request is slowing down the page loading.</p><p>There are several technics to reduce their number:<ul><li>Concatenate JS files</li><li>Concatenate CSS files</li><li>Embed or inline small JS or CSS files in the HTML</li><li>Create sprites or icon fonts</li><li>Base64 encode small images in HTML or stylesheets</li><li>Use lazyloading for images</li></ul></p>",
         "isOkThreshold": 15,
@@ -1045,17 +994,29 @@ var policies = {
         "isAbnormalThreshold": 20,
         "hasOffenders": true
     },
-    "multipleRequests": {
-        "tool": "phantomas",
-        "label": "Duplicated requests",
-        "message": "<p>This only happens when the asset has no cache and is requested more than once on the same page. Be very careful about it.</p>",
+    "identicalFiles": {
+        "tool": "redownload",
+        "label": "Identical content",
+        "message": "<p>This is the number of requests that could be avoided, because of downloaded files that have the same content but are loaded from different URLs.</p><p>Try to load them from the same URL.</p>",
         "isOkThreshold": 0,
-        "isBadThreshold": 3,
-        "isAbnormalThreshold": 10,
+        "isBadThreshold": 5,
+        "isAbnormalThreshold": 15,
+        "hasOffenders": true,
+        "offendersTransformFn": function(offenders) {
+            return offenders;
+        }
+    },
+    "emptyRequests": {
+        "tool": "redownload",
+        "label": "Empty requests",
+        "message": "<p>List of GET requests that respond with an empty body. These are probably the easiest requests to remove.</p>",
+        "isOkThreshold": 0,
+        "isBadThreshold": 1,
+        "isAbnormalThreshold": 5,
         "hasOffenders": true
     },
     "smallRequests": {
-        "tool": "weightChecker",
+        "tool": "redownload",
         "label": "Small requests",
         "message": "<p>List of all requests that are less than 2 KB. Try to merge them with other files.</p>",
         "isOkThreshold": 4,
@@ -1075,7 +1036,7 @@ var policies = {
     "hiddenImages": {
         "tool": "phantomas",
         "label": "Hidden images",
-        "message": "<p>List of all images that have a display:none property, or one of their parents. These images are loaded by the browser even if they're not visible. You might be able to find a way to lazy-load them, only when they get visible.</p><p>Trackers are an exception, you'd better hide them.</p>",
+        "message": "<p>List of all images that have a display:none property, or one of their parents. These images are loaded by the browser even if they're not visible. You might be able to find a way to lazy-load them, only when they get visible.</p><p>As images displayed in 1x1 pixels tend to be trackers, they are excluded from this rule.</p>",
         "isOkThreshold": 1,
         "isBadThreshold": 12,
         "isAbnormalThreshold": 30,

+ 5 - 5
lib/metadata/scoreProfileGeneric.json

@@ -15,7 +15,8 @@
                 "totalRequests": 5,
                 "domains": 3,
                 "notFound": 3,
-                "multipleRequests": 2,
+                "identicalFiles": 2,
+                "emptyRequests": 3,
                 "smallRequests": 1,
                 "lazyLoadableImagesBelowTheFold": 2,
                 "hiddenImages": 1
@@ -50,6 +51,7 @@
             "policies": {
                 "jsErrors": 1,
                 "documentWriteCalls": 2,
+                "synchronousXHR": 5,
                 "consoleMessages": 0.5,
                 "globalVariables": 0.5
             }
@@ -69,10 +71,9 @@
             "policies": {
                 "cssRules": 2,
                 "cssComplexSelectors": 2,
-                "cssComplexSelectorsByAttribute": 1.5,
                 "cssColors": 0.5,
                 "similarColors": 0.5,
-                "cssBreakpoints": 1,
+                "cssBreakpoints": 0.5,
                 "cssMobileFirst": 1
             }
         },
@@ -88,7 +89,6 @@
                 "cssImportants": 3,
                 "cssOldIEFixes": 1,
                 "cssOldPropertyPrefixes": 1,
-                "cssUniversalSelectors": 1,
                 "cssRedundantBodySelectors": 1,
                 "cssRedundantChildNodesSelectors": 1
             }
@@ -110,7 +110,7 @@
         "domComplexity": 2,
         "domManipulations": 2,
         "scroll": 1,
-        "badJavascript": 1,
+        "badJavascript": 2,
         "jQuery": 1,
         "cssSyntaxError": 1,
         "cssComplexity": 1,

+ 46 - 4
lib/offendersHelpers.js

@@ -113,14 +113,17 @@ var OffendersHelpers = function() {
         if (traceArray) {
             var results = [];
             var parts = null;
+            var obj;
 
             for (var i=0 ; i<traceArray.length ; i++) {
-                parts = /^(([\w$]+) )?\(?([^ ]+):(\d+)\)?$/.exec(traceArray[i]);
+                // Handle the new PhantomJS 2.x syntax
+                parts = /^(([\w$]+)@)?([^ ]+):(\d+):(\d+)$/.exec(traceArray[i]);
 
                 if (parts) {
-                    var obj = {
+                    obj = {
                         file: parts[3],
-                        line: parseInt(parts[4], 10)
+                        line: parseInt(parts[4], 10),
+                        column: parseInt(parts[5], 10)
                     };
 
                     if (parts[2]) {
@@ -128,8 +131,26 @@ var OffendersHelpers = function() {
                     }
 
                     results.push(obj);
+
                 } else {
-                    return null;
+                    // Old syntax
+                    parts = /^(([\w$]+) )?\(?([^ ]+):(\d+)\)?$/.exec(traceArray[i]);
+
+                    if (parts) {
+                        obj = {
+                            file: parts[3],
+                            line: parseInt(parts[4], 10)
+                        };
+
+                        if (parts[2]) {
+                            obj.functionName = parts[2];
+                        }
+
+                        results.push(obj);
+
+                    } else {
+                        return null;
+                    }
                 }
             }
             return results;
@@ -185,6 +206,27 @@ var OffendersHelpers = function() {
         }
     };
 
+    this.orderByFile = function(offenders) {
+        var byFile = {};
+
+        offenders.forEach(function(offender) {
+            var file = offender.file || 'Inline CSS';
+            delete offender.file;
+
+            if (!byFile[file]) {
+                byFile[file] = {
+                    count: 0,
+                    offenders: []
+                };
+            }
+
+            byFile[file].count ++;
+            byFile[file].offenders.push(offender);
+        });
+
+        return {byFile: byFile};
+    };
+
 };
 
 module.exports = new OffendersHelpers();

+ 2 - 2
lib/runner.js

@@ -6,7 +6,7 @@ var jsExecutionTransformer  = require('./tools/jsExecutionTransformer');
 var colorDiff               = require('./tools/colorDiff');
 var mediaQueriesChecker     = require('./tools/mediaQueriesChecker');
 var isHttp2                 = require('./tools/isHttp2');
-var weightChecker           = require('./tools/weightChecker/weightChecker');
+var redownload              = require('./tools/redownload/redownload');
 var rulesChecker            = require('./rulesChecker');
 var scoreCalculator         = require('./scoreCalculator');
 
@@ -38,7 +38,7 @@ var Runner = function(params) {
         data = mediaQueriesChecker.analyzeMediaQueries(data);
 
         // Redownload every file
-        return weightChecker.recheckAllFiles(data);
+        return redownload.recheckAllFiles(data);
 
     })
 

+ 3 - 2
lib/tools/phantomas/custom_modules/core/scopeYLT/scopeYLT.js

@@ -49,7 +49,7 @@ exports.module = function(phantomas) {
                             var err;
                             
                             // Before
-                            if (enabled) {
+                            if (enabled && callbackBefore) {
                                 callbackBefore.apply(this, arguments);
                             }
 
@@ -71,7 +71,8 @@ exports.module = function(phantomas) {
 
                                 // After
                                 if (enabled && callbackAfter) {
-                                    callbackAfter.call(this, result, arguments);
+                                    var args = Array.prototype.slice.call(arguments);
+                                    callbackAfter.apply(this, [result].concat(args));
                                 }
 
                                 if (err) {

+ 30 - 0
lib/tools/phantomas/custom_modules/modules/ajaxReqYLT/ajaxReqYLT.js

@@ -0,0 +1,30 @@
+/**
+ * Analyzes AJAX requests
+ */
+/* global window: true */
+
+exports.version = '0.2.a';
+
+exports.module = function(phantomas) {
+    'use strict';
+
+    phantomas.setMetric('ajaxRequests'); // @desc number of AJAX requests
+    phantomas.setMetric('synchronousXHR'); // @desc number of synchronous 
+
+    phantomas.on('init', function() {
+        phantomas.evaluate(function() {
+            (function(phantomas) {
+                phantomas.spy(window.XMLHttpRequest.prototype, 'open', null, function(result, method, url, async) {
+                    phantomas.incrMetric('ajaxRequests');
+                    phantomas.addOffender('ajaxRequests', '<%s> [%s]', url, method);
+
+                    if (async === false) {
+                        phantomas.incrMetric('synchronousXHR');
+                        phantomas.addOffender('synchronousXHR', url);
+                        phantomas.log('ajaxRequests: synchronous XMLHttpRequest call to <%s>', url);
+                    }
+                }, true);
+            })(window.__phantomas);
+        });
+    });
+};

+ 2 - 0
lib/tools/phantomas/custom_modules/modules/domHiddenYLT/domHiddenYLT.js

@@ -52,6 +52,8 @@ exports.module = function(phantomas) {
 
                                     if (src === '' || src.indexOf('data:image') === 0) continue;
 
+                                    if (images[i].width === 1 && images[i].height === 1) continue;
+
                                     if (!lazyLoadableImages[src]) {
                                         path = phantomas.getDOMPath(images[i]);
 

+ 6 - 4
lib/tools/phantomas/phantomasWrapper.js

@@ -37,19 +37,21 @@ var PhantomasWrapper = function() {
             // Mandatory
             'reporter': 'json:pretty',
             'analyze-css': true,
+            'ignore-ssl-errors': true,
             'skip-modules': [
-                'domHiddenContent', // overriden
+                'ajaxRequests', // overridden
+                'domHiddenContent', // overridden
                 'domMutations', // not compatible with webkit
-                'domQueries', // overriden
+                'domQueries', // overridden
                 'events', // overridden
                 'filmStrip', // not needed
                 'har', // not needed for the moment
                 'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT
                 'jQuery', // overridden
                 'jserrors', // overridden
-                'lazyLoadableImages', //overriden
+                'lazyLoadableImages', //overridden
                 'pageSource', // not needed
-                'windowPerformance' // overriden
+                'windowPerformance' // overridden
             ].join(','),
             'include-dirs': [
                 path.join(__dirname, 'custom_modules/core'),

+ 188 - 0
lib/tools/redownload/contentTypeChecker.js

@@ -0,0 +1,188 @@
+var debug   = require('debug')('ylt:contentTypeChecker');
+var Q       = require('q');
+var isJpg   = require('is-jpg');
+var isPng   = require('is-png');
+var isSvg   = require('is-svg');
+var isGif   = require('is-gif');
+var isWoff  = require('is-woff');
+var isWoff2 = require('is-woff2');
+var isOtf   = require('is-otf');
+var isTtf   = require('is-ttf');
+var isEot   = require('is-eot');
+
+var ContentTypeChecker = function() {
+
+    function checkContentType(entry) {
+        var deferred = Q.defer();
+
+        debug('Entering contentTypeChecker');
+        
+        // Ignore very small files as they are generally tracking pixels
+        if (entry.weightCheck && entry.weightCheck.body && entry.weightCheck.bodySize > 100) {
+            var foundType;
+
+            try {
+                foundType = findContentType(entry.weightCheck.body);
+            
+                if (!entry.contentType || entry.contentType === '') {
+                    if (foundType === null) {
+                        debug('ContentType is empty for file %s', entry.url);
+                    } else {
+                        debug('ContentType is empty for file %s. It should be %s.', entry.url, foundType.mimes[0]);
+                        entry.oldContentType = null;
+                        rewriteContentType(entry, foundType);
+                    }
+                } else {
+                    if (foundType !== null && foundType.mimes.indexOf(entry.contentType) === -1) {
+                        debug('ContentType %s is wrong for %s. It should be %s.', entry.contentType, entry.url, foundType.mimes[0]);
+                        entry.oldContentType = entry.contentType;
+                        rewriteContentType(entry, foundType);
+                    }
+                }
+
+            } catch(err) {
+                debug('Error while analyzing the contentType of %s', entry.url);
+                debug(err);
+            }
+        }
+
+        deferred.resolve(entry);
+
+        return deferred.promise;
+    }
+
+    function findContentType(body) {
+        var buffer = new Buffer(body, 'binary');
+
+        if (isJpg(buffer)) {
+            return contentTypes.jpeg;
+        }
+
+        if (isPng(buffer)) {
+            return contentTypes.png;
+        }
+
+        // https://github.com/sindresorhus/is-svg/issues/7
+        if (/<svg/.test(body) && isSvg(body)) {
+            return contentTypes.svg;
+        }
+
+        if (isGif(buffer)) {
+            return contentTypes.gif;
+        }
+
+        if (isWoff(buffer)) {
+            return contentTypes.woff;
+        }
+
+        if (isWoff2(buffer)) {
+            return contentTypes.woff2;
+        }
+
+        if (isOtf(buffer)) {
+            return contentTypes.otf;
+        }
+
+        if (isTtf(buffer)) {
+            return contentTypes.ttf;
+        }
+
+        if (isEot(buffer)) {
+            return contentTypes.eot;
+        }
+
+        return null;
+    }
+
+
+    function rewriteContentType(entry, contentTypeObj) {
+        delete(entry.isHTML);
+        delete(entry.isXML);
+        delete(entry.isCSS);
+        delete(entry.isJS);
+        delete(entry.isJSON);
+        delete(entry.isImage);
+        delete(entry.isSVG);
+        delete(entry.isVideo);
+        delete(entry.isWebFont);
+        delete(entry.isTTF);
+        delete(entry.isFavicon);
+
+        entry.contentType = contentTypeObj.mimes[0];
+        contentTypeObj.updateFn(entry);
+    }
+
+    var contentTypes = {
+        jpeg: {
+            mimes: ['image/jpeg'],
+            updateFn: function(entry) {
+                entry.type = 'image';
+                entry.isImage = true;
+            }
+        },
+        png: {
+            mimes: ['image/png'],
+            updateFn: function(entry) {
+                entry.type = 'image';
+                entry.isImage = true;
+            }
+        },
+        svg: {
+            mimes: ['image/svg+xml'],
+            updateFn: function(entry) {
+                entry.type = 'image';
+                entry.isImage = true;
+                entry.isSVG = true;
+            }
+        },
+        gif: {
+            mimes: ['image/gif'],
+            updateFn: function(entry) {
+                entry.type = 'image';
+                entry.isImage = true;
+            }
+        },
+        woff: {
+            mimes: ['application/x-font-woff', 'application/font-woff', 'font/woff'],
+            updateFn: function(entry) {
+                entry.type = 'webfont';
+                entry.isWebFont = true;
+            }
+        },
+        woff2: {
+            mimes: ['font/woff2', 'application/x-font-woff2', 'application/font-woff2'],
+            updateFn: function(entry) {
+                entry.type = 'webfont';
+                entry.isWebFont = true;
+            }
+        },
+        otf: {
+            mimes: ['application/x-font-otf', 'font/otf', 'font/opentype', 'application/x-font-opentype'],
+            updateFn: function(entry) {
+                entry.type = 'webfont';
+                entry.isWebFont = true;
+            }
+        },
+        ttf: {
+            mimes: ['application/x-font-ttf', 'font/ttf', 'application/x-font-truetype'],
+            updateFn: function(entry) {
+                entry.type = 'webfont';
+                entry.isWebFont = true;
+            }
+        },
+        eot: {
+            mimes: ['application/vnd.ms-fontobject', 'font/eot'],
+            updateFn: function(entry) {
+                entry.type = 'webfont';
+                entry.isWebFont = true;
+            }
+        }
+    };
+    
+    return {
+        checkContentType: checkContentType,
+        findContentType: findContentType
+    };
+};
+
+module.exports = new ContentTypeChecker();

+ 13 - 4
lib/tools/weightChecker/fileMinifier.js → lib/tools/redownload/fileMinifier.js

@@ -146,7 +146,16 @@ var FileMinifier = function() {
         .delay(1)
         .then(splittedUglifyStep2)
         .delay(1)
-        .then(splittedUglifyStep3)
+        .then(function(ast) {
+            // Only do the compression step for smaller files
+            // otherwise it can take a very long time compared to the gain
+            if (body.length < 200*1024) {
+                return splittedUglifyStep3(ast);
+            } else {
+                debug('Skipping step 3 because the file is too big (%d bytes)!', body.length);
+                return ast;
+            }
+        })
         .delay(1)
         .then(splittedUglifyStep4)
         .delay(1)
@@ -311,13 +320,13 @@ var FileMinifier = function() {
         return result;
     }
 
-    // Avoid loosing tome trying to compress JS files if they alreay look minified
+    // Avoid loosing some trying to compress JS files if they alreay look minified
     // by counting the number of lines compared to the total size.
-    // Less than 1000kb per line is suspicious
+    // 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 > 1024;
+        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');

+ 0 - 0
lib/tools/weightChecker/gzipCompressor.js → lib/tools/redownload/gzipCompressor.js


+ 30 - 32
lib/tools/weightChecker/imageOptimizer.js → lib/tools/redownload/imageOptimizer.js

@@ -1,8 +1,10 @@
-var debug = require('debug')('ylt:imageOptimizer');
-
-var Q           = require('q');
-var Imagemin    = require('imagemin');
-var jpegoptim   = require('imagemin-jpegoptim');
+var debug               = require('debug')('ylt:imageOptimizer');
+var Q                   = require('q');
+var imagemin            = require('imagemin');
+var imageminJpegtran    = require('imagemin-jpegtran');
+var imageminJpegoptim   = require('imagemin-jpegoptim');
+var imageminOptipng     = require('imagemin-optipng');
+var imageminSvgo        = require('imagemin-svgo');
 
 var ImageOptimizer = function() {
 
@@ -34,7 +36,7 @@ var ImageOptimizer = function() {
                     return entry;
                 }
 
-                var newFileSize = newFile.contents.length;
+                var newFileSize = newFile.length;
 
                 debug('JPEG lossless compression complete for %s', entry.url);
                 
@@ -55,11 +57,13 @@ var ImageOptimizer = function() {
                     return entry;
                 }
 
-                var newFileSize = newFile.contents.length;
+                var newFileSize = newFile.length;
 
                 debug('JPEG lossy compression complete for %s', entry.url);
 
-                if (gainIsEnough(fileSize, newFileSize)) {
+                if (entry.weightCheck.lossless && entry.weightCheck.lossless < newFileSize) {
+                    debug('Lossy compression is not as good as lossless compression. Skipping the lossy.');
+                } else if (gainIsEnough(fileSize, newFileSize)) {
                     
                     if (entry.weightCheck.isOptimized !== false || newFileSize < entry.weightCheck.lossless) {
                         entry.weightCheck.optimized = newFileSize;
@@ -90,7 +94,7 @@ var ImageOptimizer = function() {
                     return entry;
                 }
                 
-                var newFileSize = newFile.contents.length;
+                var newFileSize = newFile.length;
 
                 debug('PNG lossless compression complete for %s', entry.url);
                 
@@ -124,12 +128,12 @@ var ImageOptimizer = function() {
                     return entry;
                 }
 
-                var newFileSize = newFile.contents.length;
+                var newFileSize = newFile.length;
 
                 debug('SVG lossless compression complete for %s', entry.url);
                 
                 if (gainIsEnough(fileSize, newFileSize)) {
-                    entry.weightCheck.bodyAfterOptimization = newFile.contents.toString();
+                    entry.weightCheck.bodyAfterOptimization = newFile.toString();
                     entry.weightCheck.lossless = entry.weightCheck.optimized = newFileSize;
                     entry.weightCheck.isOptimized = false;
                     debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
@@ -194,35 +198,29 @@ var ImageOptimizer = function() {
 
         var engine;
         if (type === 'jpeg' && !lossy) {
-            engine = Imagemin.jpegtran();
+            engine = imageminJpegtran({progressive: true});
         } else if (type === 'jpeg' && lossy) {
-            engine = jpegoptim({max: MAX_JPEG_QUALITY});
+            engine = imageminJpegoptim({progressive: true, max: MAX_JPEG_QUALITY});
         } else if (type === 'png' && !lossy) {
-            engine = Imagemin.optipng({optimizationLevel: OPTIPNG_COMPRESSION_LEVEL});
+            engine = imageminOptipng({optimizationLevel: OPTIPNG_COMPRESSION_LEVEL});
         } else if (type === 'svg' && !lossy) {
-            engine = Imagemin.svgo({ plugins: [ { removeUselessDefs: false } ] });
+            engine = imageminSvgo({ plugins: [ { removeUselessDefs: false } ] });
         } else {
             deferred.reject('No optimization engine found for imagemin');
         }
 
-        try {
-
-            new Imagemin()
-                .src(imageBody)
-                .use(engine)
-                .run(function (err, files) {
-                    if (err) {
-                        deferred.reject(err);
-                    } else {
-                        deferred.resolve(files[0]);
-                        var endTime = Date.now();
-                        debug('Optimization for %s took %d ms', type, endTime - startTime);
-                    }
-                });
 
-            } catch(err) {
-                deferred.reject(err);
-            }
+        imagemin.buffer(imageBody, {use: engine})
+
+        .then(function(file) {
+            var endTime = Date.now();
+            debug('Optimization for %s took %d ms', type, endTime - startTime);
+            deferred.resolve(file);
+        })
+
+        .catch(function(err) {
+            deferred.reject(file);
+        });
 
         return deferred.promise;
     }

+ 109 - 14
lib/tools/weightChecker/weightChecker.js → lib/tools/redownload/redownload.js

@@ -4,20 +4,23 @@
  *
  */
 
+/*jshint -W069 */
 
-var debug           = require('debug')('ylt:weightChecker');
-var Q               = require('q');
-var http            = require('http');
-var zlib            = require('zlib');
-var async           = require('async');
-var request         = require('request');
+var debug               = require('debug')('ylt:redownload');
+var Q                   = require('q');
+var http                = require('http');
+var zlib                = require('zlib');
+var async               = require('async');
+var request             = require('request');
+var md5                 = require('md5');
 
-var imageOptimizer  = require('./imageOptimizer');
-var fileMinifier    = require('./fileMinifier');
-var gzipCompressor  = require('./gzipCompressor');
+var imageOptimizer      = require('./imageOptimizer');
+var fileMinifier        = require('./fileMinifier');
+var gzipCompressor      = require('./gzipCompressor');
+var contentTypeChecker  = require('./contentTypeChecker');
 
 
-var WeightChecker = function() {
+var Redownload = function() {
 
     var MAX_PARALLEL_DOWNLOADS = 10;
     var REQUEST_TIMEOUT = 15000; // 15 seconds
@@ -47,6 +50,8 @@ var WeightChecker = function() {
                 
                 redownloadEntry(entry, httpAuth)
 
+                .then(contentTypeChecker.checkContentType)
+
                 .then(imageOptimizer.optimizeImage)
 
                 .then(fileMinifier.minifyFile)
@@ -83,16 +88,32 @@ var WeightChecker = function() {
                 offenders.totalRequests = listRequestsByType(results);
                 metrics.totalRequests = offenders.totalRequests.total;
 
-                // Remove unwanted requests (redirections, about:blank)
+
+                // Remove unwanted responses (redownload failed, about:blank)
                 results = results.filter(function(result) {
-                    return (result !== null && result.weightCheck && result.weightCheck.bodySize > 0);
+                    return (result !== null && result.weightCheck && result.url.indexOf('about:blank') !== 0);
                 });
 
 
+                // Wrong contentType
+                offenders.incorrectContentTypes = listIncorrectContentTypes(results);
+                metrics.incorrectContentTypes = offenders.incorrectContentTypes.length;
+
                 // Total weight
                 offenders.totalWeight = listRequestWeight(results);
                 metrics.totalWeight = offenders.totalWeight.totalWeight;
 
+                // Empty files
+                offenders.emptyRequests = listEmptyRequests(results);
+                metrics.emptyRequests = offenders.emptyRequests.length;
+
+
+                // Now remove unwanted responses (redirections and empty files)
+                results = results.filter(function(result) {
+                    return ((result.status < 300 || result.status >= 400) && result.weightCheck.bodySize > 0);
+                });
+
+
                 // Image compression
                 offenders.imageOptimization = listImageNotOptimized(results);
                 metrics.imageOptimization = offenders.imageOptimization.totalGain;
@@ -109,7 +130,12 @@ var WeightChecker = function() {
                 offenders.smallRequests = listSmallRequests(results);
                 metrics.smallRequests = offenders.smallRequests.total;
 
-                data.toolsResults.weightChecker = {
+                // Detect identical files
+                offenders.identicalFiles = listIdenticalFiles(results);
+                metrics.identicalFiles = offenders.identicalFiles.avoidableRequests;
+
+
+                data.toolsResults.redownload = {
                     metrics: metrics,
                     offenders: offenders
                 };
@@ -121,6 +147,35 @@ var WeightChecker = function() {
         return deferred.promise;
     }
 
+    function listIncorrectContentTypes(requests) {
+        var results = [];
+        
+        requests.forEach(function(req) {
+            if (req.oldContentType || req.oldContentType === null) {
+                results.push({
+                    url: req.url,
+                    current: req.oldContentType,
+                    correct: req.contentType
+                });
+            }
+        });
+
+        return results;
+    }
+
+    function listEmptyRequests(requests) {
+        var results = [];
+        
+        requests.forEach(function(req) {
+            var weight = req.weightCheck.bodySize || req.contentLength;
+
+            if (weight === 0 && req.method === 'GET' && req.status === 200) {
+                results.push(req.url);
+            }
+        });
+
+        return results;
+    }
 
     function listRequestWeight(requests) {
         var results = {
@@ -320,6 +375,45 @@ var WeightChecker = function() {
         return results;
     }
 
+    function listIdenticalFiles(requests) {
+        var hashes = {};
+        var list = [];
+        var avoidableRequestsCount = 0;
+
+        requests.forEach(function(req) {
+            var requestHash = md5(req.weightCheck.body);
+            
+            // Try to exclude tracking pixels
+            if (req.weightCheck.bodySize < 80 && req.type === 'image') {
+                return;
+            }
+
+            if (!hashes[requestHash]) {
+                hashes[requestHash] = {
+                    weight: req.weightCheck.bodySize,
+                    urls: []
+                };
+            }
+            
+            if (hashes[requestHash].urls.indexOf(req.url) === -1) {
+                hashes[requestHash].urls.push(req.url);
+            }
+        });
+
+        for (var hash in hashes) {
+            if (hashes[hash].urls.length > 1) {
+                list.push(hashes[hash]);
+                avoidableRequestsCount += hashes[hash].urls.length - 1;
+            }
+        }
+
+        return {
+            avoidableRequests: avoidableRequestsCount,
+            count: list.length,
+            list: list
+        };
+    }
+
 
     function redownloadEntry(entry, httpAuth) {
         var deferred = Q.defer();
@@ -367,6 +461,7 @@ var WeightChecker = function() {
         // Always add a gzip header before sending, in case the server listens to it
         var reqHeaders = entry.requestHeaders;
         reqHeaders['Accept-Encoding'] = 'gzip, deflate';
+        reqHeaders['Connection'] = 'keep-alive';
 
         var requestOptions = {
             method: entry.method,
@@ -533,4 +628,4 @@ var WeightChecker = function() {
     };
 };
 
-module.exports = new WeightChecker();
+module.exports = new Redownload();

+ 41 - 28
package.json

@@ -1,6 +1,6 @@
 {
   "name": "yellowlabtools",
-  "version": "1.10.1",
+  "version": "1.11.0",
   "description": "Online tool to audit a webpage for performance and front-end quality issues",
   "license": "GPL-2.0",
   "author": {
@@ -16,55 +16,68 @@
     "yellowlabtools": "./bin/cli.js"
   },
   "engines": {
-    "node": ">= 0.12.0"
+    "node": ">= 4.0"
   },
   "main": "./lib/index.js",
   "dependencies": {
-    "angular": "1.5.2",
-    "angular-animate": "1.5.2",
-    "angular-chart.js": "0.9.0",
+    "angular": "1.5.7",
+    "angular-animate": "1.5.7",
+    "angular-chart.js": "0.10.2",
     "angular-local-storage": "0.2.7",
-    "angular-resource": "1.5.2",
-    "angular-route": "1.5.2",
-    "angular-sanitize": "1.5.2",
+    "angular-resource": "1.5.7",
+    "angular-route": "1.5.7",
+    "angular-sanitize": "1.5.7",
     "async": "1.5.2",
-    "body-parser": "1.15.0",
-    "chart.js": "1.0.2",
-    "clean-css": "3.4.10",
+    "body-parser": "1.15.2",
+    "chart.js": "1.1.1",
+    "clean-css": "3.4.18",
     "color-diff": "1.0.0",
-    "compression": "1.6.1",
+    "compression": "1.6.2",
     "cors": "2.7.1",
     "css-mq-parser": "0.0.3",
     "debug": "2.2.0",
     "easyxml": "2.0.1",
-    "express": "4.13.4",
-    "imagemin": "4.0.0",
-    "imagemin-jpegoptim": "4.1.0",
+    "express": "4.14.0",
+    "imagemin": "5.2.2",
+    "imagemin-jpegoptim": "5.0.0",
+    "imagemin-jpegtran": "5.0.2",
+    "imagemin-optipng": "5.1.0",
+    "imagemin-svgo": "5.1.0",
+    "is-eot": "1.0.0",
+    "is-gif": "1.0.0",
     "is-http2": "1.0.4",
+    "is-jpg": "1.0.0",
+    "is-otf": "0.1.2",
+    "is-png": "1.0.0",
+    "is-svg": "2.0.1",
+    "is-ttf": "0.2.2",
+    "is-woff": "1.0.3",
+    "is-woff2": "1.0.0",
     "lwip": "0.0.9",
+    "md5": "2.1.0",
     "meow": "3.7.0",
-    "minimize": "1.8.1",
+    "minimize": "2.0.0",
     "parse-color": "1.0.0",
-    "phantomas": "1.15.1",
-    "ps-node": "0.0.5",
+    "phantomas": "1.16.0",
+    "ps-node": "0.1.2",
     "q": "1.4.1",
-    "request": "2.69.0",
-    "rimraf": "2.5.2",
+    "request": "2.72.0",
+    "rimraf": "2.5.3",
     "temporary": "0.0.8",
     "try-thread-sleep": "1.0.0",
-    "uglify-js": "2.6.2"
+    "uglify-js": "2.7.0"
   },
   "devDependencies": {
     "chai": "~3.5.0",
     "grunt": "~0.4.5",
     "grunt-blanket": "~0.0.10",
     "grunt-contrib-clean": "~1.0.0",
-    "grunt-contrib-concat": "~1.0.0",
+    "grunt-contrib-concat": "~1.0.1",
     "grunt-contrib-copy": "~1.0.0",
     "grunt-contrib-cssmin": "~1.0.1",
-    "grunt-contrib-htmlmin": "~1.1.0",
+    "grunt-contrib-htmlmin": "~1.4.0",
     "grunt-contrib-jshint": "~1.0.0",
-    "grunt-contrib-less": "~1.2.0",
+    "grunt-contrib-less": "~1.3.0",
     "grunt-contrib-uglify": "~1.0.1",
     "grunt-env": "~0.4.4",
     "grunt-express": "~1.4.1",
@@ -72,12 +85,12 @@
     "grunt-inline-angular-templates": "~0.1.5",
     "grunt-line-remover": "~0.0.2",
     "grunt-mocha-test": "~0.12.7",
-    "grunt-replace": "~0.11.0",
+    "grunt-replace": "~1.0.1",
     "grunt-usemin": "~3.1.1",
-    "grunt-webfont": "~1.2.0",
+    "grunt-webfont": "~1.4.0",
     "matchdep": "~1.0.1",
-    "mocha": "~2.4.5",
-    "sinon": "~1.17.3",
+    "mocha": "~2.5.3",
+    "sinon": "~1.17.4",
     "sinon-chai": "~2.8.0"
   },
   "scripts": {

+ 20 - 0
test/core/contentTypeCheckerTest.js

@@ -0,0 +1,20 @@
+var should = require('chai').should();
+var contentTypeChecker = require('../../lib/tools/redownload/contentTypeChecker');
+var fs = require('fs');
+var path = require('path');
+
+describe('contentTypeChecker', function() {
+
+    var jpgImageContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg'));
+    var pngImageContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png'));
+    var svgImageContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
+    var cssFileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-stylesheet.css'));
+    
+    it('detect the right content type', function() {
+        contentTypeChecker.findContentType(jpgImageContent).mimes.should.deep.equal(['image/jpeg']);
+        contentTypeChecker.findContentType(pngImageContent).mimes.should.deep.equal(['image/png']);
+        contentTypeChecker.findContentType(svgImageContent).mimes.should.deep.equal(['image/svg+xml']);
+        should.equal(contentTypeChecker.findContentType(cssFileContent), null);
+    });
+
+});

+ 14 - 12
test/core/customPoliciesTest.js

@@ -184,18 +184,20 @@ describe('customPolicies', function() {
         var versions = {
             '1.2.9': 0,
             '1.4.4': 0,
-            '1.5.0': 10,
-            '1.6.3': 20,
-            '1.7.0': 30,
-            '1.8.3a': 40,
-            '1.9.2': 50,
-            '1.10.1': 70,
-            '2.0.0-rc1': 70,
-            '1.11.1': 90,
-            '2.1.1-beta1': 90,
-            '1.12.1': 100,
-            '2.3.1': 100,
-            '3.1.0': 100
+            '1.5.0': 0,
+            '1.6.3': 10,
+            '1.7.0': 20,
+            '1.8.3a': 30,
+            '1.9.2': 40,
+            '1.10.1': 50,
+            '2.0.0-rc1': 50,
+            '1.11.1': 70,
+            '2.1.1-beta1': 70,
+            '1.12.1': 90,
+            '2.2.1': 90,
+            '3.0.1': 100,
+            '3.1.0': 100,
+            '3.2.1': 100
         };
 
         for (var version in versions) {

+ 1 - 1
test/core/fileMinifierTest.js

@@ -1,5 +1,5 @@
 var should = require('chai').should();
-var fileMinifier = require('../../lib/tools/weightChecker/fileMinifier');
+var fileMinifier = require('../../lib/tools/redownload/fileMinifier');
 var fs = require('fs');
 var path = require('path');
 

+ 2 - 2
test/core/gzipCompressorTest.js

@@ -1,6 +1,6 @@
 var should = require('chai').should();
-var gzipCompressor = require('../../lib/tools/weightChecker/gzipCompressor');
-var fileMinifier = require('../../lib/tools/weightChecker/fileMinifier');
+var gzipCompressor = require('../../lib/tools/redownload/gzipCompressor');
+var fileMinifier = require('../../lib/tools/redownload/fileMinifier');
 var fs = require('fs');
 var path = require('path');
 

+ 7 - 7
test/core/imageOptimizerTest.js

@@ -1,5 +1,5 @@
 var should = require('chai').should();
-var imageOptimizer = require('../../lib/tools/weightChecker/imageOptimizer');
+var imageOptimizer = require('../../lib/tools/redownload/imageOptimizer');
 var fs = require('fs');
 var path = require('path');
 
@@ -11,7 +11,7 @@ describe('imageOptimizer', function() {
         var fileSize = fileContent.length;
 
         imageOptimizer.compressJpegLosslessly(fileContent).then(function(newFile) {
-            var newFileSize = newFile.contents.length;
+            var newFileSize = newFile.length;
             newFileSize.should.be.below(fileSize);
             done();
         }).fail(function(err) {
@@ -25,7 +25,7 @@ describe('imageOptimizer', function() {
         var fileSize = fileContent.length;
 
         imageOptimizer.compressJpegLossly(fileContent).then(function(newFile) {
-            var newFileSize = newFile.contents.length;
+            var newFileSize = newFile.length;
             newFileSize.should.be.below(fileSize);
             done();
         }).fail(function(err) {
@@ -82,7 +82,7 @@ describe('imageOptimizer', function() {
         var fileSize = fileContent.length;
 
         imageOptimizer.compressPngLosslessly(fileContent).then(function(newFile) {
-            var newFileSize = newFile.contents.length;
+            var newFileSize = newFile.length;
             newFileSize.should.be.below(fileSize);
             done();
         }).fail(function(err) {
@@ -96,7 +96,7 @@ describe('imageOptimizer', function() {
         var fileSize = fileContent.length;
 
         imageOptimizer.compressPngLosslessly(fileContent).then(function(newFile) {
-            var newFileSize = newFile.contents.length;
+            var newFileSize = newFile.length;
             newFileSize.should.equal(fileSize);
             done();
         }).fail(function(err) {
@@ -110,7 +110,7 @@ describe('imageOptimizer', function() {
         var fileSize = fileContent.length;
 
         imageOptimizer.compressPngLosslessly(fileContent).then(function(newFile) {
-            var newFileSize = newFile.contents.length;
+            var newFileSize = newFile.length;
             newFileSize.should.equal(fileSize);
             done();
         }).fail(function(err) {
@@ -166,7 +166,7 @@ describe('imageOptimizer', function() {
         var fileSize = fileContent.length;
 
         imageOptimizer.compressSvgLosslessly(fileContent).then(function(newFile) {
-            var newFileSize = newFile.contents.length;
+            var newFileSize = newFile.length;
             newFileSize.should.be.below(fileSize);
             done();
         }).fail(function(err) {

+ 41 - 0
test/core/offendersHelpersTest.js

@@ -200,6 +200,47 @@ describe('offendersHelpers', function() {
             ]);
         });
 
+        it('should transform a backtrace with the new PhantomJS 2.x syntax into an array', function() {
+            var result = offendersHelpers.backtraceToArray('each@http://m.australia.fr/js/min/vendors.js?20160706185900:4:5365 / f@http://m.australia.fr/js/min/vendors.js?20160706185900:17:82 / http://m.australia.fr/js/min/vendors.js?20160706185900:17:855 / handle@http://m.australia.fr/js/min/vendors.js?20160706185900:5:10871 / report@phantomjs://platform/phantomas.js:535:20 / phantomjs://platform/phantomas.js:524:15');
+
+            result.should.deep.equal([
+                {
+                    functionName: 'each',
+                    file: 'http://m.australia.fr/js/min/vendors.js?20160706185900',
+                    line: 4,
+                    column: 5365
+                },
+                {
+                    functionName: 'f',
+                    file: 'http://m.australia.fr/js/min/vendors.js?20160706185900',
+                    line: 17,
+                    column: 82
+                },
+                {
+                    file: 'http://m.australia.fr/js/min/vendors.js?20160706185900',
+                    line: 17,
+                    column: 855
+                },
+                {
+                    functionName: 'handle',
+                    file: 'http://m.australia.fr/js/min/vendors.js?20160706185900',
+                    line: 5,
+                    column: 10871
+                },
+                {
+                    functionName: 'report',
+                    file: 'phantomjs://platform/phantomas.js',
+                    line: 535,
+                    column: 20
+                },
+                {
+                    file: 'phantomjs://platform/phantomas.js',
+                    line: 524,
+                    column: 15
+                }
+            ]);
+        });
+
         it('should return null if it fails', function() {
             var result = offendersHelpers.backtraceToArray('http://pouet.com/js/jquery.footer-transverse-min-v1.0.20.js:1 /http://pouet.com/js/main.js:1');
 

+ 49 - 49
test/core/weightCheckerTest.js → test/core/redownloadTest.js

@@ -1,9 +1,9 @@
 var should = require('chai').should();
-var weightChecker = require('../../lib/tools/weightChecker/weightChecker');
+var redownload = require('../../lib/tools/redownload/redownload');
 var fs = require('fs');
 var path = require('path');
 
-describe('weightChecker', function() {
+describe('redownload', function() {
     
     it('should download a list of files', function(done) {
         this.timeout(10000);
@@ -121,49 +121,49 @@ describe('weightChecker', function() {
             }
         };
 
-        weightChecker.recheckAllFiles(data)
+        redownload.recheckAllFiles(data)
 
         .then(function(data) {
-            data.toolsResults.should.have.a.property('weightChecker');
-            data.toolsResults.weightChecker.should.have.a.property('metrics');
-            data.toolsResults.weightChecker.should.have.a.property('offenders');
-
-            data.toolsResults.weightChecker.offenders.should.have.a.property('totalWeight');
-            data.toolsResults.weightChecker.offenders.totalWeight.totalWeight.should.be.above(0);
-            data.toolsResults.weightChecker.offenders.totalWeight.byType.html.requests.length.should.equal(1);
-            data.toolsResults.weightChecker.offenders.totalWeight.byType.js.requests.length.should.equal(2);
-            data.toolsResults.weightChecker.offenders.totalWeight.byType.css.requests.length.should.equal(1);
-            data.toolsResults.weightChecker.offenders.totalWeight.byType.image.requests.length.should.equal(2);
-            data.toolsResults.weightChecker.offenders.totalWeight.byType.other.requests.length.should.equal(1);
-
-            data.toolsResults.weightChecker.offenders.should.have.a.property('imageOptimization');
-            data.toolsResults.weightChecker.offenders.imageOptimization.totalGain.should.be.above(0);
-            data.toolsResults.weightChecker.offenders.imageOptimization.images.length.should.equal(2);
-
-            data.toolsResults.weightChecker.offenders.should.have.a.property('gzipCompression');
-            data.toolsResults.weightChecker.offenders.gzipCompression.totalGain.should.be.above(0);
-            data.toolsResults.weightChecker.offenders.gzipCompression.files.length.should.equal(5);
-
-            data.toolsResults.weightChecker.offenders.should.have.a.property('fileMinification');
-            data.toolsResults.weightChecker.offenders.fileMinification.totalGain.should.be.above(0);
-            data.toolsResults.weightChecker.offenders.fileMinification.files.length.should.equal(2);
-
-            data.toolsResults.weightChecker.metrics.should.have.a.property('totalRequests').that.equals(7);
-            data.toolsResults.weightChecker.offenders.should.have.a.property('totalRequests');
-            data.toolsResults.weightChecker.offenders.totalRequests.byType.html.length.should.equal(1);
-            data.toolsResults.weightChecker.offenders.totalRequests.byType.js.length.should.equal(2);
-            data.toolsResults.weightChecker.offenders.totalRequests.byType.css.length.should.equal(1);
-            data.toolsResults.weightChecker.offenders.totalRequests.byType.image.length.should.equal(2);
-            data.toolsResults.weightChecker.offenders.totalRequests.byType.json.length.should.equal(0);
-            data.toolsResults.weightChecker.offenders.totalRequests.byType.webfont.length.should.equal(0);
-            data.toolsResults.weightChecker.offenders.totalRequests.byType.video.length.should.equal(0);
-            data.toolsResults.weightChecker.offenders.totalRequests.byType.other.length.should.equal(1);
-
-            data.toolsResults.weightChecker.metrics.should.have.a.property('smallRequests').that.equals(0);
-            data.toolsResults.weightChecker.offenders.should.have.a.property('smallRequests');
-            data.toolsResults.weightChecker.offenders.smallRequests.byType.js.length.should.equal(0);
-            data.toolsResults.weightChecker.offenders.smallRequests.byType.css.length.should.equal(0);
-            data.toolsResults.weightChecker.offenders.smallRequests.byType.image.length.should.equal(0);
+            data.toolsResults.should.have.a.property('redownload');
+            data.toolsResults.redownload.should.have.a.property('metrics');
+            data.toolsResults.redownload.should.have.a.property('offenders');
+
+            data.toolsResults.redownload.offenders.should.have.a.property('totalWeight');
+            data.toolsResults.redownload.offenders.totalWeight.totalWeight.should.be.above(0);
+            data.toolsResults.redownload.offenders.totalWeight.byType.html.requests.length.should.equal(1);
+            data.toolsResults.redownload.offenders.totalWeight.byType.js.requests.length.should.equal(2);
+            data.toolsResults.redownload.offenders.totalWeight.byType.css.requests.length.should.equal(1);
+            data.toolsResults.redownload.offenders.totalWeight.byType.image.requests.length.should.equal(2);
+            data.toolsResults.redownload.offenders.totalWeight.byType.other.requests.length.should.equal(1);
+
+            data.toolsResults.redownload.offenders.should.have.a.property('imageOptimization');
+            data.toolsResults.redownload.offenders.imageOptimization.totalGain.should.be.above(0);
+            data.toolsResults.redownload.offenders.imageOptimization.images.length.should.equal(2);
+
+            data.toolsResults.redownload.offenders.should.have.a.property('gzipCompression');
+            data.toolsResults.redownload.offenders.gzipCompression.totalGain.should.be.above(0);
+            data.toolsResults.redownload.offenders.gzipCompression.files.length.should.equal(5);
+
+            data.toolsResults.redownload.offenders.should.have.a.property('fileMinification');
+            data.toolsResults.redownload.offenders.fileMinification.totalGain.should.be.above(0);
+            data.toolsResults.redownload.offenders.fileMinification.files.length.should.equal(2);
+
+            data.toolsResults.redownload.metrics.should.have.a.property('totalRequests').that.equals(7);
+            data.toolsResults.redownload.offenders.should.have.a.property('totalRequests');
+            data.toolsResults.redownload.offenders.totalRequests.byType.html.length.should.equal(1);
+            data.toolsResults.redownload.offenders.totalRequests.byType.js.length.should.equal(2);
+            data.toolsResults.redownload.offenders.totalRequests.byType.css.length.should.equal(1);
+            data.toolsResults.redownload.offenders.totalRequests.byType.image.length.should.equal(2);
+            data.toolsResults.redownload.offenders.totalRequests.byType.json.length.should.equal(0);
+            data.toolsResults.redownload.offenders.totalRequests.byType.webfont.length.should.equal(0);
+            data.toolsResults.redownload.offenders.totalRequests.byType.video.length.should.equal(0);
+            data.toolsResults.redownload.offenders.totalRequests.byType.other.length.should.equal(1);
+
+            data.toolsResults.redownload.metrics.should.have.a.property('smallRequests').that.equals(0);
+            data.toolsResults.redownload.offenders.should.have.a.property('smallRequests');
+            data.toolsResults.redownload.offenders.smallRequests.byType.js.length.should.equal(0);
+            data.toolsResults.redownload.offenders.smallRequests.byType.css.length.should.equal(0);
+            data.toolsResults.redownload.offenders.smallRequests.byType.image.length.should.equal(0);
 
             done();
         })
@@ -187,7 +187,7 @@ describe('weightChecker', function() {
             type: 'js'
         };
 
-        weightChecker.redownloadEntry(entry)
+        redownload.redownloadEntry(entry)
 
         .then(function(newEntry) {
 
@@ -221,7 +221,7 @@ describe('weightChecker', function() {
             contentType: 'image/png'
         };
 
-        weightChecker.redownloadEntry(entry)
+        redownload.redownloadEntry(entry)
 
         .then(function(newEntry) {
 
@@ -258,7 +258,7 @@ describe('weightChecker', function() {
             contentLength: 999
         };
 
-        weightChecker.redownloadEntry(entry)
+        redownload.redownloadEntry(entry)
 
         .then(function(newEntry) {
             newEntry.weightCheck.should.have.a.property('message').that.equals('error while downloading: 404');
@@ -285,7 +285,7 @@ describe('weightChecker', function() {
             contentLength: 999
         };
 
-        weightChecker.redownloadEntry(entry)
+        redownload.redownloadEntry(entry)
 
         .then(function(newEntry) {
             newEntry.should.not.have.a.property('weightCheck');
@@ -298,7 +298,7 @@ describe('weightChecker', function() {
     });
 
     it('should listRequestWeight', function() {
-        var totalWeightObj = weightChecker.listRequestWeight([{
+        var totalWeightObj = redownload.listRequestWeight([{
             method: 'GET',
             url: 'http://localhost:8388/jquery1.8.3.js',
             requestHeaders: {
@@ -328,7 +328,7 @@ describe('weightChecker', function() {
     });
 
     it('should listRequestWeight even if download failed', function() {
-        var totalWeightObj = weightChecker.listRequestWeight([{
+        var totalWeightObj = redownload.listRequestWeight([{
             method: 'GET',
             url: 'http://localhost:8388/jquery1.8.3.js',
             requestHeaders: {

+ 5 - 0
test/www/jquery-page.html

@@ -190,6 +190,11 @@
         $li.parentsUntil('body', 'div');
         $li.siblings();
         $li.siblings('li');
+
+        $.ajax({
+            url: 'xml.xml',
+            async: false
+        });
     </script>
 </body>
 </html>

部分文件因为文件数量过多而无法显示