diff --git a/package.json b/package.json index c16ba0ee6..8510af0ab 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "license": "MIT", "scripts": { "build:watch": "npx tsc -w", - "build:prod": "rimraf ./dist && cross-env NODE_ENV=production tsc -b" + "build:prod": "rimraf ./dist && cross-env NODE_ENV=production tsc -p ./tsconfig.production.json", + "run:examples:file": "node ./dist/examples/express-file-server/index.js" }, "devDependencies": { "@types/express": "^4.16.1", @@ -17,5 +18,8 @@ }, "peerDependencies": { "express": "^4.16.4" + }, + "dependencies": { + "express": "^4.16.4" } } diff --git a/src/Content.ts b/src/Content.ts new file mode 100644 index 000000000..c4da1b258 --- /dev/null +++ b/src/Content.ts @@ -0,0 +1,22 @@ +import { Range } from "./parseRangeHeader"; +import { Stream } from "stream"; +export type Content = { + /** + * Returns a readable stream based on the provided range (optional). + * @param {Range} range The start-end range of stream data. + * @returns {Stream} A readable stream + */ + getStream(range?: Range): Stream; + /** + * Total size of the content + */ + readonly totalSize: number; + /** + * Mime type to be sent in Content-Type header + */ + readonly mimeType: string; + /** + * File name to be sent in Content-Disposition header + */ + readonly fileName: string; +}; diff --git a/src/ContentDoesNotExistError.ts b/src/ContentDoesNotExistError.ts new file mode 100644 index 000000000..b68aee21a --- /dev/null +++ b/src/ContentDoesNotExistError.ts @@ -0,0 +1,2 @@ +export class ContentDoesNotExistError extends Error { +} diff --git a/src/ContentProvider.ts b/src/ContentProvider.ts new file mode 100644 index 000000000..3605a78dd --- /dev/null +++ b/src/ContentProvider.ts @@ -0,0 +1,6 @@ +import { Request } from "express"; +import { Content } from "./Content"; +/** + * @type {function (Request): Promise} + */ +export type ContentProvider = (req: Request) => Promise; diff --git a/src/Logger.ts b/src/Logger.ts new file mode 100644 index 000000000..39ea13c78 --- /dev/null +++ b/src/Logger.ts @@ -0,0 +1,3 @@ +export interface Logger { + debug(message: string, extra?: any): void; +} diff --git a/src/RangeParserError.ts b/src/RangeParserError.ts new file mode 100644 index 000000000..02bee120f --- /dev/null +++ b/src/RangeParserError.ts @@ -0,0 +1,5 @@ +export class RangeParserError extends Error { + constructor(start: any, end: any) { + super(`Invalid start and end values: ${start}-${end}.`); + } +} diff --git a/src/createPartialStreamHandler.ts b/src/createPartialStreamHandler.ts new file mode 100644 index 000000000..121579ed1 --- /dev/null +++ b/src/createPartialStreamHandler.ts @@ -0,0 +1,59 @@ +import { Request, Response } from "express"; +import { parseRangeHeader } from "./parseRangeHeader"; +import { RangeParserError } from "./RangeParserError"; +import { Logger } from "./Logger"; +import { ContentProvider } from "./ContentProvider"; +import { ContentDoesNotExistError } from "./ContentDoesNotExistError"; +import { + getRangeHeader, + setContentRangeHeader, + setContentTypeHeader, + setContentDispositionHeader, + setAcceptRangesHeader, + setContentLengthHeader, + setCacheControlHeaderNoCache +} from "./utils"; +export function createPartialStreamHandler(contentProvider: ContentProvider, logger: Logger) { + return async function handler(req: Request, res: Response) { + let content; + try { + content = await contentProvider(req); + } catch (error) { + logger.debug("ContentProvider threw exception: ", error); + if (error instanceof ContentDoesNotExistError) { + return res.status(400).send(error.message); + } + return res.sendStatus(500); + } + let { getStream, mimeType, fileName, totalSize } = content; + const rangeHeader = getRangeHeader(req); + let range; + try { + range = parseRangeHeader(rangeHeader, totalSize, logger); + } catch (error) { + logger.debug(`parseRangeHeader error: `, error); + if (error instanceof RangeParserError) { + setContentRangeHeader(null, totalSize, res); + return res.status(416).send(`Invalid value for Range: ${rangeHeader}`); + } + return res.sendStatus(500); + } + setContentTypeHeader(mimeType, res); + setContentDispositionHeader(fileName, res); + setAcceptRangesHeader(res); + // If range is not specified, or the file is empty, return the full stream + if (range === null) { + logger.debug("No range found, returning full content."); + setContentLengthHeader(totalSize, res); + return getStream().pipe(res); + } + setContentRangeHeader(range, totalSize, res); + let { start, end } = range; + setContentLengthHeader(start === end ? 0 : end - start + 1, res); + setCacheControlHeaderNoCache(res); + // Return 206 Partial Content status + logger.debug("Returning partial content for range: ", JSON.stringify(range)); + res.status(206); + getStream(range).pipe(res); + }; +} diff --git a/src/examples/express-file-server/files/readme.txt b/src/examples/express-file-server/files/readme.txt new file mode 100644 index 000000000..b1118bfbe --- /dev/null +++ b/src/examples/express-file-server/files/readme.txt @@ -0,0 +1,7 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +Magna etiam tempor orci eu lobortis elementum nibh. In egestas erat imperdiet sed euismod. Amet consectetur adipiscing elit pellentesque habitant. +Vel quam elementum pulvinar etiam non quam lacus suspendisse. +Nibh sit amet commodo nulla facilisi. Vel risus commodo viverra maecenas accumsan lacus. Ornare arcu dui vivamus arcu felis bibendum ut tristique et. +Vitae semper quis lectus nulla at volutpat diam. Mauris vitae ultricies leo integer malesuada nunc. Donec massa sapien faucibus et. Senectus et netus et malesuada. Vitae tortor condimentum lacinia quis vel. Sagittis id consectetur purus ut faucibus pulvinar elementum. Nisi est sit amet facilisis magna etiam tempor orci eu. + +Dictum varius duis at consectetur lorem donec massa sapien. Odio pellentesque diam volutpat commodo. Egestas dui id ornare arcu odio ut sem nulla. Consequat id porta nibh venenatis cras sed felis eget. Placerat in egestas erat imperdiet. Dui nunc mattis enim ut tellus elementum sagittis vitae. Aliquet bibendum enim facilisis gravida neque convallis a cras. Id semper risus in hendrerit gravida. Tempor orci eu lobortis elementum nibh tellus molestie. Semper auctor neque vitae tempus quam pellentesque. Vitae proin sagittis nisl rhoncus mattis rhoncus urna neque. Bibendum at varius vel pharetra vel turpis. Tellus integer feugiat scelerisque varius morbi enim nunc. Volutpat commodo sed egestas egestas fringilla. Congue eu consequat ac felis donec et odio. Venenatis cras sed felis eget velit aliquet. Urna neque viverra justo nec. Dictum non consectetur a erat nam. Lacinia quis vel eros donec. \ No newline at end of file diff --git a/src/examples/express-file-server/index.ts b/src/examples/express-file-server/index.ts new file mode 100644 index 000000000..6c77e9613 --- /dev/null +++ b/src/examples/express-file-server/index.ts @@ -0,0 +1,56 @@ +import express, { Request } from "express"; +import { promisify } from "util"; +import fs from "fs"; +import { Range, createPartialStreamHandler, ContentDoesNotExistError, ContentProvider } from "../../index"; + +const statAsync = promisify(fs.stat); +const existsAsync = promisify(fs.exists); + +const fileContentProvider: ContentProvider = async (req: Request) => { + const fileName = req.params.name; + const file = `${__dirname}/files/${fileName}`; + if (!(await existsAsync(file))) { + throw new ContentDoesNotExistError(`File doesn't exists: ${file}`); + } + + const stats = await statAsync(file); + const totalSize = stats.size; + const mimeType = "application/octet-stream"; + const getStream = (range?: Range) => { + if (!range) { + return fs.createReadStream(file); + } + const { start, end } = range; + logger.debug(`start: ${start}, end: ${end}`); + + return fs.createReadStream(file, { start, end }); + }; + + return { + fileName, + totalSize, + mimeType, + getStream + }; +}; + +const logger = { + debug(message: string, extra?: any) { + if (extra) { + console.log(`[debug]: ${message}`, extra); + } else { + console.log(`[debug]: ${message}`); + } + } +}; + +const handler = createPartialStreamHandler(fileContentProvider, logger); + +const app = express(); +const port = 8080; + +app.get("/files/:name", handler); + +app.listen(port, () => { + logger.debug("Server started!"); +}); diff --git a/src/index.ts b/src/index.ts index 4e7450b24..147587711 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,103 +1,5 @@ -import { Request, Response } from "express"; -import { parseRangeHeader, RangeParserError, Range } from "./parseRangeHeader"; -import { Stream } from "stream"; - -/** - * @type {function (Request): Promise} - */ -export type ContentProvider = (req: Request) => Promise; - -export class ContentDoesNotExistError extends Error {} - -export interface Logger { - debug(message: string, extra?: any): void; -} - -export type Content = { - /** - * Returns a readable stream based on the provided range (optional). - * @param {Range} range The start-end range of stream data. - * @returns {Stream} A readable stream - */ - getStream(range?: Range): Stream; - /** - * Total size of the content - */ - readonly totalSize: number; - /** - * Mime type to be sent in Content-Type header - */ - readonly mimeType: string; - /** - * File name to be sent in Content-Disposition header - */ - readonly fileName: string; -}; - -const getHeader = (name: string, req: Request) => req.headers[name]; -const getRangeHeader = getHeader.bind(null, "range"); -const setHeader = (name: string, value: string, res: Response) => res.setHeader(name, value); -const setContentTypeHeader = setHeader.bind(null, "Content-Type"); -const setContentLengthHeader = setHeader.bind(null, "Content-Length"); -const setAcceptRangesHeader = setHeader.bind(null, "Accept-Ranges", "bytes"); -const setContentRangeHeader = (range: Range | null, size: number, res: Response) => - setHeader("Content-Range", `bytes ${range ? `${range.start}-${range.end}` : "*"}/${size}`, res); -const setContentDispositionHeader = (fileName: string, res: Response) => - setHeader("Content-Disposition", `attachment; filename="${fileName}"`, res); -const setCacheControlHeaderNoCache = setHeader.bind(null, "Cache-Control", "no-cache"); - -export function create(contentProvider: ContentProvider, logger: Logger) { - return async function handler(req: Request, res: Response) { - let content; - try { - content = await contentProvider(req); - } catch (error) { - logger.debug("ContentProvider threw exception: ", error); - if (error instanceof ContentDoesNotExistError) { - return res.status(400).send(error.message); - } - - return res.sendStatus(500); - } - - let { getStream, mimeType, fileName, totalSize } = content; - - const rangeHeader = getRangeHeader(req); - let range; - try { - range = parseRangeHeader(rangeHeader, totalSize); - } catch (error) { - logger.debug(`parseRangeHeader error: `, error); - if (error instanceof RangeParserError) { - setContentRangeHeader(null, totalSize, res); - - return res - .send(error.message) - .status(416) - .end(); - } - - return res.sendStatus(500); - } - - let { start, end } = range; - - setContentTypeHeader(mimeType, res); - setContentDispositionHeader(fileName, res); - setAcceptRangesHeader(res); - - // If range is not specified, or the file is empty, return the full stream - if (range === null) { - setContentLengthHeader(totalSize, res); - return getStream().pipe(res); - } - - setContentRangeHeader(range, totalSize, res); - setContentLengthHeader(start === end ? 0 : end - start + 1); - setCacheControlHeaderNoCache(res); - - // Return 206 Partial Content status - res.status(206); - getStream(range).pipe(res); - }; -} +export * from "./Content"; +export * from "./ContentDoesNotExistError"; +export * from "./ContentProvider"; +export * from "./createPartialStreamHandler"; +export * from "./Logger"; diff --git a/src/parseRangeHeader.ts b/src/parseRangeHeader.ts index c3a2359e9..5e917b083 100644 --- a/src/parseRangeHeader.ts +++ b/src/parseRangeHeader.ts @@ -1,3 +1,6 @@ +import { Logger } from "./Logger"; +import { RangeParserError } from "./RangeParserError"; + export type Range = { start: number; end: number; @@ -5,19 +8,16 @@ export type Range = { const rangeRegEx = /bytes=([0-9]*)-([0-9]*)/; -export class RangeParserError extends Error { - constructor(start: any, end: any) { - super(`Invalid start and end values: ${start}-${end}.`); - } -} - -export function parseRangeHeader(range: string, totalSize: number): Range | null { +export function parseRangeHeader(range: string, totalSize: number, logger: Logger): Range | null { + logger.debug("Un-parsed range is: ", range); // 1. If range is not specified or the file is empty, return null. - if (range === null || range.length === 0 || totalSize === 0) { + if (!range || range === null || range.length === 0 || totalSize === 0) { return null; } - const [startValue, endValue] = range.split(rangeRegEx); + const splitRange = range.split(rangeRegEx); + console.log("Parsed range is: ", JSON.stringify(splitRange)); + const [, startValue, endValue] = splitRange; let start = Number.parseInt(startValue); let end = Number.parseInt(endValue); @@ -32,26 +32,31 @@ export function parseRangeHeader(range: string, totalSize: number): Range | null // 3.1. If end is not provided, set end to the last byte (totalSize - 1). if (!Number.isNaN(start) && Number.isNaN(end)) { + logger.debug("End is not provided."); + result.start = start; result.end = totalSize - 1; - - return result; } // 3.2. If start is not provided, set it to the offset of last "end" bytes from the end of the file. // And set end to the last byte. // This way we return the last "end" bytes. if (Number.isNaN(start) && !Number.isNaN(end)) { - result.start = totalSize - end; - result.end = totalSize - 1; + logger.debug(`Start is not provided, "end" will be treated as last "end" bytes of the content.`); - return result; + result.start = Math.max(totalSize - end, 0); + result.end = totalSize - 1; } // 4. Handle invalid ranges. - if (start > end) { + if (start < 0 || start > end || end > totalSize) { throw new RangeParserError(start, end); } + logRange(logger, result); return result; } + +function logRange(logger: Logger, range: Range) { + logger.debug("Range is: ", JSON.stringify(range)); +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 000000000..7324679bb --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,11 @@ +import { Request, Response } from "express"; +import { Range } from "./parseRangeHeader"; +const getHeader = (name: string, req: Request) => req.headers[name]; +export const getRangeHeader = getHeader.bind(null, "range"); +const setHeader = (name: string, value: string, res: Response) => res.setHeader(name, value); +export const setContentTypeHeader = setHeader.bind(null, "Content-Type"); +export const setContentLengthHeader = setHeader.bind(null, "Content-Length"); +export const setAcceptRangesHeader = setHeader.bind(null, "Accept-Ranges", "bytes"); +export const setContentRangeHeader = (range: Range | null, size: number, res: Response) => setHeader("Content-Range", `bytes ${range ? `${range.start}-${range.end}` : "*"}/${size}`, res); +export const setContentDispositionHeader = (fileName: string, res: Response) => setHeader("Content-Disposition", `attachment; filename="${fileName}"`, res); +export const setCacheControlHeaderNoCache = setHeader.bind(null, "Cache-Control", "no-cache"); diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 000000000..8da1624f5 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "module": "commonjs", + "esModuleInterop": true, + "target": "es6", + "noImplicitAny": true, + "moduleResolution": "node", + "sourceMap": true, + "outDir": "dist", + "declaration": true, + "baseUrl": "src", + "paths": { + "*": ["../node_modules/*", "./types/*"] + } + } +} diff --git a/tsconfig.json b/tsconfig.json index 3ce78e686..810300780 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,4 @@ { - "compilerOptions": { - "module": "commonjs", - "esModuleInterop": true, - "target": "es6", - "noImplicitAny": true, - "moduleResolution": "node", - "sourceMap": true, - "outDir": "dist", - "declaration": true, - "baseUrl": "src", - "paths": { - "*": ["../node_modules/*", "./types/*"] - } - }, - "include": ["src/**/*"] + "extends": "./tsconfig.base.json", + "include": ["src/**/*.ts"] } diff --git a/tsconfig.production.json b/tsconfig.production.json new file mode 100644 index 000000000..00b612ea9 --- /dev/null +++ b/tsconfig.production.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.base.json", + "include": ["src/index.ts"] +} diff --git a/yarn.lock b/yarn.lock index 671efb394..9b19c8410 100644 --- a/yarn.lock +++ b/yarn.lock @@ -57,11 +57,40 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" +accepts@~1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" + integrity sha1-63d99gEXI6OxTopywIBcjoZ0a9I= + dependencies: + mime-types "~2.1.18" + negotiator "0.6.1" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +body-parser@1.18.3: + version "1.18.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4" + integrity sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ= + dependencies: + bytes "3.0.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "~1.6.3" + iconv-lite "0.4.23" + on-finished "~2.3.0" + qs "6.5.2" + raw-body "2.3.3" + type-is "~1.6.16" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -70,11 +99,36 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= +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= + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= + cross-env@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.0.tgz#6ecd4c015d5773e614039ee529076669b9d126f2" @@ -94,6 +148,102 @@ cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +express@^4.16.4: + version "4.16.4" + resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e" + integrity sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg== + dependencies: + accepts "~1.3.5" + array-flatten "1.1.1" + body-parser "1.18.3" + content-disposition "0.5.2" + content-type "~1.0.4" + cookie "0.3.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.1.1" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.2" + path-to-regexp "0.1.7" + proxy-addr "~2.0.4" + qs "6.5.2" + range-parser "~1.2.0" + safe-buffer "5.1.2" + send "0.16.2" + serve-static "1.13.2" + setprototypeof "1.1.0" + statuses "~1.4.0" + type-is "~1.6.16" + utils-merge "1.0.1" + vary "~1.1.2" + +finalhandler@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" + integrity sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.2" + statuses "~1.4.0" + unpipe "~1.0.0" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -111,6 +261,23 @@ glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" +http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +iconv-lite@0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" + integrity sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -119,11 +286,16 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2: +inherits@2, inherits@2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +ipaddr.js@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e" + integrity sha1-6qM9bd16zo9/b+DJygRA5wZzix4= + is-windows@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -134,6 +306,38 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +mime-db@~1.38.0: + version "1.38.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad" + integrity sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg== + +mime-types@~2.1.18: + version "2.1.22" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.22.tgz#fe6b355a190926ab7698c9a0556a11199b2199bd" + integrity sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog== + dependencies: + mime-db "~1.38.0" + +mime@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" + integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== + minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -141,11 +345,28 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +negotiator@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + integrity sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk= + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -153,6 +374,11 @@ once@^1.3.0: dependencies: wrappy "1" +parseurl@~1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" + integrity sha1-/CidTtiZMRlGDBViUyYs3I3mW/M= + 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" @@ -163,6 +389,39 @@ path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +proxy-addr@~2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93" + integrity sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.8.0" + +qs@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +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= + +raw-body@2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" + integrity sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw== + dependencies: + bytes "3.0.0" + http-errors "1.6.3" + iconv-lite "0.4.23" + unpipe "1.0.0" + rimraf@^2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" @@ -170,11 +429,55 @@ rimraf@^2.6.3: dependencies: glob "^7.1.3" +safe-buffer@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + semver@^5.5.0: version "5.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== +send@0.16.2: + version "0.16.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" + integrity sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.6.2" + mime "1.4.1" + ms "2.0.0" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.4.0" + +serve-static@1.13.2: + version "1.13.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" + integrity sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.2" + send "0.16.2" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -187,11 +490,44 @@ shebang-regex@^1.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +statuses@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" + integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew== + +type-is@~1.6.16: + version "1.6.16" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" + integrity sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.18" + typescript@^3.3.3333: version "3.3.3333" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.3333.tgz#171b2c5af66c59e9431199117a3bcadc66fdcfd6" integrity sha512-JjSKsAfuHBE/fB2oZ8NxtRTk5iGcg6hkYXMnZ3Wc+b2RSqejEqTaem11mHASMnFilHrax3sLK0GDzcJrekZYLw== +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"