+43
| #!/usr/bin/env node | ||
| 'use strict'; | ||
| var meow = require('meow'); | ||
| var updateNotifier = require('update-notifier'); | ||
| var psi = require('psi'); | ||
| var tmi = require('./'); | ||
| var cli = meow({ | ||
| help: [ | ||
| 'Usage', | ||
| ' tmi <url> [options]', | ||
| '', | ||
| 'Example', | ||
| ' tmi todomvc.com --strategy=desktop', | ||
| '', | ||
| 'Options', | ||
| ' --verbose Detailed summary.', | ||
| ' --key Google API Key. By default the free tier is used.', | ||
| ' --strategy Strategy to use when analyzing the page: mobile|desktop', | ||
| ' --locale Locale results should be generated in.', | ||
| ' --threshold Threshold score to pass the PageSpeed test.' | ||
| ].join('\n') | ||
| }); | ||
| updateNotifier({pkg: cli.pkg}).notify(); | ||
| if (!cli.input[0]) { | ||
| console.error('Please supply an URL'); | ||
| process.exit(1); | ||
| } | ||
| psi(cli.input[0], cli.flags, function (err, res) { | ||
| if (err) { | ||
| if (err.noStack) { | ||
| console.error(err.message); | ||
| process.exit(1); | ||
| } else { | ||
| throw err; | ||
| } | ||
| } | ||
| tmi().process(cli.flags, res); | ||
| }); |
+195
-27
@@ -1,35 +0,203 @@ | ||
| /* | ||
| * tmi (based on psi) | ||
| * http://github.com/addyosmani/tmi | ||
| * | ||
| * Copyright (c) 2014 Google Inc. | ||
| * Licensed under an Apache 2 license. | ||
| */ | ||
| 'use strict'; | ||
| var pagespeed = require('gpagespeed'); | ||
| var prependHttp = require('prepend-http'); | ||
| var output = require('./output'); | ||
| var fs = require('fs'); | ||
| var path = require('path'); | ||
| var prettyBytes = require('pretty-bytes'); | ||
| var chalk = require('chalk'); | ||
| var logSymbols = require('log-symbols'); | ||
| module.exports = function (opts, cb) { | ||
| opts = opts || {}; | ||
| cb = cb || function () { | ||
| var THRESHOLD = 70; | ||
| function Output() { | ||
| if (!(this instanceof Output)) { | ||
| return new Output(); | ||
| } | ||
| /** | ||
| * What indices from BigQuery do we want to summarize scores | ||
| * for in the CLI output? | ||
| * @type {number[]} | ||
| */ | ||
| this.indicesOfInterest = [3, 6, 9]; // 25th, 50th, 75th percentile | ||
| /** | ||
| * Is the weight higher than one of the BigQuery | ||
| * percentiles? | ||
| * @type {boolean} | ||
| */ | ||
| this.fasterThanAPercentile = false; | ||
| this.bigQueryData = { | ||
| desktop: [], | ||
| mobile: [], | ||
| titles: [] | ||
| }; | ||
| opts.strategy = opts.strategy || 'desktop'; | ||
| opts.nokey = opts.key === undefined; | ||
| opts.verbose = opts.verbose || false; | ||
| opts.url = prependHttp(opts.url); | ||
| var Output = new output(); | ||
| pagespeed(opts, function (err, data) { | ||
| if (err) { | ||
| cb(err); | ||
| return; | ||
| /** | ||
| * A subset of BigQuery data that we actually use for output | ||
| * This should represent the indices of interest from the | ||
| * bigQueryData. | ||
| * @type {{desktop: Array, mobile: Array, titles: Array}} | ||
| */ | ||
| this.outputData = { | ||
| desktop: [], | ||
| mobile: [], | ||
| titles: [] | ||
| }; | ||
| /** | ||
| * Average image weights per site, based on BigQuery data | ||
| * @type {{desktop: number, mobile: number}} | ||
| */ | ||
| this.medians = {}; | ||
| var data = fs.readFileSync(path.join(__dirname, 'data', 'bigquery.csv'), 'utf8').split('\n'); | ||
| // Complete full set of available BigQuery Data | ||
| this.bigQueryData.titles = data[0].split(','); | ||
| this.bigQueryData.desktop = data[1].split(',').slice(1).map(function (el) { | ||
| return parseInt(el, 10); | ||
| }); | ||
| this.bigQueryData.mobile = data[2].split(',').slice(1).map(function (el) { | ||
| return parseInt(el, 10); | ||
| }); | ||
| // Sub-slice portions of data we're interested | ||
| this.indicesOfInterest.forEach(function (item) { | ||
| this.outputData.desktop.push(this.bigQueryData.desktop[item]); | ||
| this.outputData.mobile.push(this.bigQueryData.mobile[item]); | ||
| this.outputData.titles.push(this.bigQueryData.titles[item]); | ||
| }, this); | ||
| this.medians.mobile = parseInt(this.bigQueryData.mobile[1], 10); | ||
| this.medians.desktop = parseInt(this.bigQueryData.desktop[1], 10); | ||
| } | ||
| /** | ||
| * Compare a supplied image weight with the weight in a supplied percentile. | ||
| * @param siteImageWeight | ||
| * @param percentileImageWeight | ||
| * @param percentile | ||
| * @returns {string} Summary message of comparisons to the percentile | ||
| */ | ||
| Output.prototype.compareWeightPercentile = function (siteImageWeight, percentileImageWeight, percentile) { | ||
| if (siteImageWeight === undefined) { | ||
| return; | ||
| } | ||
| var diff = (siteImageWeight - parseInt(percentileImageWeight, 10)) * 1000; | ||
| if (diff > 0) { | ||
| diff = chalk.red('+' + prettyBytes(diff)); | ||
| } else { | ||
| diff = diff * -1; | ||
| diff = chalk.green('-' + prettyBytes(diff)); | ||
| this.fasterThanAPercentile = true; | ||
| } | ||
| return diff + (' compared to sites in the ') + chalk.yellow(percentile.replace('p', '') + 'th') + ' percentile'; | ||
| }; | ||
| Output.prototype.compareWeights = function (siteImageWeight, sizes, percentiles) { | ||
| if (siteImageWeight === undefined) { | ||
| return; | ||
| } | ||
| var comparisons = ''; | ||
| siteImageWeight = parseInt(siteImageWeight, 10); | ||
| for (var i = 0; i < percentiles.length; i++) { | ||
| comparisons += this.compareWeightPercentile(siteImageWeight, sizes[i], percentiles[i]) + '\n'; | ||
| } | ||
| return comparisons; | ||
| }; | ||
| /** | ||
| * Check if image weight is higher than one of the available percentile sizes | ||
| * @param sizeImageWeight | ||
| * @param sizes | ||
| * @param percentiles | ||
| * @returns {*} | ||
| */ | ||
| Output.prototype.getHighestPercentile = function (sizeImageWeight, sizes, percentiles) { | ||
| if (sizeImageWeight === undefined) { | ||
| return; | ||
| } | ||
| var highestPercentileMatch = -1; | ||
| var result; | ||
| // Begin with index 2 to avoid catching unnecessary labels | ||
| // like `desktop` and `mobile` included in this row of data | ||
| for (var i = 2; i < percentiles.length; i++) { | ||
| sizes[i] = parseInt(sizes[i], 10); | ||
| if (sizeImageWeight > sizes[i]) { | ||
| highestPercentileMatch = i; | ||
| } | ||
| } | ||
| var response = data; | ||
| Output.process(opts, response, function (processErr) { | ||
| cb(processErr || err, response); | ||
| if (highestPercentileMatch === -1) { | ||
| result = '0'; | ||
| } else { | ||
| result = percentiles[highestPercentileMatch]; | ||
| } | ||
| return result; | ||
| }; | ||
| Output.prototype.process = function (opts, res) { | ||
| var threshold = opts.threshold || THRESHOLD; | ||
| var yourImageWeight = parseInt(res.pageStats.imageResponseBytes || 0, 10); | ||
| var unoptimizedImages = res.formattedResults.ruleResults.OptimizeImages.urlBlocks; | ||
| var desktopWeights = this.compareWeights(yourImageWeight / 1000, this.outputData.desktop, this.outputData.titles); | ||
| var mobileWeights = this.compareWeights(yourImageWeight / 1000, this.outputData.mobile, this.outputData.titles); | ||
| var imagesToOptimize = ''; | ||
| if (opts.verbose && unoptimizedImages[1] !== undefined) { | ||
| unoptimizedImages[1].urls.forEach(function (url) { | ||
| url.result.args.forEach(function (x) { | ||
| var result = ''; | ||
| switch (x.type) { | ||
| case 'URL': | ||
| result += chalk.green(x.value); | ||
| break; | ||
| case 'BYTES': | ||
| result += 'Size: ' + chalk.red(x.value); | ||
| break; | ||
| case 'PERCENTAGE': | ||
| result += 'Can be improved by ' + chalk.yellow(x.value) + '\n'; | ||
| break; | ||
| } | ||
| imagesToOptimize += result + '\n'; | ||
| }); | ||
| }); | ||
| }); | ||
| } | ||
| console.log([ | ||
| chalk.cyan('\nYour image weight'), prettyBytes(yourImageWeight), | ||
| chalk.gray('Median mobile site image weight'), prettyBytes(this.medians.mobile * 1000), | ||
| chalk.gray('Median desktop site image weight'), prettyBytes(this.medians.desktop * 1000), | ||
| chalk.cyan('\nOn Mobile'), | ||
| mobileWeights, | ||
| chalk.cyan('On Desktop'), | ||
| desktopWeights | ||
| ].join('\n')); | ||
| if (this.fasterThanAPercentile) { | ||
| console.log(chalk.cyan('Thanks for keeping the web fast <3')); | ||
| } | ||
| if (imagesToOptimize.length) { | ||
| console.log(chalk.underline('\nImages to optimize\n') + imagesToOptimize + chalk.cyan('This list does not include images which cannot be optimized further.\nYou may consider removing those images if possible.\n')); | ||
| } | ||
| if (res.score < threshold) { | ||
| console.error(chalk.bold(logSymbols.error + ' ' + 'Threshold of ' + threshold + ' not met with score of ' + res.score)); | ||
| process.exit(1); | ||
| } | ||
| }; | ||
| module.exports = Output; |
+23
-21
| { | ||
| "name": "tmi", | ||
| "version": "0.2.2", | ||
| "main": "index.js", | ||
| "version": "1.0.0", | ||
| "description": "Discover your image weight on the web", | ||
| "homepage": "http://github.com/addyosmani/tmi", | ||
| "bugs": "https://github.com/addyosmani/tmi/issues", | ||
| "author": { | ||
@@ -14,3 +11,3 @@ "name": "Addy Osmani", | ||
| "bin": { | ||
| "tmi": "./bin/cli.js" | ||
| "tmi": "cli.js" | ||
| }, | ||
@@ -20,35 +17,40 @@ "engines": { | ||
| }, | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/addyosmani/tmi" | ||
| }, | ||
| "license": "Apache-2", | ||
| "repository": "addyosmani/tmi", | ||
| "license": "Apache-2.0", | ||
| "scripts": { | ||
| "test": "mocha test" | ||
| "test": "mocha" | ||
| }, | ||
| "files": [ | ||
| "index.js", | ||
| "output.js", | ||
| "bin/cli.js", | ||
| "lib/utils.js", | ||
| "cli.js", | ||
| "data/bigquery.csv" | ||
| ], | ||
| "keywords": [ | ||
| "pagespeed", | ||
| "cli-app", | ||
| "cli", | ||
| "bin", | ||
| "image", | ||
| "images", | ||
| "weight", | ||
| "image-weight", | ||
| "size" | ||
| "size", | ||
| "pagespeed", | ||
| "insights", | ||
| "speed", | ||
| "page", | ||
| "website", | ||
| "measure", | ||
| "optimize" | ||
| ], | ||
| "dependencies": { | ||
| "chalk": "^0.5.1", | ||
| "csv-parse": "0.0.6", | ||
| "gpagespeed": "0.0.8", | ||
| "minimist": "^1.1.0", | ||
| "log-symbols": "^1.0.1", | ||
| "meow": "^2.1.0", | ||
| "pretty-bytes": "^1.0.1", | ||
| "prepend-http": "^1.0.0" | ||
| "psi": "^1.0.3", | ||
| "update-notifier": "^0.3.0" | ||
| }, | ||
| "devDependencies": { | ||
| "mocha": "^2.0.1" | ||
| "mocha": "*" | ||
| } | ||
| } |
+31
-140
@@ -1,161 +0,46 @@ | ||
| # tmi [](https://david-dm.org/addyosmani/tmi) [](https://david-dm.org/addyosmani/tmi#info=devDependencies) [](https://travis-ci.org/addyosmani/tmi) | ||
| # tmi - too many images [](https://travis-ci.org/addyosmani/tmi) | ||
| > TMI (Too Many Images) | ||
| > Discover your image weight on the web | ||
| Discover your image weight on the web. Find out the image weight in your pages, compare to the BigQuery medians and discover what images you can optimize further. | ||
|  | ||
| Find out the image weight in your pages, compare to the BigQuery quantiles and discover what images you can optimize further. | ||
| ## Install | ||
| ```sh | ||
| $ npm install -g tmi | ||
| ``` | ||
| ## Quick start | ||
| Summary: | ||
| ```sh | ||
| $ tmi <url> | ||
| $ npm install --global tmi | ||
| ``` | ||
| Examples: | ||
|  | ||
|  | ||
| Switch between desktop and mobile strategies: | ||
| ```sh | ||
| $ tmi <url> --strategy=mobile | ||
| ``` | ||
|  | ||
| Detailed summary with URLs you can optimize: | ||
| ```sh | ||
| $ tmi <url> --verbose | ||
| ``` | ||
|  | ||
| ## Usage | ||
| This module is modelled on [psi](http://github.com/addyosmani/psi) and follows a very similar API. | ||
| When using this module for a production-level build process, registering for an API key from the [Google Developer Console](https://developers.google.com/speed/docs/insights/v1/getting_started#auth) is recommended. | ||
| ```js | ||
| var tmi = require('tmi'); | ||
| tmi({ | ||
| // key: '...', optional | ||
| url: 'http://html5rocks.com', | ||
| paths: '', // optional | ||
| locale: 'en_GB', // optional | ||
| strategy: 'mobile', // optional | ||
| threshold: 80 // optional | ||
| }); | ||
| ``` | ||
| $ tmi --help | ||
| Optionally, a callback is also available with access to the response: | ||
| Usage | ||
| tmi <url> [options] | ||
| ```js | ||
| tmi(options, function (err, data) { | ||
| console.log(data.score); | ||
| console.log(data.pageStats); | ||
| }); | ||
| ``` | ||
| Example | ||
| tmi todomvc.com --strategy=desktop | ||
| ### Options | ||
| #### url | ||
| *Required* | ||
| Type: `string` | ||
| URL of the page for which the PageSpeed Insights API should generate results. | ||
| #### locale | ||
| Type: `string` | ||
| Default: `en_US` | ||
| The locale that results should be generated in (e.g 'en_GB'). | ||
| #### strategy | ||
| Type: `string` | ||
| Default: `desktop` | ||
| The strategy to use when analyzing the page. Valid values are desktop and mobile. | ||
| #### threshold | ||
| Type: `number` | ||
| Default: `70` | ||
| Threshold score that is needed to pass the pagespeed test | ||
| #### paths | ||
| Type: `array` | ||
| An array of URL paths that are appended to the URL | ||
| #### key | ||
| Type: `string` | ||
| Default: `nokey` | ||
| [Google API Key](https://code.google.com/apis/console/) | ||
| Unless Specified defaults to use the free tier on PageSpeed Insights. Good for getting a feel for how well this tool works for you. | ||
| ## CLI support | ||
| You will probably want to globally install tmi if using as a CLI. This can be done as follows: | ||
| ```sh | ||
| $ npm install --global tmi | ||
| Options | ||
| --verbose Detailed summary. | ||
| --key Google API Key. By default the free tier is used. | ||
| --strategy Strategy to use when analyzing the page: mobile|desktop | ||
| --locale Locale results should be generated in. | ||
| --threshold Threshold score to pass the PageSpeed test. | ||
| ``` | ||
| You can then casually use it with or without your key: | ||
| ```sh | ||
| $ tmi http://www.google.com | ||
| ``` | ||
| ## Verbose mode | ||
| ```sh | ||
| $ tmi http://www.google.com --key 'YOUR_KEY_GOES_HERE' | ||
| ``` | ||
| Verbose mode will show you a detailed summary of which images needs improving. | ||
| With or without http:// for URLs: | ||
|  | ||
| ```sh | ||
| $ tmi chrome.com | ||
| ``` | ||
| Or ask for a more detailed report including image URLs that can be optimized: | ||
| ## Good test URLs | ||
| ```sh | ||
| $ tmi http://www.google.com --verbose | ||
| ``` | ||
| Similar to gpagespeed, the following optional flags are also supported: | ||
| ```sh | ||
| $ tmi <url> --key=<key> --prettyprint=<true> --userIp=<userIp> --locale=<locale> --strategy=<desktop|mobile> | ||
| ``` | ||
| ```sh | ||
| $ tmi http://www.html5rocks.com --strategy=mobile | ||
| ``` | ||
| ### Good test URLs | ||
| * [LG Watch](http://www.lg.com/global/gwatch/index.html) | ||
@@ -167,10 +52,16 @@ * [MySpace homepage](http://myspace.com) | ||
| ## Local testing | ||
| We plan on adding support for testing localhost and local files in the | ||
| very near future. Until then, [ngrok](https://ngrok.com/) should be able | ||
| to help get you mostly there. | ||
| We plan on adding support for testing localhost and local files in the very near future. Until then, [ngrok](https://ngrok.com/) should be able to help get you mostly there. | ||
| ## API | ||
| See [`psi`](https://github.com/addyosmani/psi) if you need a programmatic API for PageSpeed Insights. | ||
| ## License | ||
| Apache 2. | ||
| Apache-2.0 | ||
| Copyright 2015 Google Inc |
-74
| #!/usr/bin/env node | ||
| 'use strict'; | ||
| // Adapted from cli.js in gpagespeed | ||
| var insights = require('../index') | ||
| , pkg = require('../package.json') | ||
| , query = process.argv[2] | ||
| , argv = require('minimist')((process.argv.slice(2))) | ||
| , opts = {} | ||
| ; | ||
| function printHelp() { | ||
| console.log(pkg.description); | ||
| console.log(''); | ||
| console.log('Usage:'); | ||
| console.log(' $ tmi <url>'); | ||
| console.log(' $ tmi <url> --key=<key>'); | ||
| console.log(' $ tmi <url> --verbose'); | ||
| console.log(''); | ||
| console.log('Optional, supply other arguments.'); | ||
| console.log('See https://developers.google.com/speed/docs/insights/v1/getting_started for description'); | ||
| console.log(' $ tmi <url> --key=<key> --prettyprint=<true> --userIp=<userIp> --locale=<locale> --strategy=<desktop|mobile>'); | ||
| } | ||
| if (!query || process.argv.indexOf('-h') !== -1 || process.argv.indexOf('--help') !== -1) { | ||
| printHelp(); | ||
| return; | ||
| } | ||
| if (process.argv.indexOf('-v') !== -1 || process.argv.indexOf('--version') !== -1) { | ||
| console.log(pkg.version); | ||
| return; | ||
| } | ||
| opts.url = argv._[0]; | ||
| if(!opts.url){ | ||
| printHelp(); | ||
| return; | ||
| } | ||
| if(argv.url){ | ||
| opts.url = argv.url; | ||
| } | ||
| if(argv.key){ | ||
| opts.key = argv.key; | ||
| } | ||
| if (process.argv.indexOf('-r') !== -1 || process.argv.indexOf('--verbose') !== -1) { | ||
| opts.verbose = true; | ||
| } | ||
| if(argv.callback){ | ||
| opts.callback = argv.callback; | ||
| } | ||
| if(argv.prettyprint){ | ||
| opts.prettyprint = argv.prettyprint; | ||
| } | ||
| if(argv.userIp){ | ||
| opts.userIp = argv.userIp; | ||
| } | ||
| if(argv.locale){ | ||
| opts.locale = argv.locale; | ||
| } | ||
| if(argv.strategy){ | ||
| opts.strategy = argv.strategy; | ||
| } | ||
| insights(opts); |
-51
| /* | ||
| * utils | ||
| * | ||
| * Collection of functions for report output | ||
| */ | ||
| 'use strict'; | ||
| var chalk = require('chalk'); | ||
| var divider = '\n' + chalk.grey(new Array(65).join('-')) + '\n'; | ||
| var buffer = function (msg, length) { | ||
| var buffer = ''; | ||
| if (length === undefined) { | ||
| length = 50; | ||
| } | ||
| if (length - msg.length > 0) { | ||
| buffer = new Array(length - msg.length).join(' '); | ||
| } | ||
| return buffer; | ||
| }; | ||
| var addSpacesToWords = function (msg) { | ||
| return msg.replace(/([A-Z]+)/g, ' $1').replace(/([A-Z][a-z])/g, '$1'); | ||
| }; | ||
| var firstToUpperCaseAndAddSpace = function (msg) { | ||
| msg = msg.replace('Bytes', ''); | ||
| return msg.charAt(0).toUpperCase() + addSpacesToWords(msg.slice(1)); | ||
| }; | ||
| var scoreColor = function(score) { | ||
| var color = chalk.yellow; | ||
| color = score < 21 ? chalk.red : color; | ||
| color = score > 79 ? chalk.green : color; | ||
| return color; | ||
| }; | ||
| var labelize = function(msg) { | ||
| var label = firstToUpperCaseAndAddSpace(msg); | ||
| return label + buffer(label) + chalk.grey('| '); | ||
| }; | ||
| module.exports = { | ||
| scoreColor: scoreColor, | ||
| buffer: buffer, | ||
| divider: divider, | ||
| labelize: labelize, | ||
| firstToUpperCaseAndAddSpace: firstToUpperCaseAndAddSpace | ||
| }; |
-240
| 'use strict'; | ||
| var prettyBytes = require('pretty-bytes'); | ||
| var chalk = require('chalk'); | ||
| var utils = require('./lib/utils'); | ||
| var csvparse = require('csv-parse'); | ||
| var fs = require('fs'); | ||
| var path = require('path'); | ||
| function Output() { | ||
| if (!(this instanceof Output)) { | ||
| return new Output; | ||
| } | ||
| this.constants = { | ||
| YOUR_IMAGE_WEIGHT : '\nYour image weight: ', | ||
| MEDIAN_MOBILE_WEIGHT : 'Median mobile site image weight: ', | ||
| MEDIAN_DESKTOP_WEIGHT : 'Median desktop site image weight: ', | ||
| ON_MOBILE : '\nOn Mobile:', | ||
| ON_DESKTOP : '\nOn Desktop:', | ||
| MORE_BYTES_THAN : 'You have more image bytes than ', | ||
| IMAGES_TO_OPTIMISE : '\nImages to optimize:\n', | ||
| PERCENTAGE_OF : '% of sites' | ||
| }; | ||
| /** | ||
| * PageSpeed Insights threshold | ||
| * @type {number} | ||
| */ | ||
| this.threshold = 70; | ||
| /** | ||
| * What indices from BigQuery do we want to summarize scores | ||
| * for in the CLI output? | ||
| * @type {number[]} | ||
| */ | ||
| this.indicesOfInterest = [3, 6, 9]; // 25th, 50th, 75th percentile | ||
| /** | ||
| * Include suggestions for URLs that could be improved score-wise | ||
| * This will not include a complete list of possible optimizations | ||
| * and relies on PageSpeed Insights API data | ||
| * @type {boolean} | ||
| */ | ||
| this.verbose = false; | ||
| /** | ||
| * Is the weight higher than one of the BigQuery | ||
| * percentiles? | ||
| * @type {boolean} | ||
| */ | ||
| this.fasterThanAPercentile = false; | ||
| this.bigQueryData = { | ||
| desktop: [], mobile: [], titles: [] | ||
| }; | ||
| /** | ||
| * A subset of BigQuery data that we actually use for output | ||
| * This should represent the indices of interest from the | ||
| * bigQueryData. | ||
| * @type {{desktop: Array, mobile: Array, titles: Array}} | ||
| */ | ||
| this.outputData = { | ||
| desktop: [], mobile: [], titles: [] | ||
| }; | ||
| /** | ||
| * Average image weights per site, based on BigQuery data | ||
| * @type {{desktop: number, mobile: number}} | ||
| */ | ||
| this.medians = {}; | ||
| /** | ||
| * Message shown when image weight is lower than one of the percentiles | ||
| * we care about. | ||
| */ | ||
| this.keepingWebFast = chalk.cyan('Thanks for keeping the web fast <3'); | ||
| var parser = csvparse({ | ||
| delimiter: ';' | ||
| }, function (err, data) { | ||
| // TODO: Refactor all of this to be less boilerplate-y. | ||
| // Complete full set of available BigQuery Data | ||
| this.bigQueryData.titles = data[0][0].split(','); | ||
| this.bigQueryData.desktop = data[1][0].split(','); | ||
| this.bigQueryData.mobile = data[2][0].split(','); | ||
| // Sub-slice portions of data we're interested | ||
| this.indicesOfInterest.forEach(function (item) { | ||
| this.outputData.desktop.push(this.bigQueryData.desktop[item]); | ||
| this.outputData.mobile.push(this.bigQueryData.mobile[item]); | ||
| this.outputData.titles.push(this.bigQueryData.titles[item]); | ||
| }.bind(this)); | ||
| this.medians.mobile = parseInt(this.bigQueryData.mobile[1], 10); | ||
| this.medians.desktop = parseInt(this.bigQueryData.desktop[1], 10); | ||
| }.bind(this)); | ||
| fs.createReadStream(path.resolve(__dirname + '/data/bigquery.csv')).pipe(parser); | ||
| }; | ||
| /** | ||
| * Compare a supplied image weight with the weight in a supplied percentile. | ||
| * @param siteImageWeight | ||
| * @param percentileImageWeight | ||
| * @param percentile | ||
| * @returns {string} Summary message of comparisons to the percentile | ||
| */ | ||
| Output.prototype.compareWeightPercentile = function (siteImageWeight, percentileImageWeight, percentile) { | ||
| if (siteImageWeight === undefined) { | ||
| return; | ||
| } | ||
| var diff = (siteImageWeight - parseInt(percentileImageWeight, 10)) * 1000; | ||
| if (diff > 0) { | ||
| diff = chalk.red('+' + prettyBytes(diff)); | ||
| } else { | ||
| diff = diff * -1; | ||
| diff = chalk.green('-' + prettyBytes(diff)); | ||
| this.fasterThanAPercentile = true; | ||
| } | ||
| return diff + (' compared to sites in the ') + chalk.yellow(percentile.replace('p', '') + 'th') + ' percentile'; | ||
| }; | ||
| Output.prototype.compareWeights = function (siteImageWeight, sizes, percentiles) { | ||
| if (siteImageWeight === undefined) { | ||
| return; | ||
| } | ||
| var comparisons = ''; | ||
| siteImageWeight = parseInt(siteImageWeight, 10); | ||
| for (var i = 0; i < percentiles.length; i++) { | ||
| comparisons += this.compareWeightPercentile(siteImageWeight, sizes[i], percentiles[i]) + '\n'; | ||
| } | ||
| return comparisons; | ||
| }; | ||
| /** | ||
| * Check if image weight is higher than one of the available percentile sizes | ||
| * @param sizeImageWeight | ||
| * @param sizes | ||
| * @param percentiles | ||
| * @returns {*} | ||
| */ | ||
| Output.prototype.getHighestPercentile = function (sizeImageWeight, sizes, percentiles) { | ||
| if (sizeImageWeight === undefined) { | ||
| return; | ||
| } | ||
| var highestPercentileMatch = -1; | ||
| var result; | ||
| // Begin with index 2 to avoid catching unnecessary labels | ||
| // like `desktop` and `mobile` included in this row of data | ||
| for (var i = 2; i < percentiles.length; i++) { | ||
| sizes[i] = parseInt(sizes[i], 10); | ||
| if (sizeImageWeight > sizes[i]) { | ||
| highestPercentileMatch = i; | ||
| } | ||
| } | ||
| if (highestPercentileMatch === -1){ | ||
| result = '0'; | ||
| } else { | ||
| result = percentiles[highestPercentileMatch]; | ||
| } | ||
| return result; | ||
| }; | ||
| Output.prototype.process = function (parameters, response, done) { | ||
| done = done || function () {}; | ||
| var logger = console.log; | ||
| var error = null; | ||
| this.threshold = parameters.threshold || this.threshold; | ||
| this.verbose = parameters.verbose; | ||
| var yourImageWeight = parseInt(response.pageStats.imageResponseBytes || 0, 10); | ||
| var unoptimizedImages = response.formattedResults.ruleResults.OptimizeImages.urlBlocks; | ||
| var desktopWeights = this.compareWeights(yourImageWeight / 1000, this.outputData.desktop, this.outputData.titles); | ||
| var mobileWeights = this.compareWeights(yourImageWeight / 1000, this.outputData.mobile, this.outputData.titles); | ||
| var highestPercentileDesktop = this.getHighestPercentile(yourImageWeight / 1000, this.bigQueryData.desktop, this.bigQueryData.titles); | ||
| var highestPercentileMobile = this.getHighestPercentile(yourImageWeight / 1000, this.bigQueryData.mobile, this.bigQueryData.titles); | ||
| var imagesToOptimize = ''; | ||
| if (this.verbose) { | ||
| if (unoptimizedImages[1] !== undefined) { | ||
| unoptimizedImages[1].urls.forEach(function (url) { | ||
| url.result.args.forEach(function (x) { | ||
| var result = ''; | ||
| switch (x.type) { | ||
| case 'URL': | ||
| result += chalk.green(x.value); | ||
| break; | ||
| case 'BYTES': | ||
| result += 'Size: ' + chalk.red(x.value); | ||
| break; | ||
| case 'PERCENTAGE': | ||
| result += 'Can be improved by ' + chalk.yellow(x.value) + '\n'; | ||
| break; | ||
| } | ||
| imagesToOptimize += result + '\n'; | ||
| }); | ||
| }); | ||
| } | ||
| } | ||
| logger([ | ||
| chalk.cyan(this.constants.YOUR_IMAGE_WEIGHT) + prettyBytes(yourImageWeight), | ||
| chalk.gray(this.constants.MEDIAN_MOBILE_WEIGHT) + prettyBytes(this.medians.mobile * 1000), | ||
| chalk.gray(this.constants.MEDIAN_DESKTOP_WEIGHT) + prettyBytes(this.medians.desktop * 1000), | ||
| chalk.cyan(this.constants.ON_MOBILE), | ||
| // chalk.magenta(this.constants.MORE_BYTES_THAN + highestPercentileDesktop.replace('p', '') + this.constants.PERCENTAGE_OF), | ||
| mobileWeights, | ||
| chalk.cyan(this.constants.ON_DESKTOP), | ||
| // chalk.magenta(this.constants.MORE_BYTES_THAN + highestPercentileMobile.replace('p', '') + this.constants.PERCENTAGE_OF), | ||
| desktopWeights | ||
| ].join('\n')); | ||
| if (this.fasterThanAPercentile) { | ||
| logger(this.keepingWebFast); | ||
| } | ||
| if (imagesToOptimize.length) { | ||
| logger(chalk.underline(this.constants.IMAGES_TO_OPTIMISE) + imagesToOptimize + chalk.cyan('\nThis list does not include images which cannot be optimized further.\nYou may consider removing those images if possible.')); | ||
| } | ||
| if (response.score < this.threshold) { | ||
| error = new Error('Threshold of ' + this.threshold + ' not met with score of ' + response.score); | ||
| } | ||
| return done(error); | ||
| }; | ||
| module.exports = Output; | ||
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
9498
-40.63%5
-28.57%207
-38.21%67
-61.93%2
Infinity%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed