├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── .prettierrc.js ├── README.md ├── examples ├── integration-cookie-session.spec.ts └── integration-express-cookie-session-flash.spec.ts ├── jest.config.js ├── package.json ├── renovate.json ├── resource └── flow-middleware.png ├── src ├── compose.test.ts ├── compose.ts ├── flow.test.ts ├── flow.ts ├── index.ts └── types.ts ├── tsconfig.compile.json ├── tsconfig.dev.json └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | 4 | parser: '@typescript-eslint/parser', 5 | 6 | plugins: ['@typescript-eslint/eslint-plugin', 'prettier', 'jest'], 7 | 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:@typescript-eslint/eslint-recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | 'plugin:prettier/recommended', 13 | ], 14 | 15 | env: { 16 | node: true, 17 | jest: true, 18 | }, 19 | 20 | rules: { 21 | 'prettier/prettier': 'error', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | '@typescript-eslint/explicit-function-return-type': 'off', 24 | '@typescript-eslint/ban-ts-comment': 'warn', 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | platform: [ 10 | ubuntu-latest, 11 | windows-latest 12 | ] 13 | node-version: 14 | - 16.x 15 | - 14.x 16 | - 12.x 17 | runs-on: ${{ matrix.platform }} 18 | 19 | steps: 20 | - uses: actions/checkout@v1 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | 26 | # Pulling caches itself it expensive. Which one is faster? 27 | # - name: Get yarn cache 28 | # id: yarn-cache 29 | # run: echo "::set-output name=dir::$(yarn cache dir)" 30 | # - uses: actions/cache@v1 31 | # with: 32 | # path: ${{ steps.yarn-cache.outputs.dir }} 33 | # key: ${{ runner.os }}-yarn-${{ hashFiles('package.json') }} 34 | # restore-keys: | 35 | # ${{ runner.os }}-yarn- 36 | 37 | - name: yarn install, lint, and test 38 | run: | 39 | yarn install --ignore-scripts 40 | yarn prepublishOnly 41 | env: 42 | CI: true 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | # Logs 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Editors and IDEs 9 | .idea 10 | *.iml 11 | .vscode/* 12 | !.vscode/settings.json 13 | !.vscode/tasks.json 14 | !.vscode/launch.json 15 | !.vscode/extensions.json 16 | 17 | # Misc 18 | .DS_Store 19 | dist 20 | *.tgz 21 | 22 | # To develop with closer deps version that users will use 23 | yarn.lock 24 | 25 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | singleQuote: true, 4 | trailingComma: 'all', 5 | }; 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flow-middleware ![Node CI](https://github.com/piglovesyou/flow-middleware/actions/workflows/nodejs.yml/badge.svg) [![npm version](https://badge.fury.io/js/flow-middleware.svg)](https://badge.fury.io/js/flow-middleware) 2 | 3 | Run Express middlewares on any Node.js server framework without hacking/polluting native `req`/`res` objects with Proxy. 4 | 5 | [Checkout the Next.js example](https://github.com/piglovesyou/nextjs-passport-oauth-example) with [Passport.js](http://www.passportjs.org/) integration. 6 | 7 |
Why, How 8 |

9 | 10 | # Why 11 | 12 | As people start using a new Node server library other than [Express](https://expressjs.com/), they encounter a lack of middlewares that Express already has, which have been well tested and production-ready many years ago. Some of them try to shape a brand new ecosystem on the new island and some just go back to Express. 13 | 14 | Let's start from admitting Express is one of the most successful, beautifully designed and battle-tested software in the Node ecosystem. Don't forget its **hundreds of outstanding middlewares** have been born on it. Then why you can't use them? The answers will be summarized: 15 | 16 | * It breaks since they depend on `req.param()` and `res.redirect()` that Express decorates native objects with. I don't want to hack to make them work in my _${Your favorite server comes here}_. 17 | * Pollution. [Express officially recommends](https://expressjs.com/en/guide/writing-middleware.html) middlewares to extend object properties such as `req.session` and `req.flash`, just where my _${Your favorite server}_ leaves them tidy. Plus, dynamic extensions don't fit today of the TypeScript era. 18 | 19 | Yeah. Let's move on. 20 | 21 | # How 22 | 23 | JavaScript `Proxy`. 24 | 25 | Wrapping `req` and `res` by `Proxy` to split using native methods and Express methods. Express exports clean prototypes that we can intercept internal calls with. It lets middlewares to call native methods like `res.writeHead()` and `res.end()` so native objects properly embed HTTP info and send the response. 26 | 27 | In the end, flow-middleware returns the extended properties like `req.session` and `req.user` so you can use them after the middlewares go through. 28 | 29 |

30 |
31 | 32 | ![flow-middleware architecture](resource/flow-middleware.png) 33 | 34 | # Getting started 35 | 36 | Install it with Express. 37 | 38 | ```bash 39 | yarn add flow-middleware express 40 | ``` 41 | 42 | ### flow(...middlewares) 43 | 44 | A function `flow` creates an http handler from some Express middlewares, processed from left to right of arguments. 45 | 46 | ```typescript 47 | import flow from 'flow-middleware'; 48 | import { ok } from "assert"; 49 | import { createServer } from 'http'; 50 | import cookieParser from 'cookie-parser'; 51 | import session from 'express-session'; 52 | import flash from 'express-flash'; 53 | 54 | // Creates an async function that handles req and res. 55 | const handle = flow( 56 | cookieParser(), 57 | session({ secret: 'x' }), 58 | flash(), 59 | (reqProxy, _resProxy, next) => { 60 | 61 | // Our wrapped objects provide accessors 62 | // that Express middlewares extended💪 63 | ok(reqProxy.cookies); 64 | ok(reqProxy.session); 65 | ok(reqProxy.flash); 66 | next(); 67 | } 68 | ); 69 | 70 | createServer(async (req, res) => { 71 | 72 | // Let's run the Express middlewares🚀 73 | const [ reqProxy, resProxy ] = await handle(req, res); 74 | 75 | // Native objects are clean thanks to our proxy✨ 76 | ok(req.cookies === undefined); 77 | ok(req.session === undefined); 78 | ok(req.flash === undefined); 79 | 80 | // You still can access to Express properties here🚚 81 | ok(reqProxy.cookies); 82 | ok(reqProxy.session); 83 | ok(reqProxy.flash); 84 | ok(resProxy.cookie); 85 | ok(resProxy.redirect); 86 | 87 | res.end('Hello!'); 88 | }).listen(3000); 89 | ``` 90 | 91 | ### compose(...middlewares)(...middlewares)() 92 | 93 | `compose` lets you hold a set of middlewares and share it on other routes. This is useful when you want the same initializing middlewares to come first while the different middlewares come at the end. **Calling it with zero arguments returns a handler function.** 94 | 95 | This is a Passport example where a login handler for `POST /api/auth/github` and an OAuth callback handler for `GET /api/auth/callback/github` share their initializing middlewares. 96 | 97 | ```typescript 98 | import cookieSession from 'cookie-session'; 99 | import { compose } from 'flow-middleware'; 100 | import passport from './passport'; 101 | 102 | const composed = compose( 103 | cookieSession(), 104 | passport.initialize(), 105 | passport.session() 106 | ); 107 | 108 | const handleToLogIn = composed(passport.authenticate('github'))(); 109 | 110 | const handleForCallback = composed(passport.authenticate('github', { 111 | failureRedirect: '/auth', 112 | successRedirect: '/', 113 | }))(); 114 | ``` 115 | 116 | Don't forget to call it with zero arguments at last to get a handler. 117 | 118 | #### Wrapper function style 119 | 120 | Or, you can simply write a wrapper function to share middlewares. 121 | 122 | ```typescript 123 | import { Handler } from 'express'; 124 | 125 | function withPassport(...middlewares: Handler[]) { 126 | return flow( 127 | cookieSession(), 128 | passport.initialize(), 129 | passport.session(), 130 | ...middlewares 131 | ); 132 | } 133 | ``` 134 | 135 | # License 136 | 137 | MIT 138 | 139 | # Author 140 | 141 | Soichi Takamura \ 142 | -------------------------------------------------------------------------------- /examples/integration-cookie-session.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/ban-ts-comment, @typescript-eslint/no-non-null-assertion */ 2 | 3 | import { promisify } from 'util'; 4 | import flow from '../src/flow'; 5 | import assert, { ok } from 'assert'; 6 | import { createServer, Server } from 'http'; 7 | import { CookieJar } from 'tough-cookie'; 8 | import fetch from 'node-fetch'; 9 | import cookieSession from 'cookie-session'; 10 | import { getPortPromise } from 'portfinder'; 11 | 12 | describe('Integration', () => { 13 | let server: Server; 14 | 15 | afterEach(async () => { 16 | await promisify(server.close).call(server); 17 | }); 18 | 19 | test('cookie-session', async () => { 20 | const port = await getPortPromise(); 21 | const expect = 'Hello!'; 22 | 23 | // Creates a simple function that handles req and res. 24 | const middlewares = flow, Record>( 25 | cookieSession({ 26 | name: 'passportSession', 27 | signed: false, 28 | }), 29 | (req, _res, next) => { 30 | ok(req.session); 31 | 32 | if (req.url === '/') { 33 | req.session!.yeah = 'yeah'; 34 | } else if (req.url === '/second') { 35 | assert.strictEqual(req.session!.yeah, 'yeah'); 36 | } else { 37 | assert.fail(); 38 | } 39 | next(); 40 | }, 41 | ); 42 | 43 | server = createServer(async (req, res) => { 44 | // Let's pass native req and res through Express middlewares 45 | const [proxiedReq, _proxiedRes] = await middlewares(req, res); 46 | 47 | // @ts-ignore 48 | ok(req.session === undefined); 49 | 50 | ok(proxiedReq.session); 51 | 52 | res.end('Hello!'); 53 | }).listen(port); 54 | 55 | const jar = new CookieJar(); 56 | const url = `http://localhost:${port}`; 57 | const actual = await fetch(url).then((res) => { 58 | const cookieStr = res.headers.get('set-cookie'); 59 | assert.strictEqual(typeof cookieStr, 'string'); 60 | jar.setCookieSync(cookieStr!, url); 61 | return res.text(); 62 | }); 63 | assert.strictEqual(actual, expect); 64 | 65 | await fetch(url + '/second', { 66 | headers: { cookie: jar.getCookiesSync(url).join('; ') }, 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /examples/integration-express-cookie-session-flash.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/ban-ts-comment, @typescript-eslint/no-non-null-assertion */ 2 | 3 | import { promisify } from 'util'; 4 | import flow from '../src/flow'; 5 | import { ok, fail, strictEqual } from 'assert'; 6 | import { createServer, Server } from 'http'; 7 | import cookieParser from 'cookie-parser'; 8 | import session from 'express-session'; 9 | import flash from 'express-flash'; 10 | import { CookieJar } from 'tough-cookie'; 11 | import fetch from 'node-fetch'; 12 | import { getPortPromise } from 'portfinder'; 13 | 14 | describe('Integration', () => { 15 | let server: Server; 16 | 17 | afterEach(async () => { 18 | await promisify(server.close).call(server); 19 | }); 20 | 21 | test('express-session, flash', async () => { 22 | const expect = 'Hello!'; 23 | const port = await getPortPromise(); 24 | 25 | // Creates a simple function that handles req and res. 26 | const middlewares = flow, Record>( 27 | cookieParser(), 28 | session({ secret: 'x', resave: true, saveUninitialized: true }), 29 | flash(), 30 | (req, _res, next) => { 31 | // cookie-session's supposed to embed "session" property, 32 | // but it's clean since our proxy wipes them out✨ 33 | ok(req.cookies); 34 | ok(req.session); 35 | ok(req.flash); 36 | 37 | if (req.url === '/') { 38 | req.session!.yeah = 'yeah'; 39 | } else if (req.url === '/second') { 40 | strictEqual( 41 | req.session!.yeah, 42 | 'yeah', 43 | 'second request should use value that first request set in the session.', 44 | ); 45 | } else { 46 | fail(); 47 | } 48 | next(); 49 | }, 50 | ); 51 | 52 | server = createServer(async (req, res) => { 53 | // Let's pass native req and res through Express middlewares 54 | const [reqProxy, resProxy] = await middlewares(req, res); 55 | 56 | // The native objects are still clean 57 | // since our proxy protects them from getting dirty✨ 58 | 59 | // @ts-ignore 60 | ok(req.cookies === undefined); 61 | // @ts-ignore 62 | ok(req.session === undefined); 63 | // @ts-ignore 64 | ok(req.flash === undefined); 65 | 66 | // You can use properties that the middlewares 67 | // extend through proxied object, if you want🚚 68 | ok(reqProxy.cookies); 69 | ok(reqProxy.session); 70 | ok(reqProxy.flash); 71 | ok(resProxy.cookie); 72 | ok(resProxy.redirect); 73 | 74 | res.end('Hello!'); 75 | }).listen(port); 76 | 77 | const jar = new CookieJar(); 78 | const url = `http://localhost:${port}`; 79 | const actual = await fetch(url).then((res) => { 80 | const cookieStr = res.headers.get('set-cookie'); 81 | strictEqual(typeof cookieStr, 'string'); 82 | jar.setCookieSync(cookieStr!, url); 83 | return res.text(); 84 | }); 85 | strictEqual(actual, expect); 86 | 87 | await fetch(url + '/second', { 88 | headers: { cookie: jar.getCookiesSync(url).join('; ') }, 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | testPathIgnorePatterns: ['/node_modules/', 'dist'], 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flow-middleware", 3 | "version": "0.2.2", 4 | "main": "dist/index.js", 5 | "repository": "git@github.com:piglovesyou/compose-middleware.git", 6 | "author": "piglovesyou ", 7 | "license": "MIT", 8 | "files": [ 9 | "dist" 10 | ], 11 | "peerDependencies": { 12 | "express": "^4.17.1" 13 | }, 14 | "devDependencies": { 15 | "@types/cookie-parser": "^1.4.2", 16 | "@types/cookie-session": "2.0.41", 17 | "@types/eslint": "^7.2.1", 18 | "@types/eslint-plugin-prettier": "^3.1.0", 19 | "@types/express": "4.17.7", 20 | "@types/express-flash": "^0.0.2", 21 | "@types/express-session": "^1.17.0", 22 | "@types/jest": "^26.0.10", 23 | "@types/node": "^14.6.0", 24 | "@types/node-fetch": "^2.5.7", 25 | "@types/prettier": "^2.0.2", 26 | "@types/tough-cookie": "^4.0.0", 27 | "@typescript-eslint/eslint-plugin": "^4.0.0", 28 | "@typescript-eslint/parser": "^3.10.1", 29 | "cookie-parser": "^1.4.5", 30 | "cookie-session": "^1.4.0", 31 | "eslint": "^7.7.0", 32 | "eslint-config-prettier": "^6.11.0", 33 | "eslint-plugin-jest": "^23.20.0", 34 | "eslint-plugin-prettier": "^3.1.4", 35 | "express": "^4.17.1", 36 | "express-flash": "^0.0.2", 37 | "express-session": "^1.17.1", 38 | "husky": "^4.2.5", 39 | "jest": "^26.4.2", 40 | "lint-staged": "^10.2.13", 41 | "node-fetch": "^2.6.0", 42 | "npm-run-all": "^4.1.5", 43 | "portfinder": "^1.0.28", 44 | "prettier": "^2.1.0", 45 | "tough-cookie": "^4.0.0", 46 | "ts-jest": "^26.2.0", 47 | "ts-node": "^9.0.0", 48 | "typescript": "4.0.2" 49 | }, 50 | "husky": { 51 | "hooks": { 52 | "pre-commit": "lint-staged" 53 | } 54 | }, 55 | "lint-staged": { 56 | "*.ts": [ 57 | "yarn fix", 58 | "git add --force" 59 | ] 60 | }, 61 | "scripts": { 62 | "lint": "eslint --ext .ts,.tsx,.js --ignore-path .gitignore .", 63 | "fix": "yarn lint --fix", 64 | "compile": "rimraf dist && tsc --declaration --project tsconfig.compile.json", 65 | "clean": "rimraf dist", 66 | "test": "jest", 67 | "build": "npm-run-all clean compile", 68 | "prepublishOnly": "npm-run-all lint build test" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "rangeStrategy": "pin", 3 | "prConcurrentLimit": 5, 4 | "packageRules": [ 5 | { 6 | "matchPackagePatterns": [".*"], 7 | "matchUpdateTypes": ["patch"], 8 | "automerge": true 9 | }, 10 | { 11 | "matchPackagePatterns": [".*"], 12 | "matchDepTypes": ["devDependencies"], 13 | "matchUpdateTypes": ["minor"], 14 | "automerge": true 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /resource/flow-middleware.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piglovesyou/flow-middleware/2927b2e95fc3a0be19dfe0017a9ca26768d7dc2d/resource/flow-middleware.png -------------------------------------------------------------------------------- /src/compose.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { compose } from './index'; 3 | 4 | describe('compose', () => { 5 | test('calls handler from left to right', async () => { 6 | let actual = 0; 7 | 8 | const handler = compose( 9 | (req, res, next) => { 10 | assert.strictEqual(actual, 0); 11 | actual++; 12 | next(); 13 | }, 14 | (req, res, next) => { 15 | assert.strictEqual(actual, 1); 16 | actual++; 17 | next(); 18 | }, 19 | (req, res, next) => { 20 | assert.strictEqual(actual, 2); 21 | actual++; 22 | next(); 23 | }, 24 | )(); 25 | await handler({} as any, {} as any); 26 | assert.strictEqual(actual, 3); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/compose.ts: -------------------------------------------------------------------------------- 1 | import flow from './flow'; 2 | import { AnyMap, TCompose } from './types'; 3 | 4 | // import {init as getExpressInitializer} from 'express/lib/middleware/init'; 5 | // const expressInit = getExpressInitializer(express()); 6 | 7 | const compose: TCompose = function ( 8 | ...handlers: any 9 | ) { 10 | if (!handlers.length) throw new Error('boom'); 11 | 12 | // XXX: Better typing...? 13 | return (...args: any[]): any => { 14 | if (args.length) { 15 | return compose(...handlers, ...args); 16 | } 17 | 18 | return flow(...handlers); 19 | }; 20 | }; 21 | 22 | export default compose; 23 | -------------------------------------------------------------------------------- /src/flow.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { createServer, get as httpGet, Server } from 'http'; 3 | import flow from './index'; 4 | import { promisify } from 'util'; 5 | 6 | describe('flow', () => { 7 | let server: Server; 8 | afterEach(async () => { 9 | await promisify(server.close).call(server); 10 | }); 11 | test('ends ServerResponse properly', async () => { 12 | const expect = 'yeah'; 13 | let actual = ''; 14 | 15 | const handler = flow((req, res, next) => { 16 | res.end('yeah'); 17 | next(); 18 | }); 19 | server = createServer(handler).listen(3030); 20 | await new Promise((resolve, reject) => { 21 | httpGet('http://localhost:3030', (res) => { 22 | res.on('data', (data: any) => (actual += String(data))); 23 | res.on('end', resolve); 24 | res.on('error', reject); 25 | }); 26 | }); 27 | assert.strictEqual(actual, expect); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/flow.ts: -------------------------------------------------------------------------------- 1 | import express, { 2 | Handler, 3 | request as expressReqProto, 4 | response as expressResProto, 5 | } from 'express'; 6 | import { IncomingMessage, ServerResponse } from 'http'; 7 | import { promisify } from 'util'; 8 | import { AnyMap, THandler } from './types'; 9 | 10 | // Since some guys behave in [pretty bad manner](https://github.com/jaredhanson/passport/blob/4ca43dac54f7ffbf97fba5c917463e7f19639d51/lib/framework/connect.js#L33-L38), 11 | // we have to know what these properties are and proxy "this" arg on these functions. 12 | const knownPropertiesExtendedInBadManner = [ 13 | 'login ', 14 | 'logIn', 15 | 'logout', 16 | 'logOut ', 17 | 'isAuthenticated ', 18 | 'isUnauthenticated ', 19 | ].reduce( 20 | (acc, property) => ({ ...acc, [property]: true }), 21 | {} as Record, 22 | ); 23 | 24 | const expressApp = express(); 25 | 26 | function enforceThisArg(fn: any, thisArg: any) { 27 | return new Proxy(fn, { 28 | apply(target: any, _: any, argArray?: any): any { 29 | return Reflect.apply(fn, thisArg, argArray); 30 | }, 31 | }); 32 | } 33 | 34 | function enforceThisArgOnPropertyDescriptor( 35 | desc: PropertyDescriptor, 36 | thisArg: any, 37 | ) { 38 | const ext: Partial = Object.create(null); 39 | 40 | if (desc.get) ext.get = enforceThisArg(desc.get, thisArg); 41 | if (desc.set) ext.set = enforceThisArg(desc.set, thisArg); 42 | if (desc.value && typeof desc.value === 'function') 43 | ext.value = enforceThisArg(desc.value, thisArg); 44 | 45 | return { ...desc, ...ext }; 46 | } 47 | 48 | function wrapWithProxy( 49 | payload: any, 50 | nativeObj: IncomingMessage | ServerResponse, 51 | expressProto: typeof expressReqProto | typeof expressResProto, 52 | ) { 53 | // Wrap req and res 54 | const proxy = new Proxy(payload, { 55 | get(_, property, proxyObj) { 56 | // Arbitrary properties such as "session" 57 | if (Reflect.has(payload, property)) { 58 | return Reflect.get(payload, property); 59 | 60 | // Access to the original http.IncomingMessage 61 | } else if (Reflect.has(nativeObj, property)) { 62 | const value = Reflect.get(nativeObj, property); 63 | 64 | if ( 65 | Reflect.has(knownPropertiesExtendedInBadManner, property) && 66 | typeof value === 'function' 67 | ) { 68 | return enforceThisArg(value, proxyObj); 69 | } 70 | 71 | if (typeof value === 'function') 72 | return enforceThisArg(value, nativeObj); 73 | return value; 74 | 75 | // Express proto should come to the last because it extends 76 | // IncomingMessage. 77 | } else if (Reflect.has(expressProto, property)) { 78 | const value = Reflect.get(expressProto, property, proxyObj); 79 | if (typeof value === 'function') return enforceThisArg(value, proxyObj); 80 | return value; 81 | } 82 | 83 | // Not found so it must be very "undefined" 84 | return undefined; 85 | }, 86 | set(_, property, value) { 87 | // Node internal setter call 88 | if (Reflect.has(nativeObj, property)) 89 | return Reflect.set(nativeObj, property, value); 90 | 91 | return Reflect.set(payload, property, value); 92 | }, 93 | defineProperty( 94 | _, 95 | property: string | number | symbol, 96 | desc: PropertyDescriptor, 97 | ) { 98 | // Node core object never extends its properties. 99 | if (Reflect.has(nativeObj, property)) throw new Error('never'); 100 | 101 | // This is the case that Express middlewares extend 102 | // Node object's property. If it's a function, we always enforce it 103 | // to be called with our proxied "this" object. 104 | const enforced = enforceThisArgOnPropertyDescriptor(desc, proxy); 105 | 106 | return Reflect.defineProperty(_, property, enforced); 107 | }, 108 | }); 109 | return proxy; 110 | } 111 | 112 | // https://github.com/expressjs/express/blob/c087a45b9cc3eb69c777e260ee880758b6e03a40/lib/middleware/init.js#L28-L42 113 | function emulateExpressInit(proxiedReq: any, proxiedRes: any) { 114 | Reflect.set(proxiedReq, 'res', proxiedRes); 115 | Reflect.set(proxiedReq, 'app', expressApp); 116 | Reflect.set(proxiedRes, 'req', proxiedReq); 117 | Reflect.set(proxiedRes, 'app', expressApp); 118 | Reflect.set(proxiedRes, 'locals', proxiedRes.locals || Object.create(null)); 119 | } 120 | 121 | export default function flow( 122 | ...middlewares: Handler[] 123 | ): THandler { 124 | const promisifiedMiddlewares = middlewares.map((m) => promisify(m)); 125 | 126 | const handler: THandler = async (req, res) => { 127 | const reqPayload: Partial = Object.create(null); 128 | const resPayload: Partial = Object.create(null); 129 | 130 | const proxiedReq = wrapWithProxy(reqPayload, req, expressReqProto); 131 | const proxiedRes = wrapWithProxy(resPayload, res, expressResProto); 132 | 133 | emulateExpressInit(proxiedReq, proxiedRes); 134 | 135 | for ( 136 | let i = 0, m = promisifiedMiddlewares[i]; 137 | i < promisifiedMiddlewares.length; 138 | m = promisifiedMiddlewares[++i] 139 | ) { 140 | try { 141 | await m(proxiedReq, proxiedRes); 142 | } catch (e) { 143 | console.error(e); 144 | throw new Error( 145 | `[flow-middlewares] Error occurs in middleware index [${i}]: ${middlewares[i].name}`, 146 | ); 147 | } 148 | } 149 | 150 | return [proxiedReq, proxiedRes]; 151 | }; 152 | 153 | return handler; 154 | } 155 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import flow from './flow'; 2 | 3 | export default flow; 4 | export { default as compose } from './compose'; 5 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Handler, 3 | Request as ExpressRequest, 4 | Response as ExpressResponse, 5 | } from 'express'; 6 | import { IncomingMessage, ServerResponse } from 'http'; 7 | 8 | export type AnyMap = Record; 9 | 10 | export type THandler = ( 11 | req: IncomingMessage, 12 | res: ServerResponse, 13 | ) => Promise< 14 | [ExpressRequest & Partial, ExpressResponse & Partial] 15 | >; 16 | 17 | export interface TCompose { 18 | (): THandler; 19 | (h1?: Handler): TCompose; 20 | (h1: Handler, h2: Handler): TCompose; 21 | (h1: Handler, h2: Handler, h3: Handler): TCompose< 22 | ReqExt, 23 | ResExt 24 | >; 25 | ( 26 | h1: Handler, 27 | h2: Handler, 28 | h3: Handler, 29 | h4: Handler, 30 | ): TCompose; 31 | ( 32 | h1: Handler, 33 | h2: Handler, 34 | h3: Handler, 35 | h4: Handler, 36 | h5: Handler, 37 | ): TCompose; 38 | ( 39 | h1: Handler, 40 | h2: Handler, 41 | h3: Handler, 42 | h4: Handler, 43 | h5: Handler, 44 | h6: Handler, 45 | ): TCompose; 46 | ( 47 | h1: Handler, 48 | h2: Handler, 49 | h3: Handler, 50 | h4: Handler, 51 | h5: Handler, 52 | h6: Handler, 53 | h7: Handler, 54 | ): TCompose; 55 | } 56 | -------------------------------------------------------------------------------- /tsconfig.compile.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["examples"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "target": "es5" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./dist" /* Redirect output structure to the directory. */, 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true /* Enable all strict type-checking options. */, 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": ["node_modules/@types", "typings"], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ 65 | 66 | "skipLibCheck": true 67 | } 68 | } 69 | --------------------------------------------------------------------------------