├── .gitignore ├── .jscsrc ├── .jshintignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── index.js ├── package.json ├── scripts └── optimize.bash └── test ├── _jscs.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "disallowEmptyBlocks": true, 3 | "disallowMixedSpacesAndTabs": true, 4 | "disallowMultiLineTernary": true, 5 | "disallowMultipleLineStrings": true, 6 | "disallowNamedUnassignedFunctions": true, 7 | "disallowNestedTernaries": true, 8 | "disallowNewlineBeforeBlockStatements": true, 9 | "disallowOperatorBeforeLineBreak": [ 10 | "+", 11 | "." 12 | ], 13 | "disallowSpaceBeforeComma": true, 14 | "disallowUnusedParams": true, 15 | "jsDoc": { 16 | "checkAnnotations": true, 17 | "checkParamExistence": true, 18 | "checkParamNames": true, 19 | "requireParamTypes": true, 20 | "checkRedundantParams": true, 21 | "checkReturnTypes": true, 22 | "checkRedundantReturns": true, 23 | "requireReturnTypes": true, 24 | "checkTypes": true, 25 | "checkRedundantAccess": true, 26 | "enforceExistence": true, 27 | "requireHyphenBeforeDescription": true, 28 | "requireNewlineAfterDescription": true, 29 | "requireDescriptionCompleteSentence": true, 30 | "requireParamDescription": true, 31 | "requireReturnDescription": true 32 | }, 33 | // "requireAlignedObjectValues": "ignoreFunction", 34 | "requireBlocksOnNewline": true, 35 | "requireCamelCaseOrUpperCaseIdentifiers": true, 36 | "requireCapitalizedComments": true, 37 | "requireCapitalizedConstructors": true, 38 | "requireEarlyReturn": true, 39 | "requireImportAlphabetized": true, 40 | "requireLineBreakAfterVariableAssignment": true, 41 | "requireMatchingFunctionName": true, 42 | "requireObjectKeysOnNewLine": true, 43 | "requirePaddingNewLinesAfterBlocks": true, 44 | "requirePaddingNewLinesBeforeExport": true, 45 | "requirePaddingNewlinesBeforeKeywords": [ 46 | "for", 47 | "if", 48 | "switch", 49 | "case", 50 | "try", 51 | "void", 52 | "while", 53 | "with", 54 | "function" 55 | ], 56 | "requirePaddingNewLinesBeforeLineComments": true, 57 | "requireParenthesesAroundArrowParam": true, 58 | "requireParenthesesAroundIIFE": true, 59 | "requireQuotedKeysInObjects": true, 60 | "requireSemicolons": false, 61 | "requireSpaceAfterComma": {"allExcept": ["trailing"]}, 62 | "requireSpaceAfterBinaryOperators": [ 63 | "=", 64 | "+", 65 | "-", 66 | "/", 67 | "*", 68 | "==", 69 | "===", 70 | "!=", 71 | "!==" 72 | ], 73 | "requireSpaceAfterComma": true, 74 | "requireSpaceAfterKeywords": [ 75 | "do", 76 | "for", 77 | "if", 78 | "else", 79 | "switch", 80 | "case", 81 | "try", 82 | "catch", 83 | "void", 84 | "while", 85 | "with", 86 | "return", 87 | "typeof", 88 | "function" 89 | ], 90 | "requireSpaceAfterLineComment": true, 91 | "requireSpaceAfterObjectKeys": true, 92 | "requireSpaceAfterPrefixUnaryOperators": [ 93 | "+", 94 | "-", 95 | "!" 96 | ], 97 | "requireSpaceBeforeBinaryOperators": [ 98 | "=", 99 | "+", 100 | "-", 101 | "/", 102 | "*", 103 | "==", 104 | "===", 105 | "!=", 106 | "!==" 107 | ], 108 | "requireSpaceBeforeBlockStatements": 1, 109 | "requireSpaceBeforeKeywords": [ 110 | "else", 111 | "while", 112 | "catch" 113 | ], 114 | "requireSpaceBeforeObjectValues": true, 115 | "requireSpaceBetweenArguments": true, 116 | "requireSpacesInCallExpression": true, 117 | "requireSpacesInConditionalExpression": { 118 | "afterTest": true, 119 | "beforeConsequent": true, 120 | "afterConsequent": true, 121 | "beforeAlternate": true 122 | }, 123 | "requireSpacesInForStatement": true, 124 | "requireSpacesInFunctionDeclaration": { 125 | "beforeOpeningRoundBrace": true, 126 | "beforeOpeningCurlyBrace": true 127 | }, 128 | "validateCommentPosition": { 129 | "position": "above" 130 | }, 131 | "validateOrderInObjectKeys": "asc-insensitive", 132 | "validateParameterSeparator": ", ", 133 | "validateQuoteMarks": { 134 | "mark": "'", 135 | "escape": true, 136 | "ignoreJSX": true 137 | } 138 | } -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules 3 | test -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test 3 | *.log 4 | .DS_Store 5 | .jshintrc 6 | .jshintignore 7 | vsconfig.js 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [1.0.3] - 2016-3-2 4 | ### Misc 5 | - Added some helper functions 6 | - Removed lodash dependency 7 | - Updated dependencies 8 | 9 | ## [1.0.0] - 2016-2-4 10 | ### Misc 11 | - Fixed some typos 12 | 13 | ## [1.0.0] - 2016-2-3 14 | ### Feature 15 | - Add support for JPEGMini 16 | 17 | ## [0.3.4] - 2015-12-2 18 | ### Misc 19 | - Added start and finished messages 20 | 21 | ## [0.3.3] - 2015-10-28 22 | ### Misc 23 | - Minor updates 24 | 25 | ## [0.3.2] - 2015-10-23 26 | ### Feature 27 | - Now does bulk optimization which has improved performance by more than 60% 28 | 29 | ## [0.2.12] - 2015-05-29 30 | ### Fixed 31 | - Fixed issue that cause files with a bracket in filename to cause an error 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | License (MIT) 2 | 3 | Copyright (c) 2015 Maxwell Berkel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gulp-ImageOptim 2 | 3 | Gulp plugin to optimize images using ImageOptim and JPEGmini. 4 | 5 | > Please note this plugin uses ImageOptim-CLI. Unfortunately currently there is no support for Windows and Linux. 6 | 7 | 8 | So why ImageOptim-CLI? My personal experience and [current benchmarks](http://jamiemason.github.io/ImageOptim-CLI/) suggest that ImageOptim and ImageAlpha currently outperform alternatives over lossless and lossy optimizations. 9 | 10 | 11 | ## Install 12 | 13 | [![NPM](https://nodei.co/npm/gulp-imageoptim.png?downloadRank=true)](https://nodei.co/npm/gulp-imageoptim/) 14 | 15 | 16 | ## Usage 17 | 18 | ``` 19 | var gulp = require('gulp'); 20 | var imageOptim = require('gulp-imageoptim'); 21 | 22 | 23 | gulp.task('images', function() { 24 | return gulp.src('src/images/**/*') 25 | .pipe(imageOptim.optimize()) 26 | .pipe(gulp.dest('build/images')); 27 | }); 28 | ``` 29 | 30 | 31 | ## Methods 32 | 33 | ### .optimize(options) 34 | 35 | Optimize images using ImageOptim-CLI 36 | 37 | 38 | ## Options 39 | 40 | ### jpegmini 41 | 42 | To enable JPEGmini support, set the option to true 43 | 44 | Type: Boolean 45 | Default: ` false ` 46 | 47 | 48 | ### status 49 | 50 | Set to false to disable optimize summary in console. 51 | 52 | Type: Boolean 53 | Default: ` true ` 54 | 55 | 56 | ### batchSize 57 | 58 | Batch size 59 | 60 | Type: Integer 61 | Default: ` 100 ` 62 | 63 | 64 | 65 | ## FAQS 66 | 67 | #### Does Gulp-ImageOptim use JPEGmini? 68 | 69 | Yes, however, please note that JPEGmini is a paid-for product. We are using ImageOptim-CLI (ImageOptim and ImageAlpha) without JPEGmini. To use JPEGmini you will need to [purchase it](http://jpegmini.com). 70 | 71 | 72 | #### Enabling JPEGmini and support for assistive devices 73 | 74 | You may be presented with the following message the first time you run Gulp Imageoptim with the jpegmini flag set to true: 75 | 76 | > To automate JPEGmini we need to add Terminal.app (or iTerm.app etc) to the 'support for assistive devices' whitelist. 77 | 78 | The JPEGmini OS X Apps don't include a command line API, so a real user is simulated by entering synthetic clicks and keyboard commands instead. This requires your permission and is easily set up in System Preferences as shown by these guides. 79 | 80 | + [Enable access for assistive devices in OS X Yosemite](http://www.klabouch.com/?p=98). 81 | + [OS X Mavericks: Enable access for assistive devices and applications](http://support.apple.com/en-us/HT6026) 82 | 83 | 84 | #### How do I enable JPEGmini in my gulp task? 85 | 86 | ``` 87 | var gulp = require('gulp'); 88 | var imageOptim = require('gulp-imageoptim'); 89 | 90 | 91 | gulp.task('images', function() { 92 | return gulp.src('src/images/**/*') 93 | .pipe(imageOptim.optimize({ 94 | jpegmini: true 95 | })) 96 | .pipe(gulp.dest('build/images')); 97 | }); 98 | ``` 99 | 100 | 101 | #### Can I use this plugin on Windows and Linux? 102 | 103 | Unfortunately, ImageOptim and ImageAlpha are not available on those platforms yet. 104 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Gulp plugin to optimize images using imageoptim. 3 | */ 4 | 5 | var chalk = require ('chalk'), 6 | exec = require ('child_process').exec, 7 | fs = require ('fs'), 8 | md5 = require ('blueimp-md5'), 9 | path = require ('path'), 10 | through = require ('through2') 11 | 12 | 13 | /** 14 | * Assert to parameter is of type object. 15 | * 16 | * @param {Mixed} param - The parameter to check the type of 17 | * 18 | * @throws {TypeError} Throws TypeError is actual type is not Object 19 | */ 20 | var assertObject = function (param) { 21 | var type = ({}).toString.call (param).replace ('[object ', '').replace (']', '') 22 | 23 | if (type !== 'Object') { 24 | throw new TypeError('Expected object but got ' + type) 25 | } 26 | } 27 | 28 | 29 | /** 30 | * Assign all properties from a source object to a destination object. 31 | * 32 | * @param {Object} dest - The object to be updated 33 | * @param {Object} src - The source object to assign missing properties from 34 | * @return {Object} - Returns updated object 35 | * 36 | * @throws {TypeError} - Throws TypeError if expected parameters are not objects 37 | */ 38 | var assign = function (dest, src) { 39 | assertObject (dest) 40 | assertObject (src) 41 | 42 | Object.keys (src).forEach (function (i) { 43 | dest[i] = src[i] 44 | }) 45 | 46 | return dest 47 | } 48 | 49 | 50 | /** 51 | * Create and return a new object with all the properties of the provided object. 52 | * 53 | * @param {Object} obj - The object to clone 54 | * @return {Object} - Returns clone of provided object 55 | * 56 | * @throws {TypeError} - Throws TypeError if expected parameter is not an object 57 | */ 58 | var clone = function (obj) { 59 | assertObject (obj) 60 | 61 | var newObj = {} 62 | 63 | Object.keys (obj).forEach (function (i) { 64 | newObj[i] = obj[i] 65 | }); 66 | 67 | return newObj 68 | } 69 | 70 | 71 | /** 72 | * Plugin. 73 | */ 74 | function BatchOptimizer () { 75 | 76 | /** 77 | * Default configuration options. 78 | * @type {Object} 79 | */ 80 | var config = {} 81 | 82 | /** 83 | * Whether to display optimization status or not. 84 | * @type {Boolean} 85 | */ 86 | config.status = true 87 | 88 | /** 89 | * Batch size is the number of files to batch optimize. 90 | * @type {Int} 91 | */ 92 | config.batchSize = 400 93 | 94 | /** 95 | * Whether to use jpegmini or not. 96 | * @type {Boolean} 97 | * 98 | * Before enabling this feature complete the following steps 99 | * 1. Install JPEGmini 100 | * 2. Open JPEGmini then add Terminal.app (or iTerm.app) to 'support for assistive devices' whitelist 101 | */ 102 | config.jpegmini = false 103 | 104 | /** 105 | * Array of files to batch optimize. 106 | * @type {Array} 107 | */ 108 | var batchFiles = [] 109 | 110 | /** 111 | * Reference to stream. 112 | * @type {Object} 113 | */ 114 | var stream = null 115 | 116 | /** 117 | * Remove directory. 118 | * 119 | * @param {String} dirPath - Path to directory 120 | */ 121 | var removeDirectory = function (dirPath) { 122 | var files = [] 123 | 124 | if (fs.lstatSync (dirPath).isDirectory ()) { 125 | files = fs.readdirSync (dirPath) 126 | 127 | files.forEach (function (file) { 128 | var curPath = path.join (dirPath, file) 129 | 130 | if (fs.lstatSync (curPath).isDirectory ()) { 131 | removeDirectory (curPath) 132 | } else { 133 | fs.unlinkSync (curPath) 134 | } 135 | }) 136 | 137 | fs.rmdirSync (dirPath) 138 | } 139 | } 140 | 141 | /** 142 | * Batch optimize files and push. 143 | * 144 | * @param {Array} files - Array of files to batch optimize 145 | * @param {Object} options - Options. Currently status is supported as boolean indicating whether to display optmization result status or not 146 | * @param {Function} next - The callback to continue processing 147 | */ 148 | var batchOptimize = function (files, options, next) { 149 | if (files.length === 0) 150 | next (files) 151 | 152 | var batchDir = md5 (Date.now ()), 153 | jpegminiEnabled = options.jpegmini ? '--jpegmini' : '', 154 | scriptParams = path.normalize (batchDir) + ' ' + jpegminiEnabled 155 | 156 | // Make batch directory 157 | fs.mkdirSync (batchDir) 158 | 159 | // Add files to optimize to batch directory 160 | for (var i = 0, length = files.length; i < length; i++) { 161 | var file = files[i] 162 | 163 | file.batchFilePath = path.join (batchDir, md5 (file.path) + path.extname (file.path)) 164 | fs.writeFileSync (file.batchFilePath, file.contents) 165 | } 166 | 167 | // Optimize files 168 | exec ('bash node_modules/gulp-imageoptim/scripts/optimize.bash ' + scriptParams, function (error, stdout) { 169 | var result = {} 170 | 171 | if (error === null) { 172 | var savings = parseInt (stdout.replace (/.*\(([0-9]+)(\.[0-9]+)?\%\)/, '$1')), 173 | msg = '' 174 | 175 | if (savings > 0) { 176 | 177 | // Copy optimized file contents to original and remove file 178 | for (var i = 0, length = files.length; i < length; i++) { 179 | var file = files[i] 180 | 181 | file.contents = new Buffer(fs.readFileSync (file.batchFilePath)) 182 | stdout = stdout.replace (file.batchFilePath, path.basename (file.path)) 183 | } 184 | 185 | msg = stdout.replace ('TOTAL was', 'Filesize total was') 186 | } else { 187 | msg = 'Saving: 0%\n' 188 | } 189 | 190 | result = { 191 | 'files' : files.length, 192 | 'msg' : msg, 193 | 'savings' : savings, 194 | 'type' : 'success' 195 | } 196 | } else { 197 | console.error (error) 198 | } 199 | 200 | // Delete batch directory 201 | removeDirectory (batchDir) 202 | 203 | // Display optmization result status? 204 | if (options.status) { 205 | displayOptimizationResults (result) 206 | } 207 | 208 | next (files) 209 | }) 210 | } 211 | 212 | /** 213 | * Push files to stream. 214 | * 215 | * @param {Array} files - Array of optimized files to push 216 | */ 217 | var pushFiles = function (files) { 218 | if (typeof stream !== 'object') 219 | return 220 | 221 | for (var i = 0, length = files.length; i < length; i++) { 222 | stream.push (files[i]) 223 | } 224 | } 225 | 226 | /** 227 | * Display batch results. 228 | * 229 | * @param {Object} result - The result to display from batch optimization 230 | */ 231 | var displayOptimizationResults = function (result) { 232 | if (typeof result === 'object') { 233 | switch (result.type) { 234 | case 'success': 235 | console.log ('Batch of ' + result.files + ' files optimized:\n' + chalk.gray (result.msg)) 236 | break 237 | 238 | case 'error': 239 | console.log (chalk.red (result.msg)) 240 | break 241 | } 242 | } 243 | } 244 | 245 | /** 246 | * Public functions 247 | */ 248 | return { 249 | 250 | /** 251 | * Optimize images. 252 | * 253 | * @param {Object} opts - Options to customize optimization 254 | * @return {Stream} - Returns through object 255 | */ 256 | 'optimize' : function (opts) { 257 | var options = clone (config) 258 | 259 | opts = opts || {}; 260 | 261 | // Set config optiosn to use for this current optimization session 262 | assign (options, opts) 263 | 264 | if (options.status) { 265 | console.log (chalk.yellow.bgBlack ('Starting image optimization ... this may take a while.')) 266 | } 267 | 268 | return through.obj (function (file, enc, cb) { 269 | stream = this 270 | 271 | // If file is null continue 272 | if (file.isNull ()) { 273 | cb (null, file) 274 | 275 | return 276 | } 277 | 278 | // Add file to batch 279 | batchFiles.push (file) 280 | 281 | if (batchFiles.length >= options.batchSize) { 282 | batchOptimize (batchFiles, options, function (files) { 283 | pushFiles (files) 284 | batchFiles = [] 285 | cb () 286 | }) 287 | } else { 288 | cb () 289 | } 290 | }, 291 | 292 | function (cb) { 293 | batchOptimize (batchFiles, options, function (files) { 294 | pushFiles (files) 295 | batchFiles = [] 296 | 297 | if (options.status) { 298 | console.log (chalk.green.bgBlack ('Optimization complete!')) 299 | } 300 | 301 | cb () 302 | }) 303 | }) 304 | } 305 | } 306 | } 307 | 308 | 309 | /** 310 | * Export 311 | */ 312 | module.exports = BatchOptimizer () 313 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-imageoptim", 3 | "version": "1.0.3", 4 | "description": "Gulp plugin to optimize images using imageoptim and jpegmini", 5 | "author": { 6 | "name": "Maxwell Berkel", 7 | "email": "maxwell@allbitsnbytes.com", 8 | "url": "http://allbitsnbytes.com" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/allbitsnbytes/gulp-imageoptim" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/allbitsnbytes/gulp-imageoptim/issues" 16 | }, 17 | "license": "MIT", 18 | "engines": { 19 | "node": ">= 0.10.25" 20 | }, 21 | "main": "index.js", 22 | "directories": { 23 | "test": "test" 24 | }, 25 | "scripts": { 26 | "test": "mocha --reporter spec" 27 | }, 28 | "keywords": [ 29 | "compression", 30 | "gulpplugin", 31 | "image", 32 | "imagealpha", 33 | "imageoptim", 34 | "imageoptim-cli", 35 | "jpeg", 36 | "jpg", 37 | "jpegmini", 38 | "node", 39 | "optimization", 40 | "png" 41 | ], 42 | "devDependencies": { 43 | "chai": "^3.5.0", 44 | "mocha": "^2.4.5", 45 | "mocha-jshint": "^2.3.0" 46 | }, 47 | "dependencies": { 48 | "blueimp-md5": "^2.2.0", 49 | "chalk": "~1.1.1", 50 | "imageoptim-cli": "^1.14.8", 51 | "mocha-jscs": "^4.2.0", 52 | "through2": "^2.0.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /scripts/optimize.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Run ImageOptim to optimize batch of images 4 | 5 | # Variables 6 | BATCH_IMAGES="${1}" 7 | JPEGMINI="" 8 | CWD="$( dirname "${BASH_SOURCE[0]}" )" 9 | LOGFILE="gulp_imageoptim_results.log" 10 | 11 | # Check for the path to imageoptim-cli 12 | if [[ -d ${CWD}/../node_modules/imageoptim-cli ]]; then 13 | IMAGEOPTIM="${CWD}/../node_modules/imageoptim-cli/bin/imageOptim" 14 | elif [[ -d ${CWD}/../../imageoptim-cli ]]; then 15 | IMAGEOPTIM="${CWD}/../../imageoptim-cli/bin/imageOptim" 16 | else 17 | echo "Could not find imageoptim-cli library" 18 | exit 1 19 | fi 20 | 21 | # Check if path to images specified 22 | if [[ -z "${BATCH_IMAGES}" ]]; then 23 | printf "No images were specified" 24 | exit 1 25 | fi 26 | 27 | # Toggle jpegmini flag 28 | if [[ "${2}" = "--jpegmini" ]]; then 29 | JPEGMINI="-j" 30 | fi 31 | 32 | # Let the optimization begin ... 33 | ${IMAGEOPTIM} -a -c -q ${JPEGMINI} -d ${BATCH_IMAGES} > ${LOGFILE} 34 | EXIT_STATUS=$? 35 | 36 | # Output result 37 | if [[ ${EXIT_STATUS} -eq 0 ]]; then 38 | if [[ -f ${LOGFILE} ]]; then 39 | cat ${LOGFILE} | grep TOTAL 40 | fi 41 | else 42 | cat ${LOGFILE} | grep Error: 43 | fi 44 | 45 | # Clean up 46 | if [[ -f ${LOGFILE} ]]; then 47 | rm ${LOGFILE} 48 | fi 49 | 50 | exit ${EXIT_STATUS} -------------------------------------------------------------------------------- /test/_jscs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Lint source files using JSCS 3 | */ 4 | 5 | require ('mocha-jscs') () -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test Gulp Imageoptim 3 | */ 4 | 5 | var expect = require ('chai').expect; 6 | var optimizer = require ('../'); 7 | 8 | 9 | describe ('Test Gulp Imageoptim', function () { 10 | 11 | it ('Should be an object', function () { 12 | expect (optimizer).to.be.a ('object'); 13 | }) 14 | 15 | it ('Should have imageoptim-cli dependency', function () { 16 | var path = require.resolve ('imageoptim-cli'); 17 | 18 | expect (path).to.be.a ('string').and.to.contain ('imageoptim-cli/bin/imageOptim'); 19 | }) 20 | 21 | it ('Should have method: optimize', function () { 22 | expect (optimizer['optimize']).to.be.a ('function'); 23 | }) 24 | 25 | }) --------------------------------------------------------------------------------