├── .gitignore ├── public ├── .gitignore └── test_image.jpg ├── pixie.config.json.example ├── package.json ├── LICENSE.md ├── README.md ├── bin └── pixie └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /pixie.config.json 3 | -------------------------------------------------------------------------------- /public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !test_image.jpg -------------------------------------------------------------------------------- /public/test_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/pixie/master/public/test_image.jpg -------------------------------------------------------------------------------- /pixie.config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "port": 3000, 3 | "root": null, 4 | "hashSecret": "SomeSecretString", 5 | "hashLength": 8, 6 | "resizeStrategy": "gravity" 7 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pixie", 3 | "description": "A simple, on-the-fly image resizing server written in NodeJS.", 4 | "version": "0.1.0", 5 | "license": "MIT", 6 | "private": true, 7 | "author": { 8 | "name": "Colin Viebrock", 9 | "email": "colin@viebrock.ca", 10 | "url": "http://viebrock.ca/" 11 | }, 12 | "dependencies": { 13 | "cosmiconfig": "^3.0.1", 14 | "dotenv": "^4.0.0", 15 | "express": "^4.15.4", 16 | "fs": "^0.0.1-security", 17 | "md5": "^2.2.1", 18 | "path": "^0.12.7", 19 | "sharp": "^0.18.4" 20 | }, 21 | "scripts": { 22 | "start": "node ./bin/pixie" 23 | }, 24 | "bin": "./bin/pixie" 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Colin Viebrock 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 all 13 | > 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 THE 21 | > SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pixie 2 | 3 | A simple, on-the-fly image resizing server written in NodeJS. 4 | 5 | > **NOTE**: Not production-ready yet, and my JS isn't the best so _caveat emptor_. 6 | 7 | 8 | 9 | ## Installation 10 | 11 | Get the source code (clone it, download a zip, whatever), then: 12 | 13 | ``` shell 14 | cd pixie 15 | cp pixie.config.json.example pixie.config.json 16 | yarn 17 | yarnpkg start 18 | ``` 19 | 20 | There are probably some libraries that your OS will need to build the 21 | [sharp](http://sharp.dimens.io/) image resizing library that Pixie uses. 22 | See their docs for more details on what prerequisites you might need 23 | and how to get them. 24 | 25 | After starting Pixie, it will log information about any new files it 26 | generates, and any errors, to stdout. 27 | 28 | 29 | 30 | ## Configuration 31 | 32 | Configuration is handled by the `.env` file. 33 | 34 | #### PORT 35 | 36 | The port that Pixie will listen on. Defaults to 3000. 37 | 38 | #### PATH 39 | 40 | The root path for images that will be served. Defaults to the `public` 41 | subdirectory of Pixie, but you probably want to set this to the (absolute) 42 | storage path of your other application that is using Pixie. 43 | 44 | #### HASH_KEY 45 | 46 | A random string that is used to generate image hashes, mitigating DOS attacks. See 47 | [Hashing](#hashing) below for more details. If left empty, then no hash 48 | protection is enforced. 49 | 50 | #### HASH_LENGTH 51 | 52 | The length of the generated hash strings. Defaults to 8. 53 | 54 | 55 | 56 | ## Requesting Images 57 | 58 | Any original file that already exists in the `ROOT` directory will be served 59 | immediately (via ExpressJS's `use()` middleware). 60 | 61 | A request for a resized image takes the following format: 62 | 63 | ``` 64 | FILENAME~~WIDTHxHEIGHT[~HASH].EXT[.FORMAT] 65 | ``` 66 | 67 | So, for example if the original image is `test_image.jpg`, and you would like a 68 | 160x90 JPEG version of the image, the file would be called: 69 | 70 | ``` 71 | test_image~~160x90.jpg 72 | ``` 73 | 74 | If you wanted a .webp version of the same image, simply add `.webp` on to the 75 | file: 76 | 77 | ``` 78 | test_image~~160x90.jpg.webp 79 | ``` 80 | 81 | If you want to only specify one of the dimensions (width or height) and have 82 | Pixie resize the image while maintaining aspect ratio, then just set the other 83 | dimension as zero: 84 | 85 | ``` 86 | test_image~~320x0.jpg 87 | ``` 88 | 89 | Generated files are served and also stored in the `ROOT` directory, so that 90 | subsequent requests to that file can be served immediately without requiring 91 | resizing again. 92 | 93 | If you have hashing enabled -- which you should! -- then the hash is added to 94 | the requested file name: 95 | 96 | ``` 97 | test_image~~160x90~60609adb.jpg 98 | test_image~~160x90~60609adb.jpg.webp 99 | ``` 100 | 101 | 102 | 103 | ## Hashing 104 | 105 | You probably don't want Pixie to generate images of arbitrary sizes. To prevent 106 | this, a hashing algorithm is implemented to verify that the requested image is 107 | valid. The hash is the MD5 of the original file name, the new width and height 108 | of the resized image (in a `WWWxHHH` formatted string), and the secret `HASH_KEY` 109 | configuration value. This hash is then truncated to the rightmost number of 110 | characters as defined by `HASH_LENGTH` (simply to keep file names a reasonable 111 | length). 112 | 113 | Some sample code (in PHP): 114 | 115 | ```php 116 | define('HASH_KEY', 'SomeSecretString'); 117 | define('HASH_LENGTH', 8); 118 | 119 | $file = '/path/to/test_image.jpg'; 120 | $newWidth = 160; 121 | $newHeight = 90; 122 | 123 | $hash = md5($file . $newWidth . 'x' . $newHeight . HASH_KEY); 124 | $hash = substr($hash, -1 * HASH_LENGTH); 125 | ``` 126 | 127 | As long as you maintain consistency between the key and length in both Pixie's 128 | configuration and your application, then resized files will have deterministic 129 | names. 130 | 131 | 132 | 133 | ## Issues / Planned Features 134 | 135 | Pixie relies on Node and ExpressJS to deal with race conditions (e.g. thousands 136 | of requests for the same image that needs to be generated). This could be an 137 | issue on high-traffic sites, but Node's single thread system should take care 138 | of things. More work is planned in this area. 139 | 140 | Image sizes are restricted to 9999x9999 pixels. This could be made into 141 | configurable options, but those seem like reasonable limits for most cases. 142 | 143 | There is no consideration for restricting images from being up-sized (and thus 144 | possibly losing some detail). Again, this could be configurable in future 145 | versions. 146 | 147 | 148 | ## Copyright and License 149 | 150 | [Pixie](https://github.com/cviebrock/pixie) was written by 151 | [Colin Viebrock](http://viebrock.ca) and is released under the 152 | [MIT License](LICENSE.md). 153 | 154 | Copyright (c) 2017 Colin Viebrock 155 | -------------------------------------------------------------------------------- /bin/pixie: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Load configuration 5 | */ 6 | const cosmicconfig = require('cosmiconfig'); 7 | const PIXIE = cosmicconfig('pixie', {sync: true, rc: 'pixie.config.json'}) 8 | .load(__dirname); 9 | 10 | if (PIXIE === null) { 11 | console.error('No pixie.config.json file found!'); 12 | process.exit(1); 13 | } 14 | 15 | console.info('Loading configuration from', PIXIE.filepath); 16 | 17 | /** 18 | * Load dependencies, and set up exceptions 19 | */ 20 | const path = require('path'), 21 | express = require('express'), 22 | md5 = require('md5'), 23 | sharp = require('sharp'), 24 | packageVersion = process.env.npm_package_version, 25 | PixieException = (message, status) => { 26 | this.name = 'PixieException'; 27 | this.message = message; 28 | this.status = status; 29 | 30 | return this; 31 | }, 32 | outputFormats = { 33 | 'jpg': 'jpeg', 34 | 'webp': 'webp', 35 | }, 36 | cropStrategies = { 37 | 'north': sharp.gravity.north, 38 | 'northeast': sharp.gravity.northeast, 39 | 'east': sharp.gravity.east, 40 | 'southeast': sharp.gravity.southeast, 41 | 'south': sharp.gravity.south, 42 | 'southwest': sharp.gravity.southwest, 43 | 'west': sharp.gravity.west, 44 | 'northwest': sharp.gravity.northwest, 45 | 'center': sharp.gravity.center, 46 | 'centre': sharp.gravity.centre, 47 | 'entropy': sharp.strategy.entropy, 48 | 'attention': sharp.strategy.attention, 49 | }; 50 | 51 | /** 52 | * Build the express app 53 | */ 54 | 55 | const app = express(); 56 | app.set('x-powered-by', false); 57 | /** 58 | * Initialize the path for existing assets 59 | */ 60 | 61 | const htmlPath = path.normalize(PIXIE.config.root || path.join(__dirname, '..', 'public')); 62 | app.use(express.static(htmlPath, { 63 | index: false, 64 | maxAge: 0, 65 | fallthrough: true 66 | })); 67 | 68 | /** 69 | * Look for paths that have resizing data 70 | */ 71 | app.get(/(.*)~~(\d{1,4})x(\d{1,4})(~[0-9a-f]+)?\.(.*)/, function (req, res) { 72 | res.header('X-Pixie', packageVersion); 73 | 74 | const cropStrategy = PIXIE.config.resizeStrategy || 'center'; 75 | 76 | try { 77 | 78 | let {0: file, 1: width, 2: height, 3: hash, 4: extension} = req.params; 79 | 80 | // prevent resizing already resized images 81 | if (file.indexOf('~~') !== -1) { 82 | throw PixieException('Duplicate resize forbidden', 404); 83 | } 84 | 85 | // check for new image format request 86 | let newExtension = extension, 87 | oldExtension = extension, 88 | test = extension.split('.', 2); 89 | if (test.length === 2) { 90 | [oldExtension, newExtension] = test; 91 | } 92 | 93 | validateHashing(file, oldExtension, width, height, hash); 94 | 95 | const originalFile = file + '.' + oldExtension, 96 | originalPath = path.join(htmlPath, originalFile), 97 | resizedFile = req.originalUrl.substr(1), 98 | resizedPath = path.join(htmlPath, resizedFile); 99 | 100 | width = parseInt(width, 10) || null; 101 | height = parseInt(height, 10) || null; 102 | 103 | sharp(originalPath) 104 | .resize(width, height) 105 | .crop(cropStrategies[cropStrategy]) 106 | .toFormat(outputFormats[newExtension]) 107 | .toFile(resizedPath, function (err, info) { 108 | if (err) { 109 | throw PixieException(err.message, 500); 110 | } 111 | log(req, 200, 'Wrote ' + info.size + ' bytes'); 112 | res.sendFile(resizedPath); 113 | }); 114 | 115 | } catch (e) { 116 | log(req, e.status, e.message); 117 | return res.sendStatus(e.status).end(); 118 | } 119 | }); 120 | 121 | /** 122 | * Otherwise, abort 123 | */ 124 | 125 | app.get('*', (req, res) => { 126 | res.header('X-Pixie', packageVersion); 127 | log(req, 404); 128 | return res.sendStatus(404).end(); 129 | }); 130 | 131 | /** 132 | * Start the server 133 | */ 134 | 135 | const port = PIXIE.config.port || 3000; 136 | let server = app.listen(port, () => { 137 | const host = 'localhost', 138 | port = server.address().port; 139 | console.log('Pixie server starting'); 140 | console.log('Static asset path: ' + htmlPath); 141 | if (PIXIE.config.hashSecret) { 142 | console.log('Enforce hashing: enabled; length ' + PIXIE.config.hashLength); 143 | } else { 144 | console.log('Enforce hashing: disabled'); 145 | } 146 | console.log('Listening on: //' + host + ':' + port + '/'); 147 | }); 148 | 149 | 150 | const log = (req, status, message) => { 151 | if (typeof message === 'undefined') { 152 | message = '-'; 153 | } 154 | 155 | console.log( 156 | new Date().toISOString(), 157 | req.ip, 158 | 'GET', req.originalUrl, 159 | req.get('Referer') || '-', 160 | status, 161 | message 162 | ); 163 | }; 164 | 165 | const validateHashing = (file, extension, width, height, hash) => { 166 | 167 | // if we aren't hashing, return nothing 168 | if (!PIXIE.config.hashSecret) { 169 | return; 170 | } 171 | 172 | // if no has was provided, throw error 173 | if (typeof hash === 'undefined') { 174 | throw PixieException('Missing hash', 403); 175 | } 176 | 177 | const filename = file + '.' + extension, 178 | generatedHash = '~' + md5(filename + width + 'x' + height + PIXIE.config.hashSecret) 179 | .substr(-1 * PIXIE.config.hashLength); 180 | 181 | // confirm generated hash equals given one 182 | if (generatedHash !== hash) { 183 | throw PixieException('Invalid hash (expected ' + generatedHash + ')', 403); 184 | } 185 | }; 186 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | accepts@~1.3.3: 6 | version "1.3.4" 7 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" 8 | dependencies: 9 | mime-types "~2.1.16" 10 | negotiator "0.6.1" 11 | 12 | argparse@^1.0.7: 13 | version "1.0.9" 14 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" 15 | dependencies: 16 | sprintf-js "~1.0.2" 17 | 18 | array-flatten@1.1.1: 19 | version "1.1.1" 20 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 21 | 22 | caw@^2.0.0: 23 | version "2.0.1" 24 | resolved "https://registry.yarnpkg.com/caw/-/caw-2.0.1.tgz#6c3ca071fc194720883c2dc5da9b074bfc7e9e95" 25 | dependencies: 26 | get-proxy "^2.0.0" 27 | isurl "^1.0.0-alpha5" 28 | tunnel-agent "^0.6.0" 29 | url-to-options "^1.0.1" 30 | 31 | charenc@~0.0.1: 32 | version "0.0.2" 33 | resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" 34 | 35 | chownr@^1.0.1: 36 | version "1.0.1" 37 | resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" 38 | 39 | color-convert@^1.8.2: 40 | version "1.9.0" 41 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" 42 | dependencies: 43 | color-name "^1.1.1" 44 | 45 | color-name@^1.0.0, color-name@^1.1.1: 46 | version "1.1.3" 47 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 48 | 49 | color-string@^1.4.0: 50 | version "1.5.2" 51 | resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.2.tgz#26e45814bc3c9a7cbd6751648a41434514a773a9" 52 | dependencies: 53 | color-name "^1.0.0" 54 | simple-swizzle "^0.2.2" 55 | 56 | color@^2.0.0: 57 | version "2.0.0" 58 | resolved "https://registry.yarnpkg.com/color/-/color-2.0.0.tgz#e0c9972d1e969857004b101eaa55ceab5961d67d" 59 | dependencies: 60 | color-convert "^1.8.2" 61 | color-string "^1.4.0" 62 | 63 | config-chain@^1.1.11: 64 | version "1.1.11" 65 | resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.11.tgz#aba09747dfbe4c3e70e766a6e41586e1859fc6f2" 66 | dependencies: 67 | ini "^1.3.4" 68 | proto-list "~1.2.1" 69 | 70 | content-disposition@0.5.2: 71 | version "0.5.2" 72 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" 73 | 74 | content-type@~1.0.2: 75 | version "1.0.4" 76 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 77 | 78 | cookie-signature@1.0.6: 79 | version "1.0.6" 80 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 81 | 82 | cookie@0.3.1: 83 | version "0.3.1" 84 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" 85 | 86 | cosmiconfig@^3.0.1: 87 | version "3.0.1" 88 | resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-3.0.1.tgz#d290e2b657a7f3a335257a0d306587836650fdc0" 89 | dependencies: 90 | is-directory "^0.3.1" 91 | js-yaml "^3.9.0" 92 | parse-json "^3.0.0" 93 | require-from-string "^2.0.1" 94 | 95 | crypt@~0.0.1: 96 | version "0.0.2" 97 | resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" 98 | 99 | debug@2.6.9: 100 | version "2.6.9" 101 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 102 | dependencies: 103 | ms "2.0.0" 104 | 105 | decompress-response@^3.3.0: 106 | version "3.3.0" 107 | resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" 108 | dependencies: 109 | mimic-response "^1.0.0" 110 | 111 | depd@1.1.1, depd@~1.1.1: 112 | version "1.1.1" 113 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" 114 | 115 | destroy@~1.0.4: 116 | version "1.0.4" 117 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 118 | 119 | detect-libc@^0.2.0: 120 | version "0.2.0" 121 | resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-0.2.0.tgz#47fdf567348a17ec25fcbf0b9e446348a76f9fb5" 122 | 123 | dotenv@^4.0.0: 124 | version "4.0.0" 125 | resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d" 126 | 127 | ee-first@1.1.1: 128 | version "1.1.1" 129 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 130 | 131 | encodeurl@~1.0.1: 132 | version "1.0.1" 133 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" 134 | 135 | error-ex@^1.3.1: 136 | version "1.3.1" 137 | resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" 138 | dependencies: 139 | is-arrayish "^0.2.1" 140 | 141 | escape-html@~1.0.3: 142 | version "1.0.3" 143 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 144 | 145 | esprima@^4.0.0: 146 | version "4.0.0" 147 | resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" 148 | 149 | etag@~1.8.0, etag@~1.8.1: 150 | version "1.8.1" 151 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 152 | 153 | express@^4.15.4: 154 | version "4.15.5" 155 | resolved "https://registry.yarnpkg.com/express/-/express-4.15.5.tgz#670235ca9598890a5ae8170b83db722b842ed927" 156 | dependencies: 157 | accepts "~1.3.3" 158 | array-flatten "1.1.1" 159 | content-disposition "0.5.2" 160 | content-type "~1.0.2" 161 | cookie "0.3.1" 162 | cookie-signature "1.0.6" 163 | debug "2.6.9" 164 | depd "~1.1.1" 165 | encodeurl "~1.0.1" 166 | escape-html "~1.0.3" 167 | etag "~1.8.0" 168 | finalhandler "~1.0.6" 169 | fresh "0.5.2" 170 | merge-descriptors "1.0.1" 171 | methods "~1.1.2" 172 | on-finished "~2.3.0" 173 | parseurl "~1.3.1" 174 | path-to-regexp "0.1.7" 175 | proxy-addr "~1.1.5" 176 | qs "6.5.0" 177 | range-parser "~1.2.0" 178 | send "0.15.6" 179 | serve-static "1.12.6" 180 | setprototypeof "1.0.3" 181 | statuses "~1.3.1" 182 | type-is "~1.6.15" 183 | utils-merge "1.0.0" 184 | vary "~1.1.1" 185 | 186 | finalhandler@~1.0.6: 187 | version "1.0.6" 188 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.6.tgz#007aea33d1a4d3e42017f624848ad58d212f814f" 189 | dependencies: 190 | debug "2.6.9" 191 | encodeurl "~1.0.1" 192 | escape-html "~1.0.3" 193 | on-finished "~2.3.0" 194 | parseurl "~1.3.2" 195 | statuses "~1.3.1" 196 | unpipe "~1.0.0" 197 | 198 | forwarded@~0.1.0: 199 | version "0.1.2" 200 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" 201 | 202 | fresh@0.5.2: 203 | version "0.5.2" 204 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 205 | 206 | fs@^0.0.1-security: 207 | version "0.0.1-security" 208 | resolved "https://registry.yarnpkg.com/fs/-/fs-0.0.1-security.tgz#8a7bd37186b6dddf3813f23858b57ecaaf5e41d4" 209 | 210 | get-proxy@^2.0.0: 211 | version "2.1.0" 212 | resolved "https://registry.yarnpkg.com/get-proxy/-/get-proxy-2.1.0.tgz#349f2b4d91d44c4d4d4e9cba2ad90143fac5ef93" 213 | dependencies: 214 | npm-conf "^1.1.0" 215 | 216 | has-symbol-support-x@^1.4.1: 217 | version "1.4.1" 218 | resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.4.1.tgz#66ec2e377e0c7d7ccedb07a3a84d77510ff1bc4c" 219 | 220 | has-to-string-tag-x@^1.2.0: 221 | version "1.4.1" 222 | resolved "https://registry.yarnpkg.com/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz#a045ab383d7b4b2012a00148ab0aa5f290044d4d" 223 | dependencies: 224 | has-symbol-support-x "^1.4.1" 225 | 226 | http-errors@~1.6.2: 227 | version "1.6.2" 228 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" 229 | dependencies: 230 | depd "1.1.1" 231 | inherits "2.0.3" 232 | setprototypeof "1.0.3" 233 | statuses ">= 1.3.1 < 2" 234 | 235 | inherits@2.0.1: 236 | version "2.0.1" 237 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" 238 | 239 | inherits@2.0.3: 240 | version "2.0.3" 241 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 242 | 243 | ini@^1.3.4: 244 | version "1.3.4" 245 | resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" 246 | 247 | ipaddr.js@1.4.0: 248 | version "1.4.0" 249 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.4.0.tgz#296aca878a821816e5b85d0a285a99bcff4582f0" 250 | 251 | is-arrayish@^0.2.1: 252 | version "0.2.1" 253 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" 254 | 255 | is-arrayish@^0.3.1: 256 | version "0.3.1" 257 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.1.tgz#c2dfc386abaa0c3e33c48db3fe87059e69065efd" 258 | 259 | is-buffer@~1.1.1: 260 | version "1.1.5" 261 | resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc" 262 | 263 | is-directory@^0.3.1: 264 | version "0.3.1" 265 | resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" 266 | 267 | is-object@^1.0.1: 268 | version "1.0.1" 269 | resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470" 270 | 271 | isurl@^1.0.0-alpha5: 272 | version "1.0.0" 273 | resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67" 274 | dependencies: 275 | has-to-string-tag-x "^1.2.0" 276 | is-object "^1.0.1" 277 | 278 | js-yaml@^3.9.0: 279 | version "3.10.0" 280 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" 281 | dependencies: 282 | argparse "^1.0.7" 283 | esprima "^4.0.0" 284 | 285 | md5@^2.2.1: 286 | version "2.2.1" 287 | resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" 288 | dependencies: 289 | charenc "~0.0.1" 290 | crypt "~0.0.1" 291 | is-buffer "~1.1.1" 292 | 293 | media-typer@0.3.0: 294 | version "0.3.0" 295 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 296 | 297 | merge-descriptors@1.0.1: 298 | version "1.0.1" 299 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 300 | 301 | methods@~1.1.2: 302 | version "1.1.2" 303 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 304 | 305 | mime-db@~1.30.0: 306 | version "1.30.0" 307 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" 308 | 309 | mime-types@~2.1.15, mime-types@~2.1.16: 310 | version "2.1.17" 311 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" 312 | dependencies: 313 | mime-db "~1.30.0" 314 | 315 | mime@1.3.4: 316 | version "1.3.4" 317 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" 318 | 319 | mimic-response@^1.0.0: 320 | version "1.0.0" 321 | resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.0.tgz#df3d3652a73fded6b9b0b24146e6fd052353458e" 322 | 323 | minimist@0.0.8: 324 | version "0.0.8" 325 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 326 | 327 | minipass@^2.0.0, minipass@^2.0.2: 328 | version "2.2.1" 329 | resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.2.1.tgz#5ada97538b1027b4cf7213432428578cb564011f" 330 | dependencies: 331 | yallist "^3.0.0" 332 | 333 | minizlib@^1.0.3: 334 | version "1.0.3" 335 | resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.0.3.tgz#d5c1abf77be154619952e253336eccab9b2a32f5" 336 | dependencies: 337 | minipass "^2.0.0" 338 | 339 | mkdirp@^0.5.0: 340 | version "0.5.1" 341 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 342 | dependencies: 343 | minimist "0.0.8" 344 | 345 | ms@2.0.0: 346 | version "2.0.0" 347 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 348 | 349 | nan@^2.6.2: 350 | version "2.7.0" 351 | resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46" 352 | 353 | negotiator@0.6.1: 354 | version "0.6.1" 355 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 356 | 357 | npm-conf@^1.1.0: 358 | version "1.1.2" 359 | resolved "https://registry.yarnpkg.com/npm-conf/-/npm-conf-1.1.2.tgz#170a2c48a0c6ad0495f03f87aec2da11ef47a525" 360 | dependencies: 361 | config-chain "^1.1.11" 362 | pify "^3.0.0" 363 | 364 | on-finished@~2.3.0: 365 | version "2.3.0" 366 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 367 | dependencies: 368 | ee-first "1.1.1" 369 | 370 | once@^1.3.1: 371 | version "1.4.0" 372 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 373 | dependencies: 374 | wrappy "1" 375 | 376 | parse-json@^3.0.0: 377 | version "3.0.0" 378 | resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-3.0.0.tgz#fa6f47b18e23826ead32f263e744d0e1e847fb13" 379 | dependencies: 380 | error-ex "^1.3.1" 381 | 382 | parseurl@~1.3.1, parseurl@~1.3.2: 383 | version "1.3.2" 384 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" 385 | 386 | path-to-regexp@0.1.7: 387 | version "0.1.7" 388 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 389 | 390 | path@^0.12.7: 391 | version "0.12.7" 392 | resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f" 393 | dependencies: 394 | process "^0.11.1" 395 | util "^0.10.3" 396 | 397 | pify@^3.0.0: 398 | version "3.0.0" 399 | resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" 400 | 401 | process@^0.11.1: 402 | version "0.11.10" 403 | resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" 404 | 405 | proto-list@~1.2.1: 406 | version "1.2.4" 407 | resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" 408 | 409 | proxy-addr@~1.1.5: 410 | version "1.1.5" 411 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.5.tgz#71c0ee3b102de3f202f3b64f608d173fcba1a918" 412 | dependencies: 413 | forwarded "~0.1.0" 414 | ipaddr.js "1.4.0" 415 | 416 | qs@6.5.0: 417 | version "6.5.0" 418 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.0.tgz#8d04954d364def3efc55b5a0793e1e2c8b1e6e49" 419 | 420 | range-parser@~1.2.0: 421 | version "1.2.0" 422 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" 423 | 424 | require-from-string@^2.0.1: 425 | version "2.0.1" 426 | resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.1.tgz#c545233e9d7da6616e9d59adfb39fc9f588676ff" 427 | 428 | safe-buffer@^5.0.1: 429 | version "5.1.1" 430 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 431 | 432 | semver@^5.3.0: 433 | version "5.4.1" 434 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" 435 | 436 | send@0.15.6: 437 | version "0.15.6" 438 | resolved "https://registry.yarnpkg.com/send/-/send-0.15.6.tgz#20f23a9c925b762ab82705fe2f9db252ace47e34" 439 | dependencies: 440 | debug "2.6.9" 441 | depd "~1.1.1" 442 | destroy "~1.0.4" 443 | encodeurl "~1.0.1" 444 | escape-html "~1.0.3" 445 | etag "~1.8.1" 446 | fresh "0.5.2" 447 | http-errors "~1.6.2" 448 | mime "1.3.4" 449 | ms "2.0.0" 450 | on-finished "~2.3.0" 451 | range-parser "~1.2.0" 452 | statuses "~1.3.1" 453 | 454 | serve-static@1.12.6: 455 | version "1.12.6" 456 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.6.tgz#b973773f63449934da54e5beba5e31d9f4211577" 457 | dependencies: 458 | encodeurl "~1.0.1" 459 | escape-html "~1.0.3" 460 | parseurl "~1.3.2" 461 | send "0.15.6" 462 | 463 | setprototypeof@1.0.3: 464 | version "1.0.3" 465 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" 466 | 467 | sharp@^0.18.4: 468 | version "0.18.4" 469 | resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.18.4.tgz#fe329c0f06896c28aa24376df1fff02ae57f2d34" 470 | dependencies: 471 | caw "^2.0.0" 472 | color "^2.0.0" 473 | detect-libc "^0.2.0" 474 | nan "^2.6.2" 475 | semver "^5.3.0" 476 | simple-get "^2.7.0" 477 | tar "^3.1.5" 478 | 479 | simple-concat@^1.0.0: 480 | version "1.0.0" 481 | resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6" 482 | 483 | simple-get@^2.7.0: 484 | version "2.7.0" 485 | resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-2.7.0.tgz#ad37f926d08129237ff08c4f2edfd6f10e0380b5" 486 | dependencies: 487 | decompress-response "^3.3.0" 488 | once "^1.3.1" 489 | simple-concat "^1.0.0" 490 | 491 | simple-swizzle@^0.2.2: 492 | version "0.2.2" 493 | resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" 494 | dependencies: 495 | is-arrayish "^0.3.1" 496 | 497 | sprintf-js@~1.0.2: 498 | version "1.0.3" 499 | resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" 500 | 501 | "statuses@>= 1.3.1 < 2", statuses@~1.3.1: 502 | version "1.3.1" 503 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" 504 | 505 | tar@^3.1.5: 506 | version "3.2.1" 507 | resolved "https://registry.yarnpkg.com/tar/-/tar-3.2.1.tgz#9aa8e41c88f09e76c166075bc71f93d5166e61b1" 508 | dependencies: 509 | chownr "^1.0.1" 510 | minipass "^2.0.2" 511 | minizlib "^1.0.3" 512 | mkdirp "^0.5.0" 513 | yallist "^3.0.2" 514 | 515 | tunnel-agent@^0.6.0: 516 | version "0.6.0" 517 | resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" 518 | dependencies: 519 | safe-buffer "^5.0.1" 520 | 521 | type-is@~1.6.15: 522 | version "1.6.15" 523 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" 524 | dependencies: 525 | media-typer "0.3.0" 526 | mime-types "~2.1.15" 527 | 528 | unpipe@~1.0.0: 529 | version "1.0.0" 530 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 531 | 532 | url-to-options@^1.0.1: 533 | version "1.0.1" 534 | resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" 535 | 536 | util@^0.10.3: 537 | version "0.10.3" 538 | resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" 539 | dependencies: 540 | inherits "2.0.1" 541 | 542 | utils-merge@1.0.0: 543 | version "1.0.0" 544 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" 545 | 546 | vary@~1.1.1: 547 | version "1.1.2" 548 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 549 | 550 | wrappy@1: 551 | version "1.0.2" 552 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 553 | 554 | yallist@^3.0.0, yallist@^3.0.2: 555 | version "3.0.2" 556 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9" 557 | --------------------------------------------------------------------------------