From 29837f0eeb9fcb4c53426e5bd89e9bdf7e9d961b Mon Sep 17 00:00:00 2001 From: Justin Wind Date: Wed, 16 Sep 2020 13:36:56 -0700 Subject: [PATCH 1/1] initial release --- .eslintrc.json | 82 + .gitignore | 4 + .nycrc.json | 8 + README.md | 31 + index.js | 13 + lib/common.js | 257 +++ lib/content-negotiation.js | 163 ++ lib/dingus.js | 630 ++++++ lib/enum.js | 104 + lib/errors.js | 28 + lib/mime-helper.js | 28 + lib/patches/incoming-message.js | 17 + lib/patches/index.js | 3 + lib/router.js | 206 ++ lib/template/error-html.js | 15 + lib/template/index.js | 5 + package-lock.json | 2902 ++++++++++++++++++++++++++ package.json | 37 + test/index.js | 14 + test/lib/common.js | 381 ++++ test/lib/content-negotiation.js | 207 ++ test/lib/dingus.js | 873 ++++++++ test/lib/enum.js | 21 + test/lib/mime-helper.js | 28 + test/lib/patches/incoming-message.js | 37 + test/lib/router.js | 352 ++++ test/test-data/.example | 1 + test/test-data/example.html | 1 + test/test-data/example.html.gz | Bin 0 -> 41 bytes 29 files changed, 6448 insertions(+) create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 .nycrc.json create mode 100644 README.md create mode 100644 index.js create mode 100644 lib/common.js create mode 100644 lib/content-negotiation.js create mode 100644 lib/dingus.js create mode 100644 lib/enum.js create mode 100644 lib/errors.js create mode 100644 lib/mime-helper.js create mode 100644 lib/patches/incoming-message.js create mode 100644 lib/patches/index.js create mode 100644 lib/router.js create mode 100644 lib/template/error-html.js create mode 100644 lib/template/index.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 test/index.js create mode 100644 test/lib/common.js create mode 100644 test/lib/content-negotiation.js create mode 100644 test/lib/dingus.js create mode 100644 test/lib/enum.js create mode 100644 test/lib/mime-helper.js create mode 100644 test/lib/patches/incoming-message.js create mode 100644 test/lib/router.js create mode 100644 test/test-data/.example create mode 100644 test/test-data/example.html create mode 100644 test/test-data/example.html.gz diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..7602b33 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,82 @@ +{ + "env": { + "browser": false, + "es6": true, + "node": true + }, + "extends": [ + "eslint:recommended", + "plugin:node/recommended", + "plugin:security/recommended", + "plugin:sonarjs/recommended" + ], + "parserOptions": { + "ecmaVersion": 2018 + }, + "plugins": [ + "node", + "security", + "sonarjs" + ], + "rules": { + "array-element-newline": [ + "error", + "consistent" + ], + "arrow-parens": [ + "error", + "always" + ], + "arrow-spacing": [ + "error", + { + "after": true, + "before": true + } + ], + "block-scoped-var": "error", + "block-spacing": "error", + "brace-style": "error", + "callback-return": "error", + "camelcase": "error", + "capitalized-comments": "warn", + "class-methods-use-this": "error", + "comma-dangle": [ + "error", + "always-multiline" + ], + "comma-spacing": [ + "error", + { + "after": true, + "before": false + } + ], + "comma-style": [ + "error", + "last" + ], + "sonarjs/cognitive-complexity": "warn", + "keyword-spacing": "error", + "linebreak-style": [ + "error", + "unix" + ], + "no-unused-vars": [ + "error", { + "varsIgnorePattern": "^_" + } + ], + "object-curly-spacing": [ + "error", + "always" + ], + "prefer-const": "error", + "quotes": [ + "error", + "single" + ], + "strict": "error", + "vars-on-top": "error" + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..defafb1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +coverage +node_modules +.nyc_output +.vscode diff --git a/.nycrc.json b/.nycrc.json new file mode 100644 index 0000000..9633f6d --- /dev/null +++ b/.nycrc.json @@ -0,0 +1,8 @@ +{ + "check-coverage": true, + "functions": 100, + "lines": 100, + "per-file": true, + "reporter": ["lcov", "text"], + "statements": 100 +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..37d9afb --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# Another In-House API Server Frameworklet + +I just wanted a basic little API server for toy projects, without having to park a container-ship of modules under it. + +This is in no way intended to replace any mature, full-featured framework. It is spartan in some aspects, brings some unexpected baggage in others, makes some questionable design decisions, has the occasional opinion, and is likely somewhat idiosyncratic from an outside perspective. +The primary design goals are: +- self-contained: as few external dependencies as feasible +- not-infinitely-extensible: only does the things it needs to do as dictated by the projects it is used in + +## Getting Started + +Construct it with a console-level-compatible logger capable of doing something meaningful with calls like level(scopeString, messageString, dataObject). + +Within the server request handler: +* dispatch(req, res) makes things go. + +Within the application implementation: +* on(method, urlPath, handler) declares a thing to do when a request matches. +* preHandler(req, res, ctx) can be overridden to do something to every request before it is handled. + +Handled content types can be extended by overriding: +* parseBody(contentType, ctx) for incoming types. +* renderError(contentType, err) for outgoing types. + +Within your handlers: +* setResponseType(responseTypes, req, res, ctx) can be called to negotiate content types. +* async ingestBody(req, res, ctx) will parse request body data. +* throw an Error.ResponseError with an Enum.ErrorResponse for a simple status code with optional details, when something goes awry. + +Parameters and metadata are set in each request context. + diff --git a/index.js b/index.js new file mode 100644 index 0000000..dcf66e8 --- /dev/null +++ b/index.js @@ -0,0 +1,13 @@ +'use strict'; + +const Dingus = require('./lib/dingus'); +const common = require('./lib/common'); +const Enum = require('./lib/enum'); +const Errors = require('./lib/errors'); + +module.exports = { + common, + Dingus, + Enum, + Errors, +}; diff --git a/lib/common.js b/lib/common.js new file mode 100644 index 0000000..284427a --- /dev/null +++ b/lib/common.js @@ -0,0 +1,257 @@ +/* eslint-disable security/detect-object-injection */ +'use strict'; + +/** + * Utility and miscellaneous functions. + */ + +const path = require('path'); +const crypto = require('crypto'); +const uuid = require('uuid'); +const Enum = require('./enum'); + +/** + * Return a function which combines a part of the filename with a scope, for use in logging. + * @param {string} filename + */ +const fileScope = (filename) => { + let fScope = path.basename(filename, '.js'); + if (fScope === 'index') { + fScope = path.basename(path.dirname(filename)); + } + return (scope) => `${fScope}:${scope}`; +} + +/** + * Simple ETag from data. + * @param {string} filePath + * @param {object} fileStat + * @param {*} fileData + */ +const generateETag = (_filePath, fileStat, fileData) => { + const hash = crypto.createHash('sha256'); + if (fileStat && fileStat.mtimeMs) { + hash.update(fileStat.mtimeMs.toString()); + } + hash.update(fileData); + const digest = hash.digest('base64').replace('=', ''); + return `"${digest}"`; +}; + +/** + * @param {object} obj + * @param {string} prop + * @param {*} def + */ +const get = (obj, prop, def) => obj && prop && (prop in obj) ? obj[prop] : def; + +/** + * @param {http.ClientRequest} req + * @param {http.ServerResponse} res + * @param {object} ctx + */ +const handlerLogData = (req, res, ctx) => ({ + req: requestLogData(req), + res: responseLogData(res), + ctx, +}); + +/** + * + * @param {http.ClientRequest} req + * @param {Number} modifiedTimeMs + * @param {string} eTag + */ +const isClientCached = (req, modifiedTimeMs, eTag) => { + let clientCached = false; + + const ifModifiedSince = req.getHeader(Enum.Header.IfModifiedSince); + if (ifModifiedSince) { + const ifModifiedSinceMs = Date.parse(ifModifiedSince); + if (modifiedTimeMs < ifModifiedSinceMs) { + clientCached = true; + } + } + + const ifNoneMatch = req.getHeader(Enum.Header.IfNoneMatch); + if (ifNoneMatch) { + const matches = ifNoneMatch.split(',').map((m) => m.trim()); + if (matches.includes(eTag) + || (ifNoneMatch === '*' && eTag)) { + clientCached = true; + } else { + // If if-none-matched header is present, it takes precedence over modified-since. + clientCached = false; + } + } + + return clientCached; +}; + +/** + * Shallow merge for enums, to be called by derived constructor. + * Expects only one-level deep, is not recursive! + * @param {Object} origEnum + * @param {Object} additionalEnum + */ +const mergeEnum = (origEnum, additionalEnum) => { + for (const e of Object.keys(additionalEnum)) { + if (typeof additionalEnum[e] === 'object') { + if (! (e in origEnum)) { + origEnum[e] = {}; + } + Object.assign(origEnum[e], additionalEnum[e]); + } else { + origEnum[e] = additionalEnum[e]; + } + } + return origEnum; +}; + +/** + * Isolate the general category of an http status code. + * @param {Number} statusCode + * @returns {Number} + */ +const httpStatusCodeClass = (statusCode) => Math.floor(statusCode / 100); + +const _isObject = (obj) => obj && typeof obj === 'object'; +const _isArray = (obj) => Array.isArray(obj); +/** + * Return a new object with all objects combined. + * @param {...any} objects + * @returns + */ +const mergeDeep = (...objects) => { + return objects.reduce((acc, obj) => { + const objectProperties = [...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertySymbols(obj)]; + objectProperties.forEach((k) => { + const aVal = acc[k]; + const oVal = obj[k]; + if (_isArray(oVal)) { + acc[k] = (_isArray(aVal) ? aVal : []).concat(oVal); + } else if (_isObject(oVal)) { + acc[k] = mergeDeep(_isObject(aVal) ? aVal : {}, oVal); + } else { + acc[k] = oVal; + } + }); + return acc; + }, {}); +}; + + +/** + * Return a new object with selected props. + * @param {Object} obj + * @param {string[]} props + */ +const pick = (obj, props) => { + const picked = {}; + props.forEach((prop) => { + if (prop in obj) { + picked[prop] = obj[prop]; + } + }); + return picked; +}; + +/** + * Return a subset of a request object, suitable for logging. + * @param {http.ClientRequest} req + */ +const requestLogData = (req) => { + return pick(req, [ + 'method', + 'url', + 'httpVersion', + 'headers', + 'trailers', + ]); +}; + + +/** + * Return a subset of a response object, suitable for logging. + * @param {http.ServerResponse} res + */ +const responseLogData = (res) => { + const response = pick(res, [ + 'statusCode', + 'statusMessage', + ]); + response.headers = res.getHeaders(); + return response; +}; + + +/** + * Store updates to defaultOptions, but no new properties. + * @param {Object} target + * @param {Object} defaultOptions + * @param {Object} options + */ +const setOptions = (target, defaultOptions, options) => { + Object.assign(target, defaultOptions, pick(options, Object.keys(defaultOptions))); +}; + +/** + * Return a list of source split at first delimiter. + * @param {string} src + * @param {string} delimiter + * @param {string} fill trailing stand-in if no delimiter in src + */ +const splitFirst = (src, delimiter, fill) => { + const idx = src.indexOf(delimiter); + if (idx >= 0) { + return [ src.slice(0, idx), src.slice(idx + 1) ]; + } else { + return [ src, fill ]; + } +}; + +/** + * Generate a new request identifier. + * @returns {String} + */ +const requestId = () => { + return uuid.v1(); +}; + +const nop = () => { /**/ }; +const nullLogger = { + error: nop, + warn: nop, + info: nop, + log: nop, + debug: nop, +}; + +const ensureLoggerLevels = (logger = {}) => { + for (const level in nullLogger) { + if (! (level in logger)) { + logger[level] = nullLogger[level]; + } + } + return logger; +}; + +module.exports = { + fileScope, + generateETag, + get, + handlerLogData, + isClientCached, + httpStatusCodeClass, + mergeDeep, + mergeEnum, + nop, + nullLogger, + ensureLoggerLevels, + pick, + requestId, + requestLogData, + responseLogData, + setOptions, + splitFirst, +}; diff --git a/lib/content-negotiation.js b/lib/content-negotiation.js new file mode 100644 index 0000000..1425852 --- /dev/null +++ b/lib/content-negotiation.js @@ -0,0 +1,163 @@ +'use strict'; + +const common = require('./common'); +const Enum = require('./enum'); + + +// A weight value smaller than the allowed resolution, for minute wildcard de-preferencing. +const WeightIota = 0.0001; + +class ContentNegotiation { + + /** + * Convert accept clause string to object. + * Adjust weight based on wildcards, to prefer literal matches. + * @param {string} clause + */ + static _unpackAcceptClause(clause) { + let params = clause.split(';'); + const type = params.shift().trim(); + if (type) { + let weight = 1.0; + params = params.reduce((acc, param) => { + const [p, v] = common.splitFirst(param, '=').map((x) => x && x.trim()); + if (p && v) { + if (p === 'q') { + weight = Number(v); + // Enforce max precision from spec, so that we can... + weight = Number(weight.toFixed(3)); + // De-tune the quality slightly for wildcards. + const blur = (type.split('*').length - 1) * WeightIota; + if (weight) { + weight -= blur; + } + } else { + // eslint-disable-next-line security/detect-object-injection + acc[p] = v; + } + } + return acc; + }, {}); + return { type, weight, params }; + } + } + + /** + * Split an accept field into clauses, return list of clauses sorted by heaviest weights first. + * @param {string} acceptHeader + */ + static _acceptClauses(acceptHeader) { + const clauses = (acceptHeader||'').split(',').map((clause) => ContentNegotiation._unpackAcceptClause(clause)).filter((clause) => clause); + return clauses.sort((a, b) => b.weight - a.weight); + } + + /** + * Check if an Accept-able Content-Type matches a fixed Content-Type. + * (Allows for '*' fields in Accept-able type.) + * @param {string} pattern + * @param {string} type + */ + static _matchType(acceptableType, fixedType) { + acceptableType = common.splitFirst(acceptableType, '/', '*'); + fixedType = common.splitFirst(fixedType, '/', '*'); + for (let i = 0; i < acceptableType.length; i++) { + // eslint-disable-next-line security/detect-object-injection + const v = acceptableType[i]; + // eslint-disable-next-line security/detect-object-injection + const f = fixedType[i]; + if (v !== f && v !== '*') { + return false; + } + } + return true; + } + + /** + * Return the best match between available and acceptable types. + * @param {string[]} acceptableTypes + * @param {string} acceptHeader + */ + static accept(acceptableTypes, acceptHeader) { + const validTypesQuality = {}; + if (!acceptHeader) { + return acceptableTypes[0]; + } + // For each type offered in the header, from heaviest to lightest... + ContentNegotiation._acceptClauses(acceptHeader).forEach((a) => { + // Consider each supported type... + acceptableTypes.forEach((t) => { + // Remember the heaviest weighting if it matches. + if (ContentNegotiation._matchType(a.type, t) + // eslint-disable-next-line security/detect-object-injection + && (!(t in validTypesQuality) || validTypesQuality[t] < a.weight)) { + // eslint-disable-next-line security/detect-object-injection + validTypesQuality[t] = a.weight; + } + }); + }); + return Object.keys(validTypesQuality).reduce((acc, cur) => { + // eslint-disable-next-line security/detect-object-injection + if (acc === undefined && validTypesQuality[cur] !== 0.0) { + return cur; + } + // eslint-disable-next-line security/detect-object-injection + return validTypesQuality[acc] < validTypesQuality[cur] ? cur : acc; + }, undefined); + } + + + /** + * Return all viable matches between acceptable and requested encodings, ordered by highest preference first. + * TODO: sort equal q-values by server-preference rather than header order + * @param {string[]} acceptableEncodings e.g. ['br', 'gzip'] in order of server preference + * @param {string} acceptHeader + */ + static preferred(acceptableEncodings, acceptHeader) { + const Identity = Enum.EncodingType.Identity; + const Any = '*'; + + // Don't munge caller's list. + acceptableEncodings = [...acceptableEncodings]; + + // Server is always capable of identity encoding. + if (!(acceptableEncodings.includes(Identity))) { + acceptableEncodings.push(Identity); + } + + const acceptClauses = ContentNegotiation._acceptClauses(acceptHeader); + + // Add identity as fallback clause if an Any type was not explicitly mentioned. + const acceptTypes = acceptClauses.map((enc) => enc.type); + if (!(acceptTypes.includes(Any)) && !(acceptTypes.includes(Identity))) { + acceptClauses.push({ + type: Identity, + weight: WeightIota, + params: [], + }); + } + + // Explicitly forbidden encodings will not be considered. + const forbidden = acceptClauses.filter((enc) => enc.weight == 0).map((enc) => enc.type); + + acceptableEncodings = acceptableEncodings.filter((enc) => { + // If * is forbidden, don't allow identity encoding. + const fallbackIsForbidden = forbidden.includes(Any) && enc === Identity && !acceptTypes.includes(Identity); + + const typeIsForbidden = forbidden.includes(enc.type); + return !typeIsForbidden && !fallbackIsForbidden; + }); + + // Strip forbidden and unsupported from working set. + const allowedClauses = acceptClauses.filter((enc) => { + const isAllowed = enc.weight > 0; + const isSupported = enc.type === Any || acceptableEncodings.includes(enc.type); + return isAllowed && isSupported; + }); + + // Only the types. + return allowedClauses.map((enc) => enc.type); + } + +} + +module.exports = ContentNegotiation; diff --git a/lib/dingus.js b/lib/dingus.js new file mode 100644 index 0000000..c8e4909 --- /dev/null +++ b/lib/dingus.js @@ -0,0 +1,630 @@ +/* eslint-disable security/detect-object-injection */ +'use strict'; + +/** + * A very minimal API server framework. + * Just a self-contained router and some request glue. + */ + +require('./patches'); +const { promises: fsPromises } = require('fs'); +const path = require('path'); +const querystring = require('querystring'); +const common = require('./common'); +const ContentNegotiation = require('./content-negotiation'); +const Enum = require('./enum'); +const { DingusError, ResponseError } = require('./errors'); +const { extensionToMime } = require('./mime-helper'); +const Router = require('./router'); +const Template = require('./template'); + +// For logging. +const _fileScope = common.fileScope(__filename); + +const defaultOptions = { + ignoreTrailingSlash: false, + proxyPrefix: '', + strictAccept: true, + selfBaseUrl: '', + trustProxy: true, + querystring, +}; + +class Dingus { + /** + * @param {Object} logger object which implements logging methods + * @param {Object} options + * @param {Boolean} options.ignoreTrailingSlash + * @param {string} options.proxyPrefix leading part of url path to strip + * @param {Boolean} options.strictAccept whether to error on unsupported Accept type + * @param {string} options.selfBaseUrl for constructing links + * @param {Boolean} options.trustProxy trust some header data to be provided by proxy + * @param {Object} options.querystring alternate qs parser to use + */ + constructor(logger = common.nullLogger, options = {}) { + common.setOptions(this, defaultOptions, options); + + this.router = new Router(options); + + if (!this.proxyPrefix) { + this._stripPrefix = (p) => p; + } + + this.responseTypes = [ + Enum.ContentType.TextHTML, + Enum.ContentType.TextPlain, + Enum.ContentType.ApplicationJson, + ]; + + this.logger = logger; + common.ensureLoggerLevels(this.logger); + } + + + /** + * Resolve relative and empty paths in url + * @param {string} p path + */ + _normalizePath(p) { + const pathNorm = path.normalize(p); // This isn't perfectly correct, but it's easy... + return this._stripPrefix(pathNorm); + } + + + /** + * Remove a leading portion of url path + * N.B. This method gets obliterated if there is no prefix defined at construction + * @param {string} p path + */ + _stripPrefix(p) { + if (p.startsWith(this.proxyPrefix)) { + return p.slice(this.proxyPrefix.length); + } + return p; + } + + + /** + * Returns the path part, and querystring object, from a request url. + * @param {string} url + */ + _splitUrl(url) { + const [ p, qs ] = common.splitFirst(url, '?'); + return { + pathPart: this._normalizePath(p), + queryParams: this.querystring.parse(qs), + }; + } + + + /** + * Insert a new path handler + * @param {string} method + * @param {string} urlPath + * @param {fn} handler + */ + on(method, urlPath, handler) { + this.router.on(method, urlPath, handler); + } + + + /** + * Common header tagging for all requests. + * Add our own identifier, and persist any external transit identifiers. + * @param {http.ClientRequest} req + * @param {http.ServerResponse} res + * @param {object} ctx + */ + static tagContext(req, res, ctx) { + const requestId = common.requestId(); + ctx.requestId = requestId; + res.setHeader(Enum.Header.RequestId, requestId); + [Enum.Header.XRequestId, Enum.Header.XCorrelationId].forEach((h) => { + const v = req.getHeader(h); + if (v) { + ctx[h.replace(/-/g, '')] = v; + res.setHeader(h, v); + } + }); + return requestId; + } + + + /** + * + * @param {http.ClientRequest} req + */ + _getAddress(req) { + // TODO: RFC7239 Forwarded support + const address = (this.trustProxy && req && req.getHeader(Enum.Header.XForwardedFor)) || + (this.trustProxy && req && req.getHeader(Enum.Header.XRealIP)) || + (req && req.connection && req.connection.remoteAddress) || + ''; + return address.split(/\s*,\s*/u)[0]; + } + + + /** + * + * @param {http.ClientRequest} req + */ + _getProtocol(req) { + // TODO: RFC7239 Forwarded support + const protocol = (this.trustProxy && req && req.getHeader(Enum.Header.XForwardedProto)) || + ((req && req.connection && req.connection.encrypted) ? 'https' : 'http'); + return protocol.split(/\s*,\s*/u)[0]; + } + + + /** + * + * @param {http.ClientRequest} req + * @param {http.ServerResponse} res + * @param {object} ctx + */ + clientAddressContext(req, res, ctx) { + ctx.clientAddress = this._getAddress(req); + ctx.clientProtocol = this._getProtocol(req); + } + + + /** + * Called before every request handler. + * @param {http.ClientRequest} req + * @param {http.ServerResponse} res + * @param {object} ctx + */ + async preHandler(req, res, ctx) { + Dingus.tagContext(req, res, ctx); + this.clientAddressContext(req, res, ctx); + } + + + /** + * Helper for collecting chunks as array of buffers. + * @param {Buffer[]} chunks + * @param {string|Buffer} chunk + * @param {string} encoding + */ + static pushBufChunk(chunks, chunk, encoding = 'utf8') { + if (chunk) { + if (typeof chunk === 'string') { + chunk = Buffer.from(chunk, encoding); + } + chunks.push(chunk); + } + } + + + /** + * Sets ctx.responseBody and calls handler upon res.end(). + * @param {http.ClientRequest} req + * @param {http.ServerResponse} res + * @param {object} ctx + * @param {*} handler fn(req, res, ctx) + */ + static setEndBodyHandler(req, res, ctx, handler) { + const origWrite = res.write.bind(res); + const origEnd = res.end.bind(res); + const chunks = []; + res.write = function (chunk, encoding, ...rest) { + Dingus.pushBufChunk(chunks, chunk, encoding); + return origWrite(chunk, encoding, ...rest); + }; + res.end = function (data, encoding, ...rest) { + Dingus.pushBufChunk(chunks, data, encoding); + ctx.responseBody = Buffer.concat(chunks); + handler(req, res, ctx); + return origEnd(data, encoding, ...rest); + }; + } + + + /** + * Intercept writes for head requests, do not send to client, + * but send length, and make body available in context. + * @param {http.ClientRequest} req + * @param {http.ServerResponse} res + * @param {object} ctx + */ + static setHeadHandler(req, res, ctx) { + if (req.method === 'HEAD') { + const origEnd = res.end.bind(res); + const chunks = []; + res.write = function (chunk, encoding) { + Dingus.pushBufChunk(chunks, chunk, encoding); + // No call to original res.write. + }; + res.end = function (data, encoding, ...rest) { + Dingus.pushBufChunk(chunks, data, encoding); + ctx.responseBody = Buffer.concat(chunks); + res.setHeader(Enum.Header.ContentLength, Buffer.byteLength(ctx.responseBody)); + return origEnd(undefined, encoding, ...rest); + }; + } + } + + + /** + * Dispatch the handler for a request + * @param {http.ClientRequest} req + * @param {http.ServerResponse} res + * @param {object} ctx + */ + async dispatch(req, res, ctx = {}) { + const _scope = _fileScope('dispatch'); + + const { pathPart, queryParams } = this._splitUrl(req.url); + ctx.queryParams = queryParams; + + let handler; + try { + handler = this.router.lookup(req.method, pathPart, ctx); + } catch (e) { + if (e instanceof DingusError) { + switch (e.message) { + case 'NoPath': + handler = this.handlerNotFound.bind(this); + break; + case 'NoMethod': + handler = this.handlerMethodNotAllowed.bind(this); + break; + default: + this.logger.error(_scope, 'unknown dingus error', { error: e }); + handler = this.handlerInternalServerError.bind(this); + } + } else if (e instanceof URIError) { + handler = this.handlerBadRequest.bind(this); + } else { + this.logger.error(_scope, 'lookup failure', { error: e }); + handler = this.handlerInternalServerError.bind(this); + } + } + + try { + await this.preHandler(req, res, ctx); + return await handler(req, res, ctx); + } catch (e) { + ctx.error = e; + this.sendErrorResponse(e, req, res, ctx); + } + } + + + /** + * Return normalized type, without any parameters. + * @param {http.ClientRequest} req + * @returns {string} + */ + static getRequestContentType(req) { + const contentType = req.getHeader(Enum.Header.ContentType); + return (contentType || '').split(';')[0].trim().toLowerCase(); + } + + + /** + * Parse rawBody from ctx as contentType into parsedBody. + * @param {string} contentType + * @param {object} ctx + */ + parseBody(contentType, ctx) { + const _scope = _fileScope('parseBody'); + + switch (contentType) { + case Enum.ContentType.ApplicationForm: + ctx.parsedBody = this.querystring.parse(ctx.rawBody); + break; + + case Enum.ContentType.ApplicationJson: + try { + ctx.parsedBody = JSON.parse(ctx.rawBody); + } catch (e) { + this.logger.debug(_scope, 'JSON parse failed', { requestId: ctx.requestId, error: e }); + throw new ResponseError(Enum.ErrorResponse.BadRequest, e.message); + } + break; + + default: + this.logger.debug(_scope, 'unhandled content-type', { requestId: ctx.requestId, contentType }); + throw new ResponseError(Enum.ErrorResponse.UnsupportedMediaType); + } + } + + + /** + * Return all body data from a request. + * @param {http.ClientRequest} req + */ + async bodyData(req) { + const _scope = _fileScope('bodyData'); + return new Promise((resolve, reject) => { + const body = []; + req.on('data', (chunk) => body.push(chunk)); + req.on('end', () => resolve(Buffer.concat(body).toString())); + req.on('error', (e) => { + this.logger.error(_scope, 'failed', { error: e }); + reject(e); + }); + }); + } + + + /** + * Read and parse request body data. + * @param {http.ClientRequest} req + * @param {http.ServerResponse} res + * @param {object} ctx + */ + async ingestBody(req, res, ctx) { + ctx.rawBody = await this.bodyData(req); + const contentType = Dingus.getRequestContentType(req); + this.parseBody(contentType, ctx); + } + + + /** + * Return the best matching response type. + * @param {string[]} responseTypes + * @param {http.ClientRequest} req + */ + static getResponseContentType(responseTypes, req) { + const acceptHeader = req.getHeader(Enum.Header.Accept); + return ContentNegotiation.accept(responseTypes, acceptHeader); + } + + + /** + * Returns a list of the most-preferred content encodings for the response. + * @param {string[]} responseEncodings + * @param {http.ClientRequest} req + */ + static getResponseEncoding(responseEncodings, req) { + const acceptEncodingHeader = req.getHeader(Enum.Header.AcceptEncoding); + return ContentNegotiation.preferred(responseEncodings, acceptEncodingHeader); + } + + + /** + * Set the best content type for the response. + * @param {string[]} responseTypes default first + * @param {http.ClientRequest} req + * @param {http.ServerResponse} res + * @param {object} ctx + */ + setResponseType(responseTypes, req, res, ctx) { + const _scope = _fileScope('setResponseType'); + ctx.responseType = Dingus.getResponseContentType(responseTypes, req); + if (!ctx.responseType) { + if (this.strictAccept) { + this.logger.debug(_scope, 'unhandled strict accept', { requestId: req.requestId }); + throw new ResponseError(Enum.ErrorResponse.NotAcceptable); + } else { + ctx.responseType = responseTypes[0]; + } + } + res.setHeader(Enum.Header.ContentType, ctx.responseType); + } + + + /** + * Inserts an encoding + * @param {http.ServerResponse} res + * @param {string} encoding + */ + static addEncodingHeader(res, encoding) { + const existingEncodings = res.getHeader(Enum.Header.ContentEncoding); + if (existingEncodings) { + encoding = `${encoding}, ${existingEncodings}`; + } + res.setHeader(Enum.Header.ContentEncoding, encoding); + } + + + /** + * Attempt to fetch both data and metadata for a file. + * @param {string} filePath + */ + async _readFileInfo(filePath) { + const _scope = _fileScope('_readFileInfo'); + let result; + try { + // eslint-disable-next-line security/detect-non-literal-fs-filename + const stat = fsPromises.stat(filePath); + // eslint-disable-next-line security/detect-non-literal-fs-filename + const data = fsPromises.readFile(filePath); + result = await Promise.all([stat, data]); + } catch (e) { + if (['ENOENT', 'EACCES', 'EISDIR', 'ENAMETOOLONG', 'EINVAL'].includes(e.code)) { + return [null, null]; + } + this.logger.error(_scope, 'fs error', { error: e, filePath }); + throw e; + } + return result; + } + + + /** + * Serve a file from a directory, with rudimentary cache awareness. + * This will also serve pre-encoded variations if available and requested. + * @param {http.ClientRequest} req + * @param {http.ServerResponse} res + * @param {object} ctx + * @param {string} directory + * @param {string} fileName + */ + async serveFile(req, res, ctx, directory, fileName) { + const _scope = _fileScope('serveFile'); + this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx }); + + // Normalize the supplied path, as encoded path-navigation may have been (maliciously) present. + fileName = path.normalize(fileName); + + // We will not deal with any subdirs, nor any dot-files. + // (Note that we could not deal with subdirs even if we wanted, due to simple router matching scheme.) + if (fileName.indexOf(path.sep) >= 0 + || fileName.charAt(0) === '.') { + this.logger.debug(_scope, 'rejected filename', { fileName }); + return this.handlerNotFound(req, res, ctx); + } + + const filePath = path.join(directory, fileName); + + // File must exist, before any alternate static encodings will be considered. + let [stat, data] = await this._readFileInfo(filePath); + if (!stat) { + return this.handlerNotFound(req, res, ctx); + } + + // If encodings were requested, check for static versions to serve. + // Update stat and data if matching version is found. + ctx.availableEncodings = Dingus.getResponseEncoding(Object.values(Enum.EncodingType), req); + if (ctx.availableEncodings.length === 0) { + // Identity encoding was specifically denied, and nothing else available. + this.logger.debug(_scope, 'no suitable encodings', { ctx }); + return this.handlerMethodNotAllowed(req, res, ctx); + } + for (const encoding of ctx.availableEncodings) { + if (encoding === Enum.EncodingType.Identity) { + break; + } + const suffix = Enum.EncodingTypeSuffix[encoding]; + if (suffix) { + const encodedFilePath = `${filePath}${suffix}`; + const [ encodedStat, encodedData ] = await this._readFileInfo(encodedFilePath); + if (encodedStat) { + ([ stat, data ] = [ encodedStat, encodedData ]); + ctx.selectedEncoding = encoding; + Dingus.addEncodingHeader(res, encoding); + res.setHeader(Enum.Header.Vary, Enum.Header.AcceptEncoding); + this.logger.debug(_scope, 'serving encoded version', { ctx, encodedFilePath }); + } + break; + } + } + + const lastModifiedDate = new Date(stat.mtimeMs); + res.setHeader(Enum.Header.LastModified, lastModifiedDate.toGMTString()); + + const eTag = common.generateETag(filePath, stat, data); + res.setHeader(Enum.Header.ETag, eTag); + + if (common.isClientCached(req, stat.mtimeMs, eTag)) { + this.logger.debug(_scope, 'client cached file', { filePath }); + res.statusCode = 304; // Not Modified + res.end(); + return; + } + + // Set the type based on extension of un-encoded filename. + const ext = path.extname(filePath).slice(1); // Drop the dot + const contentType = extensionToMime(ext); + res.setHeader(Enum.Header.ContentType, contentType); + + // We presume static files are relatively cacheable. + res.setHeader(Enum.Header.CacheControl, 'public'); + + this.logger.debug(_scope, 'serving file', { filePath, contentType }); + res.end(data); + } + + + /** + * Return a content-type appropriate rendering of an errorResponse object. + * @param {string} type content-type of response + * @param {object} err either an Error object, or an error response + * @param {number} err.statusCode + * @param {string} err.errorMessage + * @param {string|string[]} err.details + */ + // eslint-disable-next-line class-methods-use-this + renderError(contentType, err) { + switch (contentType) { + case Enum.ContentType.ApplicationJson: + return JSON.stringify(err); + + case Enum.ContentType.TextHTML: + return Template.errorHTML(err); + + case Enum.ContentType.TextPlain: + default: + return [err.errorMessage, err.details].join('\r\n'); + } + } + + + /** + * Send an error response. Terminal. + * Logs any non-error-response errors as such. + * @param {object} err either an Error object, or an error response + * @param {http.ClientRequest} req + * @param {http.ServerResponse} res + * @param {object} ctx + */ + sendErrorResponse(err, req, res, ctx) { + const _scope = _fileScope('sendErrorResponse'); + let body; + + // Default to a content type if one is not yet present + if (!res.hasHeader(Enum.Header.ContentType)) { + res.setHeader(Enum.Header.ContentType, Enum.ContentType.TextPlain); + } + + if (err && err.statusCode) { + res.statusCode = err.statusCode; + body = this.renderError(res.getHeader(Enum.Header.ContentType), err); + this.logger.debug(_scope, 'handler error', { err, ...common.handlerLogData(req, res, ctx) }); + } else { + res.statusCode = 500; + body = this.renderError(res.getHeader(Enum.Header.ContentType), Enum.ErrorResponse.InternalServerError); + this.logger.error(_scope, 'handler exception', { err, ...common.handlerLogData(req, res, ctx) }); + } + res.end(body); + } + + + /** + * @param {http.ClientRequest} req + * @param {http.ServerResponse} res + * @param {object} ctx + */ + async handlerMethodNotAllowed(req, res, ctx) { + this.setResponseType(this.responseTypes, req, res, ctx); + throw new ResponseError(Enum.ErrorResponse.MethodNotAllowed); + } + + + /** + * @param {http.ClientRequest} req + * @param {http.ServerResponse} res + * @param {object} ctx + */ + async handlerNotFound(req, res, ctx) { + this.setResponseType(this.responseTypes, req, res, ctx); + throw new ResponseError(Enum.ErrorResponse.NotFound); + } + + + /** + * @param {http.ClientRequest} req + * @param {http.ServerResponse} res + * @param {object} ctx + */ + async handlerBadRequest(req, res, ctx) { + this.setResponseType(this.responseTypes, req, res, ctx); + throw new ResponseError(Enum.ErrorResponse.BadRequest); + } + + /** + * @param {http.ClientRequest} req + * @param {http.ServerResponse} res + * @param {object} ctx + */ + async handlerInternalServerError(req, res, ctx) { + this.setResponseType(this.responseTypes, req, res, ctx); + throw new ResponseError(Enum.ErrorResponse.InternalServerError); + } + +} + +module.exports = Dingus; diff --git a/lib/enum.js b/lib/enum.js new file mode 100644 index 0000000..83e5094 --- /dev/null +++ b/lib/enum.js @@ -0,0 +1,104 @@ +'use strict'; + + +const ContentType = { + TextHTML: 'text/html', + TextPlain: 'text/plain', + ApplicationForm: 'application/x-www-form-urlencoded', + ApplicationJson: 'application/json', +}; + +// Supported encoding types +const EncodingType = { + Brotli: 'br', + Gzip: 'gzip', + XGzip: 'x-gzip', + Identity: 'identity', +}; + +// Filename suffixes for serving pre-encoded files +const EncodingTypeSuffix = { + [EncodingType.Brotli]: '.br', + [EncodingType.Gzip]: '.gz', + [EncodingType.XGzip]: '.gz', +}; + +const ErrorResponse = { + BadRequest: { + statusCode: 400, + errorMessage: 'Bad Request', + }, + Unauthorized: { + statusCode: 401, + errorMessage: 'Unauthorized', + }, + Forbidden: { + statusCode: 403, + errorMessage: 'Forbidden', + }, + ReallyForbidden: { + statusCode: 403, + errorMessage: 'F̦̩̫̼͔̫͓̃ͤ̈̆̀͑o̖̟͙̫̯̗̳̽ͦ̆́ͨr̩͉̰̗͉b̬̂͘į̟̬̓d͂͗҉̟͈̜͙ͅd͎̜̺̝͇͑̒̋̾ë̴̳̺͓̦̘́ͮ̈́ǹ͈̦̫̙', + }, + NotFound: { + statusCode: 404, + errorMessage: 'Not Found', + }, + MethodNotAllowed: { + statusCode: 405, + errorMessage: 'Method Not Allowed', + }, + NotAcceptable: { + statusCode: 406, + errorMessage: 'Not Acceptable', + }, + Gone: { + statusCode: 410, + errorMessage: 'Gone', + }, + UnsupportedMediaType: { + statusCode: 415, + errorMessage: 'Unsupported Media Type', + }, + InternalServerError: { + statusCode: 500, + errorMessage: 'Internal Server Error', + }, +}; + +const ErrorResponseDefaultProxy = new Proxy(ErrorResponse, { + get: (target, property) => { + // eslint-disable-next-line security/detect-object-injection + return (property in target) ? target[property] : { + errorMessage: `undefined error response '${property}'`, + }; + }, +}); + +const Header = { + Accept: 'Accept', + AcceptEncoding: 'Accept-Encoding', + CacheControl: 'Cache-Control', + ContentEncoding: 'Content-Encoding', + ContentLength: 'Content-Length', + ContentType: 'Content-Type', + ETag: 'ETag', + IfModifiedSince: 'If-Modified-Since', + IfNoneMatch: 'If-None-Match', + LastModified: 'Last-Modified', + RequestId: 'Request-ID', + Vary: 'Vary', + XCorrelationId: 'X-Correlation-ID', + XForwardedFor: 'X-Forwarded-For', + XForwardedProto: 'X-Forwarded-Proto', + XRealIP: 'X-Real-IP', + XRequestId: 'X-Request-ID', +}; + +module.exports = { + ContentType, + EncodingType, + EncodingTypeSuffix, + ErrorResponse: ErrorResponseDefaultProxy, + Header, +}; diff --git a/lib/errors.js b/lib/errors.js new file mode 100644 index 0000000..ed5b151 --- /dev/null +++ b/lib/errors.js @@ -0,0 +1,28 @@ +'use strict'; + +class DingusError extends Error { + constructor(...args) { + super(...args); + Error.captureStackTrace(DingusError); + } + + get name() { + return this.constructor.name; + } +} + +class ResponseError extends DingusError { + constructor(errorResponse, details) { + super(errorResponse.errorMessage); + Object.assign(this, { + ...errorResponse, + ...(details && { details }), + }); + delete this.stack; // No need for stacktrace here. + } +} + +module.exports = { + DingusError, + ResponseError, +}; diff --git a/lib/mime-helper.js b/lib/mime-helper.js new file mode 100644 index 0000000..086dc6a --- /dev/null +++ b/lib/mime-helper.js @@ -0,0 +1,28 @@ +/* eslint-disable security/detect-object-injection */ +'use strict'; + +const mimeDb = require('mime-db'); + +const defaultType = 'application/octet-stream'; +const extension = {}; +for (const [mimeType, entry] of Object.entries(mimeDb)) { + if (entry.extensions) { + entry.extensions.forEach((ext) => { + if (!(ext in extension)) { + extension[ext] = []; + } + extension[ext].push(mimeType); + }); + } +} + +/** + * Return a suitable type for a file extension. + * @param {string} ext file extension + * @param {string} def type to return if no match + */ +const extensionToMime = (ext, def = defaultType) => extension[ext] && extension[ext][extension[ext].length - 1] || def; + +module.exports = { + extensionToMime, +}; diff --git a/lib/patches/incoming-message.js b/lib/patches/incoming-message.js new file mode 100644 index 0000000..894ca65 --- /dev/null +++ b/lib/patches/incoming-message.js @@ -0,0 +1,17 @@ +'use strict'; +const { IncomingMessage } = require('http'); + +/** + * Welp, here we are, already into the crazy. + * Per documentation (https://nodejs.org/docs/latest-v12.x/api/http.html#http_request_getheader_name) + * this should exist, yet (as of 12.18.4) it does not. So let us change this pitch up, and patch +*/ +/* istanbul ignore else */ +if (typeof IncomingMessage.getHeader !== 'function') { + IncomingMessage.prototype.getHeader = function (name) { + if (typeof name !== 'string') { + throw new TypeError('\'name\' must be a string'); + } + return this.headers && this.headers[name.toLowerCase()]; + }; +} diff --git a/lib/patches/index.js b/lib/patches/index.js new file mode 100644 index 0000000..dcf4726 --- /dev/null +++ b/lib/patches/index.js @@ -0,0 +1,3 @@ +'use strict'; + +require('./incoming-message'); diff --git a/lib/router.js b/lib/router.js new file mode 100644 index 0000000..92a8500 --- /dev/null +++ b/lib/router.js @@ -0,0 +1,206 @@ +/* eslint-disable security/detect-object-injection */ +'use strict'; + +/** + * A very simple router. + */ + +const { METHODS: httpMethods } = require('http'); +const common = require('./common'); +const { DingusError } = require('./errors'); + +// Internal identifiers for route entries. +const METHODS = Symbol('METHODS'); +const PARAM = Symbol('PARAM'); + +const defaultOptions = { + ignoreTrailingSlash: false, + paramPrefix: ':', +}; + +class Router { + /** + * @param {Object} options + * @param {Boolean} options.ignoreTrailingSlash + * @param {Boolean} options.paramPrefix + */ + constructor(options = {}) { + common.setOptions(this, defaultOptions, options); + + // Keep lists of paths to match, indexed by path length. + this.pathsByLength = { + 1: [], + }; + + this.METHODS = METHODS; + this.PARAM = PARAM; + } + + + /** + * Prepare a path for insertion into search list. + * A searchable path is a list of path parts, with a property of method handlers. + * @param {string} pathDefinition + */ + _pathDefinitionToPathMatch(pathDefinition) { + const pathMatch = pathDefinition.split('/').map((p) => p.startsWith(this.paramPrefix) ? { [PARAM]: p.slice(this.paramPrefix.length) } : p); + if (this.ignoreTrailingSlash + && pathMatch[pathMatch.length - 1] === '') { + pathMatch.pop(); + } + pathMatch[METHODS] = {}; + pathMatch.forEach((p) => Object.freeze(p)); + Object.freeze(pathMatch); + return pathMatch; + } + + + /** + * Compare checkPath to fixedPath, no param substitution, params must match. + * @param {*} fixedPath + * @param {*} checkPath + */ + static _pathCompareExact(fixedPath, checkPath) { + if (fixedPath.length !== checkPath.length) { + return false; + } + for (let i = 0; i < fixedPath.length; i++) { + const fixedPart = fixedPath[i]; + const checkPart = checkPath[i]; + if (typeof fixedPart === 'object' && typeof checkPart === 'object') { + if (fixedPart[PARAM] !== checkPart[PARAM]) { + return false; + } + } else if (fixedPart !== checkPart) { + return false; + } + } + return true; + } + + + /** + * Compare checkPath to fixedPath, populating params. + * @param {*} fixedPath + * @param {*} checkPath + * @param {*} returnParams + */ + static _pathCompareParam(fixedPath, checkPath, returnParams = {}) { + const params = {}; + + if (fixedPath.length !== checkPath.length) { + return false; + } + for (let i = 0; i < fixedPath.length; i++) { + const fixedPart = fixedPath[i]; + const checkPart = checkPath[i]; + if (typeof fixedPart === 'object') { + params[fixedPart[PARAM]] = checkPart; + } else if (fixedPart !== checkPart) { + return false; + } + } + Object.assign(returnParams, params); + return true; + } + + + /** + * Search for an existing path, return matched path and path parameters. + * @param {Array} matchParts + */ + _pathFind(matchParts) { + const result = { + pathParams: {}, + matchedPath: undefined, + }; + const pathsByLength = this.pathsByLength[matchParts.length]; + if (pathsByLength) { + for (const p of pathsByLength) { + if (Router._pathCompareParam(p, matchParts, result.pathParams)) { + result.matchedPath = p; + break; + } + } + } + return result; + } + + + /** + * Return a matching path, no param substitution, params must match + * @param {*} matchParts + */ + _pathFindExact(matchParts) { + const pathsByLength = this.pathsByLength[matchParts.length]; + if (pathsByLength) { + for (const p of pathsByLength) { + if (Router._pathCompareExact(p, matchParts)) { + return p; + } + } + } + return undefined; + } + + + /** + * Insert a new path handler. + * @param {string|string[]} methods + * @param {string} urlPath + * @param {fn} handler + */ + on(methods, urlPath, handler) { + const matchParts = this._pathDefinitionToPathMatch(urlPath); + let existingPath = this._pathFindExact(matchParts); + if (!existingPath) { + existingPath = matchParts; + if (!(matchParts.length in this.pathsByLength)) { + this.pathsByLength[matchParts.length] = []; + } + this.pathsByLength[matchParts.length].push(existingPath); + } + if (!Array.isArray(methods)) { + methods = [methods]; + } + methods.forEach((method) => { + if (!httpMethods.includes(method) && method !== '*') { + throw new DingusError(`invalid method '${method}'`); + } + existingPath[METHODS][method] = handler; + }); + } + + + /** + * Return a matching handler for a request, sets path parameters on context. + * @param {string} method + * @param {string[]} urlPath + * @param {object} ctx + */ + lookup(method, urlPath, ctx = {}) { + const pathParts = urlPath.split('/').map((part) => decodeURIComponent(part)); + if (this.ignoreTrailingSlash + && pathParts[pathParts.length - 1] === '') { + pathParts.pop(); + } + const { matchedPath, pathParams } = this._pathFind(pathParts); + ctx.params = pathParams; + if (matchedPath) { + ctx.matchedPath = matchedPath; + if (method in matchedPath[METHODS]) { + return matchedPath[METHODS][method]; + } + if ('*' in matchedPath[METHODS]) { + return matchedPath[METHODS]['*']; + } + throw new DingusError('NoMethod'); + } + ctx.unmatchedPath = pathParts; + throw new DingusError('NoPath'); + } + + +} + +module.exports = Router; diff --git a/lib/template/error-html.js b/lib/template/error-html.js new file mode 100644 index 0000000..fb82ce6 --- /dev/null +++ b/lib/template/error-html.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = (err) => { + const detailsArr = Array.isArray(err.details) ? err.details : [ err.details ]; + return ` + + + ${err.statusCode} ${err.errorMessage} + + +

${err.errorMessage}

` + + (!err.details ? '' : detailsArr.map((d) => `\n

${d}

`).join('')) + ` + +`; +}; \ No newline at end of file diff --git a/lib/template/index.js b/lib/template/index.js new file mode 100644 index 0000000..75c07d5 --- /dev/null +++ b/lib/template/index.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + errorHTML: require('./error-html'), +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e32d0ce --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2902 @@ +{ + "name": "@squeep/api-dingus", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/core": { + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.11.6.tgz", + "integrity": "sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.6", + "@babel/helper-module-transforms": "^7.11.0", + "@babel/helpers": "^7.10.4", + "@babel/parser": "^7.11.5", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.11.5", + "@babel/types": "^7.11.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.19", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz", + "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==", + "dev": true, + "requires": { + "@babel/types": "^7.11.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz", + "integrity": "sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + } + }, + "@babel/helper-module-imports": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", + "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-module-transforms": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz", + "integrity": "sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.10.4", + "@babel/helper-replace-supers": "^7.10.4", + "@babel/helper-simple-access": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/template": "^7.10.4", + "@babel/types": "^7.11.0", + "lodash": "^4.17.19" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", + "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-replace-supers": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz", + "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.10.4", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-simple-access": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz", + "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==", + "dev": true, + "requires": { + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/helpers": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.4.tgz", + "integrity": "sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==", + "dev": true, + "requires": { + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, + "@babel/parser": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", + "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", + "dev": true + }, + "@babel/template": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/traverse": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.5.tgz", + "integrity": "sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.5", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.11.5", + "@babel/types": "^7.11.5", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + }, + "dependencies": { + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + } + }, + "@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", + "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", + "dev": true + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true + }, + "@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", + "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@sinonjs/samsam": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.0.2.tgz", + "integrity": "sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true + }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "requires": { + "default-require-extensions": "^3.0.0" + } + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "requires": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "chokidar": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "default-require-extensions": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", + "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", + "dev": true, + "requires": { + "strip-bom": "^4.0.0" + } + }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "dev": true, + "requires": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "eslint-plugin-es": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", + "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", + "dev": true, + "requires": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + } + }, + "eslint-plugin-node": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", + "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", + "dev": true, + "requires": { + "eslint-plugin-es": "^3.0.0", + "eslint-utils": "^2.0.0", + "ignore": "^5.1.1", + "minimatch": "^3.0.4", + "resolve": "^1.10.1", + "semver": "^6.1.0" + }, + "dependencies": { + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "eslint-plugin-security": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-1.4.0.tgz", + "integrity": "sha512-xlS7P2PLMXeqfhyf3NpqbvbnW04kN8M9NtmhpR3XGyOvt/vNKS7XPXT5EDbwKW9vCjWH4PpfQvgD/+JgN0VJKA==", + "dev": true, + "requires": { + "safe-regex": "^1.1.0" + } + }, + "eslint-plugin-sonarjs": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.9.1.tgz", + "integrity": "sha512-KKFofk1LPjGHWeAZijYWv32c/C4mz+OAeBNVxhxHu1hknrTOhu415MWC8qKdAdsmOlBPShs9evM4mI1o7MNMhw==", + "dev": true + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz", + "integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==", + "dev": true + }, + "foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + } + }, + "fromentries": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.2.1.tgz", + "integrity": "sha512-Xu2Qh8yqYuDhQGOhD5iJGninErSfI9A3FrriD3tjUgV5VbJFeH8vfgZ9HnC6jWN80QDVNQK5vmxRAmEAp7Mevw==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.1", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", + "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.10.0.tgz", + "integrity": "sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + }, + "dependencies": { + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "hasha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.0.tgz", + "integrity": "sha512-2W+jKdQbAdSIrggA8Q35Br8qKadTrqCTC8+XZvBWepKDK6m9XkX6Iz1a2yh2KP01kzAR/dpuMeUnocoLYDcskw==", + "dev": true, + "requires": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "requires": { + "append-transform": "^2.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "istanbul-lib-processinfo": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", + "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.0", + "istanbul-lib-coverage": "^3.0.0-alpha.1", + "make-dir": "^3.0.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^3.3.3" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", + "dev": true + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "mime-db": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", + "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mocha": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.0.3.tgz", + "integrity": "sha512-hnYFrSefHxYS2XFGtN01x8un0EwNu2bzKvhpRFhgoybIvMaOkkL60IVPmkb5h6XDmUl4IMSB+rT5cIO4/4bJgg==", + "dev": true, + "requires": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.2", + "debug": "4.3.1", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.1.7", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "3.0.4", + "ms": "2.1.3", + "nanoid": "3.1.23", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "wide-align": "1.1.3", + "workerpool": "6.1.5", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "nanoid": { + "version": "3.1.23", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", + "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "nise": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz", + "integrity": "sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^7.0.4", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "requires": { + "process-on-spawn": "^1.0.0" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "requires": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "os-shim": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz", + "integrity": "sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc=", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + }, + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + } + } + }, + "pre-commit": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/pre-commit/-/pre-commit-1.2.2.tgz", + "integrity": "sha1-287g7p3nI15X95xW186UZBpp7sY=", + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "spawn-sync": "^1.0.15", + "which": "1.2.x" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "which": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", + "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + } + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "requires": { + "fromentries": "^1.2.0" + } + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + }, + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "sinon": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.2.tgz", + "integrity": "sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": "^7.1.2", + "@sinonjs/samsam": "^6.0.2", + "diff": "^5.0.0", + "nise": "^5.1.0", + "supports-color": "^7.2.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "spawn-sync": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz", + "integrity": "sha1-sAeZVX63+wyDdsKdROih6mfldHY=", + "dev": true, + "requires": { + "concat-stream": "^1.4.7", + "os-shim": "^0.1.2" + } + }, + "spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "requires": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "table": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", + "integrity": "sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==", + "dev": true, + "requires": { + "ajv": "^8.0.1", + "lodash.clonedeep": "^4.5.0", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", + "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "workerpool": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz", + "integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + } + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "dependencies": { + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + } + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4202607 --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "@squeep/api-dingus", + "version": "1.0.0", + "description": "A minimal API server framework", + "main": "index.js", + "scripts": { + "coverage": "nyc --all npm test", + "eslint": "eslint index.js lib", + "test": "mocha --recursive" + }, + "pre-commit": [ + "coverage" + ], + "engines": { + "node": ">=12" + }, + "repository": { + "type": "git", + "url": "https://git.squeep.com/squeep-api-dingus/" + }, + "author": "Justin Wind ", + "license": "ISC", + "dependencies": { + "mime-db": "^1.49.0", + "uuid": "^8.3.2" + }, + "devDependencies": { + "eslint": "^7.32.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-security": "^1.4.0", + "eslint-plugin-sonarjs": "^0.9.1", + "mocha": "^9.0.3", + "nyc": "^15.1.0", + "pre-commit": "^1.2.2", + "sinon": "^11.1.2" + } +} diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..6075304 --- /dev/null +++ b/test/index.js @@ -0,0 +1,14 @@ +/* eslint-disable capitalized-comments */ +/* eslint-env mocha */ +'use strict'; + +const assert = require('assert'); +const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require +const apiDingus = require('..'); + + +describe('apiDingus', function () { + it('covers', function () { + assert(apiDingus); + }); +}); // apiDingus diff --git a/test/lib/common.js b/test/lib/common.js new file mode 100644 index 0000000..b802003 --- /dev/null +++ b/test/lib/common.js @@ -0,0 +1,381 @@ +/* eslint-disable capitalized-comments */ +/* eslint-env mocha */ +'use strict'; + +const assert = require('assert'); +const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require +const common = require('../../lib/common'); + + +describe('common', function () { + + describe('fileScope', function () { + it('names a file path', function () { + const filename = 'lib/foo/bar.js'; + const result = common.fileScope(filename)('baz'); + assert.strictEqual(result, 'bar:baz'); + }); + it('names an index path', function () { + const filename = 'lib/foo/index.js'; + const result = common.fileScope(filename)('baz'); + assert.strictEqual(result, 'foo:baz'); + }); + }); // fileScope + + describe('generateETag', function () { + it('generates a tag from data', function () { + const expected = '"RHUvNyculE/SyROjU0LqzN0arxibrlBnazAashP8UGE"'; + const data = 'example data'; + const result = common.generateETag(undefined, undefined, data); + assert.strictEqual(result, expected); + }); + it('generates a tag from data and stats', function () { + const expected = '"mtI0qCyqXsZVfX0Pgi+G6mQM10y9yVyi1NZejXSYttk"'; + const stat = { + mtimeMs: 1600113038270.2375, + }; + const data = 'example data'; + const result = common.generateETag(undefined, stat, data); + assert.strictEqual(result, expected); + }); + }); // generateETag + + describe('get', function () { + const def = 'default'; + it('covers missing obj', function () { + const result = common.get(undefined, undefined, def); + assert.strictEqual(result, def); + }); + it('covers missing prop', function () { + const result = common.get({}, undefined, def); + assert.strictEqual(result, def); + }); + it('covers default', function () { + const result = common.get({ k: 'v' }, 'x', def); + assert.strictEqual(result, def); + }); + it('covers prop', function () { + const result = common.get({ k: 'v' }, 'k', def); + assert.strictEqual(result, 'v'); + }); + }); // get + + describe('isClientCached', function () { + let req, modifiedTimeMs, eTag; + const blueMoon = 'Sat, 31 Oct 2020 07:51:00 GMT'; + const blueMoonMs = 1604130660000; + const oneDayMs = 86400000; + const anotherETag = '"RHUvNyculE/SyROjU0LqzN0arxibrlBnazAashP8UGE"'; + beforeEach(function () { + req = { + getHeader: sinon.stub(), + }; + modifiedTimeMs = 0; + eTag = '"mtI0qCyqXsZVfX0Pgi+G6mQM10y9yVyi1NZejXSYttk"'; + }); + it('no headers, not cached', function () { + const result = common.isClientCached(req, modifiedTimeMs, eTag); + assert.strictEqual(result, false); + }); + it('modified header, not cached', function () { + req.getHeader.onCall(0).returns(blueMoon); + modifiedTimeMs = blueMoonMs + oneDayMs; + const result = common.isClientCached(req, modifiedTimeMs, eTag); + assert.strictEqual(result, false); + }); + it('modified header, cached', function () { + req.getHeader.onCall(0).returns(blueMoon); + modifiedTimeMs = blueMoonMs - oneDayMs; + const result = common.isClientCached(req, modifiedTimeMs, eTag); + assert.strictEqual(result, true); + }); + it('match header, not matched', function () { + req.getHeader.onCall(1).returns(anotherETag); + const result = common.isClientCached(req, modifiedTimeMs, eTag); + assert.strictEqual(result, false); + }); + it('match header, matched', function () { + req.getHeader.onCall(1).returns(`${anotherETag}, ${eTag}, ${anotherETag}`); + const result = common.isClientCached(req, modifiedTimeMs, eTag); + assert.strictEqual(result, true); + }); + it('match header any, matched', function () { + req.getHeader.onCall(1).returns('*'); + const result = common.isClientCached(req, modifiedTimeMs, eTag); + assert.strictEqual(result, true); + }); + it('modified header cached, match header not matched, not cached', function () { + req.getHeader.onCall(0).returns(blueMoon); + modifiedTimeMs = blueMoonMs - oneDayMs; + req.getHeader.onCall(1).returns(`${anotherETag}, ${anotherETag}`); + const result = common.isClientCached(req, modifiedTimeMs, eTag); + assert.strictEqual(result, false); + }); + }); // iscClientCached + + describe('pick', function () { + it('picks', function () { + const srcObj = { + a: 1, + b: 2, + c: 3, + }; + const result = common.pick(srcObj, ['a', 'c', 'd']); + assert.deepStrictEqual(result, { + 'a': 1, + 'c': 3, + }); + }); + }); // pick + + describe('requestLogData', function () { + it('gives data', function () { + const req = { + method: 'GET', + somethingElse: 'blah', + }; + const result = common.requestLogData(req); + assert.deepStrictEqual(result, { + method: 'GET', + }); + }); + }); // requestLogData + + describe('responseLogData', function () { + it('gives data', function () { + const res = { + getHeaders: () => ({}), + statusCode: 200, + blah: 'blah', + }; + const result = common.responseLogData(res); + assert.deepStrictEqual(result, { + headers: {}, + statusCode: 200, + }); + }); + }); // responseLogData + + describe('setOptions', function () { + it('sets options', function () { + const expected = { + keyOne: 'default', + keyTwo: 'option', + keyThree: 'default', + }; + const target = {}; + const defaultOptions = { + keyOne: 'default', + keyTwo: 'default', + keyThree: 'default', + }; + const options = { + keyTwo: 'option', + keyFour: 'option', + }; + common.setOptions(target, defaultOptions, options); + assert.deepStrictEqual(target, expected); + }); + }); // setOptions + + describe('splitFirst', function () { + it('splits', function () { + const expected = ['foo', 'awoo']; + const src = 'foo?awoo'; + const delim = '?'; + const fill = 'fill'; + const result = common.splitFirst(src, delim, fill); + assert.deepStrictEqual(result, expected); + }); + it('fills', function () { + const expected = ['foo', 'fill']; + const src = 'foo'; + const delim = '?'; + const fill = 'fill'; + const result = common.splitFirst(src, delim, fill); + assert.deepStrictEqual(result, expected); + }); + }); // splitFirst + + describe('mergeEnum', function () { + it('merges enums', function () { + const origEnum = { + ContentType: { + TextHTML: 'text/html', + }, + ErrorResponse: { + BadRequest: { + statusCode: 400, + errorMessage: 'Bad Request', + }, + }, + }; + const newEnum = { + ContentType: { + TextPlain: 'text/plain', + }, + ErrorResponse: { + BadResponse: { + statusCode: 401, + errorMessage: 'Unauthorized', + }, + }, + Header: { + Accept: 'accept', + }, + Value: 'value', + }; + const mergedEnum = { + ContentType: { + TextHTML: 'text/html', + TextPlain: 'text/plain', + }, + ErrorResponse: { + BadRequest: { + statusCode: 400, + errorMessage: 'Bad Request', + }, + BadResponse: { + statusCode: 401, + errorMessage: 'Unauthorized', + }, + }, + Header: { + Accept: 'accept', + }, + Value: 'value', + }; + const result = common.mergeEnum(origEnum, newEnum); + assert.deepStrictEqual(result, mergedEnum); + }); + }); // mergeEnum + + describe('requestId', function () { + it('returns a string', function () { + const id = common.requestId(); + assert.strictEqual(typeof id, 'string'); + assert(id.length > 0); + }); + }); // requestId + + + describe('ensureLoggerLevels', function () { + it('adds missing levels', function () { + const result = common.ensureLoggerLevels(); + assert.deepStrictEqual(result, common.nullLogger); + }); + }); // ensureLoggerLevels + + describe('httpStatusCodeClass', function () { + it('works', function () { + for (const [statusCode, statusClassExpected] of Object.entries({ + 200: 2, + 304: 3, + 404: 4, + 500: 5, + })) { + const statusClass = common.httpStatusCodeClass(statusCode); + assert.strictEqual(statusClass, statusClassExpected); + } + }); + }); // ensureLoggerLevels + + describe('mergeDeep', function () { + it('simple merge', function () { + const o1 = { + foo1: 'one', + }; + const o2 = { + foo2: 'two', + }; + const o3 = { + foo3: 'three', + }; + const expected = { + foo1: 'one', + foo2: 'two', + foo3: 'three', + }; + const result = common.mergeDeep(o1, o2, o3); + assert.deepStrictEqual(result, expected); + }); + it('deep merge', function () { + const o1 = { + foo: { + quux: 1, + }, + bar: 'baz', + }; + const o2 = { + foo: { + myList: ['fancy'], + gleep: { + fraz: 2, + }, + }, + blah: 'frop', + }; + const o3 = { + foo: { + myList: ['pants'], + gleep: { + troz: 'poit', + }, + narf: 3, + }, + bar: 'yup', + }; + const expected = { + foo: { + myList: ['fancy', 'pants'], + gleep: { + fraz: 2, + troz: 'poit', + }, + narf: 3, + quux: 1, + }, + bar: 'yup', + blah: 'frop', + }; + const result = common.mergeDeep(o1, o2, o3); + assert.deepStrictEqual(result, expected); + }); + it('merged object is distinct', function () { + const o1 = { + foo: { + bar: 3, + }, + }; + const o2 = { + baz: { + quux: 4, + }, + }; + const expected = { + foo: { + bar: 3, + }, + baz: { + quux: 4, + }, + }; + const result = common.mergeDeep(o1, o2); + assert.deepStrictEqual(result, expected); + const expected2 = { + foo: { + bar: 5, + }, + baz: { + quux: 5, + }, + }; + result.foo.bar = 5; + result.baz.quux = 5; + assert.deepStrictEqual(result, expected2); + assert.strictEqual(o1.foo.bar, 3); + assert.strictEqual(o2.baz.quux, 4); + }); + }); // mergeDeep + +}); diff --git a/test/lib/content-negotiation.js b/test/lib/content-negotiation.js new file mode 100644 index 0000000..79a73aa --- /dev/null +++ b/test/lib/content-negotiation.js @@ -0,0 +1,207 @@ +/* eslint-disable capitalized-comments */ +/* eslint-env mocha */ +'use strict'; + +const assert = require('assert'); +const ContentNegotiation = require('../../lib/content-negotiation'); +const Enum = require('../../lib/enum'); + +const TYPES = { + Any: '*/*', + ImageAny: 'image/*', + ImagePng: 'image/png', + TextAny: 'text/*', + TextHtml: 'text/html', + TextPlain: 'text/plain', + AppJson: 'application/json', +}; + +describe('ContentNegotiation', function () { + describe('_unpackAcceptClause', function () { + it('handles bare type', function () { + const clause = TYPES.TextPlain; + const expected = { + type: TYPES.TextPlain, + weight: 1.0, + params: {}, + }; + const result = ContentNegotiation._unpackAcceptClause(clause); + assert.deepStrictEqual(result, expected); + }); + it('handles weighted type', function () { + const clause = ` ${TYPES.TextPlain}; q=0.2`; + const expected = { + type: TYPES.TextPlain, + weight: 0.2, + params: {}, + }; + const result = ContentNegotiation._unpackAcceptClause(clause); + assert.deepStrictEqual(result, expected); + }); + it('handles type with extension parameters', function () { + const clause = `${TYPES.TextPlain}; format=flowed;charset=UTF-8`; + const expected = { + type: TYPES.TextPlain, + weight: 1.0, + params: { + format: 'flowed', + charset: 'UTF-8', + }, + }; + const result = ContentNegotiation._unpackAcceptClause(clause); + assert.deepStrictEqual(result, expected); + }); + it('handles type with params and weight', function () { + const clause = `${TYPES.AppJson};charset=UTF-8;q=0.123;ext=local`; + const expected = { + type: TYPES.AppJson, + weight: 0.123, + params: { + charset: 'UTF-8', + ext: 'local', + }, + }; + const result = ContentNegotiation._unpackAcceptClause(clause); + assert.deepStrictEqual(result, expected); + }); + it('ignores empty type', function () { + const clause = ''; + const expected = undefined; + const result = ContentNegotiation._unpackAcceptClause(clause); + assert.deepStrictEqual(result, expected); + }); + it('ignores weird param', function () { + const clause = `${TYPES.AppJson};blah`; + const expected = { + type: TYPES.AppJson, + weight: 1.0, + params: {}, + }; + const result = ContentNegotiation._unpackAcceptClause(clause); + assert.deepStrictEqual(result, expected); + }); + }); // _unpackAcceptClause + + describe('_acceptClauses', function () { + it('orders multiple types properly', function () { + const acceptHeader = 'text/*;q=0.3, text/html;q=0.7, text/html;level=1,text/html;level=2;q=0.4, */*;q=0.5'; + const expected = [ + { type: TYPES.TextHtml, weight: 1.0, params: { 'level': '1' } }, + { type: TYPES.TextHtml, weight: 0.7, params: {} }, + { type: TYPES.Any, weight: 0.4998, params: {} }, // N.B. Implementation detail + { type: TYPES.TextHtml, weight: 0.4, params: { 'level': '2' } }, + { type: TYPES.TextAny, weight: 0.2999, params: {} }, // N.B. Implementation detail + ]; + const result = ContentNegotiation._acceptClauses(acceptHeader); + assert.deepStrictEqual(result, expected); + }); + it('does the expected', function () { + const acceptHeader = 'text/*;q=0.5, text/plain;q=0.7, */*;q=0.2'; + const expected = [ + { type: TYPES.TextPlain, weight: 0.7, params: {} }, + { type: TYPES.TextAny, weight: 0.4999, params: {} }, // N.B. Implementation detail + { type: TYPES.Any, weight: 0.1998, params: {} }, // N.B. Implementation detail + ]; + const result = ContentNegotiation._acceptClauses(acceptHeader); + assert.deepStrictEqual(result, expected); + }); + it('covers missing data', function () { + const result = ContentNegotiation._acceptClauses(undefined); + assert.deepStrictEqual(result, []); + }); + }); // _acceptClauses + + describe('_matchType', function () { + it('matches simmple', function () { + const result = ContentNegotiation._matchType(TYPES.TextPlain, TYPES.TextPlain); + assert.strictEqual(result, true); + }); + it('does not match simmple', function () { + const result = ContentNegotiation._matchType(TYPES.TextPlain, TYPES.TextHtml); + assert.strictEqual(result, false); + }); + it('matches subtype wildcard', function () { + const result = ContentNegotiation._matchType(TYPES.ImageAny, TYPES.ImagePng); + assert.strictEqual(result, true); + }); + it('does not match subtype wildcard', function () { + const result = ContentNegotiation._matchType(TYPES.ImageAny, TYPES.TextPlain); + assert.strictEqual(result, false); + }); + it('matches any wildcard', function () { + const result = ContentNegotiation._matchType(TYPES.Any, TYPES.AppJson); + assert.strictEqual(result, true); + }); + it('matches main type', function () { + const result = ContentNegotiation._matchType('image', TYPES.ImagePng); + assert.strictEqual(result, true); + }); + it('does not match main type', function () { + const result = ContentNegotiation._matchType('image', TYPES.AppJson); + assert.strictEqual(result, false); + }); + }); // _matchType + + describe('accept', function () { + it('accepts anything', function () { + const types = [ TYPES.AppJson, TYPES.TextPlain ]; + const acceptHeader = undefined; + const result = ContentNegotiation.accept(types, acceptHeader); + assert.strictEqual(result, TYPES.AppJson); + }); + + it('picks the most acceptible type', function () { + const types = [ TYPES.AppJson, TYPES.TextPlain ]; + const acceptHeader = 'text/*;q=0.5, text/html;q=0.6, text/plain;q=0.7, */*;q=0.2, image/png;q=0.0, image/gif;q=0.0'; + const result = ContentNegotiation.accept(types, acceptHeader); + assert.strictEqual(result, TYPES.TextPlain); + }); + it('picks the most acceptible type', function () { + const types = [ TYPES.AppJson, TYPES.TextPlain ]; + const acceptHeader = 'image/png;q=0.0, image/gif;q=0.0, text/html;q=0.6, text/*;q=0.5, text/plain;q=0.7, */*;q=0.7'; + const result = ContentNegotiation.accept(types, acceptHeader); + assert.strictEqual(result, TYPES.TextPlain); + }); + it('refuses to pick a zero-weight type', function () { + const types = [ TYPES.AppJson, TYPES.TextPlain ]; + const acceptHeader = 'application/*;q=0.0, unhandled/type;q=0.7, none/none;q=0.2'; + const result = ContentNegotiation.accept(types, acceptHeader); + assert.strictEqual(result, undefined); + }); + it('picks least-wild match', function () { + const types = [ TYPES.TextHtml, TYPES.TextPlain ]; + const acceptHeader = '*/*;q=0.5, text/plain;q=0.5, text/*;q=0.5'; + const result = ContentNegotiation.accept(types, acceptHeader); + assert.strictEqual(result, TYPES.TextPlain); + }); + }); // accept + + describe('preferred', function () { + it('falls back to identity', function () { + const encodings = []; + const header = 'br, gzip'; + const result = ContentNegotiation.preferred(encodings, header); + assert.deepStrictEqual(result, [Enum.EncodingType.Identity]); + }); + it('picks appropriate', function () { + const encodings = ['br', 'identity']; + const header = 'lz4, br, gzip;q=.5'; + const result = ContentNegotiation.preferred(encodings, header); + assert.deepStrictEqual(result, [Enum.EncodingType.Brotli, Enum.EncodingType.Identity]); + }); + it('respects forbidding', function () { + const encodings = ['br']; + const header = 'gzip, *;q=0'; + const result = ContentNegotiation.preferred(encodings, header); + assert.deepStrictEqual(result, []); + }); + it('respects identity', function () { + const encodings = ['br']; + const header = 'gzip, identity;q=0.1, *;q=0'; + const result = ContentNegotiation.preferred(encodings, header); + assert.deepStrictEqual(result, [Enum.EncodingType.Identity]); + }); + }); // preferred + +}); // ContentNegotiation + diff --git a/test/lib/dingus.js b/test/lib/dingus.js new file mode 100644 index 0000000..a1070a5 --- /dev/null +++ b/test/lib/dingus.js @@ -0,0 +1,873 @@ +/* eslint-disable capitalized-comments */ +/* eslint-env mocha */ +'use strict'; + +const assert = require('assert'); +const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require +const fs = require('fs'); + +const Dingus = require('../../lib/dingus'); +const { DingusError } = require('../../lib/errors'); +const Enum = require('../../lib/enum'); + +const noExpectedException = 'did not get expected exception'; + +describe('Dingus', function () { + const dingus = new Dingus(); + + afterEach(function () { + sinon.restore(); + }); + + describe('constructor', function () { + it('covers', function () { + const d = new Dingus({}, {}); + assert(d); + assert('log' in d.logger); + }); + }); // constructor + + describe('_normalizePath', function () { + it('returns normal path', function () { + const p = '/a/b/c'; + const r = dingus._normalizePath(p); + assert.strictEqual(r, p); + }); + it('returns normal path', function () { + const p = '////a///b/./bar/..///c'; + const expected = '/a/b/c' + const r = dingus._normalizePath(p); + assert.strictEqual(r, expected); + }); + }); // _normalizePath + + describe('_splitUrl', function () { + const nullObject = Object.create(null); + + it('splits a simple path', function () { + const p = '/a/b/c'; + const expected = { + pathPart: p, + queryParams: nullObject, + }; + const r = dingus._splitUrl(p); + assert.deepStrictEqual(r, expected); + }); + it('splits a path with trailing slash preserved', function () { + const p = '/a/b/c/'; + const expected = { + pathPart: p, + queryParams: nullObject, + }; + const r = dingus._splitUrl(p); + assert.deepStrictEqual(r, expected); + }); + it('splits a path with trailing slash ignored', function () { + const p = '/a/b/c/'; + const expected = { + pathPart: p, + queryParams: nullObject, + }; + dingus.ignoreTrailingSlash = true; + const r = dingus._splitUrl(p); + assert.deepStrictEqual(r, expected); + }); + it('splits a path with empty query string', function () { + const p = '/a/b/c?'; + const expected = { + pathPart: '/a/b/c', + queryParams: nullObject, + }; + const r = dingus._splitUrl(p); + assert.deepStrictEqual(r, expected); + }); + it('splits a path with query string', function () { + const p = '/a/b/c?x=1&y=2&z'; + const expected = { + pathPart: '/a/b/c', + queryParams: Object.assign(Object.create(null), { + x: '1', + y: '2', + z: '', // Subjective Editorial: disagree with the default querystring parser behavior here: null would be better than empty string, esp as result is null-prototyped object. + }), + }; + const r = dingus._splitUrl(p); + assert.deepStrictEqual(r, expected); + }); + }); // _splitUrl + + describe('tagContext', function () { + let req, res, ctx; + beforeEach(function () { + req = { + getHeader: sinon.stub(), + setHeader: sinon.stub(), + }; + res = { + getHeader: sinon.stub(), + setHeader: sinon.stub(), + }; + ctx = {}; + }); + it ('sets id in context', function () { + const result = Dingus.tagContext(req, res, ctx); + assert.strictEqual(ctx.requestId, result); + assert(res.setHeader.called); + }); + it ('sets provided header', function () { + req.getHeader.onCall(0).returns('abc'); // X-Request-ID + const result = Dingus.tagContext(req, res, ctx); + assert.strictEqual(ctx.requestId, result); + assert.strictEqual(res.setHeader.getCall(0).args[0], 'Request-ID'); + assert.strictEqual(res.setHeader.getCall(1).args[0], 'X-Request-ID'); + assert.strictEqual(res.setHeader.getCall(1).args[1], 'abc'); + assert.strictEqual(res.setHeader.callCount, 2); + }); + }); // tagContext + + describe('clientAddressContext', function () { + let req, res, ctx; + let _tp; + before(function () { + _tp = dingus.trustProxy; + }); + after(function () { + dingus.trustProxy = _tp; + }); + beforeEach(function () { + req = { + getHeader: sinon.stub(), + setHeader: sinon.stub(), + connection: {}, + }; + res = { + getHeader: sinon.stub(), + setHeader: sinon.stub(), + }; + ctx = {}; + }); + it ('covers untrusted proxy', function () { + dingus.trustProxy = false; + const expected = { + clientAddress: '', + clientProtocol: 'http', + } + dingus.clientAddressContext(req, res, ctx); + assert.deepStrictEqual(ctx, expected); + assert(!req.getHeader.called); + }); + it ('covers missing', function () { + dingus.trustProxy = true; + const expected = { + clientAddress: '::1', + clientProtocol: 'https', + } + req.connection.remoteAddress = '::1'; + req.connection.encrypted = true; + dingus.clientAddressContext(req, res, ctx); + assert(req.getHeader.called); + assert.deepStrictEqual(ctx, expected); + }); + }); // clientAddressContext + + describe('getRequestContentType', function () { + let req; + beforeEach(function () { + req = { + getHeader: sinon.stub(), + setHeader: sinon.stub(), + }; + }); + it('handles missing header', function () { + const result = Dingus.getRequestContentType(req); + assert.strictEqual(result, ''); + }); + it('parses simple type', function () { + req.getHeader.onCall(0).returns(Enum.ContentType.ApplicationJson); + const result = Dingus.getRequestContentType(req); + assert.strictEqual(result, Enum.ContentType.ApplicationJson); + }); + it('parses complex type', function () { + req.getHeader.onCall(0).returns('application/json ; charset=UTF-8'); + const result = Dingus.getRequestContentType(req); + assert.strictEqual(result, Enum.ContentType.ApplicationJson); + }); + }); // getRequestContentType + + describe('setResponseContentType', function () { + let req, responseTypes; + beforeEach(function () { + responseTypes = []; + req = { + setHeader: sinon.stub(), + getHeader: sinon.stub(), + }; + }); + it('handles missing header', function () { + const result = Dingus.getResponseContentType(responseTypes, req); + assert.strictEqual(result, undefined); + }); + it('behaves as expected', function () { + responseTypes.push(Enum.ContentType.ApplicationJson); + req.getHeader.onCall(0).returns('text, image/png;q=0.5, application/*;q=0.2, audio;q=0.1'); + const result = Dingus.getResponseContentType(responseTypes, req); + assert.strictEqual(result, Enum.ContentType.ApplicationJson); + }); + }); // setResponseContentType + + describe('on', function () { + let stubOn; + beforeEach(function () { + stubOn = sinon.stub(dingus.router, 'on'); + }); + it('covers', function () { + dingus.on('GET', '/', () => {}); + assert(stubOn.called); + }); + }); + + describe('setEndBodyHandler', function () { + let req, res, ctx, handler, origEnd, origWrite; + beforeEach(function () { + origEnd = sinon.stub(); + origWrite = sinon.stub(); + req = {}; + res = { + write: origWrite, + end: origEnd, + }; + ctx = {}; + handler = sinon.stub(); + }); + it('collects body and handles', function () { + Dingus.setEndBodyHandler(req, res, ctx, handler); + res.write(Buffer.from('foo')); + res.write('baz'); + res.write(); + res.end('quux'); + assert(origWrite.called); + assert(origEnd.called); + assert.deepStrictEqual(ctx.responseBody, Buffer.from('foobazquux')); + assert(handler.called); + }); + }); // setEndBodyHandler + + describe('setHeadHandler', function () { + let req, res, ctx, origEnd, origWrite; + beforeEach(function () { + origEnd = sinon.stub(); + origWrite = sinon.stub(); + req = { + method: 'HEAD', + }; + res = { + end: origEnd, + write: origWrite, + setHeader: sinon.stub(), + }; + ctx = {}; + }); + it('collects body without writing', function () { + Dingus.setHeadHandler(req, res, ctx); + res.write(Buffer.from('foo')); + res.write('baz'); + res.write(); + res.end('quux'); + assert(!origWrite.called); + assert(origEnd.called); + assert.deepStrictEqual(ctx.responseBody, Buffer.from('foobazquux')); + }); + it('ignores non-head method', function () { + req.method = 'GET'; + Dingus.setHeadHandler(req, res, ctx); + res.write(Buffer.from('foo')); + res.end('bar'); + assert(origWrite.called); + assert(origEnd.called); + }); + }); // setHeadHandler + + describe('addEncodingHeader', function () { + let res, encoding; + beforeEach(function () { + res = { + _headers: {}, + // eslint-disable-next-line security/detect-object-injection + getHeader: (h) => res._headers[h], + // eslint-disable-next-line security/detect-object-injection + setHeader: (h, v) => res._headers[h] = v, + }; + }); + it('adds', function () { + encoding = 'gzip'; + Dingus.addEncodingHeader(res, encoding); + assert.strictEqual(res._headers[Enum.Header.ContentEncoding], 'gzip'); + }); + it('extends', function () { + encoding = 'utf8'; + Dingus.addEncodingHeader(res, encoding); + assert.strictEqual(res._headers[Enum.Header.ContentEncoding], 'utf8'); + encoding = 'gzip'; + Dingus.addEncodingHeader(res, encoding); + assert.strictEqual(res._headers[Enum.Header.ContentEncoding], 'gzip, utf8'); + }); + }); // addEncodingHeader + + describe('dispatch', function () { + let pathsByLengthOrig; + let req, res, ctx; + let stubHandler; + + beforeEach(function () { + req = { + url: '/', + method: 'GET', + setHeader: sinon.stub(), + getHeader: sinon.stub(), + }; + res = { + statusCode: 200, + end: sinon.stub(), + setHeader: sinon.stub(), + hasHeader: sinon.stub(), + getHeader: sinon.stub(), + getHeaders: sinon.stub(), + }; + ctx = {}; + pathsByLengthOrig = dingus.pathsByLength; + sinon.spy(dingus, 'handlerMethodNotAllowed'); + sinon.spy(dingus, 'handlerNotFound'); + sinon.spy(dingus, 'handlerBadRequest'); + sinon.spy(dingus, 'handlerInternalServerError'); + stubHandler = sinon.stub(); + }); + afterEach(function () { + dingus.pathsByLength = pathsByLengthOrig; + }); + + it('calls handler', async function () { + const urlPath = '/:id'; + const method = 'GET'; + dingus.on(method, urlPath, stubHandler); + req.url = '/abc'; + req.method = method; + + await dingus.dispatch(req, res, ctx); + assert(stubHandler.called); + assert(!dingus.handlerMethodNotAllowed.called); + assert(!dingus.handlerNotFound.called); + }); + it('calls handler without context', async function () { + const urlPath = '/:id'; + const method = 'GET'; + dingus.on(method, urlPath, stubHandler); + req.url = '/abc'; + req.method = method; + + await dingus.dispatch(req, res); + assert(stubHandler.called); + assert(!dingus.handlerMethodNotAllowed.called); + assert(!dingus.handlerNotFound.called); + }); + it('calls fallback handler', async function () { + const urlPath = '/abc/:id'; + const method = 'GET'; + dingus.on('*', urlPath, stubHandler); + req.url = '/abc/def'; + req.method = method; + + await dingus.dispatch(req, res, ctx); + assert(stubHandler.called); + assert(!dingus.handlerMethodNotAllowed.called); + assert(!dingus.handlerNotFound.called); + }); + it('handles error in handler', async function () { + const urlPath = '/:id'; + const method = 'GET'; + dingus.on(method, urlPath, stubHandler); + req.url = '/abc'; + req.method = method; + stubHandler.rejects(new Error('blah')); + + await dingus.dispatch(req, res, ctx); + assert(stubHandler.called); + assert(!dingus.handlerMethodNotAllowed.called); + assert(!dingus.handlerNotFound.called); + }); + it('calls unsupported method', async function () { + const urlPath = '/:id'; + const method = 'POST'; + dingus.on('GET', urlPath, stubHandler); + req.url = '/abc'; + req.method = method; + + await dingus.dispatch(req, res, ctx); + assert(!stubHandler.called); + assert(dingus.handlerMethodNotAllowed.called); + assert(!dingus.handlerNotFound.called); + }); + it('does not lookup nonexistent path', async function () { + req.url = '/foo/bar'; + req.method = 'GET'; + + await dingus.dispatch(req, res, ctx); + assert(!stubHandler.called); + assert(!dingus.handlerMethodNotAllowed.called); + assert(dingus.handlerNotFound.called); + }); + it('covers unhandled dingus exception', async function () { + const expectedException = new DingusError('blah'); + sinon.stub(dingus.router, 'lookup').throws(expectedException); + + await dingus.dispatch(req, res, ctx); + assert(!stubHandler.called); + assert(dingus.handlerInternalServerError.called); + }); + it('covers other exception', async function () { + const expectedException = new Error('blah'); + sinon.stub(dingus.router, 'lookup').throws(expectedException); + + await dingus.dispatch(req, res, ctx); + assert(!stubHandler.called); + assert(dingus.handlerInternalServerError.called); + }); + it('covers bad uri', async function () { + req.url = '/%f'; + + await dingus.dispatch(req, res, ctx); + assert(dingus.handlerBadRequest.called); + }); + + }); // dispatch + + describe('parseBody', function () { + let ctx; + beforeEach(function () { + ctx = {}; + }); + it('does not parse unknown type', function () { + try { + dingus.parseBody('unknown/type', ctx); + assert.fail(noExpectedException); + } catch (e) { + assert.strictEqual(e.statusCode, 415); + } + }); + it('parses json', function () { + const src = { foo: 'bar' }; + ctx.rawBody = JSON.stringify(src); + dingus.parseBody(Enum.ContentType.ApplicationJson, ctx); + assert.deepStrictEqual(ctx.parsedBody, src); + }); + it('handles unparsable json', function () { + ctx.rawBody = 'not json'; + try { + dingus.parseBody(Enum.ContentType.ApplicationJson, ctx); + assert.fail(noExpectedException); + } catch (e) { + assert.strictEqual(e.statusCode, 400); + } + }); + it('parses form', function () { + const expected = Object.assign(Object.create(null), { + foo: 'bar', + }); + ctx.rawBody = 'foo=bar'; + dingus.parseBody('application/x-www-form-urlencoded', ctx); + assert.deepStrictEqual(ctx.parsedBody, expected); + }); + + }); // parseBody + + describe('bodyData', function () { + let res, resEvents; + beforeEach(function () { + resEvents = {}; + res = { + // eslint-disable-next-line security/detect-object-injection + on: (ev, fn) => resEvents[ev] = fn, + }; + }); + it('provides data', async function () { + const p = dingus.bodyData(res); + resEvents['data'](Buffer.from('foo')); + resEvents['data'](Buffer.from('bar')); + resEvents['end'](); + const result = await p; + assert.strictEqual(result, 'foobar'); + }); + it('handles error', async function () { + const p = dingus.bodyData(res); + resEvents['error']('foo'); + try { + await p; + assert.fail(noExpectedException); + } catch (e) { + assert.strictEqual(e, 'foo'); + } + }); + }); // bodyData + + describe('ingestBody', function () { + it('covers', async function () { + const req = {}; + const res = {}; + const ctx = {}; + sinon.stub(dingus, 'bodyData').resolves('{"foo":"bar"}') + sinon.stub(Dingus, 'getRequestContentType').returns(Enum.ContentType.ApplicationJson); + await dingus.ingestBody(req, res, ctx); + assert.deepStrictEqual(ctx.parsedBody, { foo: 'bar' }); + }); + }); // ingestBody + + describe('setResponseType', function () { + let req, res, ctx; + let _sa; // Preserve strictAccept + before(function () { + _sa = dingus.strictAccept; + }); + after(function () { + dingus.strictAccept = _sa; + }); + beforeEach(function () { + ctx = {}; + req = {}; + res = { + setHeader: sinon.stub(), + }; + sinon.stub(Dingus, 'getResponseContentType').returns(); + }); + it('rejects missing', function () { + dingus.strictAccept = true; + try { + dingus.setResponseType(['my/type'], req, res, ctx); + assert.fail(noExpectedException); + } catch (e) { + assert.strictEqual(e.statusCode, 406, 'did not get expected status code'); + } + }); + it('accepts missing', function () { + dingus.strictAccept = false; + dingus.setResponseType(['my/type'], req, res, ctx); + assert.strictEqual(ctx.responseType, 'my/type'); + }); + + }); // setResponseType + + describe('_readFileInfo', function () { + let stat, data, statRes, dataRes, filename; + beforeEach(function () { + sinon.stub(fs.promises, 'stat'); + sinon.stub(fs.promises, 'readFile'); + statRes = { + mtimeMs:1612553697186, + }; + dataRes = 'data'; + filename = 'dummy.txt'; + }); + it('succeeds', async function () { + fs.promises.stat.resolves(statRes); + fs.promises.readFile.resolves('data'); + [stat, data] = await dingus._readFileInfo(filename); + assert.deepStrictEqual(stat, statRes); + assert.deepStrictEqual(data, dataRes); + }); + it('returns null for non-existant file', async function () { + const noEnt = { + code: 'ENOENT', + }; + fs.promises.stat.rejects(noEnt); + fs.promises.readFile.rejects(noEnt); + [stat, data] = await dingus._readFileInfo(filename); + assert.strictEqual(stat, null); + assert.strictEqual(data, null); + }); + it('throws unexpected error', async function () { + const expectedException = new Error('blah'); + fs.promises.stat.rejects(expectedException); + await assert.rejects(async () => { + await dingus._readFileInfo(filename); + }, expectedException); + }); + }); // _readFileInfo + + describe('serveFile', function () { + const path = require('path'); + let ctx, req, res, directory, fileName, filestats; + beforeEach(function () { + directory = path.join(__dirname, '..', 'test-data'); + fileName = 'example.html'; + ctx = {}; + req = { + _headers: { + [Enum.Header.Accept]: undefined, + [Enum.Header.IfModifiedSince]: undefined, + [Enum.Header.AcceptEncoding]: undefined, + [Enum.Header.IfNoneMatch]: undefined, + }, + getHeader: (header) => { + if (header in req._headers) { + // eslint-disable-next-line security/detect-object-injection + return req._headers[header]; + } + assert.fail(`unexpected getHeader ${header}`); + }, + }; + res = { + end: sinon.stub(), + getHeader: sinon.stub(), + getHeaders: sinon.stub(), + hasHeader: sinon.stub().returns(true), + setHeader: sinon.stub(), + }; + filestats = { + dev: 39, + mode: 33188, + nlink: 1, + uid: 1002, + gid: 1002, + rdev: 0, + blksize: 512, + ino: 897653, + size: 8, + blocks: 17, + atimeMs: 1613253436842.815, + mtimeMs: 1603485933192.8610, + ctimeMs: 1603485933192.8610, + birthtimeMs: 0, + atime: '2021-02-13T21:57:16.843Z', + mtime: '2020-10-23T13:45:33.193Z', + ctime: '2020-10-23T13:45:33.193Z', + birthtime: '1970-01-01T00:00:00.000Z', + }; + sinon.stub(dingus, 'handlerNotFound'); + sinon.stub(fs.promises, 'stat').resolves(filestats); + sinon.spy(fs.promises, 'readFile'); + }); + it('serves a file', async function () { + await dingus.serveFile(req, res, ctx, directory, fileName); + assert(fs.promises.readFile.called); + assert(!dingus.handlerNotFound.called); + }); + it('does not serve dot-file', async function () { + fileName = '.example'; + await dingus.serveFile(req, res, ctx, directory, fileName); + assert(!fs.promises.readFile.called); + assert(dingus.handlerNotFound.called); + }); + it('does not serve encoded navigation', async function () { + fileName = '/example.html'; + await dingus.serveFile(req, res, ctx, directory, fileName); + assert(!fs.promises.readFile.called); + assert(dingus.handlerNotFound.called); + }); + it('does not serve missing file', async function () { + fileName = 'no-file.here'; + await dingus.serveFile(req, res, ctx, directory, fileName); + assert(dingus.handlerNotFound.called); + }); + it('covers fs error', async function () { + const expectedException = new Error('blah'); + fs.promises.stat.restore(); + sinon.stub(fs.promises, 'stat').rejects(expectedException); + try { + await dingus.serveFile(req, res, ctx, directory, fileName); + assert.fail('should have thrown'); + } catch (e) { + assert.strictEqual(e, expectedException); + } + }); + it('caches by modified', async function () { + req._headers[Enum.Header.IfModifiedSince] = 'Fri, 23 Oct 2020 23:11:16 GMT'; + await dingus.serveFile(req, res, ctx, directory, fileName); + assert.strictEqual(res.statusCode, 304); + }); + it('does not cache old modified', async function () { + req._headers[Enum.Header.IfModifiedSince] = 'Fri, 23 Oct 2020 01:11:16 GMT'; + await dingus.serveFile(req, res, ctx, directory, fileName); + assert.notStrictEqual(res.statusCode, 304); + assert(!dingus.handlerNotFound.called); + }); + it('caches ETag match', async function () { + req._headers[Enum.Header.IfNoneMatch] = '"zPPQVfXV36sgXq4fRLdsm+7rRMb8IUfb/eJ6N6mnwWs"'; + await dingus.serveFile(req, res, ctx, directory, fileName); + assert.strictEqual(res.statusCode, 304); + }); + it('does not cache ETag non-match', async function () { + req._headers[Enum.Header.IfNoneMatch] = '"foo", "bar"'; + await dingus.serveFile(req, res, ctx, directory, fileName); + assert.notStrictEqual(res.statusCode, 304); + assert(!dingus.handlerNotFound.called); + }); + it('handles no possible encodings', async function () { + req._headers[Enum.Header.AcceptEncoding] = '*;q=0'; + await assert.rejects(async () => { + await dingus.serveFile(req, res, ctx, directory, fileName); + }, { + name: 'ResponseError', + }); + }); + it('handles a valid encoding', async function () { + req._headers[Enum.Header.AcceptEncoding] = 'gzip'; + await dingus.serveFile(req, res, ctx, directory, fileName); + assert(res.end.called); + }); + it('handles a valid encoding among others', async function () { + req._headers[Enum.Header.AcceptEncoding] = 'flarp, br, gzip'; + fs.promises.stat.restore(); + sinon.stub(fs.promises, 'stat') + .onCall(0).resolves(filestats) // identity file + .onCall(1).resolves(null) // br encoding + .onCall(2).resolves(filestats); // gzip encoding + await dingus.serveFile(req, res, ctx, directory, fileName); + assert(res.end.called); + }); + }); // serveFile + + describe('renderError', function () { + let err; + beforeEach(function () { + err = { + statusCode: '200', + errorMessage: 'OK', + details: 'hunkydorey', + }; + }); + it('renders unknown type', function () { + const contentType = 'unknown/type'; + const result = dingus.renderError(contentType, err); + assert.deepStrictEqual(result, 'OK\r\nhunkydorey'); + }); + it('renders text', function () { + const contentType = 'text/plain'; + const result = dingus.renderError(contentType, err); + assert.deepStrictEqual(result, 'OK\r\nhunkydorey'); + }); + it('renders json', function () { + const contentType = Enum.ContentType.ApplicationJson; + const result = dingus.renderError(contentType, err); + assert.deepStrictEqual(result, JSON.stringify(err)); + }); + it('renders html without details', function () { + err = { + statusCode: '201', + errorMessage: 'Created', + }; + const contentType = 'text/html'; + const result = dingus.renderError(contentType, err); + assert.deepStrictEqual(result, ` + + + ${err.statusCode} ${err.errorMessage} + + +

${err.errorMessage}

+ +`); + }); + it('renders html', function () { + const contentType = 'text/html'; + const result = dingus.renderError(contentType, err); + assert.deepStrictEqual(result, ` + + + ${err.statusCode} ${err.errorMessage} + + +

${err.errorMessage}

+

${err.details}

+ +`); + }); + it('renders html, multiple details', function () { + const contentType = 'text/html'; + err.details = ['one detail', 'two detail']; + const result = dingus.renderError(contentType, err); + assert.deepStrictEqual(result, ` + + + ${err.statusCode} ${err.errorMessage} + + +

${err.errorMessage}

+

one detail

+

two detail

+ +`); + }); + }); // renderError + + describe('sendErrorResponse', function () { + let ctx, req, res; + beforeEach(function () { + ctx = {}; + req = {}; + res = { + end: sinon.stub(), + getHeader: sinon.stub(), + getHeaders: sinon.stub(), + hasHeader: sinon.stub().returns(true), + setHeader: sinon.stub(), + }; + sinon.stub(dingus, 'renderError'); + }); + it('covers', function () { + const err = { + statusCode: 444, + }; + dingus.sendErrorResponse(err, req, res, ctx); + assert(res.end.called); + }); + }); // sendErrorResponse + + describe('proxyPrefix', function () { + let req, res, ctx, stubHandler, pfxDingus; + const pfx = '/pfx'; + + beforeEach(function () { + pfxDingus = new Dingus(console, { proxyPrefix: pfx }); + req = { + setHeader: sinon.stub(), + getHeader: sinon.stub(), + }; + res = { + statusCode: 200, + end: sinon.stub(), + setHeader: sinon.stub(), + getHeader: sinon.stub(), + }; + ctx = {}; + sinon.stub(pfxDingus, 'handlerMethodNotAllowed'); + sinon.stub(pfxDingus, 'handlerNotFound'); + stubHandler = sinon.stub(); + }); + afterEach(function () { + sinon.restore(); + }); + + it('handles prefixed route', async function () { + const urlPath = '/:id'; + const method = 'GET'; + pfxDingus.on(method, urlPath, stubHandler); + req.url = pfx + '/abc'; + req.method = method; + + await pfxDingus.dispatch(req, res, ctx); + assert(stubHandler.called); + assert(!pfxDingus.handlerMethodNotAllowed.called); + assert(!pfxDingus.handlerNotFound.called); + }); + it('does not handle prefixed route', async function () { + const urlPath = '/:id'; + const method = 'GET'; + pfxDingus.on(method, urlPath, stubHandler); + req.url = '/wrongpfx/abc'; + req.method = method; + + await pfxDingus.dispatch(req, res, ctx); + assert(!stubHandler.called); + assert(!pfxDingus.handlerMethodNotAllowed.called); + assert(pfxDingus.handlerNotFound.called); + }); + }); // proxyPrefix +}); \ No newline at end of file diff --git a/test/lib/enum.js b/test/lib/enum.js new file mode 100644 index 0000000..0a86ec5 --- /dev/null +++ b/test/lib/enum.js @@ -0,0 +1,21 @@ +/* eslint-disable capitalized-comments */ +/* eslint-env mocha */ +'use strict'; + +const assert = require('assert'); +const Enum = require('../../lib/enum'); + + +describe('enum', function () { + + describe('ErrorResponse', function () { + it('covers default', function () { + const result = Enum.ErrorResponse.notPresent; + const expected = { + errorMessage: 'undefined error response \'notPresent\'', + }; + assert.deepStrictEqual(result, expected); + }); + }); // ErrorResponse + +}); diff --git a/test/lib/mime-helper.js b/test/lib/mime-helper.js new file mode 100644 index 0000000..b3e0ba3 --- /dev/null +++ b/test/lib/mime-helper.js @@ -0,0 +1,28 @@ +/* eslint-disable capitalized-comments */ +/* eslint-env mocha */ +'use strict'; + +const assert = require('assert'); +const MimeHelper = require('../../lib/mime-helper'); + +describe('ext-to-mime', function () { + + describe('extensionToMime', function () { + it('returns default for unknown extension', function () { + const expected = 'application/octet-stream'; + const result = MimeHelper.extensionToMime('unknown_extension'); + assert.strictEqual(result, expected); + }); + it('returns overridden default', function () { + const expected = 'extension/unknown'; + const result = MimeHelper.extensionToMime('unknown_extension', expected); + assert.strictEqual(result, expected); + }); + it('returns expected type', function () { + const expected = 'text/plain'; + const result = MimeHelper.extensionToMime('txt'); + assert.strictEqual(result, expected); + }); + }); // extensionToMime + +}); diff --git a/test/lib/patches/incoming-message.js b/test/lib/patches/incoming-message.js new file mode 100644 index 0000000..057c4b6 --- /dev/null +++ b/test/lib/patches/incoming-message.js @@ -0,0 +1,37 @@ +/* eslint-disable capitalized-comments */ +/* eslint-env mocha */ +'use strict'; + +const assert = require('assert'); +const { IncomingMessage } = require('http'); +require('../../../lib/patches/incoming-message'); + +describe('IncomingMessage.getHeader', function () { + let im; + beforeEach(function () { + im = new IncomingMessage(); + }); + it('requires string arg', function () { + try { + im.getHeader(1); + assert.fail('did not get expected exception'); + } catch (e) { + assert.strictEqual(e.name, 'TypeError'); + } + }); + it('is callable', function () { + const name = 'x-header'; + const value = undefined; + const result = im.getHeader(name); + assert.strictEqual(result, value); + }); + it('returns a header', function () { + const name = 'x-header'; + const value = 'foo'; + // eslint-disable-next-line security/detect-object-injection + im.headers[name] = value; + const result = im.getHeader(name); + assert.strictEqual(result, value); + }); +}); + diff --git a/test/lib/router.js b/test/lib/router.js new file mode 100644 index 0000000..f4c54a9 --- /dev/null +++ b/test/lib/router.js @@ -0,0 +1,352 @@ +/* eslint-disable capitalized-comments */ +/* eslint-env mocha */ +'use strict'; + +const assert = require('assert'); +const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require +const Router = require('../../lib/router'); +const { DingusError } = require('../../lib/errors'); + +const noExpectedException = 'did not get expected exception'; + +describe('Router', function () { + const router = new Router(); + let _its; // Save and restore ignoreTrailingSlash + + beforeEach(function () { + _its = router.ignoreTrailingSlash; + }); + + afterEach(function () { + sinon.restore(); + router.ignoreTrailingSlash = _its; + }); + + describe('_pathDefinitionToPathMatch', function () { + it('defines a simple path', function () { + const p = '/a/b/c'; + const expected = ['', 'a', 'b', 'c']; + expected[router.METHODS] = {}; + const r = router._pathDefinitionToPathMatch(p); + assert.deepStrictEqual(r, expected); + }); + it('defines a path with parameter', function () { + const p = '/a/b/:c'; + const expected = ['', 'a', 'b', { [router.PARAM]: 'c' }]; + expected[router.METHODS] = {}; + const r = router._pathDefinitionToPathMatch(p); + assert.deepStrictEqual(r, expected); + }); + it('defines a path with trailing slash', function () { + router.ignoreTrailingSlash = true; + const p = '/a/b/:c/'; + const expected = ['', 'a', 'b', { [router.PARAM]: 'c' }]; + expected[router.METHODS] = {}; + const r = router._pathDefinitionToPathMatch(p); + assert.deepStrictEqual(r, expected); + }); + }); // _pathDefinitionToPathMatch + + describe('_pathCompareExact', function () { + let fixedPath, checkPath; + + it('compares static paths which match', function () { + fixedPath = router._pathDefinitionToPathMatch('/a/b/c'); + checkPath = router._pathDefinitionToPathMatch('/a/b/c'); + const r = Router._pathCompareExact(fixedPath, checkPath); + assert.strictEqual(r, true); + }); + it('compares static paths which do not match', function () { + fixedPath = router._pathDefinitionToPathMatch('/a/b/c'); + checkPath = router._pathDefinitionToPathMatch('/a/b/d'); + const r = Router._pathCompareExact(fixedPath, checkPath); + assert.strictEqual(r, false); + }); + it('compares unequal static paths', function () { + fixedPath = router._pathDefinitionToPathMatch('/a/b/c'); + checkPath = router._pathDefinitionToPathMatch('/a/b'); + const r = Router._pathCompareExact(fixedPath, checkPath); + assert.strictEqual(r, false); + }); + it('compares param paths which match', function () { + fixedPath = router._pathDefinitionToPathMatch('/a/:b/c'); + checkPath = router._pathDefinitionToPathMatch('/a/:b/c'); + const r = Router._pathCompareExact(fixedPath, checkPath); + assert.strictEqual(r, true); + }); + it('compares param paths which do not match', function () { + fixedPath = router._pathDefinitionToPathMatch('/a/:b/c'); + checkPath = router._pathDefinitionToPathMatch('/a/:g/c'); + const r = Router._pathCompareExact(fixedPath, checkPath); + assert.strictEqual(r, false); + }); + }); // _pathCompareExact + + describe('_pathCompareParam', function () { + let fixedPath, checkPath; + + it('compares static paths which match', function () { + const params = {}; + const expectedParams = {}; + fixedPath = router._pathDefinitionToPathMatch('/a/b/c'); + checkPath = router._pathDefinitionToPathMatch('/a/b/c'); + const r = Router._pathCompareParam(fixedPath, checkPath); + assert.strictEqual(r, true); + assert.deepStrictEqual(params, expectedParams); + }); + it('compares static paths which do not match', function () { + const params = {}; + const expectedParams = {}; + fixedPath = router._pathDefinitionToPathMatch('/a/b/c'); + checkPath = router._pathDefinitionToPathMatch('/a/b/d'); + const r = Router._pathCompareParam(fixedPath, checkPath, params); + assert.strictEqual(r, false); + assert.deepStrictEqual(params, expectedParams); + }); + it('compares unequal static paths', function () { + const params = {}; + const expectedParams = {}; + fixedPath = router._pathDefinitionToPathMatch('/a/b/c'); + checkPath = router._pathDefinitionToPathMatch('/a/b'); + const r = Router._pathCompareParam(fixedPath, checkPath, params); + assert.strictEqual(r, false); + assert.deepStrictEqual(params, expectedParams); + }); + it('compares param paths which match', function () { + const params = {}; + const expectedParams = { + b: 'bar', + }; + fixedPath = router._pathDefinitionToPathMatch('/a/:b/c'); + checkPath = router._pathDefinitionToPathMatch('/a/bar/c'); + const r = Router._pathCompareParam(fixedPath, checkPath, params); + assert.strictEqual(r, true); + assert.deepStrictEqual(params, expectedParams); + }); + it('compares multi param path which match', function () { + const params = {}; + const expectedParams = { + b: 'gaz', + c: '123', + }; + fixedPath = router._pathDefinitionToPathMatch('/a/:b/:c'); + checkPath = router._pathDefinitionToPathMatch('/a/gaz/123'); + const r = Router._pathCompareParam(fixedPath, checkPath, params); + assert.strictEqual(r, true); + assert.deepStrictEqual(params, expectedParams); + }); + }); // _pathCompareParam + + describe('_pathFind / _pathFindExact', function () { + let pathsByLengthOrig; + + beforeEach(function () { + pathsByLengthOrig = router.pathsByLength; + router.pathsByLength = { + 2: [ router._pathDefinitionToPathMatch('/:id') ], + 3: [ router._pathDefinitionToPathMatch('/a/b') ], + }; + }); + afterEach(function () { + router.pathsByLength = pathsByLengthOrig; + }); + + describe('_pathFind', function () { + it('finds a path', function () { + const pathParts = ['', '46123f1e-bdca-40ee-9f62-93ad38647aa1']; + const { matchedPath, pathParams } = router._pathFind(pathParts); + assert.strictEqual(matchedPath, router.pathsByLength[2][0]); + assert.deepStrictEqual(pathParams, { id: pathParts[1] }); + }); + it('does not find a path', function () { + const pathParts = ['', 'flarp', 'baz']; + const { matchedPath } = router._pathFind(pathParts); + assert.strictEqual(matchedPath, undefined); + }); + }); // _pathFind + + describe('_pathFindExact', function () { + it('finds a path', function () { + const pathParts = ['', { [router.PARAM]: 'id' }]; + const r = router._pathFindExact(pathParts); + assert.strictEqual(r, router.pathsByLength[2][0]); + }); + it('does not find a path', function () { + const pathParts = ['', 'flarp', 'baz']; + const r = router._pathFindExact(pathParts); + assert.strictEqual(r, undefined); + }); + }); // _pathFindExact + + }); // _pathFind / _pathFindExact + + describe('on', function () { + let pathsByLengthOrig; + const stubHandler = () => {}; + + beforeEach(function () { + pathsByLengthOrig = router.pathsByLength; + router.pathsByLength = { + 2: [ router._pathDefinitionToPathMatch('/:id') ], + }; + }); + afterEach(function () { + router.pathsByLength = pathsByLengthOrig; + }); + + it('adds new path', function () { + const urlPath = '/a/:id'; + const expected = router._pathDefinitionToPathMatch(urlPath); + expected[router.METHODS]['GET'] = stubHandler; + router.on('GET', urlPath, stubHandler); + assert.deepStrictEqual(router.pathsByLength[3][0], expected); + }); + + it('adds new method to path', function () { + const urlPath = '/a/:id'; + const expected = router._pathDefinitionToPathMatch(urlPath); + expected[router.METHODS]['GET'] = stubHandler; + expected[router.METHODS]['POST'] = stubHandler; + router.on('GET', urlPath, stubHandler); + + router.on('POST', urlPath, stubHandler); + assert.deepStrictEqual(router.pathsByLength[3][0], expected); + }); + + it('add some more paths', function () { + let urlPath = '/a/b/c/d'; + const expected = router._pathDefinitionToPathMatch(urlPath); + expected[router.METHODS]['GET'] = stubHandler; + router.on('GET', urlPath, stubHandler); + urlPath = '/a/b/x/y'; + router.on('GET', urlPath, stubHandler); + + assert.strictEqual(router.pathsByLength[5].length, 2); + }); + + it('adds multiple methods', function () { + const urlPath = '/:id'; + const expected = router._pathDefinitionToPathMatch(urlPath); + expected[router.METHODS]['GET'] = stubHandler; + expected[router.METHODS]['HEAD'] = stubHandler; + + router.on(['GET', 'HEAD'], urlPath, stubHandler); + assert.deepStrictEqual(router.pathsByLength[2][0], expected); + }); + + it('adds new wildcard path', function () { + const urlPath = '/a/:id'; + const expected = router._pathDefinitionToPathMatch(urlPath); + expected[router.METHODS]['*'] = stubHandler; + router.on('*', urlPath, stubHandler); + assert.deepStrictEqual(router.pathsByLength[3][0], expected); + }); + + it('fails to add unknown method path', function () { + const urlPath = '/a/:id'; + try { + router.on('FLARP', urlPath, stubHandler); + assert.fail('expected an exception'); + } catch (e) { + assert.strictEqual(e.name, 'DingusError'); + assert.strictEqual(e.message, 'invalid method \'FLARP\''); + } + }); + }); // on + + describe('lookup', function () { + let pathsByLengthOrig; + let ctx; + let stubHandler; + + beforeEach(function () { + ctx = {}; + pathsByLengthOrig = router.pathsByLength; + stubHandler = sinon.stub(); + }); + afterEach(function () { + router.pathsByLength = pathsByLengthOrig; + }); + + it('finds handler', function () { + const urlPath = '/:id'; + const method = 'GET'; + router.on(method, urlPath, stubHandler); + const path = '/abc'; + + const handler = router.lookup(method, path, ctx); + assert.strictEqual(handler, stubHandler); + }); + it('does not find handler with trailing slash', function () { + router.ignoreTrailingSlash = false; + const urlPath = '/:id'; + const method = 'GET'; + router.on(method, urlPath, stubHandler); + const path = '/abc/'; + + try { + router.lookup(method, path, ctx); + assert.fail(noExpectedException); + } catch (e) { + assert(e instanceof DingusError); + assert.strictEqual(e.message, 'NoPath'); + } + }); + it('finds handler', function () { + router.ignoreTrailingSlash = true; + const urlPath = '/:id'; + const method = 'GET'; + router.on(method, urlPath, stubHandler); + const path = '/abc/'; + + const handler = router.lookup(method, path, ctx); + assert.strictEqual(handler, stubHandler); + }); + it('finds handler without context', async function () { + const urlPath = '/:id'; + const method = 'GET'; + router.on(method, urlPath, stubHandler); + const path = '/abc'; + + const handler = router.lookup(method, path); + assert.strictEqual(handler, stubHandler); + }); + it('finds fallback handler', async function () { + const urlPath = '/abc/:id'; + const method = 'GET'; + router.on('*', urlPath, stubHandler); + const path = '/abc/def'; + + const handler = router.lookup(method, path, ctx); + assert.strictEqual(handler, stubHandler); + }); + it('calls unsupported method', async function () { + const urlPath = '/:id'; + const method = 'POST'; + router.on('GET', urlPath, stubHandler); + const path = '/abc'; + + try { + router.lookup(method, path, ctx); + assert.fail(noExpectedException); + } catch (e) { + assert(e instanceof DingusError); + assert.strictEqual(e.message, 'NoMethod'); + } + }); + it('does not lookup nonexistant path', async function () { + const path = '/foo/bar'; + const method = 'GET'; + + try { + router.lookup(method, path, ctx); + assert.fail(noExpectedException); + } catch (e) { + assert(e instanceof DingusError); + assert.strictEqual(e.message, 'NoPath'); + } + }); + + }); // lookup + +}); diff --git a/test/test-data/.example b/test/test-data/.example new file mode 100644 index 0000000..33a9488 --- /dev/null +++ b/test/test-data/.example @@ -0,0 +1 @@ +example diff --git a/test/test-data/example.html b/test/test-data/example.html new file mode 100644 index 0000000..33a9488 --- /dev/null +++ b/test/test-data/example.html @@ -0,0 +1 @@ +example diff --git a/test/test-data/example.html.gz b/test/test-data/example.html.gz new file mode 100644 index 0000000000000000000000000000000000000000..63eee7d7dd88571aba3aa939adb3c16d4a1e3caa GIT binary patch literal 41 xcmb2|=HPhiFgc!qIkh4&w;(4~FQX(khrxTT#~Ixdo==$=?xt`CaWF72001jN4S@gv literal 0 HcmV?d00001 -- 2.43.2