From 38c0f42593411820459bc01b39a8c48884ad61e0 Mon Sep 17 00:00:00 2001 From: Ward Peeters Date: Fri, 8 Jan 2021 20:57:38 +0700 Subject: [PATCH] fix(gatsby-source-filesystem): fix broken stream with gzipped files (#28913) (cherry picked from commit a8b516fc59987a50dc8ccfa0aafbc6415414b009) --- .../gatsby-source-filesystem/package.json | 3 +- .../create-remote-file-node-integration.js | 268 ++++++++++++++++++ .../src/__tests__/create-remote-file-node.js | 15 +- .../src/__tests__/fixtures/dog-thumbnail.jpg | Bin 0 -> 19559 bytes .../src/__tests__/fixtures/gatsby-logo.svg | 5 + .../src/create-remote-file-node.js | 14 +- yarn.lock | 126 +++++++- 7 files changed, 413 insertions(+), 18 deletions(-) create mode 100644 packages/gatsby-source-filesystem/src/__tests__/create-remote-file-node-integration.js create mode 100644 packages/gatsby-source-filesystem/src/__tests__/fixtures/dog-thumbnail.jpg create mode 100644 packages/gatsby-source-filesystem/src/__tests__/fixtures/gatsby-logo.svg diff --git a/packages/gatsby-source-filesystem/package.json b/packages/gatsby-source-filesystem/package.json index 7a07b7333794f..bb33c6b1e0536 100644 --- a/packages/gatsby-source-filesystem/package.json +++ b/packages/gatsby-source-filesystem/package.json @@ -25,7 +25,8 @@ "@babel/cli": "^7.12.1", "@babel/core": "^7.12.3", "babel-preset-gatsby-package": "^0.10.0", - "cross-env": "^7.0.3" + "cross-env": "^7.0.3", + "msw": "^0.25.0" }, "homepage": "https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-source-filesystem#readme", "keywords": [ diff --git a/packages/gatsby-source-filesystem/src/__tests__/create-remote-file-node-integration.js b/packages/gatsby-source-filesystem/src/__tests__/create-remote-file-node-integration.js new file mode 100644 index 0000000000000..2efe48ad27002 --- /dev/null +++ b/packages/gatsby-source-filesystem/src/__tests__/create-remote-file-node-integration.js @@ -0,0 +1,268 @@ +import * as path from "path" +import * as zlib from "zlib" +import * as os from "os" +import { rest } from "msw" +import { setupServer } from "msw/node" +import { Writable } from "stream" +import got from "got" +import createRemoteFileNode from "../create-remote-file-node" + +const fs = jest.requireActual(`fs-extra`) + +const gotStream = jest.spyOn(got, `stream`) +const urlCount = new Map() + +async function getFileSize(file) { + const stat = await fs.stat(file) + + return stat.size +} + +/** + * A utility to help create file responses + * - Url with attempts will use maxBytes for x amount of time until it delivers the full response + * - MaxBytes indicates how much bytes we'll be sending + * + * @param {string} file File path on disk + * @param {Object} req Is the request object from msw + * @param {{ compress: boolean}} options Options for the getFilecontent (use gzip or not) + */ +async function getFileContent(file, req, options = {}) { + const cacheKey = req.url.origin + req.url.pathname + const maxRetry = req.url.searchParams.get(`attempts`) + const maxBytes = req.url.searchParams.get(`maxBytes`) + const currentRetryCount = urlCount.get(cacheKey) || 0 + urlCount.set(cacheKey, currentRetryCount + 1) + + let fileContentBuffer = await fs.readFile(file) + if (options.compress) { + fileContentBuffer = zlib.deflateSync(fileContentBuffer) + } + + const content = await new Promise(resolve => { + const fileStream = fs.createReadStream(file, { + end: + currentRetryCount < Number(maxRetry) + ? Number(maxBytes) + : Number.MAX_SAFE_INTEGER, + }) + + const writableStream = new Writable() + const result = [] + writableStream._write = (chunk, encoding, next) => { + result.push(chunk) + + next() + } + + writableStream.on(`finish`, () => { + resolve(Buffer.concat(result)) + }) + + // eslint-disable-next-line no-unused-vars + let stream = fileStream + if (options.compress) { + stream = stream.pipe(zlib.createDeflate()) + } + + stream.pipe(writableStream) + }) + + return { + content, + contentLength: + req.url.searchParams.get(`contentLength`) === `false` + ? undefined + : fileContentBuffer.length, + } +} + +const server = setupServer( + rest.get(`http://external.com/logo.svg`, async (req, res, ctx) => { + const { content, contentLength } = await getFileContent( + path.join(__dirname, `./fixtures/gatsby-logo.svg`), + req + ) + + return res( + ctx.set(`Content-Type`, `image/svg+xml`), + ctx.set(`Content-Length`, contentLength), + ctx.status(200), + ctx.body(content) + ) + }), + rest.get(`http://external.com/logo-gzip.svg`, async (req, res, ctx) => { + const { content, contentLength } = await getFileContent( + path.join(__dirname, `./fixtures/gatsby-logo.svg`), + req, + { + compress: true, + } + ) + + return res( + ctx.set(`Content-Type`, `image/svg+xml`), + ctx.set(`content-encoding`, `gzip`), + ctx.set(`Content-Length`, contentLength), + ctx.status(200), + ctx.body(content) + ) + }), + rest.get(`http://external.com/dog.jpg`, async (req, res, ctx) => { + const { content, contentLength } = await getFileContent( + path.join(__dirname, `./fixtures/dog-thumbnail.jpg`), + req + ) + + return res( + ctx.set(`Content-Type`, `image/svg+xml`), + ctx.set(`Content-Length`, contentLength), + ctx.status(200), + ctx.body(content) + ) + }) +) + +function createMockCache() { + const tmpDir = fs.mkdtempSync( + path.join(os.tmpdir(), `gatsby-source-filesystem-`) + ) + + return { + get: jest.fn(), + set: jest.fn(), + directory: tmpDir, + } +} + +const reporter = jest.fn(() => { + return {} +}) + +describe(`create-remote-file-node`, () => { + let cache + + beforeAll(() => { + cache = createMockCache() + // Establish requests interception layer before all tests. + server.listen() + }) + afterAll(() => { + if (cache) { + fs.removeSync(cache.directory) + } + + // Clean up after all tests are done, preventing this + // interception layer from affecting irrelevant tests. + server.close() + }) + + beforeEach(() => { + gotStream.mockClear() + urlCount.clear() + }) + + it(`downloads and create a file`, async () => { + const fileNode = await createRemoteFileNode({ + url: `http://external.com/logo.svg`, + store: {}, + getCache: () => cache, + createNode: jest.fn(), + createNodeId: jest.fn(), + reporter, + }) + + expect(fileNode.base).toBe(`logo.svg`) + expect(fileNode.size).toBe( + await getFileSize(path.join(__dirname, `./fixtures/gatsby-logo.svg`)) + ) + expect(gotStream).toBeCalledTimes(1) + }) + + it(`downloads and create a gzip file`, async () => { + const fileNode = await createRemoteFileNode({ + url: `http://external.com/logo-gzip.svg`, + store: {}, + getCache: () => cache, + createNode: jest.fn(), + createNodeId: jest.fn(), + reporter, + }) + + expect(fileNode.base).toBe(`logo-gzip.svg`) + expect(fileNode.size).toBe( + await getFileSize(path.join(__dirname, `./fixtures/gatsby-logo.svg`)) + ) + expect(gotStream).toBeCalledTimes(1) + }) + + it(`downloads and create a file`, async () => { + const fileNode = await createRemoteFileNode({ + url: `http://external.com/dog.jpg`, + store: {}, + getCache: () => cache, + createNode: jest.fn(), + createNodeId: jest.fn(), + reporter, + }) + + expect(fileNode.base).toBe(`dog.jpg`) + expect(fileNode.size).toBe( + await getFileSize(path.join(__dirname, `./fixtures/dog-thumbnail.jpg`)) + ) + expect(gotStream).toBeCalledTimes(1) + }) + + it(`doesn't retry when no content-length is given`, async () => { + const fileNode = await createRemoteFileNode({ + url: `http://external.com/logo-gzip.svg?attempts=1&maxBytes=300&contentLength=false`, + store: {}, + getCache: () => cache, + createNode: jest.fn(), + createNodeId: jest.fn(), + reporter, + }) + + expect(fileNode.base).toBe(`logo-gzip.svg`) + expect(fileNode.size).not.toBe( + await getFileSize(path.join(__dirname, `./fixtures/gatsby-logo.svg`)) + ) + expect(gotStream).toBeCalledTimes(1) + }) + + describe(`retries the download`, () => { + it(`Retries when gzip compression file is incomplete`, async () => { + const fileNode = await createRemoteFileNode({ + url: `http://external.com/logo-gzip.svg?attempts=1&maxBytes=300`, + store: {}, + getCache: () => cache, + createNode: jest.fn(), + createNodeId: jest.fn(), + reporter, + }) + + expect(fileNode.base).toBe(`logo-gzip.svg`) + expect(fileNode.size).toBe( + await getFileSize(path.join(__dirname, `./fixtures/gatsby-logo.svg`)) + ) + expect(gotStream).toBeCalledTimes(2) + }) + + it(`Retries when binary file is incomplete`, async () => { + const fileNode = await createRemoteFileNode({ + url: `http://external.com/dog.jpg?attempts=1&maxBytes=300`, + store: {}, + getCache: () => cache, + createNode: jest.fn(), + createNodeId: jest.fn(), + reporter, + }) + + expect(fileNode.base).toBe(`dog.jpg`) + expect(fileNode.size).toBe( + await getFileSize(path.join(__dirname, `./fixtures/dog-thumbnail.jpg`)) + ) + expect(gotStream).toBeCalledTimes(2) + }) + }) +}) diff --git a/packages/gatsby-source-filesystem/src/__tests__/create-remote-file-node.js b/packages/gatsby-source-filesystem/src/__tests__/create-remote-file-node.js index 102ff9c517dfa..240fa703be71d 100644 --- a/packages/gatsby-source-filesystem/src/__tests__/create-remote-file-node.js +++ b/packages/gatsby-source-filesystem/src/__tests__/create-remote-file-node.js @@ -100,11 +100,7 @@ describe(`create-remote-file-node`, () => { describe(`valid url`, () => { let uuid = 0 - const setup = ( - args = {}, - type = `response`, - response = { statusCode: 200 } - ) => { + const setup = (args = {}, response = { statusCode: 200 }) => { const url = `https://images.whatever.com/real-image-trust-me-${uuid}.png` const gotMock = { @@ -121,7 +117,7 @@ describe(`create-remote-file-node`, () => { got.stream.mockReturnValueOnce({ pipe: jest.fn(() => gotMock), on: jest.fn((mockType, mockCallback) => { - if (mockType === type) { + if (mockType === `response`) { // got throws on 404/500 so we mimic this behaviour if (response.statusCode === 404) { throw new Error(`Response code 404 (Not Found)`) @@ -129,6 +125,13 @@ describe(`create-remote-file-node`, () => { mockCallback(response) } + if (mockType === `downloadProgress`) { + mockCallback({ + progress: 1, + transferred: 1, + total: 1, + }) + } return gotMock }), diff --git a/packages/gatsby-source-filesystem/src/__tests__/fixtures/dog-thumbnail.jpg b/packages/gatsby-source-filesystem/src/__tests__/fixtures/dog-thumbnail.jpg new file mode 100644 index 0000000000000000000000000000000000000000..331026eae75de694bfc9c31af65ccd33ff5f316d GIT binary patch literal 19559 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 LwRHwQftvr>%9VBI literal 0 HcmV?d00001 diff --git a/packages/gatsby-source-filesystem/src/__tests__/fixtures/gatsby-logo.svg b/packages/gatsby-source-filesystem/src/__tests__/fixtures/gatsby-logo.svg new file mode 100644 index 0000000000000..0c233ea41d1e1 --- /dev/null +++ b/packages/gatsby-source-filesystem/src/__tests__/fixtures/gatsby-logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/gatsby-source-filesystem/src/create-remote-file-node.js b/packages/gatsby-source-filesystem/src/create-remote-file-node.js index dcedd301356f6..85ceb0a330c13 100644 --- a/packages/gatsby-source-filesystem/src/create-remote-file-node.js +++ b/packages/gatsby-source-filesystem/src/create-remote-file-node.js @@ -159,6 +159,14 @@ const requestRemoteNode = (url, headers, tmpFilename, httpOpts, attempt = 1) => }, ...httpOpts, }) + + let haveAllBytesBeenWritten = false + responseStream.on(`downloadProgress`, progress => { + if (progress.transferred === progress.total || progress.total === null) { + haveAllBytesBeenWritten = true + } + }) + const fsWriteStream = fs.createWriteStream(tmpFilename) responseStream.pipe(fsWriteStream) @@ -180,12 +188,12 @@ const requestRemoteNode = (url, headers, tmpFilename, httpOpts, attempt = 1) => responseStream.on(`response`, response => { resetTimeout() - const contentLength = - response.headers && Number(response.headers[`content-length`]) fsWriteStream.on(`finish`, () => { + fsWriteStream.close() + // We have an incomplete download - if (contentLength && contentLength !== fsWriteStream.bytesWritten) { + if (!haveAllBytesBeenWritten) { fs.removeSync(tmpFilename) if (attempt < INCOMPLETE_RETRY_LIMIT) { diff --git a/yarn.lock b/yarn.lock index 708a29ce2b754..71bfe89fa6105 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3325,6 +3325,11 @@ dependencies: "@types/node" ">= 8" +"@open-draft/until@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca" + integrity sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q== + "@pieh/friendly-errors-webpack-plugin@1.7.0-chalk-2": version "1.7.0-chalk-2" resolved "https://registry.yarnpkg.com/@pieh/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.7.0-chalk-2.tgz#2e9da9d3ade9d18d013333eb408c457d04eabac0" @@ -4020,6 +4025,11 @@ dependencies: "@types/node" "*" +"@types/cookie@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.0.tgz#14f854c0f93d326e39da6e3b6f34f7d37513d108" + integrity sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg== + "@types/debug@^0.0.30": version "0.0.30" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-0.0.30.tgz#dc1e40f7af3b9c815013a7860e6252f6352a84df" @@ -7402,6 +7412,15 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + clone-buffer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" @@ -7995,6 +8014,11 @@ cookie@0.4.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== +cookie@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" + integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== + cookies@0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90" @@ -8735,6 +8759,13 @@ debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6: dependencies: ms "^2.1.1" +debug@^4.3.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== + dependencies: + ms "2.1.2" + debug@~4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" @@ -11536,13 +11567,6 @@ gather-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/gather-stream/-/gather-stream-1.0.0.tgz#b33994af457a8115700d410f317733cbe7a0904b" -gatsby-design-tokens@^2.0.2: - version "2.0.13" - resolved "https://registry.yarnpkg.com/gatsby-design-tokens/-/gatsby-design-tokens-2.0.13.tgz#33f5fa84a399b821ae224b9921847d7b37c45600" - integrity sha512-I4i1pYG5y8yQ9uul1uvUZ1sy5skCOg33kHUUeYCWvW7vPdtBcQLgYqT2tLPQMMCc16ljbJF0DL4g52zQ7tVAhQ== - dependencies: - hex2rgba "^0.0.1" - gatsby-interface@^0.0.225: version "0.0.225" resolved "https://registry.yarnpkg.com/gatsby-interface/-/gatsby-interface-0.0.225.tgz#f2962d8f2e40163ff1bf908f3083ef634a3f4730" @@ -11652,7 +11676,7 @@ get-caller-file@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" -get-caller-file@^2.0.1: +get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -12391,6 +12415,11 @@ graphql@^14.6.0, graphql@^14.7.0: dependencies: iterall "^1.2.2" +graphql@^15.4.0: + version "15.4.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.4.0.tgz#e459dea1150da5a106486ba7276518b5295a4347" + integrity sha512-EB3zgGchcabbsU9cFe1j+yxdzKQKAbGUWRb13DsrsMN1yyfmmIq+2+L5MqVWcDCE4V89R5AyUOi7sMOGxdsYtA== + gray-matter@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-2.1.1.tgz#3042d9adec2a1ded6a7707a9ed2380f8a17a430e" @@ -12819,6 +12848,11 @@ header-case@^1.0.0: no-case "^2.2.0" upper-case "^1.1.3" +headers-utils@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/headers-utils/-/headers-utils-1.2.0.tgz#5e10d1bc9d2bccf789547afca5b991a3167241e8" + integrity sha512-4/BMXcWrJErw7JpM87gF8MNEXcIMLzepYZjNRv/P9ctgupl2Ywa3u1PgHtNhSRq84bHH9Ndlkdy7bSi+bZ9I9A== + hex-color-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" @@ -17312,6 +17346,25 @@ ms@2.1.2, ms@^2.0.0, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +msw@^0.25.0: + version "0.25.0" + resolved "https://registry.yarnpkg.com/msw/-/msw-0.25.0.tgz#a743cd2c11e7aec28b2597cb133afa8dc7044acc" + integrity sha512-KWW9Qwf4ecw7lgf7KM0gRfFLvfmUPw0ljCvii2IluhObZggoN/dBMFe5YJf1utnW7q9qHBc4oMNLrd8zFE6BfQ== + dependencies: + "@open-draft/until" "^1.0.3" + "@types/cookie" "^0.4.0" + chalk "^4.1.0" + chokidar "^3.4.2" + cookie "^0.4.1" + graphql "^15.4.0" + headers-utils "^1.2.0" + node-fetch "^2.6.1" + node-match-path "^0.6.0" + node-request-interceptor "^0.6.3" + statuses "^2.0.0" + strict-event-emitter "^0.1.0" + yargs "^16.2.0" + multicast-dns-service-types@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" @@ -17654,6 +17707,11 @@ node-libs-browser@^2.2.1: util "^0.11.0" vm-browserify "^1.0.1" +node-match-path@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/node-match-path/-/node-match-path-0.6.0.tgz#4674742f4e15854045198ebc40d8b9b2d64aa2f6" + integrity sha512-mld1LbiLaufULAYFPAWgNEG4P0ccL49otlL/nbF5VBQLATuzfS1BGYV1rjRMsxbc0vcnasikFqGHoKDFMQylMw== + node-modules-regexp@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" @@ -17711,6 +17769,16 @@ node-releases@^1.1.66: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.66.tgz#609bd0dc069381015cd982300bae51ab4f1b1814" integrity sha512-JHEQ1iWPGK+38VLB2H9ef2otU4l8s3yAMt9Xf934r6+ojCYDMHPMqvCc9TnzfeFSP1QEOeU6YZEd3+De0LTCgg== +node-request-interceptor@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/node-request-interceptor/-/node-request-interceptor-0.6.3.tgz#f2f0dbec2d421584419dd39ff6782ce1e02b42a7" + integrity sha512-8I2V7H2Ch0NvW7qWcjmS0/9Lhr0T6x7RD6PDirhvWEkUQvy83x8BA4haYMr09r/rig7hcgYSjYh6cd4U7G1vLA== + dependencies: + "@open-draft/until" "^1.0.3" + debug "^4.3.0" + headers-utils "^1.2.0" + strict-event-emitter "^0.1.0" + nodemon@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.6.tgz#1abe1937b463aaf62f0d52e2b7eaadf28cc2240d" @@ -23433,6 +23501,11 @@ static-site-generator-webpack-plugin@^3.4.2: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" +statuses@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + stealthy-require@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" @@ -23484,6 +23557,11 @@ stream-shift@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" +strict-event-emitter@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.1.0.tgz#fd742c1fb7e3852f0b964ecdae2d7666a6fb7ef8" + integrity sha512-8hSYfU+WKLdNcHVXJ0VxRXiPESalzRe7w1l8dg9+/22Ry+iZQUoQuoJ27R30GMD1TiyYINWsIEGY05WrskhSKw== + strict-ui@^0.2.0-0: version "0.2.0-0" resolved "https://registry.yarnpkg.com/strict-ui/-/strict-ui-0.2.0-0.tgz#43d30f0e0e63578032bc6c0675a875854b03fc27" @@ -26425,6 +26503,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrapped@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wrapped/-/wrapped-1.0.1.tgz#c783d9d807b273e9b01e851680a938c87c907242" @@ -26655,6 +26742,11 @@ y18n@^3.2.1: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" +y18n@^5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18" + integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg== + yaassertion@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/yaassertion/-/yaassertion-1.0.0.tgz#630c5c44c660d064006f1f15d79bd256d373fc9e" @@ -26722,6 +26814,11 @@ yargs-parser@^18.1.1, yargs-parser@^18.1.2, yargs-parser@^18.1.3: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^20.2.2: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + yargs-parser@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-8.1.0.tgz#f1376a33b6629a5d063782944da732631e966950" @@ -26853,6 +26950,19 @@ yargs@^15.3.1, yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + yauzl@^2.10.0, yauzl@^2.4.2: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"