Comparing version 3.4.2 to 4.0.0
@@ -45,2 +45,14 @@ import { Server as HttpServer, IncomingMessage, ServerResponse } from 'http' | ||
interface Router <P extends Protocol> { | ||
get: RegisterRoute<P> | ||
delete: RegisterRoute<P> | ||
patch: RegisterRoute<P> | ||
post: RegisterRoute<P> | ||
put: RegisterRoute<P> | ||
head: RegisterRoute<P> | ||
options: RegisterRoute<P> | ||
trace: RegisterRoute<P> | ||
all: RegisterRoute<P> | ||
} | ||
type Response<P extends Protocol> = P extends Protocol.HTTP2 | ||
@@ -78,3 +90,2 @@ ? Http2ServerResponse & ResponseExtensions | ||
handler: RequestHandler<P>, | ||
context?: {}, | ||
middlewares?: RequestHandler<P>[] | ||
@@ -87,3 +98,2 @@ ): Service<P> | ||
handler: RequestHandler<P>, | ||
context?: {} | ||
): Service<P> | ||
@@ -96,3 +106,2 @@ | ||
handler: RequestHandler<P>, | ||
context?: {} | ||
): Service<P> | ||
@@ -106,3 +115,2 @@ | ||
handler: RequestHandler<P>, | ||
context?: {} | ||
): Service<P> | ||
@@ -115,47 +123,24 @@ } | ||
handler: RequestHandler<P> | ||
ctx: {} | ||
middlewares: RequestHandler<P>[] | ||
} | ||
interface Router { } | ||
interface Options<P extends Protocol> { | ||
server?: Server<P> | ||
routerFactory?(options: Options<P>): Router | ||
prioRequestsProcessing?: boolean | ||
ignoreTrailingSlash?: boolean | ||
allowUnsafeRegex?: boolean | ||
maxParamLength?: number | ||
routerCacheSize?: number | ||
defaultRoute?: RequestHandler<P> | ||
disableResponseEvent?: boolean | ||
errorHandler?: ErrorHandler<P> | ||
} | ||
interface Service<P extends Protocol> { | ||
interface Service<P extends Protocol> extends Router<P> { | ||
getRouter(): Router<P>, | ||
newRouter(): Router<P> | ||
errorHandler: ErrorHandler<P>, | ||
getServer(): Server<P>, | ||
getConfigOptions(): Options<P> | ||
use(middleware: RequestHandler<P>, context?: {}): void | ||
route( | ||
method: Method, | ||
path: string, | ||
handler: RequestHandler<P>, | ||
ctx?: {}, | ||
middlewares?: RequestHandler<P>[] | ||
): Route<P> | ||
use(middleware: RequestHandler<P>): restana.Service<P> | ||
use(prefix: string, middleware: RequestHandler<P>): restana.Service<P> | ||
handle(req: Request<P>, res: Response<P>): void | ||
start(port?: number, host?: string): Promise<Server<P>> | ||
close(): Promise<void> | ||
routes(): string[] | ||
addRoute (methods: String | Array<String>): RegisterRoute<P> | ||
get: RegisterRoute<P> | ||
delete: RegisterRoute<P> | ||
patch: RegisterRoute<P> | ||
post: RegisterRoute<P> | ||
put: RegisterRoute<P> | ||
head: RegisterRoute<P> | ||
options: RegisterRoute<P> | ||
trace: RegisterRoute<P> | ||
all: RegisterRoute<P> | ||
} | ||
@@ -162,0 +147,0 @@ } |
198
index.js
@@ -7,13 +7,4 @@ /** | ||
/** | ||
* importing supported HTTP methods | ||
*/ | ||
const methods = require('./libs/methods') | ||
/** | ||
* importing request router builder function | ||
*/ | ||
const shortcuts = ['get', 'delete', 'patch', 'post', 'put', 'head', 'options', 'trace', 'all'] | ||
const requestRouter = require('./libs/request-router') | ||
/** | ||
* preparing request/response objects extensions | ||
*/ | ||
const exts = { | ||
@@ -23,26 +14,7 @@ request: {}, | ||
} | ||
/** | ||
* importing middlewares chain caller | ||
*/ | ||
const next = require('./libs/middleware-chain') | ||
/** | ||
* importing route handler caller | ||
*/ | ||
const handlerCall = require('./libs/route-handler-caller') | ||
/** | ||
* importing route registration handler | ||
*/ | ||
const routeRegister = require('./libs/route-register') | ||
/** | ||
* Application instance contructor like function | ||
* | ||
* @param options Object Configuration options | ||
*/ | ||
module.exports = (options = {}) => { | ||
// create HTTP server instance | ||
const router = requestRouter(options) | ||
const server = options.server || require('http').createServer() | ||
// should we prio requests processing? | ||
const prp = undefined === options.prioRequestsProcessing ? true : options.prioRequestsProcessing | ||
// registering 'request' handler | ||
if (prp) { | ||
@@ -58,29 +30,19 @@ server.on('request', (req, res) => { | ||
// creating request router instance, considers custom router override | ||
const router = options.routerFactory ? options.routerFactory(options) : requestRouter(options) | ||
// routes holder | ||
const routes = {} | ||
const app = { | ||
getRouter () { | ||
return router | ||
}, | ||
// global middlewares holder | ||
const middlewares = [{ | ||
handler: (req, res, next) => router.lookup(req, res), | ||
context: {} | ||
}] | ||
errorHandler: options.errorHandler || ((err, req, res) => { | ||
res.send(err) | ||
}), | ||
// the "restana" service interface | ||
const app = { | ||
/** | ||
* Application global error handler | ||
*/ | ||
errorHandler: options.errorHandler || ((err, req, res) => res.send(err)), | ||
newRouter () { | ||
return requestRouter(options) | ||
}, | ||
/** | ||
* HTTP server instance | ||
*/ | ||
getServer () { | ||
return server | ||
}, | ||
/** | ||
* Application configuration options reference | ||
*/ | ||
getConfigOptions () { | ||
@@ -90,102 +52,11 @@ return options | ||
/** | ||
* Register global middleware | ||
* | ||
* @param {Object} middleware The middleware function | ||
* @param {Object} context The middleware invokation context object | ||
*/ | ||
use: (middleware, context = {}) => { | ||
middlewares.splice( | ||
middlewares.length - 1, | ||
0, | ||
{ handler: middleware, context } | ||
) | ||
}, | ||
use: router.use, | ||
/** | ||
* Register a request handler. | ||
* Optionally the invokation context and pre-handler middlewares can be defined. | ||
* | ||
* @param {String} method HTTP method / verb | ||
* @param {String} path Request path | ||
* @param {Function} handler Request handler function like (req, res, ctx) => {} | ||
* @param {Object} ctx Optional request handler invokation context object. Default: {} | ||
* @param {Array} middlewares Optional request middlewares | ||
* @returns {Object} Route object | ||
*/ | ||
route: (method, path, handler, ctx = {}, middlewares = []) => { | ||
// mapping middlewares to required format { handler: Function, context: Object } | ||
middlewares = middlewares.map( | ||
middleware => (typeof middleware === 'function') | ||
? { handler: middleware, context: {} } | ||
: middleware | ||
) | ||
// creating routing key | ||
const key = `[${method.toString().toUpperCase()}]${path}` | ||
// caching route | ||
const route = { | ||
method, | ||
path, | ||
handler, | ||
ctx, | ||
middlewares | ||
} | ||
routes[key] = true | ||
// Allow override of routes, by first removing the old route | ||
router.off(method, path) | ||
// registering request handler | ||
router.on(method, path, (req, res, params) => { | ||
// populate req.params | ||
req.params = params | ||
if (middlewares.length > 0) { | ||
// call route middlewares and route handler | ||
return next([ | ||
...middlewares.slice(0), | ||
{ | ||
context: {}, | ||
handler: handlerCall(handler, ctx, app.errorHandler) // -> Function | ||
} | ||
], req, res, app.errorHandler) | ||
} else { | ||
// directly call the route handler only | ||
// NOTE: we do this to increase performance | ||
return handlerCall(handler, ctx, app.errorHandler)(req, res) | ||
} | ||
}) | ||
return route | ||
}, | ||
/** | ||
* Handle on 'request' event from HTTP server instance | ||
* | ||
* @param {Object} req Request object | ||
* @param {Object} res Response object | ||
*/ | ||
handle: (req, res) => { | ||
// request object population | ||
req.originalUrl = req.url | ||
res.send = exts.response.send(options, req, res) | ||
if (middlewares.length > 1) { | ||
// call route middlewares and route handler | ||
next(middlewares, req, res, app.errorHandler) | ||
} else { | ||
// directly call the request router | ||
// NOTE: we do this to increase performance | ||
router.lookup(req, res) | ||
} | ||
router.lookup(req, res) | ||
}, | ||
/** | ||
* Start application HTTP server | ||
* | ||
* @param {Number} port Optional HTTP server port. Default 3000 | ||
* @param {String} host Optional HTTP server binding network interface | ||
* @returns {Promise} | ||
*/ | ||
start: (port = 3000, host) => new Promise((resolve, reject) => { | ||
@@ -198,7 +69,2 @@ server.listen(port, host, (err) => { | ||
/** | ||
* Close application HTTP server | ||
* | ||
* @returns {Promise} | ||
*/ | ||
close: () => new Promise((resolve, reject) => { | ||
@@ -209,33 +75,21 @@ server.close((err) => { | ||
}) | ||
}), | ||
}) | ||
/** | ||
* Application routes [`[${method.toUpperCase()}]${path}`] | ||
* | ||
* @returns {Array} | ||
*/ | ||
routes: () => Object.keys(routes) | ||
} | ||
// exposing raw route registration to improve extensibility | ||
app.addRoute = (methods) => (path, ...args) => { | ||
routeRegister(app, methods, path, args) | ||
shortcuts.forEach((method) => { | ||
app[method] = router[method] | ||
}) | ||
// supporting method chaining for routes registration | ||
return app | ||
} | ||
app.callback = () => app.handle | ||
// exposing "all" HTTP verbs as request routing registration | ||
app.all = app.addRoute(methods) | ||
// exposing HTTP verbs as request routing methods | ||
// express.js like routes middlewares signature is supported: app.get('/', m1, m2, handler) | ||
methods.forEach((method) => { | ||
app[method] = app.addRoute(method) | ||
app.use(async (req, res, next) => { | ||
try { | ||
await next() | ||
} catch (err) { | ||
return app.errorHandler(err, req, res) | ||
} | ||
}) | ||
// integrator callback | ||
app.callback = () => app.handle | ||
return app | ||
} |
@@ -17,3 +17,3 @@ const methods = require('./methods') | ||
args.unshift((req, res, next) => { | ||
apm.setTransactionName(`${method.toUpperCase()} ${path}`) | ||
apm.setTransactionName(`${req.method} ${path}`) | ||
@@ -20,0 +20,0 @@ return next() |
/** | ||
* Supported HTTP methods | ||
*/ | ||
module.exports = ['get', 'delete', 'patch', 'post', 'put', 'head', 'options', 'trace'] | ||
module.exports = ['get', 'delete', 'patch', 'post', 'put', 'head', 'options', 'trace', 'all'] |
@@ -6,9 +6,13 @@ /** | ||
*/ | ||
const router = require('find-my-way') | ||
const sequential = require('0http/lib/router/sequential') | ||
module.exports = (options) => router({ | ||
ignoreTrailingSlash: options.ignoreTrailingSlash || false, | ||
allowUnsafeRegex: options.allowUnsafeRegex || false, | ||
maxParamLength: options.maxParamLength || 100, | ||
defaultRoute: options.defaultRoute || ((req, res) => res.send(404)) | ||
}) | ||
module.exports = (options) => { | ||
const router = sequential({ | ||
cacheSize: options.routerCacheSize || 2000, | ||
defaultRoute: options.defaultRoute || ((req, res) => { | ||
res.send(404) | ||
}) | ||
}) | ||
return router | ||
} |
@@ -6,12 +6,6 @@ const CONTENT_TYPE_HEADER = 'content-type' | ||
* No comments needed ;) | ||
* | ||
* @param {Object} options Application configuration options | ||
* @param {Object} req Request object | ||
* @param {Object} res Response object | ||
*/ | ||
module.exports.send = (options, req, res) => (data = 200, code = 200, headers = null, cb = () => {}) => { | ||
if (headers !== null) { | ||
// attach custom headers on the response | ||
Object.keys(headers).forEach((key) => { | ||
// IMPORTANT: 'key.toLowerCase()' give us big performance gain | ||
res.setHeader(key.toLowerCase(), headers[key]) | ||
@@ -22,11 +16,8 @@ }) | ||
if (typeof data === 'number') { | ||
// shortcut was used, check if data payload was set in res.body | ||
code = parseInt(data, 10) | ||
data = res.body | ||
} else if (data instanceof Error) { | ||
// transparently supporting Error instances | ||
const errorCode = data.status || data.code || data.statusCode | ||
code = typeof errorCode === 'number' ? parseInt(errorCode) : 500 | ||
data = { | ||
errClass: data.constructor.name, | ||
code, | ||
@@ -36,29 +27,16 @@ message: data.message, | ||
} | ||
res.setHeader(CONTENT_TYPE_HEADER, 'application/json') | ||
} | ||
// emit response event to allow post-processing | ||
// TODO: We need to make this event notification async without affecting performance | ||
const params = { | ||
res, | ||
req, | ||
data, | ||
code | ||
} | ||
if (options.disableResponseEvent !== true) { | ||
res.emit('response', params) | ||
} | ||
if (typeof data === 'object' && data instanceof Buffer === false) { | ||
if (!res.hasHeader(CONTENT_TYPE_HEADER)) { | ||
// transparently setting the 'content-type' header if JSON | ||
res.setHeader(CONTENT_TYPE_HEADER, 'application/json') | ||
} | ||
params.data = JSON.stringify(params.data) | ||
data = JSON.stringify(data) | ||
} | ||
// setting res.statusCode with post-processed result, developers might want to override here... | ||
res.statusCode = params.code | ||
res.statusCode = code | ||
// finally end request | ||
res.end(params.data, cb) | ||
res.end(data, cb) | ||
} |
{ | ||
"name": "restana", | ||
"version": "3.4.2", | ||
"version": "4.0.0", | ||
"description": "Super fast and minimalist web framework for building REST micro-services.", | ||
@@ -10,3 +10,3 @@ "main": "index.js", | ||
"format": "npx standard --fix", | ||
"test": "PORT=3000 NODE_ENV=testing npx nyc --check-coverage --lines 95 node ./node_modules/mocha/bin/mocha tests.js specs/*.test.js", | ||
"test": "PORT=3000 NODE_ENV=testing npx nyc --check-coverage --lines 95 node ./node_modules/mocha/bin/mocha specs/*.test.js", | ||
"postinstall": "node ./libs/tasks/postinstall.js" | ||
@@ -26,3 +26,3 @@ }, | ||
"engines": { | ||
"node": ">=7.x" | ||
"node": ">=10.x" | ||
}, | ||
@@ -43,6 +43,5 @@ "author": "Rolando Santamaria Maso <kyberneees@gmail.com>", | ||
"dependencies": { | ||
"find-my-way": "^2.2.1" | ||
"0http": "^2.1.0" | ||
}, | ||
"devDependencies": { | ||
"0http": "^1.2.4", | ||
"@hapi/hapi": "^18.4.0", | ||
@@ -53,4 +52,5 @@ "anumargak": "^2.2.0", | ||
"express": "^4.17.1", | ||
"express-jwt": "^5.3.1", | ||
"fastify": "^2.11.0", | ||
"http-cache-middleware": "^1.2.3", | ||
"http-cache-middleware": "^1.2.4", | ||
"koa": "^2.11.0", | ||
@@ -61,8 +61,10 @@ "koa-router": "^7.4.0", | ||
"muneem": "^2.4.5", | ||
"nyc": "^14.1.1", | ||
"nyc": "^15.0.0", | ||
"pem": "^1.14.3", | ||
"polka": "^0.5.2", | ||
"response-time": "^2.3.2", | ||
"restify": "^8.5.0", | ||
"restify": "^8.5.1", | ||
"serve-static": "^1.14.1", | ||
"socket.io": "^2.3.0", | ||
"socket.io-client": "^2.3.0", | ||
"standard": "^14.3.1", | ||
@@ -69,0 +71,0 @@ "supertest": "^3.4.2", |
136
README.md
@@ -5,7 +5,9 @@ # restana | ||
Blazing fast, tiny and minimalist *connect-like* web framework for building REST micro-services. | ||
[> Check how much faster!](https://github.com/the-benchmarker/web-frameworks#full-table-1) | ||
> Uses 'find-my-way' router: https://www.npmjs.com/package/find-my-way | ||
What else? *[Building ultra-fast REST APIs with Node.js (restana vs express vs fastify)](https://medium.com/@kyberneees/building-ultra-fast-rest-apis-with-node-js-and-restana-1d65b0d524b7)* | ||
![Performance Benchmarks](benchmark-30122019.png) | ||
> MacBook Pro 2019, 2,4 GHz Intel Core i9, 32 GB 2400 MHz DDR4 | ||
> - wrk -t8 -c40 -d5s http://127.0.0.1:3000/hi | ||
Read more: *[Building ultra-fast REST APIs with Node.js (restana vs express vs fastify)](https://medium.com/@kyberneees/building-ultra-fast-rest-apis-with-node-js-and-restana-1d65b0d524b7)* | ||
## Usage | ||
@@ -35,41 +37,11 @@ ```bash | ||
### Configuration | ||
- `server`: Allows to override the HTTP server instance to be used. | ||
- `routerFactory`: Router factory function to allow default `find-my-way` router override. | ||
### Configuration options | ||
- `server`: Allows to optionally override the HTTP server instance to be used. | ||
- `prioRequestsProcessing`: If `TRUE`, HTTP requests processing/handling is prioritized using `setImmediate`. Default value: `TRUE` | ||
- `ignoreTrailingSlash`: If `TRUE`, trailing slashes on routes are ignored. Default value: `FALSE` | ||
- `allowUnsafeRegex`: If `TRUE`, potentially catastrophic exponential-time regular expressions are disabled. Default value: `FALSE` | ||
- `maxParamLength`: Defines the custom length for parameters in parametric (standard, regex and multi) routes. Default value: `100` | ||
- `defaultRoute`: Default route handler when no route match occurs. Default value: `((req, res) => res.send(404))` | ||
- `disableResponseEvent`: If `TRUE`, there won't be `response` events triggered on the `res` object. Default value: `FALSE` | ||
- `defaultRoute`: Optional route handler when no route match occurs. Default value: `((req, res) => res.send(404))` | ||
- `errorHandler`: Optional global error handler function. Default value: `(err, req, res) => res.send(err)` | ||
- `routerCacheSize`: The router matching cache size, indicates how many request matches will be kept in memory. Default value: `2000` | ||
```js | ||
// accessing service configuration | ||
service.getConfigOptions() | ||
// accessing restana HTTP server instance | ||
service.getServer() | ||
``` | ||
#### Example usage: | ||
```js | ||
const service = require('restana')({ | ||
ignoreTrailingSlash: true | ||
}); | ||
``` | ||
#### Optionally overwrite router factory method: | ||
> In this example we use `anumargak` router instead of `find-my-way`. | ||
```js | ||
const anumargak = require('anumargak') | ||
const service = require('restana')({ | ||
routerFactory: (options) => { | ||
return anumargak(options) | ||
} | ||
}) | ||
... | ||
``` | ||
> Please consider that when using `anumargak` router, request params are accessible via: `req._path.params` | ||
### Creating a micro-service & routes registration | ||
### Full service example | ||
```js | ||
@@ -85,3 +57,3 @@ const bodyParser = require('body-parser') | ||
// registering routes using method chaining | ||
// registering service routes | ||
service | ||
@@ -105,8 +77,11 @@ .get('/pets/:id', async (req, res) => { | ||
service.get('/version', function (req, res) { | ||
res.body = { // optionally you can send the response data in the body property | ||
// optionally you can send the response data in the body property | ||
res.body = { | ||
version: '1.0.0' | ||
} | ||
res.send() // 200 is the default response code | ||
// 200 is the default response code | ||
res.send() | ||
}) | ||
``` | ||
Supported HTTP methods: | ||
@@ -120,3 +95,3 @@ ```js | ||
```js | ||
service.all('/allmethodsroute', function (req, res) { | ||
service.all('/allmethodsroute', (req, res) => { | ||
res.send(200) | ||
@@ -143,9 +118,6 @@ }) | ||
return stars | ||
res.send({ stars }) | ||
}) | ||
``` | ||
> IMPORTANT: Returned value can't be `undefined`, for such cases use `res.send(...` | ||
### Sending custom headers: | ||
@@ -180,23 +152,22 @@ ```js | ||
### Middlewares support: | ||
### Global middlewares | ||
```js | ||
const service = require('restana')({}) | ||
const service = require('restana')() | ||
// custom middleware to attach the X-Response-Time header to the response | ||
service.use((req, res, next) => { | ||
const now = new Date().getTime() | ||
res.on('response', e => { | ||
e.res.setHeader('X-Response-Time', new Date().getTime() - now) | ||
}) | ||
return next() | ||
// do something | ||
next() | ||
}); | ||
... | ||
``` | ||
// the /v1/welcome route handler | ||
service.get('/v1/welcome', (req, res) => { | ||
res.send('Hello World!') | ||
}) | ||
### Prefix middlewares | ||
```js | ||
const service = require('restana')() | ||
// start the server | ||
service.start() | ||
service.use('/admin', (req, res, next) => { | ||
// do something | ||
next() | ||
}); | ||
... | ||
``` | ||
@@ -207,16 +178,14 @@ | ||
```js | ||
service.get('/hi/:name', async (req, res) => { | ||
return 'Hello ' + req.params.name // -> "name" will be uppercase here | ||
}, {}, [(req, res, next) => { | ||
req.params.name = req.params.name.toUpperCase() | ||
const service = require('restana')() | ||
service.get('/admin', (req, res, next) => { | ||
// do something | ||
next() | ||
}]) // route middlewares can be passed in an Array after the handler context param | ||
}, (req, res) => { | ||
res.send('admin data') | ||
}); | ||
... | ||
``` | ||
Express.js like signature also supported: | ||
```js | ||
service.get('/hi/:name', m1, m2, handler [, ctx]) | ||
``` | ||
#### Third party middlewares support: | ||
> Almost all middlewares using the *function (req, res, next)* signature format should work, considering that no custom framework feature is used. | ||
> All middlewares using the `function (req, res, next)` signature format are compatible with restana. | ||
@@ -230,13 +199,14 @@ Examples : | ||
#### Async middlewares support | ||
Starting from `v3.3.x`, you can now also use async middlewares as described below: | ||
Since version `v3.3.x`, you can also use async middlewares as described below: | ||
```js | ||
service.use(async (req, res, next) => { | ||
await next() | ||
console.log('Global middlewares execution completed!') | ||
console.log('All middlewares and route handler executed!') | ||
})) | ||
service.use(logging()) | ||
service.use(jwt()) | ||
... | ||
``` | ||
In the same way you can also capture uncaught exceptions inside your async middlewares: | ||
In the same way you can also capture uncaught exceptions inside the request processing flow: | ||
```js | ||
@@ -254,4 +224,2 @@ service.use(async (req, res, next) => { | ||
``` | ||
> NOTE: Global and Route level middlewares execution run separately! | ||
## AWS Serverless Integration | ||
@@ -335,3 +303,17 @@ `restana` is compatible with the [serverless-http](https://github.com/dougmoscrop/serverless-http) library, so restana based services can also run as AWS lambdas 🚀 | ||
## Breacking changes | ||
### 4.x: | ||
> Restana version 4.x is much more simple to maintain, mature and faster! | ||
#### Added | ||
- Node.js v10.x+ is required. | ||
- `0http` sequential router is now the default and only HTTP router. | ||
- Overall middlewares support was improved. | ||
- Nested routers are now supported. | ||
- Improved error handler through async middlewares. | ||
- New `getRouter` and `newRouter` methods are added for accesing default and nested routers. | ||
#### Removed | ||
- The `response` event was removed. | ||
- `find-my-way` router is replaced by `0http` sequential router. | ||
- Returning result inside async handler is not allowed anymore. Use `res.send...` | ||
### 3.x: | ||
#### Removed | ||
- Support for `turbo-http` library was dropped. |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
19450
26
10
278
308
+ Added0http@^2.1.0
+ Added0http@2.5.2(transitive)
+ Addedlru-cache@5.1.1(transitive)
+ Addedregexparam@1.3.0(transitive)
+ Addedtrouter@3.2.1(transitive)
+ Addedyallist@3.1.1(transitive)
- Removedfind-my-way@^2.2.1
- Removedfast-decode-uri-component@1.0.1(transitive)
- Removedfind-my-way@2.2.5(transitive)
- Removedret@0.2.2(transitive)
- Removedsafe-regex2@2.0.0(transitive)
- Removedsemver-store@0.3.0(transitive)