diff --git a/package-lock.json b/package-lock.json
index e29b09ec8..385c78bae 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5323,6 +5323,10 @@
"resolved": "packages/gasket-plugin-metadata",
"link": true
},
+ "node_modules/@gasket/plugin-middleware": {
+ "resolved": "packages/gasket-plugin-middleware",
+ "link": true
+ },
"node_modules/@gasket/plugin-mocha": {
"resolved": "packages/gasket-plugin-mocha",
"link": true
@@ -41484,12 +41488,6 @@
"name": "@gasket/plugin-fastify",
"version": "7.0.0-next.34",
"license": "MIT",
- "dependencies": {
- "compression": "^1.7.4",
- "cookie-parser": "^1.4.6",
- "diagnostics": "^2.0.2",
- "middie": "^5.4.0"
- },
"devDependencies": {
"@gasket/core": "^7.0.0-next.34",
"cross-env": "^7.0.3",
@@ -41816,6 +41814,24 @@
"typescript": "^5.4.5"
}
},
+ "packages/gasket-plugin-middleware": {
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "compression": "^1.7.4",
+ "cookie-parser": "^1.4.6",
+ "diagnostics": "^2.0.2",
+ "middie": "^5.4.0"
+ },
+ "devDependencies": {
+ "eslint": "^8.56.0",
+ "eslint-config-godaddy": "^7.1.0",
+ "eslint-plugin-jest": "^27.6.3",
+ "eslint-plugin-json": "^3.1.0",
+ "eslint-plugin-unicorn": "^44.0.0",
+ "jest": "^29.7.0"
+ }
+ },
"packages/gasket-plugin-mocha": {
"name": "@gasket/plugin-mocha",
"version": "7.0.0-next.34",
@@ -46883,10 +46899,7 @@
"version": "file:packages/gasket-plugin-fastify",
"requires": {
"@gasket/core": "^7.0.0-next.34",
- "compression": "^1.7.4",
- "cookie-parser": "^1.4.6",
"cross-env": "^7.0.3",
- "diagnostics": "^2.0.2",
"eslint": "^8.56.0",
"eslint-config-godaddy": "^7.1.0",
"eslint-plugin-jest": "^27.6.3",
@@ -46894,7 +46907,6 @@
"eslint-plugin-unicorn": "^44.0.0",
"fastify": "^3.3.0",
"jest": "^29.7.0",
- "middie": "^5.4.0",
"typescript": "^5.4.5"
}
},
@@ -47108,6 +47120,21 @@
"typescript": "^5.4.5"
}
},
+ "@gasket/plugin-middleware": {
+ "version": "file:packages/gasket-plugin-middleware",
+ "requires": {
+ "compression": "^1.7.4",
+ "cookie-parser": "^1.4.6",
+ "diagnostics": "^2.0.2",
+ "eslint": "^8.56.0",
+ "eslint-config-godaddy": "^7.1.0",
+ "eslint-plugin-jest": "^27.6.3",
+ "eslint-plugin-json": "^3.1.0",
+ "eslint-plugin-unicorn": "^44.0.0",
+ "jest": "^29.7.0",
+ "middie": "^5.4.0"
+ }
+ },
"@gasket/plugin-mocha": {
"version": "file:packages/gasket-plugin-mocha",
"requires": {
diff --git a/packages/gasket-plugin-express/README.md b/packages/gasket-plugin-express/README.md
index 0bc32e7be..31407cd6d 100644
--- a/packages/gasket-plugin-express/README.md
+++ b/packages/gasket-plugin-express/README.md
@@ -60,29 +60,6 @@ module.exports = {
}
```
-### Middleware paths
-
-The `gasket.config.js` can contain a `middleware` property, which is an array of
-objects that map plugins to route or path patterns, allowing apps to tune which
-middleware are triggered for which requests.
-
-```js
- middleware: [
- {
- plugin:'gasket-plugin-example', // Name of the Gasket plugin
- paths: ['/api']
- },
- {
- plugin:'@some/gasket-plugin-example',
- paths: [/\/default/]
- },
- {
- plugin: '@another/gasket-plugin-example',
- paths: ['/proxy', /\/home/]
- }
- ]
-```
-
## Logging
This plugin attaches a `logger` object to the request object. This object has a `metadata` method that allows you to attach details to any log entry related to the request. For example, you can add the user ID to each log entry. When logging within the context of a request, use the `req.logger` object instead of the global `gasket.logger` so that contextual information is included in the log entry. Here is an example of how to attach metadata to `req.logger` object and how to use it:
@@ -101,30 +78,6 @@ function someOtherMiddleware(req, res, next) {
## Lifecycles
-### middleware
-
-Executed when the `express` server has been created, it will apply all returned
-functions as middleware.
-
-```js
-module.exports = {
- hooks: {
- /**
- * Add Express middleware
- *
- * @param {Gasket} gasket The Gasket API
- * @param {Express} app - Express app instance
- * @returns {function|function[]} middleware(s)
- */
- middleware: function (gasket, app) {
- return require('x-xss-protection')();
- }
- }
-}
-```
-
-You may also return an `Array` to inject more than one middleware.
-
### express
Executed **after** the `middleware` event for when you need full control over
diff --git a/packages/gasket-plugin-express/lib/create-servers.js b/packages/gasket-plugin-express/lib/create-servers.js
index 1d15d25d7..109d27e57 100644
--- a/packages/gasket-plugin-express/lib/create-servers.js
+++ b/packages/gasket-plugin-express/lib/create-servers.js
@@ -1,7 +1,6 @@
/* eslint-disable max-statements */
///
///
-const debug = require('diagnostics')('gasket:express');
/**
* Create the Express instance and setup the lifecycle hooks.
@@ -9,119 +8,16 @@ const debug = require('diagnostics')('gasket:express');
*/
module.exports = async function createServers(gasket, serverOpts) {
const express = require('express');
- const cookieParser = require('cookie-parser');
- const compression = require('compression');
- const { config, logger } = gasket;
+ const { config } = gasket;
const {
express: {
- routes,
- excludedRoutesRegex,
- middlewareInclusionRegex,
- compression: compressionConfig = true,
- trustProxy = false
+ routes
} = {},
- http2,
- middleware: middlewareConfig
+ http2
} = config;
- if (excludedRoutesRegex) {
- // eslint-disable-next-line no-console
- const warn = logger ? logger.warn : console.warn;
- warn('DEPRECATED express config `excludedRoutesRegex` - use `middlewareInclusionRegex`');
- }
-
const app = http2 ? require('http2-express-bridge')(express) : express();
- const useForAllowedPaths = (...middleware) =>
- middleware.forEach((mw) => {
- if (excludedRoutesRegex) {
- app.use(excludedRoutesRegex, mw);
- } else {
- app.use(mw);
- }
- });
-
- if (trustProxy) {
- app.set('trust proxy', trustProxy);
- }
-
- if (http2) {
- app.use(
- /*
- * This is a patch for the undocumented _implicitHeader used by the
- * compression middleware which is not present the http2 request object
- * @see: https://github.com/expressjs/compression/pull/128
- * and also, by the 'compiled' version in Next.js
- * @see: https://github.com/vercel/next.js/issues/11669
- */
- function http2Patch(req, res, next) {
- // @ts-ignore
- if (!res._implicitHeader) {
- // @ts-ignore
- res._implicitHeader = () => res.writeHead(res.statusCode);
- }
- return next();
- }
- );
- }
-
-
- // eslint-disable-next-line jsdoc/require-jsdoc
- function attachLogEnhancer(req) {
- req.logger.metadata = (metadata) => {
- req.logger = req.logger.child(metadata);
- attachLogEnhancer(req);
- };
- }
-
- useForAllowedPaths((req, res, next) => {
- req.logger = gasket.logger;
- attachLogEnhancer(req);
- next();
- });
- useForAllowedPaths(cookieParser());
-
- const middlewarePattern = middlewareInclusionRegex || excludedRoutesRegex;
- if (middlewarePattern) {
- app.use(middlewarePattern, cookieParser());
- } else {
- app.use(cookieParser());
- }
-
- if (compressionConfig) {
- app.use(compression());
- }
-
- const middlewares = [];
- await gasket.execApply('middleware', async (plugin, handler) => {
- const middleware = await handler(app);
-
- if (middleware && (!Array.isArray(middleware) || middleware.length)) {
- if (middlewareConfig) {
- const pluginName = plugin && plugin.name ? plugin.name : '';
- const mwConfig = middlewareConfig.find(mw => mw.plugin === pluginName);
- if (mwConfig) {
- middleware.paths = mwConfig.paths;
- if (middlewarePattern) {
- middleware.paths.push(middlewarePattern);
- }
- }
- }
- middlewares.push(middleware);
- }
- });
-
- debug('applied %s middleware layers to express', middlewares.length);
- middlewares.forEach((layer) => {
- const { paths } = layer;
- if (paths) {
- app.use(paths, layer);
- } else if (middlewarePattern) {
- app.use(middlewarePattern, layer);
- } else {
- app.use(layer);
- }
- });
await gasket.exec('express', app);
diff --git a/packages/gasket-plugin-express/test/plugin.test.js b/packages/gasket-plugin-express/test/plugin.test.js
index 65b492843..59cc1ef0f 100644
--- a/packages/gasket-plugin-express/test/plugin.test.js
+++ b/packages/gasket-plugin-express/test/plugin.test.js
@@ -1,6 +1,5 @@
/* eslint-disable jsdoc/require-jsdoc */
const { setTimeout } = require('timers/promises');
-const { GasketEngine } = require('@gasket/core');
const app = { use: jest.fn(), post: jest.fn(), set: jest.fn() };
const mockExpress = jest.fn().mockReturnValue(app);
const bridgedApp = { use: jest.fn() };
@@ -82,14 +81,6 @@ describe('createServers', () => {
expect(result).toEqual({ handler: bridgedApp });
});
- it('executes the `middleware` lifecycle', async function () {
- await plugin.hooks.createServers(gasket, {});
- expect(gasket.execApply).toHaveBeenCalledWith(
- 'middleware',
- expect.any(Function)
- );
- });
-
it('executes the `express` lifecycle', async function () {
await plugin.hooks.createServers(gasket, {});
expect(gasket.exec).toHaveBeenCalledWith('express', app);
@@ -121,19 +112,6 @@ describe('createServers', () => {
expect(gasket.exec).toHaveBeenCalledWith('errorMiddleware');
});
- it('executes the `middleware` lifecycle before the `express` lifecycle', async function () {
- await plugin.hooks.createServers(gasket, {});
- expect(gasket.execApply.mock.calls[0]).toContain('middleware');
- expect(gasket.exec.mock.calls[0]).toContain('express', app);
- });
-
- it('does not use empty middleware arrays', async function () {
- mockMwPlugins = [{ name: 'middleware-1' }, []];
-
- await plugin.hooks.createServers(gasket, {});
-
- expect(app.use).not.toHaveBeenCalledWith([]);
- });
it('executes the `errorMiddleware` lifecycle after the `express` lifecycle', async function () {
await plugin.hooks.createServers(gasket, {});
@@ -176,190 +154,6 @@ describe('createServers', () => {
expect(errorIndex).toEqual(app.use.mock.calls.length - 1);
});
- it('setups up patch middleware for http2', async () => {
- gasket.config.http2 = 8080;
- await plugin.hooks.createServers(gasket, {});
-
- // Added as the first middleware
- const patchMiddleware = bridgedApp.use.mock.calls[0][0];
- expect(patchMiddleware).toHaveProperty('name', 'http2Patch');
-
- // attaches _implicitHeader to the response object
- const req = {};
- const res = {};
- const next = jest.fn();
- patchMiddleware(req, res, next);
-
- expect(res).toHaveProperty('_implicitHeader');
- expect(next).toHaveBeenCalled();
- });
-
- it('adds the cookie-parser middleware before plugin middleware', async () => {
- await plugin.hooks.createServers(gasket, {});
-
- const cookieParserUsage = findCall(
- app.use,
- (mw) => mw === cookieParserMiddleware
- );
- expect(cookieParserUsage).not.toBeNull();
-
- // callId can be used to determine relative call ordering
- expect(mockCookieParser.mock.invocationCallOrder[0]).toBeLessThan(
- gasket.exec.mock.invocationCallOrder[0]
- );
- expect(mockCookieParser.mock.invocationCallOrder[0]).toBeLessThan(
- gasket.execApply.mock.invocationCallOrder[0]
- );
- });
-
- it('adds the cookie-parser middleware with a excluded path', async () => {
- gasket.config.express = { excludedRoutesRegex: /^(?!\/_next\/)/ };
- await plugin.hooks.createServers(gasket, {});
-
- const cookieParserUsage = findCall(
- app.use,
- (url, mw) => mw === cookieParserMiddleware
- );
- expect(cookieParserUsage).not.toBeNull();
- });
-
- it('supports the deprecated property name', async () => {
- gasket.config.express = { middlewareInclusionRegex: /^(?!\/_next\/)/ };
- await plugin.hooks.createServers(gasket, {});
-
- const cookieParserUsage = findCall(
- app.use,
- (url, mw) => mw === cookieParserMiddleware
- );
- expect(cookieParserUsage).not.toBeNull();
- });
-
- it('adds the compression middleware by default', async () => {
- await plugin.hooks.createServers(gasket, {});
-
- const compressionUsage = findCall(
- app.use,
- (mw) => mw === compressionMiddleware
- );
- expect(compressionUsage).not.toBeNull();
- });
-
- it('adds the compression middleware when enabled from gasket config', async () => {
- gasket.config.express = { compression: true };
- await plugin.hooks.createServers(gasket, {});
-
- const compressionUsage = findCall(
- app.use,
- (mw) => mw === compressionMiddleware
- );
- expect(compressionUsage).not.toBeNull();
- });
-
- it('does not add the compression middleware when disabled from gasket config', async () => {
- gasket.config.express = { compression: false };
- await plugin.hooks.createServers(gasket, {});
-
- const compressionUsage = findCall(
- app.use,
- (mw) => mw === compressionMiddleware
- );
- expect(compressionUsage).toBeNull();
- });
-
- it('adds middleware from lifecycle (ignores falsy)', async () => {
- await plugin.hooks.createServers(gasket, {});
- expect(app.use).toHaveBeenCalledTimes(4);
-
- app.use.mockClear();
- mockMwPlugins = [{ name: 'middlware-1' }, null];
-
- await plugin.hooks.createServers(gasket, {});
- expect(app.use).toHaveBeenCalledTimes(5);
- });
-
- it('supports async middleware hooks', async () => {
- const middleware = Symbol();
- gasket = new GasketEngine([
- plugin,
- {
- name: '@mock/gasket-plugin',
- hooks: {
- middleware: async () => middleware
- }
- }
- ]);
-
- gasket.config = {};
-
- await gasket.exec('createServers');
-
- const middlewares = app.use.mock.calls.flat();
- expect(middlewares).toContain(middleware);
- });
-
- it('middleware paths in the config are used', async () => {
- const paths = ['/home'];
- gasket.config.middleware = [
- {
- plugin: 'middlware-1',
- paths
- }
- ];
- mockMwPlugins = [{ name: 'middlware-1' }];
- await plugin.hooks.createServers(gasket, {});
-
- expect(app.use.mock.calls[4]).toContain(paths);
- });
-
- it('adds errorMiddleware from lifecycle (ignores falsy)', async () => {
- await plugin.hooks.createServers(gasket, {});
- expect(app.use).toHaveBeenCalledTimes(4);
- lifecycles.errorMiddleware.mockResolvedValue([() => {
- }, null]);
- app.use.mockClear();
- await plugin.hooks.createServers(gasket, {});
- expect(app.use).toHaveBeenCalledTimes(5);
- });
-
- it('attaches a logger to the request', async () => {
- const augmentedLogger = { info: jest.fn() };
- gasket.logger = { child: jest.fn().mockReturnValue(augmentedLogger) };
- await plugin.hooks.createServers(gasket, {});
-
- const req = {};
- const res = {};
- const next = jest.fn();
-
- app.use.mock.calls.forEach(([middleware]) => middleware(req, res, next));
- expect(req).toHaveProperty('logger', gasket.logger);
-
- req.logger.metadata({ userId: '123' });
- expect(gasket.logger.child).toHaveBeenCalledWith({ userId: '123' });
-
- req.logger.info('test');
- expect(augmentedLogger.info).toHaveBeenCalledWith('test');
- });
-
- it('does not enable trust proxy by default', async () => {
- await plugin.hooks.createServers(gasket, {});
-
- expect(app.set).not.toHaveBeenCalled();
- });
-
- it('does enable trust proxy by if set to true', async () => {
- gasket.config.express = { trustProxy: true };
- await plugin.hooks.createServers(gasket, {});
-
- expect(app.set).toHaveBeenCalledWith('trust proxy', true);
- });
-
- it('does enable trust proxy by if set to string', async () => {
- gasket.config.express = { trustProxy: '127.0.0.1' };
- await plugin.hooks.createServers(gasket, {});
-
- expect(app.set).toHaveBeenCalledWith('trust proxy', '127.0.0.1');
- });
-
function findCall(aSpy, aPredicate) {
const callIdx = findCallIndex(aSpy, aPredicate);
return callIdx === -1 ? null : aSpy.mock.calls[callIdx][0];
diff --git a/packages/gasket-plugin-fastify/README.md b/packages/gasket-plugin-fastify/README.md
index 6f4c6f17f..9506eac30 100644
--- a/packages/gasket-plugin-fastify/README.md
+++ b/packages/gasket-plugin-fastify/README.md
@@ -34,7 +34,6 @@ All the configurations for the plugin are added under `fastify` in the config:
- `compression`: true by default. Can be set to false if applying compression
differently.
-- `excludedRoutesRegex`: Routes to be excluded based on a regex
- `trustProxy`: Enable trust proxy option, [see Fastify documentation for possible values](https://fastify.dev/docs/latest/Reference/Server/#trustproxy)
#### Example configuration
@@ -46,61 +45,15 @@ module.exports = {
},
fastify: {
compression: false,
+ routes: 'api/*.js',
excludedRoutesRegex: /^(?!\/_next\/)/,
trustProxy: true
}
}
```
-### Middleware paths
-
-The `gasket.config.js` can contain a `middleware` property, which is an array of
-objects that map plugins to route or path patterns, allowing apps to tune which
-middleware are triggered for which requests.
-
-```js
- middleware: [
- {
- plugin:'gasket-plugin-example', // Name of the Gasket plugin
- paths: ['/api']
- },
- {
- plugin:'@some/gasket-plugin-example',
- paths: [/\/default/]
- },
- {
- plugin: '@another/gasket-plugin-example',
- paths: ['/proxy', /\/home/]
- }
- ]
-```
-
## Lifecycles
-### middleware
-
-Executed when the `fastify` server has been created, it will apply all returned
-functions as middleware.
-
-```js
-module.exports = {
- hooks: {
- /**
- * Add Fastify middleware
- *
- * @param {Gasket} gasket The Gasket API
- * @param {Fastify} app - Fastify app instance
- * @returns {function|function[]} middleware(s)
- */
- middleware: function (gasket, app) {
- return require('x-xss-protection')();
- }
- }
-}
-```
-
-You may also return an `Array` to inject more than one middleware.
-
### fastify
Executed **after** the `middleware` event for when you need full control over
diff --git a/packages/gasket-plugin-fastify/lib/create-servers.js b/packages/gasket-plugin-fastify/lib/create-servers.js
index 7d6992aed..f84cd21fa 100644
--- a/packages/gasket-plugin-fastify/lib/create-servers.js
+++ b/packages/gasket-plugin-fastify/lib/create-servers.js
@@ -1,12 +1,6 @@
///
///
-const fastify = require('fastify');
-const middie = require('middie');
-const cookieParser = require('cookie-parser');
-const compression = require('compression');
-const debug = require('diagnostics')('gasket:fastify');
-
/**
* Create the Fastify instance and setup the lifecycle hooks.
* Fastify is compatible with express middleware out of the box, so we can
@@ -15,73 +9,32 @@ const debug = require('diagnostics')('gasket:fastify');
*/
// eslint-disable-next-line max-statements
module.exports = async function createServers(gasket, serverOpts) {
- const { logger, config } = gasket;
- const { middleware: middlewareConfig } = config;
- const trustProxy = config.fastify?.trustProxy ?? false;
- const excludedRoutesRegex =
- config.fastify && config.fastify.excludedRoutesRegex;
- // @ts-ignore
- const app = fastify({ logger, trustProxy });
-
- // Enable middleware for fastify@3
- await app.register(middie);
-
- // Add express-like `res.locals` object attaching data
- app.use(function attachLocals(req, res, next) {
- res.locals = {};
- next();
- });
+ const fastify = require('fastify');
+ const { config, logger } = gasket;
+ const {
+ fastify: {
+ routes,
+ trustProxy = false
+ } = {},
+ http2
+ } = config;
- if (excludedRoutesRegex) {
- app.use(excludedRoutesRegex, cookieParser());
- } else {
- app.use(cookieParser());
- }
-
- const { compression: compressionConfig = true } = config.fastify || {};
- if (compressionConfig) {
- app.use(compression());
- }
+ // @ts-ignore
+ const app = fastify({ logger, trustProxy, http2 });
- const middlewares = [];
+ // allow consuming apps to directly append options to their server
+ await gasket.exec('fastify', app);
- await gasket.execApply('middleware', async (plugin, handler) => {
- const middleware = await handler(app);
- if (middleware) {
- if (middlewareConfig) {
- const pluginName = plugin && plugin.name ? plugin.name : '';
- const mwConfig = middlewareConfig.find(
- (mw) => mw.plugin === pluginName
- );
- if (mwConfig) {
- middleware.paths = mwConfig.paths;
- if (excludedRoutesRegex) {
- middleware.paths.push(excludedRoutesRegex);
- }
- }
+ if (routes) {
+ for (const route of routes) {
+ if (typeof route !== 'function') {
+ throw new Error('Route must be a function');
}
- middlewares.push(middleware);
- }
- });
-
- debug('applied %s middleware layers to fastify', middlewares.length);
- middlewares.forEach((layer) => {
- const { paths } = layer;
- if (paths) {
- app.use(paths, layer);
- } else if (excludedRoutesRegex) {
- app.use(excludedRoutesRegex, layer);
- } else {
- app.use(layer);
+ await route(app);
}
- });
-
- // allow consuming apps to directly append options to their server
- await gasket.exec('fastify', app);
+ }
- const postRenderingStacks = (await gasket.exec('errorMiddleware')).filter(
- Boolean
- );
+ const postRenderingStacks = (await gasket.exec('errorMiddleware')).filter(Boolean);
postRenderingStacks.forEach((stack) => app.use(stack));
return {
diff --git a/packages/gasket-plugin-fastify/lib/index.d.ts b/packages/gasket-plugin-fastify/lib/index.d.ts
index f1c4958ce..a0f5aa1f1 100644
--- a/packages/gasket-plugin-fastify/lib/index.d.ts
+++ b/packages/gasket-plugin-fastify/lib/index.d.ts
@@ -11,12 +11,14 @@ declare module '@gasket/core' {
fastify?: {
/** Enable compression */
compression?: boolean;
- /** Regular expression for excluded routes */
+ /** Filter for which request URLs invoke Gasket middleware */
+ middlewareInclusionRegex?: RegExp;
+ /** @deprecated */
excludedRoutesRegex?: RegExp;
/** Trust proxy configuration */
trustProxy?: FastifyServerOptions['trustProxy'];
/** Glob pattern for source files setting up fastify routes */
- routes?: string;
+ routes?: Array void>>;
};
/** Middleware configuration */
middleware?: {
diff --git a/packages/gasket-plugin-fastify/lib/index.js b/packages/gasket-plugin-fastify/lib/index.js
index 3450b5c64..8d5a5f06a 100644
--- a/packages/gasket-plugin-fastify/lib/index.js
+++ b/packages/gasket-plugin-fastify/lib/index.js
@@ -26,7 +26,7 @@ const plugin = {
context.files.add(`${generatorDir}/**/*`);
context.gasketConfig.add('fastify', {
- routes: './routes/*'
+ routes: []
});
}
},
diff --git a/packages/gasket-plugin-fastify/package.json b/packages/gasket-plugin-fastify/package.json
index 82d3d8bc1..804c40d3c 100644
--- a/packages/gasket-plugin-fastify/package.json
+++ b/packages/gasket-plugin-fastify/package.json
@@ -38,12 +38,6 @@
"url": "https://github.com/godaddy/gasket/issues"
},
"homepage": "https://github.com/godaddy/gasket/tree/main/packages/gasket-plugin-fastify",
- "dependencies": {
- "compression": "^1.7.4",
- "cookie-parser": "^1.4.6",
- "diagnostics": "^2.0.2",
- "middie": "^5.4.0"
- },
"devDependencies": {
"@gasket/core": "^7.0.0-next.34",
"cross-env": "^7.0.3",
diff --git a/packages/gasket-plugin-fastify/test/plugin.test.js b/packages/gasket-plugin-fastify/test/plugin.test.js
index 894e0215d..647b1320a 100644
--- a/packages/gasket-plugin-fastify/test/plugin.test.js
+++ b/packages/gasket-plugin-fastify/test/plugin.test.js
@@ -1,6 +1,3 @@
-const middie = require('middie');
-const { GasketEngine } = require('@gasket/core');
-
const app = {
ready: jest.fn(),
server: {
@@ -97,11 +94,6 @@ describe('createServers', () => {
expect(mockFastify).toHaveBeenCalledWith({ logger: gasket.logger, trustProxy: false });
});
- it('executes the `middleware` lifecycle', async function () {
- await plugin.hooks.createServers(gasket, {});
- expect(gasket.execApply).toHaveBeenCalledWith('middleware', expect.any(Function));
- });
-
it('executes the `fastify` lifecycle', async function () {
await plugin.hooks.createServers(gasket, {});
expect(gasket.exec).toHaveBeenCalledWith('fastify', app);
@@ -112,12 +104,6 @@ describe('createServers', () => {
expect(gasket.exec).toHaveBeenCalledWith('errorMiddleware');
});
- it('executes the `middleware` lifecycle before the `fastify` lifecycle', async function () {
- await plugin.hooks.createServers(gasket, {});
- expect(gasket.execApply.mock.calls[0]).toContain('middleware');
- expect(gasket.exec.mock.calls[0]).toContain('fastify', app);
- });
-
it('executes the `errorMiddleware` lifecycle after the `fastify` lifecycle', async function () {
await plugin.hooks.createServers(gasket, {});
expect(gasket.exec.mock.calls[0]).toContain('fastify', app);
@@ -136,138 +122,6 @@ describe('createServers', () => {
expect(errorMiddleware).not.toBeNull();
});
- it('registers the middie middleware plugin', async () => {
- await plugin.hooks.createServers(gasket, {});
-
- expect(app.register).toHaveBeenCalledWith(middie);
- });
-
- it('adds middleware to attach res.locals', async () => {
- await plugin.hooks.createServers(gasket, {});
-
- const middleware = app.use.mock.calls[0][0];
- expect(middleware.name).toEqual('attachLocals');
-
- const res = {};
- const next = jest.fn();
- middleware({}, res, next);
-
- expect(res).toHaveProperty('locals');
- expect(res.locals).toEqual({});
- expect(next).toHaveBeenCalled();
- });
-
- it('adds the cookie-parser middleware before plugin middleware', async () => {
- await plugin.hooks.createServers(gasket, {});
-
- const cookieParserUsage = findCall(
- app.use,
- (mw) => mw === cookieParserMiddleware);
- expect(cookieParserUsage).not.toBeNull();
-
- // invocationCallOrder can be used to determine relative call ordering
- expect(mockCookieParser.mock.invocationCallOrder[0]).toBeLessThan(gasket.exec.mock.invocationCallOrder[0]);
- expect(mockCookieParser.mock.invocationCallOrder[0]).toBeLessThan(gasket.execApply.mock.invocationCallOrder[0]);
- });
-
- it('adds the cookie-parser middleware with a excluded path', async () => {
- gasket.config.fastify = { excludedRoutesRegex: /^(?!\/_next\/)/ };
- await plugin.hooks.createServers(gasket, {});
-
- const cookieParserUsage = findCall(
- app.use,
- (path, mw) => mw === cookieParserMiddleware);
- expect(cookieParserUsage).not.toBeNull();
- });
-
- it('adds the compression middleware by default', async () => {
- await plugin.hooks.createServers(gasket, {});
-
- const compressionUsage = findCall(
- app.use,
- mw => mw === compressionMiddleware);
- expect(compressionUsage).not.toBeNull();
- });
-
- it('adds the compression middleware when enabled from gasket config', async () => {
- gasket.config.fastify = { compression: true };
- await plugin.hooks.createServers(gasket, {});
-
- const compressionUsage = findCall(
- app.use,
- mw => mw === compressionMiddleware);
- expect(compressionUsage).not.toBeNull();
- });
-
- it('does not add the compression middleware when disabled from gasket config', async () => {
- gasket.config.fastify = { compression: false };
- await plugin.hooks.createServers(gasket, {});
-
- const compressionUsage = findCall(
- app.use,
- mw => mw === compressionMiddleware);
- expect(compressionUsage).toBeNull();
- });
-
- it('adds middleware from lifecycle (ignores falsy)', async () => {
- await plugin.hooks.createServers(gasket, {});
- expect(app.use).toHaveBeenCalledTimes(3);
-
- app.use.mockClear();
- mockMwPlugins = [
- { name: 'middlware-1' },
- null
- ];
-
- await plugin.hooks.createServers(gasket, {});
- expect(app.use).toHaveBeenCalledTimes(4);
- });
-
- it('supports async middleware hooks', async () => {
- const middleware = Symbol();
- gasket = new GasketEngine([
- plugin,
- {
- name: '@mock/gasket-plugin',
- hooks: {
- middleware: async () => middleware
- }
- }
- ]);
-
- gasket.config = {};
-
- await gasket.exec('createServers');
-
- const middlewares = app.use.mock.calls.flat();
- expect(middlewares).toContain(middleware);
- });
-
- it('middleware paths in the config are used', async () => {
- const paths = ['/home'];
- gasket.config.middleware = [{
- plugin: 'middlware-1',
- paths
- }];
- mockMwPlugins = [
- { name: 'middlware-1' }
- ];
- await plugin.hooks.createServers(gasket, {});
- expect(app.use.mock.calls[app.use.mock.calls.length - 1]).toContain(paths);
- });
-
- it('adds errorMiddleware from lifecycle (ignores falsy)', async () => {
- await plugin.hooks.createServers(gasket, {});
- expect(app.use).toHaveBeenCalledTimes(3);
-
- app.use.mockClear();
- lifecycles.errorMiddleware.mockResolvedValue([() => {
- }, null]);
-
- await plugin.hooks.createServers(gasket, {});
- expect(app.use).toHaveBeenCalledTimes(4);
- });
-
it('does not enable trust proxy by default', async () => {
await plugin.hooks.createServers(gasket, {});
diff --git a/packages/gasket-plugin-middleware/CHANGELOG.md b/packages/gasket-plugin-middleware/CHANGELOG.md
new file mode 100644
index 000000000..319b57309
--- /dev/null
+++ b/packages/gasket-plugin-middleware/CHANGELOG.md
@@ -0,0 +1,5 @@
+# `@gasket/plugin-server`
+
+### 1.0.0
+
+- Initial release.
diff --git a/packages/gasket-plugin-middleware/LICENSE.md b/packages/gasket-plugin-middleware/LICENSE.md
new file mode 100644
index 000000000..724bce9a7
--- /dev/null
+++ b/packages/gasket-plugin-middleware/LICENSE.md
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2019 GoDaddy Operating Company, LLC.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/gasket-plugin-middleware/README.md b/packages/gasket-plugin-middleware/README.md
new file mode 100644
index 000000000..b4bb614c7
--- /dev/null
+++ b/packages/gasket-plugin-middleware/README.md
@@ -0,0 +1,101 @@
+# @gasket/plugin-middleware
+
+An optional plugin when used applies middleware to express or fastify.
+
+
+## Configuration
+
+All the configurations for middleware setup are added under `express` or `fastify` in the config:
+
+- `compression`: true by default. Can be set to false if applying compression
+ differently.
+- `excludedRoutesRegex`: (deprecated) renamed to more correct `middlewareInclusionRegex`.
+- `middlewareInclusionRegex`: RegExp filter to apply toward request URLs to determine when Gasket middleware will run. You can use negative lookahead patterns to exclude routes like static resource paths.
+- `trustProxy`: Enable trust proxy option, [Fastify trust proxy documentation] or [Express trust proxy documentation]
+- `routes`: for source files exporting route-defining functions. These functions will be passed the fastify `app` object, and therein they can attach handlers and middleware.
+
+### Example Express configuration
+
+```js
+module.exports = {
+ plugins: {
+ add: ['@gasket/plugin-express', '@gasket/plugin-middleware']
+ },
+ express: {
+ compression: false,
+ routes: 'api/*.js',
+ middlewareInclusionRegex: /^(?!\/_next\/)/,
+ trustProxy: true
+ }
+}
+```
+
+### Example Fastify configuration
+
+```js
+module.exports = {
+ plugins: {
+ add: ['@gasket/fastify', '@gasket/plugin-middleware']
+ },
+ fastify: {
+ compression: false,
+ routes: 'api/*.js',
+ middlewareInclusionRegex: /^(?!\/_next\/)/,
+ trustProxy: true
+ }
+}
+```
+
+## Middleware paths
+
+The `gasket.config.js` can contain a `middleware` property, which is an array of
+objects that map plugins to route or path patterns, allowing apps to tune which
+middleware are triggered for which requests.
+
+```js
+ middleware: [
+ {
+ plugin:'gasket-plugin-example', // Name of the Gasket plugin
+ paths: ['/api']
+ },
+ {
+ plugin:'@some/gasket-plugin-example',
+ paths: [/\/default/]
+ },
+ {
+ plugin: '@another/gasket-plugin-example',
+ paths: ['/proxy', /\/home/]
+ }
+ ]
+```
+
+## Lifecycles
+
+### middleware
+
+Executed when the `fastify` or `express` server has been created, it will apply all returned
+functions as middleware.
+
+```js
+module.exports = {
+ hooks: {
+ /**
+ * Add Fastify middleware
+ *
+ * @param {Gasket} gasket The Gasket API
+ * @param {Fastify} app - Fastify app instance
+ * @returns {function|function[]} middleware(s)
+ */
+ middleware: function (gasket, app) {
+ return require('x-xss-protection')();
+ }
+ }
+}
+```
+
+You may also return an `Array` to inject more than one middleware.
+
+
+
+[Fastify trust proxy documentation]:https://fastify.dev/docs/latest/Reference/Server/#trustproxy
+[Express trust proxy documentation]:https://expressjs.com/en/guide/behind-proxies.html
diff --git a/packages/gasket-plugin-middleware/lib/express.js b/packages/gasket-plugin-middleware/lib/express.js
new file mode 100644
index 000000000..dbf3c4d64
--- /dev/null
+++ b/packages/gasket-plugin-middleware/lib/express.js
@@ -0,0 +1,78 @@
+///
+///
+///
+
+const cookieParser = require('cookie-parser');
+const { applyCompression, applyCookieParser, executeMiddlewareLifecycle } = require('./utils');
+
+/**
+ * Express lifecycle to add an endpoint to serve service worker script
+ * @type {import('@gasket/core').HookHandler<'express'>}
+ */
+module.exports = async function express(gasket, app) {
+ const { config } = gasket;
+ const {
+ express: {
+ middlewareInclusionRegex,
+ excludedRoutesRegex,
+ compression: compressionConfig = true,
+ trustProxy = false
+ } = {},
+ http2
+ } = config;
+
+ const middlewarePattern = middlewareInclusionRegex || excludedRoutesRegex;
+
+ const useForAllowedPaths = (...middleware) =>
+ middleware.forEach((mw) => {
+ if (excludedRoutesRegex) {
+ app.use(excludedRoutesRegex, mw);
+ } else {
+ app.use(mw);
+ }
+ });
+
+ if (http2) {
+ app.use(
+ /*
+ * This is a patch for the undocumented _implicitHeader used by the
+ * compression middleware which is not present the http2 request object
+ * @see: https://github.com/expressjs/compression/pull/128
+ * and also, by the 'compiled' version in Next.js
+ * @see: https://github.com/vercel/next.js/issues/11669
+ */
+ function http2Patch(req, res, next) {
+ // @ts-ignore
+ if (!res._implicitHeader) {
+ // @ts-ignore
+ res._implicitHeader = () => res.writeHead(res.statusCode);
+ }
+ return next();
+ }
+ );
+ }
+
+ if (trustProxy) {
+ app.set('trust proxy', trustProxy);
+ }
+ // eslint-disable-next-line jsdoc/require-jsdoc
+ function attachLogEnhancer(req) {
+ req.logger.metadata = (metadata) => {
+ req.logger = req.logger.child(metadata);
+ attachLogEnhancer(req);
+ };
+ }
+
+ useForAllowedPaths((req, res, next) => {
+ req.logger = gasket.logger;
+ attachLogEnhancer(req);
+ next();
+ });
+ useForAllowedPaths(cookieParser());
+
+ applyCookieParser(app, middlewarePattern);
+ applyCompression(app, compressionConfig);
+
+ executeMiddlewareLifecycle(gasket, app, middlewarePattern);
+
+};
diff --git a/packages/gasket-plugin-middleware/lib/fastify.js b/packages/gasket-plugin-middleware/lib/fastify.js
new file mode 100644
index 000000000..b5889d7e3
--- /dev/null
+++ b/packages/gasket-plugin-middleware/lib/fastify.js
@@ -0,0 +1,35 @@
+///
+
+const middie = require('middie');
+const { applyCompression, applyCookieParser, executeMiddlewareLifecycle } = require('./utils');
+
+/**
+ * Fastify lifecycle to add middleware
+ * @type {import('@gasket/core').HookHandler<'fastify'>}
+ */
+module.exports = async function fastify(gasket, app) {
+ const { config } = gasket;
+ const {
+ fastify: {
+ middlewareInclusionRegex,
+ excludedRoutesRegex,
+ compression: compressionConfig = true
+ } = {}
+ } = config;
+
+ // Enable middleware for fastify@3
+ await app.register(middie);
+
+ // Add express-like `res.locals` object attaching data
+ app.use(function attachLocals(req, res, next) {
+ res.locals = {};
+ next();
+ });
+
+ const middlewarePattern = middlewareInclusionRegex || excludedRoutesRegex;
+
+ applyCookieParser(app, middlewarePattern);
+ applyCompression(app, compressionConfig);
+
+ executeMiddlewareLifecycle(gasket, app, middlewarePattern);
+};
diff --git a/packages/gasket-plugin-middleware/lib/index.d.ts b/packages/gasket-plugin-middleware/lib/index.d.ts
new file mode 100644
index 000000000..26f0a3f9a
--- /dev/null
+++ b/packages/gasket-plugin-middleware/lib/index.d.ts
@@ -0,0 +1,4 @@
+export default {
+ name: '@gasket/plugin-middleware',
+ hooks: {}
+};
diff --git a/packages/gasket-plugin-middleware/lib/index.js b/packages/gasket-plugin-middleware/lib/index.js
new file mode 100644
index 000000000..3b4892648
--- /dev/null
+++ b/packages/gasket-plugin-middleware/lib/index.js
@@ -0,0 +1,16 @@
+const { name, version, description } = require('../package.json');
+const express = require('./express');
+const fastify = require('./fastify');
+
+/** @type {import('@gasket/core').Plugin} */
+const plugin = {
+ name,
+ version,
+ description,
+ hooks: {
+ express,
+ fastify
+ }
+};
+
+module.exports = plugin;
diff --git a/packages/gasket-plugin-middleware/lib/internal.d.ts b/packages/gasket-plugin-middleware/lib/internal.d.ts
new file mode 100644
index 000000000..d2afafc97
--- /dev/null
+++ b/packages/gasket-plugin-middleware/lib/internal.d.ts
@@ -0,0 +1,19 @@
+import { Gasket } from '@gasket/core';
+import type { Application as ExpressApp } from 'express';
+import type { Fastify as FastifyApp } from 'fastify';
+
+export function applyCookieParser(
+ app: ExpressApp | FastifyApp,
+ middlewarePattern: RegExp
+): void;
+
+export function applyCompression(
+ app: ExpressApp | FastifyApp,
+ compressionConfig: boolean
+): void;
+
+export function executeMiddlewareLifecycle(
+ gasket: Gasket,
+ app: ExpressApp | FastifyApp,
+ middlewarePattern: RegExp
+): void;
diff --git a/packages/gasket-plugin-middleware/lib/utils.js b/packages/gasket-plugin-middleware/lib/utils.js
new file mode 100644
index 000000000..5992c3d0f
--- /dev/null
+++ b/packages/gasket-plugin-middleware/lib/utils.js
@@ -0,0 +1,75 @@
+const cookieParser = require('cookie-parser');
+const compression = require('compression');
+const diagnostics = require('diagnostics');
+
+const debug = diagnostics('gasket:middleware');
+
+/**
+ * Applies the cookie parser based on the middleware pattern.
+ * @type {import('./internal').applyCookieParser}
+ */
+function applyCookieParser(app, middlewarePattern) {
+ if (middlewarePattern) {
+ app.use(middlewarePattern, cookieParser());
+ } else {
+ app.use(cookieParser());
+ }
+}
+
+/**
+ * Applies compression to the application if a compression config is present.
+ * @type {import('./internal').applyCompression}
+ */
+function applyCompression(app, compressionConfig) {
+ if (compressionConfig) {
+ app.use(compression());
+ }
+}
+
+/**
+ * Executes the middleware lifecycle for the application
+ * @type {import('./internal').executeMiddlewareLifecycle}
+ */
+async function executeMiddlewareLifecycle(gasket, app, middlewarePattern) {
+ const { config } = gasket;
+ const {
+ middleware: middlewareConfig
+ } = config;
+
+ const middlewares = [];
+ await gasket.execApply('middleware', async (plugin, handler) => {
+ const middleware = await handler(app);
+
+ if (middleware && (!Array.isArray(middleware) || middleware.length)) {
+ if (middlewareConfig) {
+ const pluginName = plugin && plugin.name ? plugin.name : '';
+ const mwConfig = middlewareConfig.find(mw => mw.plugin === pluginName);
+ if (mwConfig) {
+ middleware.paths = mwConfig.paths;
+ if (middlewarePattern) {
+ middleware.paths.push(middlewarePattern);
+ }
+ }
+ }
+ middlewares.push(middleware);
+ }
+ });
+
+ debug('applied %s middleware layers', middlewares.length);
+ middlewares.forEach(async (layer) => {
+ const { paths } = layer;
+ if (paths) {
+ app.use(paths, layer);
+ } else if (middlewarePattern) {
+ app.use(middlewarePattern, layer);
+ } else {
+ app.use(layer);
+ }
+ });
+}
+
+module.exports = {
+ applyCookieParser,
+ applyCompression,
+ executeMiddlewareLifecycle
+};
diff --git a/packages/gasket-plugin-middleware/package.json b/packages/gasket-plugin-middleware/package.json
new file mode 100644
index 000000000..0df6ee571
--- /dev/null
+++ b/packages/gasket-plugin-middleware/package.json
@@ -0,0 +1,54 @@
+{
+ "name": "@gasket/plugin-middleware",
+ "version": "1.0.0",
+ "description": "Handles common server engine setups for routing and executing lifecycles.",
+ "main": "lib",
+ "scripts": {
+ "lint": "eslint .",
+ "lint:fix": "npm run lint -- --fix",
+ "test": "cross-env NODE_OPTIONS='--unhandled-rejections=strict' jest",
+ "test:watch": "jest --watch",
+ "test:coverage": "jest --coverage",
+ "posttest": "npm run lint && npm run typecheck",
+ "typecheck": "tsc",
+ "typecheck:watch": "tsc --watch"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+ssh://git@github.com/godaddy/gasket.git"
+ },
+ "author": "GoDaddy Operating Company, LLC",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/godaddy/gasket/issues"
+ },
+ "homepage": "https://github.com/godaddy/gasket#readme",
+ "dependencies": {
+ "compression": "^1.7.4",
+ "cookie-parser": "^1.4.6",
+ "diagnostics": "^2.0.2",
+ "middie": "^5.4.0"
+ },
+ "devDependencies": {
+ "eslint": "^8.56.0",
+ "eslint-config-godaddy": "^7.1.0",
+ "eslint-plugin-jest": "^27.6.3",
+ "eslint-plugin-json": "^3.1.0",
+ "eslint-plugin-unicorn": "^44.0.0",
+ "jest": "^29.7.0"
+ },
+ "eslintConfig": {
+ "extends": [
+ "godaddy",
+ "plugin:jest/recommended",
+ "plugin:jsdoc/recommended-typescript-flavor"
+ ],
+ "plugins": [
+ "unicorn",
+ "jsdoc"
+ ],
+ "rules": {
+ "unicorn/filename-case": "error"
+ }
+ }
+}
diff --git a/packages/gasket-plugin-middleware/test/express.test.js b/packages/gasket-plugin-middleware/test/express.test.js
new file mode 100644
index 000000000..0c31bd58f
--- /dev/null
+++ b/packages/gasket-plugin-middleware/test/express.test.js
@@ -0,0 +1,103 @@
+const app = {
+ use: jest.fn(),
+ post: jest.fn(),
+ set: jest.fn()
+};
+const cookieParserMiddleware = jest.fn();
+const mockCookieParser = jest.fn().mockReturnValue(cookieParserMiddleware);
+
+jest.mock('cookie-parser', () => mockCookieParser);
+
+const express = require('../lib/express');
+
+describe('Express', () => {
+ let gasket, lifecycles, mockMwPlugins;
+ const sandbox = jest.fn();
+
+ beforeEach(() => {
+ mockMwPlugins = [];
+
+ lifecycles = {
+ errorMiddleware: jest.fn().mockResolvedValue([]),
+ express: jest.fn().mockResolvedValue()
+ };
+
+ gasket = {
+ middleware: {},
+ logger: {
+ warn: jest.fn()
+ },
+ config: {},
+ exec: jest
+ .fn()
+ .mockImplementation((lifecycle, ...args) =>
+ lifecycles[lifecycle](args)
+ ),
+ execApply: sandbox.mockImplementation(async function (lifecycle, fn) {
+ for (let i = 0; i < mockMwPlugins.length; i++) {
+ // eslint-disable-next-line no-loop-func
+ fn(mockMwPlugins[i], () => mockMwPlugins[i]);
+ }
+ return jest.fn();
+ })
+ };
+
+ jest.clearAllMocks();
+ });
+
+ it('does not enable trust proxy by default', async () => {
+ await express(gasket, app);
+ expect(app.set).not.toHaveBeenCalled();
+ });
+
+ it('does enable trust proxy by if set to true', async () => {
+ gasket.config.express = { trustProxy: true };
+ await express(gasket, app);
+
+ expect(app.set).toHaveBeenCalledWith('trust proxy', true);
+ });
+
+ it('does enable trust proxy by if set to string', async () => {
+ gasket.config.express = { trustProxy: '127.0.0.1' };
+ await express(gasket, app);
+
+ expect(app.set).toHaveBeenCalledWith('trust proxy', '127.0.0.1');
+ });
+
+ it('setups up patch middleware for http2', async () => {
+ gasket.config.http2 = 8080;
+ await express(gasket, app);
+
+ // Added as the first middleware
+ const patchMiddleware = app.use.mock.calls[0][0];
+ expect(patchMiddleware).toHaveProperty('name', 'http2Patch');
+
+ // attaches _implicitHeader to the response object
+ const req = {};
+ const res = {};
+ const next = jest.fn();
+ patchMiddleware(req, res, next);
+
+ expect(res).toHaveProperty('_implicitHeader');
+ expect(next).toHaveBeenCalled();
+ });
+
+ it('attaches a logger to the request', async () => {
+ const augmentedLogger = { info: jest.fn() };
+ gasket.logger = { child: jest.fn().mockReturnValue(augmentedLogger) };
+ await express(gasket, app);
+
+ const req = {};
+ const res = {};
+ const next = jest.fn();
+
+ app.use.mock.calls.forEach(([middleware]) => middleware(req, res, next));
+ expect(req).toHaveProperty('logger', gasket.logger);
+
+ req.logger.metadata({ userId: '123' });
+ expect(gasket.logger.child).toHaveBeenCalledWith({ userId: '123' });
+
+ req.logger.info('test');
+ expect(augmentedLogger.info).toHaveBeenCalledWith('test');
+ });
+});
diff --git a/packages/gasket-plugin-middleware/test/fastify.test.js b/packages/gasket-plugin-middleware/test/fastify.test.js
new file mode 100644
index 000000000..9539b1601
--- /dev/null
+++ b/packages/gasket-plugin-middleware/test/fastify.test.js
@@ -0,0 +1,66 @@
+const middie = require('middie');
+
+const app = {
+ ready: jest.fn(),
+ server: {
+ emit: jest.fn()
+ },
+ register: jest.fn(),
+ use: jest.fn()
+};
+
+const mockFastify = jest.fn().mockReturnValue(app);
+const cookieParserMiddleware = jest.fn();
+const compressionMiddleware = jest.fn();
+const mockCookieParser = jest.fn().mockReturnValue(cookieParserMiddleware);
+const mockCompression = jest.fn().mockReturnValue(compressionMiddleware);
+
+
+jest.mock('fastify', () => mockFastify);
+jest.mock('cookie-parser', () => mockCookieParser);
+jest.mock('compression', () => mockCompression);
+
+const fastify = require('../lib/fastify');
+
+describe('fastify', () => {
+ let gasket, mockMwPlugins;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockMwPlugins = [];
+
+ gasket = {
+ middleware: {},
+ logger: {},
+ config: {},
+ execApply: jest.fn(async function (lifecycle, fn) {
+ for (let i = 0; i < mockMwPlugins.length; i++) {
+ // eslint-disable-next-line no-loop-func
+ fn(mockMwPlugins[i], () => mockMwPlugins[i]);
+ }
+ return jest.fn();
+ })
+ };
+ });
+
+ it('registers the middie middleware plugin', async () => {
+ await fastify(gasket, app);
+
+ expect(app.register).toHaveBeenCalledWith(middie);
+ });
+
+ it('adds middleware to attach res.locals', async () => {
+ await fastify(gasket, app);
+
+ const middleware = app.use.mock.calls[0][0];
+ expect(middleware.name).toEqual('attachLocals');
+
+ const res = {};
+ const next = jest.fn();
+ middleware({}, res, next);
+
+ expect(res).toHaveProperty('locals');
+ expect(res.locals).toEqual({});
+ expect(next).toHaveBeenCalled();
+ });
+});
diff --git a/packages/gasket-plugin-middleware/test/index.test.js b/packages/gasket-plugin-middleware/test/index.test.js
new file mode 100644
index 000000000..e5465c91d
--- /dev/null
+++ b/packages/gasket-plugin-middleware/test/index.test.js
@@ -0,0 +1,24 @@
+const plugin = require('../lib');
+
+describe('Plugin', function () {
+ it('is an object', function () {
+ expect(typeof plugin).toBe('object');
+ });
+
+ it('has expected name', function () {
+ expect(plugin).toHaveProperty('name', require('../package').name);
+ });
+
+ it('has expected hooks', function () {
+ const expected = [
+ 'express',
+ 'fastify'
+ ];
+
+ expect(plugin).toHaveProperty('hooks');
+
+ const hooks = Object.keys(plugin.hooks);
+ expect(hooks).toEqual(expected);
+ expect(hooks).toHaveLength(expected.length);
+ });
+});
diff --git a/packages/gasket-plugin-middleware/test/utils.test.js b/packages/gasket-plugin-middleware/test/utils.test.js
new file mode 100644
index 000000000..6638db202
--- /dev/null
+++ b/packages/gasket-plugin-middleware/test/utils.test.js
@@ -0,0 +1,137 @@
+/* eslint-disable max-nested-callbacks */
+const app = {
+ ready: jest.fn(),
+ server: {
+ emit: jest.fn()
+ },
+ register: jest.fn(),
+ use: jest.fn()
+};
+
+const cookieParserMiddleware = jest.fn();
+const compressionMiddleware = jest.fn();
+const mockCookieParser = jest.fn().mockReturnValue(cookieParserMiddleware);
+const mockCompression = jest.fn().mockReturnValue(compressionMiddleware);
+
+jest.mock('cookie-parser', () => mockCookieParser);
+jest.mock('compression', () => mockCompression);
+
+const { applyCookieParser, applyCompression, executeMiddlewareLifecycle } = require('../lib/utils');
+
+
+describe('utils', function () {
+
+ describe('applyCookieParser', function () {
+ let middlewarePattern;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('adds the cookie-parser middleware before plugin middleware', async () => {
+ applyCookieParser(app, middlewarePattern);
+
+ const cookieParserUsage = findCall(
+ app.use,
+ (mw) => mw === cookieParserMiddleware);
+ expect(cookieParserUsage).not.toBeNull();
+ });
+
+ it('adds the cookie-parser middleware with a excluded path', async () => {
+ middlewarePattern = /somerandompattern/;
+ applyCookieParser(app, middlewarePattern);
+
+ const cookieParserUsage = findCall(
+ app.use,
+ (url, mw) => mw === cookieParserMiddleware
+ );
+ expect(cookieParserUsage).not.toBeNull();
+ });
+ });
+
+ describe('applyCompression', function () {
+ let compressionConfig;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('adds the compression middleware when enabled', async () => {
+ compressionConfig = true;
+ applyCompression(app, compressionConfig);
+
+ const compressionUsage = findCall(
+ app.use,
+ (mw) => mw === compressionMiddleware
+ );
+ expect(compressionUsage).not.toBeNull();
+ });
+
+ it('does not add the compression middleware when disabled', async () => {
+ compressionConfig = false;
+ applyCompression(app, compressionConfig);
+
+ const compressionUsage = findCall(
+ app.use,
+ (mw) => mw === compressionMiddleware
+ );
+ expect(compressionUsage).toBeNull();
+ });
+ });
+
+ describe('executeMiddlewareLifecycle', function () {
+ let gasket, mockMwPlugins, middlewarePattern, lifecycles;
+ const sandbox = jest.fn();
+ beforeEach(() => {
+ mockMwPlugins = [];
+
+ lifecycles = {
+ middleware: jest.fn().mockResolvedValue([])
+ };
+
+ gasket = {
+ middleware: {},
+ logger: {
+ warn: jest.fn()
+ },
+ config: {},
+ exec: jest.fn().mockImplementation((lifecycle, ...args) => lifecycles[lifecycle](args)),
+ execApply: sandbox.mockImplementation(async function (lifecycle, fn) {
+ for (let i = 0; i < mockMwPlugins.length; i++) {
+ // eslint-disable-next-line no-loop-func
+ fn(mockMwPlugins[i], () => mockMwPlugins[i]);
+ }
+ return jest.fn();
+ })
+ };
+
+ jest.clearAllMocks();
+ });
+
+ it('executes the `middleware` lifecycle', async function () {
+ executeMiddlewareLifecycle(gasket, app, middlewarePattern);
+ expect(gasket.execApply).toHaveBeenCalledWith(
+ 'middleware',
+ expect.any(Function)
+ );
+ });
+
+ it('does not use empty middleware arrays', async function () {
+ mockMwPlugins = [{ name: 'middleware-1' }, []];
+ executeMiddlewareLifecycle(gasket, app, middlewarePattern);
+
+ expect(app.use).not.toHaveBeenCalledWith([]);
+ });
+ });
+
+ /**
+ * Find the first call in a spy that matches a predicate
+ * @param aSpy
+ * @param aPredicate
+ * @returns {any}
+ */
+ function findCall(aSpy, aPredicate) {
+ const callIdx = aSpy.mock.calls.map(args => aPredicate(...args)).indexOf(true);
+ return callIdx === -1 ? null : aSpy.mock.calls[callIdx][0];
+ }
+});
diff --git a/packages/gasket-plugin-middleware/tsconfig.json b/packages/gasket-plugin-middleware/tsconfig.json
new file mode 100644
index 000000000..a3d035ec1
--- /dev/null
+++ b/packages/gasket-plugin-middleware/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "noEmit": true,
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "lib": [
+ "esnext",
+ "dom"
+ ],
+ "types": [
+ "@types/jest"
+ ]
+ },
+ "exclude": [
+ "test",
+ "coverage",
+ "generator"
+ ]
+}