diff --git a/docs/command-line-arguments.md b/docs/command-line-arguments.md index afe37887..59781c95 100644 --- a/docs/command-line-arguments.md +++ b/docs/command-line-arguments.md @@ -22,6 +22,9 @@ yarn loki test -- --port 9009 | **`--difference`** | Path to image diff folder | `./.loki/difference` | | **`--diffingEngine`** | What diffing engine to use, currently supported are `looks-same` and `gm` | `gm` if available | | **`--chromeConcurrency`** | How many stories to test in parallel when using chrome | `4` | +| **`--chromeDockerUseExisting`**| Whether to use an already running container wiht headless chrome or spin up a new one. See also `--chromeDockerHost` and `--chromeDockerHost` | `false` | +| **`--chromeDockerHost`** | At which host to look for a docker container running headless chrome. Only relevant if chromeDockerUseExisting is set | `localhost` | +| **`--chromeDockerPort`** | At what port to look for a docker container running headless chrome. Only relevant if chromeDockerUseExisting is set | `9222` | | **`--chromeDockerImage`** | What docker image to use when running chrome | `yukinying/chrome-headless:63.0.3230.2` | | **`--chromeEnableAnimations`** | Enable CSS transitions and animations. | `false` | | **`--chromeFlags`** | Custom chrome flags. | `--headless --disable-gpu --hide-scrollbars` | diff --git a/docs/continuous-integration.md b/docs/continuous-integration.md index 91c579b2..277e16fb 100644 --- a/docs/continuous-integration.md +++ b/docs/continuous-integration.md @@ -9,3 +9,8 @@ build-storybook && loki --requireReference --reactUri file:./storybook-static ``` See the [loki react example project](https://github.com/oblador/loki/tree/master/examples/react) for a reference implementation of this approach. + +## Using Docker + +Some CIs like e. g. [CircleCI](http://circleci.com/) run the code in a Docker container already, so setting up a Chrome headless docker container inside the container isn't a viable solution. Instead it is possible to spin up a headless-chrome container in the CI and connect it to the test. +To do this you can run loki with `--chromeDockerUseExisting`. This way loki will look for an already running container at `localhost:9222` (or any other location you specify via `--chromeDockerHost` and `--chromeDockerHost`). We recommend to use the exact same image for headless chrome on your CI that you use for local testing to avoid discrepancies in rendering. diff --git a/package.json b/package.json index 28aa7e3d..d6847815 100644 --- a/package.json +++ b/package.json @@ -44,21 +44,25 @@ }, "license": "MIT", "dependencies": { + "bluebird": "^3.5.3", "chalk": "^2.4.1", "chrome-launcher": "^0.10.5", "chrome-remote-interface": "^0.27.0", "ci-info": "^1.6.0", "debug": "^4.1.0", "execa": "^1.0.0", + "express": "^4.16.4", "fs-extra": "^7.0.1", "get-port": "^4.0.0", "gm": "^1.23.1", "hoist-non-react-statics": "^3.2.1", + "lighthouse": "^2.2.1", "listr": "^0.14.3", "looks-same": "^4.0.0", "minimist": "^1.2.0", "osnap": "^1.1.0", "ramda": "^0.26.1", + "serve-handler": "^5.0.7", "shelljs": "^0.8.3", "transliteration": "^1.6.6", "wait-on": "^3.2.0", diff --git a/src/commands/test/default-options.json b/src/commands/test/default-options.json index 78e037bb..f259a323 100644 --- a/src/commands/test/default-options.json +++ b/src/commands/test/default-options.json @@ -1,5 +1,7 @@ { "chromeConcurrency": "4", + "chromeDockerHost": "localhost", + "chromeDockerPort": "9222", "chromeDockerImage": "yukinying/chrome-headless:63.0.3230.2", "chromeFlags": "--headless --disable-gpu --hide-scrollbars", "chromeLoadTimeout": "60000", diff --git a/src/commands/test/index.js b/src/commands/test/index.js index 2948f53a..c3698b50 100644 --- a/src/commands/test/index.js +++ b/src/commands/test/index.js @@ -63,7 +63,11 @@ async function test(args) { } try { - await runTests(configurations, options); + await runTests(configurations, options) + .then(() => process.exit(0)) + .catch((err) => { + throw err; + }); } catch (err) { if (err.name === 'ListrError') { const imageErrors = err.errors.filter( diff --git a/src/commands/test/parse-options.js b/src/commands/test/parse-options.js index 615fe115..7663be88 100644 --- a/src/commands/test/parse-options.js +++ b/src/commands/test/parse-options.js @@ -7,7 +7,7 @@ const getAbsoluteURL = require('./get-absolute-url'); function parseOptions(args, config) { const argv = minimist(args, { - boolean: ['requireReference', 'chromeEnableAnimations', 'verboseRenderer'], + boolean: ['requireReference', 'chromeEnableAnimations', 'chromeDockerUseExisting', 'verboseRenderer'], }); const $ = key => argv[key] || config[key] || defaults[key]; @@ -21,6 +21,9 @@ function parseOptions(args, config) { `http://${$('host')}:${argv.port || $('reactPort')}`, reactNativeUri: `ws://${$('host')}:${argv.port || $('reactNativePort')}`, chromeConcurrency: parseInt($('chromeConcurrency'), 10), + chromeDockerUseExisting: $('chromeDockerUseExisting'), + chromeDockerHost: $('chromeDockerHost'), + chromeDockerPort: $('chromeDockerPort'), chromeDockerImage: $('chromeDockerImage'), chromeEnableAnimations: $('chromeEnableAnimations'), chromeFlags: $('chromeFlags').split(' '), diff --git a/src/commands/test/run-tests.js b/src/commands/test/run-tests.js index d7c4d0b2..bd154f49 100644 --- a/src/commands/test/run-tests.js +++ b/src/commands/test/run-tests.js @@ -179,7 +179,10 @@ async function runTests(flatConfigurations, options) { createChromeDockerTarget({ baseUrl: options.reactUri, chromeDockerImage: options.chromeDockerImage, - chromeFlags: options.chromeFlags, + chromeDockerUseExisting: options.chromeDockerUseExisting, + chromeDockerHost: options.chromeDockerHost, + chromeDockerPort: options.chromeDockerPort, + chromeFlags: options.chromeFlags }), configurations, options.chromeConcurrency, diff --git a/src/targets/chrome/create-chrome-target.js b/src/targets/chrome/create-chrome-target.js index d82e9583..54b0afad 100644 --- a/src/targets/chrome/create-chrome-target.js +++ b/src/targets/chrome/create-chrome-target.js @@ -15,7 +15,7 @@ const { FetchingURLsError, ServerError } = require('../../errors'); const LOADING_STORIES_TIMEOUT = 60000; const CAPTURING_SCREENSHOT_TIMEOUT = 30000; -const REQUEST_STABILIZATION_TIMEOUT = 100; +const REQUEST_STABILIZATION_TIMEOUT = 1000; function createChromeTarget( start, @@ -149,8 +149,10 @@ function createChromeTarget( } debug(`Navigating to ${url}`); - await Promise.all([Page.navigate({ url }), awaitRequestsFinished()]); - + await Promise.all([ + Page.navigate({ url, transitionType: 'auto_subframe' }), + awaitRequestsFinished() + ]); debug('Awaiting runtime setup'); await executeFunctionWithWindow(awaitLokiReady); diff --git a/src/targets/chrome/create-existing-docker-target.js b/src/targets/chrome/create-existing-docker-target.js new file mode 100644 index 00000000..704a77ea --- /dev/null +++ b/src/targets/chrome/create-existing-docker-target.js @@ -0,0 +1,144 @@ +const debug = require('debug')('loki:chrome:existing'); +const CDP = require('chrome-remote-interface'); +const serveHandler = require('serve-handler'); +const http = require('http'); +const Promise = require('bluebird'); +const waitOnCDPAvailable = require('./helpers/wait-on-CDP-available'); +const getLocalIPAddress = require('./helpers/get-local-ip-address'); +const createChromeTarget = require('./create-chrome-target'); + +const SERVER_STARTUP_TIMEOUT = 10000; /* ms */ + +function createChromeDockerTarget({ + baseUrl = 'http://localhost:6006', + chromeFlags = ['--headless', '--disable-gpu', '--hide-scrollbars'], + chromeDockerHost = 'localhost', + chromeDockerPort = '9222' +}) { + let storybookUrl = baseUrl; + + const RUN_MODES = { + local: 'local', + remote: 'remote', + file: 'file' + }; + + let runMode = RUN_MODES.remote; + if (baseUrl.indexOf('http://localhost') === 0) { + runMode = RUN_MODES.local; + } else if (baseUrl.indexOf('file:') === 0) { + runMode = RUN_MODES.file; + } + debug(`Running in ${runMode} mode`); + + let server; + let storybookPort; + let storybookHost; + if (runMode === RUN_MODES.file) { + const staticPath = baseUrl.substr('file:'.length); + storybookPort = 8080; + const ip = getLocalIPAddress(); + if (!ip) { + throw new Error( + 'Unable to detect local IP address, try passing --host argument' + ); + } + storybookUrl = `http://${ip}:${storybookPort}`; + storybookHost = ip; + server = http.createServer((req, res) => + serveHandler(req, res, { + public: staticPath, + cleanUrls: false, + renderSingle: true + }) + ); + debug(`Serving files from ${staticPath}`); + } + + if (runMode === RUN_MODES.local) { + const ip = getLocalIPAddress(); + if (!ip) { + throw new Error( + 'Unable to detect local IP address, try passing --host argument' + ); + } + storybookUrl = baseUrl.replace('localhost', ip); + } + + debug(`Looking for storybook at ${storybookUrl}`); + + let dockerHost = chromeDockerHost; + if (chromeDockerHost.indexOf('localhost') === 0) { + const ip = getLocalIPAddress(); + if (!ip) { + throw new Error( + 'Unable to detect local IP address, try passing --host argument' + ); + } + dockerHost = chromeDockerHost.replace('localhost', ip); + } + + async function start() { + debug( + `Trying to connect to Chrome at http://${dockerHost}:${chromeDockerPort}` + ); + if (server) { + debug(`Serving storybook at http://${storybookHost}:${storybookPort}`); + server.listen({ host: storybookHost, port: storybookPort }); + await Promise.all([ + waitOnCDPAvailable(dockerHost, chromeDockerPort), + new Promise((resolve, reject) => { + server.addListener('listening', resolve); + server.addListener('error', reject); + }).timeout(SERVER_STARTUP_TIMEOUT) + ]); + debug('Set up complete'); + } else { + await waitOnCDPAvailable(dockerHost, chromeDockerPort); + } + } + + async function stop() { + if (server) { + const serverClosed = new Promise((resolve, reject) => { + server.on('close', resolve); + server.on('error', reject); + }); + server.close(); + await serverClosed; + } + } + + async function createNewDebuggerInstance() { + debug( + `Launching new tab with debugger at port ${dockerHost}:${chromeDockerPort}` + ); + const target = await CDP.New({ host: dockerHost, port: chromeDockerPort }); + debug(`Launched with target id ${target.id}`); + const client = await CDP({ + host: dockerHost, + port: chromeDockerPort, + target + }); + + client.close = () => { + debug('New closing tab'); + return CDP.Close({ + host: dockerHost, + port: chromeDockerPort, + id: target.id + }); + }; + + return client; + } + + return createChromeTarget( + start, + stop, + createNewDebuggerInstance, + storybookUrl + ); +} + +module.exports = createChromeDockerTarget; diff --git a/src/targets/chrome/docker.js b/src/targets/chrome/docker.js index 55927188..e9a93d87 100644 --- a/src/targets/chrome/docker.js +++ b/src/targets/chrome/docker.js @@ -1,44 +1,14 @@ const debug = require('debug')('loki:chrome:docker'); -const os = require('os'); const { execSync } = require('child_process'); const execa = require('execa'); -const waitOn = require('wait-on'); +const getLocalIPAddress = require('./helpers/get-local-ip-address'); +const waitOnCDPAvailable = require('./helpers/wait-on-CDP-available'); const CDP = require('chrome-remote-interface'); const fs = require('fs-extra'); const getRandomPort = require('get-port'); const { ensureDependencyAvailable } = require('../../dependency-detection'); const createChromeTarget = require('./create-chrome-target'); - -const getLocalIPAddress = () => { - const interfaces = os.networkInterfaces(); - const ips = Object.keys(interfaces) - .map(key => - interfaces[key] - .filter(({ family, internal }) => family === 'IPv4' && !internal) - .map(({ address }) => address) - ) - .reduce((acc, current) => acc.concat(current), []); - return ips[0]; -}; - -const waitOnCDPAvailable = (host, port) => - new Promise((resolve, reject) => { - waitOn( - { - resources: [`tcp:${host}:${port}`], - delay: 50, - interval: 100, - timeout: 5000, - }, - err => { - if (err) { - reject(err); - } else { - resolve(); - } - } - ); - }); +const createExistingDockerTarget = require('./create-existing-docker-target'); const getNetworkHost = async dockerId => { let host = '127.0.0.1'; @@ -72,11 +42,20 @@ function createChromeDockerTarget({ baseUrl = 'http://localhost:6006', chromeDockerImage = 'yukinying/chrome-headless', chromeFlags = ['--headless', '--disable-gpu', '--hide-scrollbars'], + chromeDockerUseExisting = false, + chromeDockerHost = 'localhost', + chromeDockerPort = '9222', }) { + if (chromeDockerUseExisting) { + return createExistingDockerTarget({ + baseUrl, chromeFlags, chromeDockerHost, chromeDockerPort + }) + } + let port; let dockerId; let host; - let dockerUrl = baseUrl; + let storybookUrl = baseUrl; const dockerPath = 'docker'; const runArgs = ['run', '--rm', '-d', '-P']; @@ -91,13 +70,13 @@ function createChromeDockerTarget({ 'Unable to detect local IP address, try passing --host argument' ); } - dockerUrl = baseUrl.replace('localhost', ip); + storybookUrl = baseUrl.replace('localhost', ip); } else if (baseUrl.indexOf('file:') === 0) { const staticPath = baseUrl.substr('file:'.length); const staticMountPath = '/var/loki'; runArgs.push('-v'); runArgs.push(`${staticPath}:${staticMountPath}`); - dockerUrl = `file://${staticMountPath}`; + storybookUrl = `file://${staticMountPath}`; } async function getIsImageDownloaded(imageName) { @@ -188,7 +167,7 @@ function createChromeDockerTarget({ start, stop, createNewDebuggerInstance, - dockerUrl, + storybookUrl, ensureImageDownloaded ); } diff --git a/src/targets/chrome/helpers/get-local-ip-address.js b/src/targets/chrome/helpers/get-local-ip-address.js new file mode 100644 index 00000000..bdd021c7 --- /dev/null +++ b/src/targets/chrome/helpers/get-local-ip-address.js @@ -0,0 +1,17 @@ +const os = require('os'); + +/** + * Returns the first external IPv4 address this machine can be reached at. + * @returns {String} The IPv4 address + */ +module.exports = function getLocalIPAddress () { + const interfaces = os.networkInterfaces(); + const ips = Object.keys(interfaces) + .map(key => + interfaces[key] + .filter(({ family, internal }) => family === 'IPv4' && !internal) + .map(({ address }) => address) + ) + .reduce((acc, current) => acc.concat(current), []); + return ips[0]; +}; diff --git a/src/targets/chrome/helpers/wait-on-CDP-available.js b/src/targets/chrome/helpers/wait-on-CDP-available.js new file mode 100644 index 00000000..d4f563e3 --- /dev/null +++ b/src/targets/chrome/helpers/wait-on-CDP-available.js @@ -0,0 +1,27 @@ +const waitOn = require('wait-on'); + +/** + * Waits for the Chrome DevTools Protocol to be available. It will timeout after 5 seconds. + * @param host The host where to look for CDP + * @param port THe port where to look for CDP + * @returns {Promise} Will resolve without content when CDP is available or error out otherwise + */ +module.exports = function waitOnCDPAvailable (host, port) { + return new Promise((resolve, reject) => { + waitOn( + { + resources: [`tcp:${host}:${port}`], + delay: 50, + interval: 100, + timeout: 5000, + }, + err => { + if (err) { + reject(err); + } else { + resolve(); + } + } + ); + }); +} diff --git a/yarn.lock b/yarn.lock index 48469d38..b4f77a5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -695,6 +695,11 @@ bluebird@^3.5.0, bluebird@~3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c" +bluebird@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7" + integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw== + boolbase@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -783,6 +788,11 @@ builtins@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= + cacache@^9.2.9, cacache@~9.2.9: version "9.2.9" resolved "https://registry.yarnpkg.com/cacache/-/cacache-9.2.9.tgz#f9d7ffe039851ec94c28290662afa4dd4bb9e8dd" @@ -1159,6 +1169,11 @@ contains-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" +content-disposition@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ= + convert-source-map@^1.1.0, convert-source-map@^1.4.0: version "1.5.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" @@ -1964,6 +1979,13 @@ fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" +fast-url-parser@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" + integrity sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0= + dependencies: + punycode "^1.3.2" + fb-watchman@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" @@ -4037,6 +4059,11 @@ mime-db@~1.30.0: version "1.30.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" +mime-db@~1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" + integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ== + mime-db@~1.35.0: version "1.35.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.35.0.tgz#0569d657466491283709663ad379a99b90d9ab47" @@ -4046,6 +4073,13 @@ mime-db@~1.37.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8" integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg== +mime-types@2.1.18: + version "2.1.18" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" + integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ== + dependencies: + mime-db "~1.33.0" + mime-types@^2.1.11: version "2.1.17" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" @@ -4075,7 +4109,7 @@ mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" -minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.3: +minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.3: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" dependencies: @@ -4903,7 +4937,7 @@ path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" -path-is-inside@^1.0.1, path-is-inside@^1.0.2, path-is-inside@~1.0.0, path-is-inside@~1.0.2: +path-is-inside@1.0.2, path-is-inside@^1.0.1, path-is-inside@^1.0.2, path-is-inside@~1.0.0, path-is-inside@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" @@ -4915,6 +4949,11 @@ path-parse@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" +path-to-regexp@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45" + integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ== + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -5115,14 +5154,14 @@ pumpify@^1.3.3: inherits "^2.0.1" pump "^1.0.0" +punycode@^1.3.2, punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + punycode@2.x.x, punycode@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - q@1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1" @@ -5151,6 +5190,11 @@ randomatic@^1.1.3: is-number "^3.0.0" kind-of "^4.0.0" +range-parser@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4= + rc@^1.0.1, rc@^1.1.6: version "1.2.1" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95" @@ -5697,6 +5741,20 @@ semver@~5.1.0: version "5.1.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.1.1.tgz#a3292a373e6f3e0798da0b20641b9a9c5bc47e19" +serve-handler@^5.0.7: + version "5.0.7" + resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-5.0.7.tgz#317877420925913e99e4dc228e67f6e5774e5387" + integrity sha512-PuLoJHAO2jj3p1fYWfXVHsEqNesx1+h+6qj0FIWrCe526ZtpDqeYuKA4knE5pjK9xoOVShoB+qGOP93EY46xEw== + dependencies: + bytes "3.0.0" + content-disposition "0.5.2" + fast-url-parser "1.1.3" + mime-types "2.1.18" + minimatch "3.0.4" + path-is-inside "1.0.2" + path-to-regexp "2.2.1" + range-parser "1.2.0" + set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"