├── .babelrc ├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── appveyor.yml ├── package.json ├── src └── index.js └── test ├── express.test.js ├── fixtures └── a.js ├── koa-alone.test.js ├── koa.test.js └── run.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "6" 8 | } 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*.{js,css,md}] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # babel dist 2 | /lib 3 | 4 | # coverage 5 | .nyc_output 6 | *.lcov 7 | 8 | # node 9 | *.tgz 10 | 11 | package-lock.json 12 | 13 | # Numerous always-ignore extensions 14 | *.bak 15 | *.patch 16 | *.diff 17 | *.err 18 | *.orig 19 | *.log 20 | *.rej 21 | *.swo 22 | *.swp 23 | *.zip 24 | *.vi 25 | *~ 26 | *.sass-cache 27 | *.lock 28 | *.rdb 29 | *.db 30 | nohup.out 31 | 32 | # OS or Editor folders 33 | .DS_Store 34 | ._* 35 | .cache 36 | .project 37 | .settings 38 | .tmproj 39 | *.esproj 40 | *.sublime-project 41 | *.sublime-workspace 42 | nbproject 43 | thumbs.db 44 | no-track.js 45 | 46 | # Folders to ignore 47 | .hg 48 | .svn 49 | .CVS 50 | .idea 51 | node_modules 52 | old/ 53 | *-old/ 54 | *-notrack/ 55 | no-track/ 56 | build/ 57 | combo/ 58 | reference/ 59 | jscoverage_lib/ 60 | temp/ 61 | tmp/ 62 | 63 | # codecov.io 64 | coverage/ 65 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /src 2 | .travis.yml 3 | .appveyor.yml 4 | .babelrc 5 | .editorconfig 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "8" 5 | - "10" 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 kaelzhang , contributors 2 | http://kael.me/ 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/kaelzhang/express-to-koa.svg?branch=master)](https://travis-ci.org/kaelzhang/express-to-koa) 2 | [![Coverage](https://codecov.io/gh/kaelzhang/express-to-koa/branch/master/graph/badge.svg)](https://codecov.io/gh/kaelzhang/express-to-koa) 3 | 4 | 7 | 10 | 13 | 14 | # express-to-koa 15 | 16 | Use express middlewares in Koa2 (not support koa1 for now), the one that **REALLY WORKS**. 17 | 18 | - Handle koa2 http status code, which fixes the common issue that we always get 404 with [koa-connect](https://www.npmjs.com/package/koa-connect) 19 | - Handle express middlewares that contains `.pipe(res)`, such as `express.static` which based on [`send`](https://www.npmjs.com/package/send) 20 | 21 | ## Usage 22 | 23 | ```js 24 | const e2k = require('express-to-koa') 25 | 26 | // Some express middleware 27 | const devMiddleware = require('webpack-dev-middleware')(compiler, { 28 | publicPath, 29 | quiet: true 30 | }) 31 | 32 | const app = new Koa() 33 | app.use(e2k(devMiddleware)) 34 | ``` 35 | 36 | ## What Kind of Express Middlewares are Supported? 37 | 38 | NEARLY **ALL** express middlewares built with best practices. 39 | 40 | **TL;NR** 41 | 42 | `express-to-koa` does not support all arbitrary express middlewares, but only for those who only uses **Express-Independent** APIs like `res.write` and `res.end`, i.e. the APIs that node [http.ServerResponse](https://nodejs.org/dist/latest-v7.x/docs/api/http.html#http_class_http_serverresponse) provides. 43 | 44 | However, if a middleware uses APIs like `res.send` or something, `express-to-koa` will do far too much work to convert those logic to koa2, which is not easier than creating both express and koa2 from 0 to 1. 45 | 46 | So, it is a good practice to write framework-agnostic middlewares or libraries. 47 | 48 | ## Supported Middlewares 49 | 50 | - [webpack-dev-middleware](https://www.npmjs.com/package/webpack-dev-middleware) 51 | - [webpack-hot-middleware](https://www.npmjs.com/package/webpack-hot-middleware) 52 | - [next.getRequestHandler()](https://github.com/zeit/next.js/#custom-server-and-routing) 53 | - Other middlewares which are waiting for you to add to the README. Any contributions are welcome. 54 | 55 | ## License 56 | 57 | MIT 58 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Test against this version of Node.js 2 | environment: 3 | matrix: 4 | # - nodejs_version: "0.6" # not supported by appveyor 5 | # - nodejs_version: "0.8" # not supported by appveyor 6 | # - nodejs_version: "0.10" 7 | # - nodejs_version: "0.11" 8 | # - nodejs_version: "4" 9 | # - nodejs_version: "5" 10 | - nodejs_version: "6" 11 | - nodejs_version: "7" 12 | 13 | # Install scripts. (runs after repo cloning) 14 | install: 15 | # Get the latest stable version of Node.js or io.js 16 | - ps: Install-Product node $env:nodejs_version 17 | # install modules 18 | - npm install 19 | 20 | # Post-install test scripts. 21 | test_script: 22 | # Output useful info for debugging. 23 | - node --version 24 | - npm --version 25 | - npm test 26 | 27 | # Don't actually build. 28 | build: off 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-to-koa", 3 | "version": "2.0.0", 4 | "description": "Use express middlewares in Koa2, the one that really works.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "babel src --out-dir lib", 8 | "test": "npm run build && NODE_DEBUG=express-to-koa nyc ava --verbose --timeout=10s", 9 | "posttest": "nyc report --reporter=text-lcov > coverage.lcov && codecov", 10 | "prepublishOnly": "npm run build" 11 | }, 12 | "files": [ 13 | "lib/" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/kaelzhang/express-to-koa.git" 18 | }, 19 | "keywords": [ 20 | "express-to-koa", 21 | "express", 22 | "koa", 23 | "koa2", 24 | "middleware", 25 | "converter", 26 | "connect", 27 | "framework" 28 | ], 29 | "engines": { 30 | "node": ">=4" 31 | }, 32 | "author": "kaelzhang", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/kaelzhang/express-to-koa/issues" 36 | }, 37 | "ava": { 38 | "require": [ 39 | "@babel/register" 40 | ], 41 | "files": [ 42 | "test/*.test.js" 43 | ] 44 | }, 45 | "devDependencies": { 46 | "@babel/cli": "^7.2.3", 47 | "@babel/preset-env": "^7.4.2", 48 | "@babel/register": "^7.4.0", 49 | "ava": "^1.4.1", 50 | "codecov": "^3.2.0", 51 | "express": "^4.16.4", 52 | "koa": "^2.7.0", 53 | "koa-router": "^7.4.0", 54 | "nyc": "^13.3.0", 55 | "send": "^0.16.2", 56 | "supertest": "^4.0.2" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const symbol = key => Symbol(`express-to-koa:${key}`) 2 | 3 | const STATUS_SET_EXPLICITLY = symbol('is-status-set-explicitly') 4 | const CONTEXT = symbol('context') 5 | const STATUS_CODE = symbol('status') 6 | const ARGUMENTED = symbol('argumented') 7 | const WRITE_HEAD = symbol('write-head') 8 | const UNDEFINED = undefined 9 | 10 | const markExplicitStatus = self => { 11 | const context = self[CONTEXT] 12 | if (context) { 13 | context._explicitStatus = true 14 | } 15 | } 16 | 17 | const BASE_RESPONSE_PROPERTIES = { 18 | [ARGUMENTED]: { 19 | value: true 20 | }, 21 | 22 | statusCode: { 23 | get () { 24 | const context = this[CONTEXT] 25 | return !context || context._explicitStatus 26 | ? this[STATUS_CODE] 27 | // The default statusCode of http server is `200` 28 | : 200 29 | }, 30 | 31 | set (code) { 32 | markExplicitStatus(this) 33 | this[STATUS_CODE] = code 34 | } 35 | }, 36 | 37 | writeHead: { 38 | // We allow other middlewares to modify the response object 39 | writable: true, 40 | value (...args) { 41 | markExplicitStatus(this) 42 | return this[WRITE_HEAD].apply(this, args) 43 | } 44 | } 45 | } 46 | 47 | // 1. 48 | // At the beginning 49 | // koa set ctx.req.statusCode = 404 first 50 | // but ctx._explicitStatus === undefined 51 | // 2. 52 | // run connect/express middlewares 53 | // - the initial statusCode should be 200 for both http.createServer and express 54 | // - if the 55 | // 3. 56 | // after all 57 | // if body == null, res.end(statusToMessage(ctx.req.statusCode)) 58 | 59 | // We HAVE to manipulate the vanilla OutGoing response instead of prototype, 60 | // otherwise, if the response is piped, the request will stuck and hang 61 | 62 | // Further research is required to figure out why. 63 | const makeResponse = ctx => { 64 | const {res} = ctx 65 | 66 | res[CONTEXT] = ctx 67 | 68 | // Never redefine properties 69 | if (!res[ARGUMENTED]) { 70 | const { 71 | statusCode, 72 | writeHead 73 | } = res 74 | 75 | Object.defineProperties(res, BASE_RESPONSE_PROPERTIES) 76 | 77 | res[STATUS_CODE] = statusCode 78 | res[WRITE_HEAD] = writeHead 79 | } 80 | 81 | return res 82 | } 83 | 84 | const cleanResponseContext = ctx => { 85 | delete ctx.res[CONTEXT] 86 | } 87 | 88 | const wrap = (ctx, middleware, next) => new Promise((resolve, reject) => { 89 | middleware(ctx.req, makeResponse(ctx), err => { 90 | // We need to clean [CONTEXT] to prevent memleak 91 | cleanResponseContext(ctx) 92 | 93 | if (err) { 94 | reject(err) 95 | return 96 | } 97 | 98 | // #2 99 | if (!next) { 100 | return resolve() 101 | } 102 | 103 | resolve(next()) 104 | }) 105 | }) 106 | 107 | module.exports = middleware => (ctx, next) => wrap(ctx, middleware, next) 108 | -------------------------------------------------------------------------------- /test/express.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const app = require('express')() 3 | 4 | const run = require('./run') 5 | 6 | run(test, 'express', app, app, app) 7 | -------------------------------------------------------------------------------- /test/fixtures/a.js: -------------------------------------------------------------------------------- 1 | // a.js 2 | -------------------------------------------------------------------------------- /test/koa-alone.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const Koa = require('koa') 3 | const supertest = require('supertest') 4 | 5 | const e2k = require('../src') 6 | 7 | test('#2', async t => { 8 | const app = new Koa 9 | const m = ctx => e2k((req, res, next) => { 10 | next() 11 | })(ctx) 12 | 13 | app.use(m) 14 | 15 | const request = supertest(app.callback()) 16 | 17 | const { 18 | statusCode, 19 | text 20 | } = await request.get('/not-found') 21 | 22 | t.is(statusCode, 404) 23 | }) 24 | -------------------------------------------------------------------------------- /test/koa.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const Koa = require('koa') 3 | const Router = require('koa-router') 4 | 5 | const e2k = require('../src') 6 | const run = require('./run') 7 | 8 | const app = new Koa 9 | const router = new Router() 10 | 11 | app.use(router.routes()) 12 | 13 | run(test, 'koa', app, router, app.callback(), e2k) 14 | -------------------------------------------------------------------------------- /test/run.js: -------------------------------------------------------------------------------- 1 | const log = require('util').debuglog('express-to-koa') 2 | const path = require('path') 3 | const supertest = require('supertest') 4 | const send = require('send') 5 | 6 | const fixture = (...args) => path.join(__dirname, 'fixtures', ...args) 7 | 8 | const wrapKoa = c => { 9 | c.koa = true 10 | return c 11 | } 12 | 13 | const ROUTES = [ 14 | ['get', '/get', (req, res, next) => { 15 | res.end('get') 16 | }], 17 | 18 | ['get', '/get2', (req, res) => { 19 | res.end('get2') 20 | }], 21 | 22 | ['post', '/post', (req, res, next) => { 23 | next() 24 | }], 25 | 26 | ['post', '/post', (req, res, next) => { 27 | res.end('post') 28 | }], 29 | 30 | ['post', '/post2', (req, res, next) => { 31 | res.statusCode = 201 32 | res.end('post2') 33 | }], 34 | 35 | ['post', '/post3', (req, res, next) => { 36 | // #1 37 | const code = res.statusCode 38 | res.statusCode = code || 200 39 | res.end('post3') 40 | }], 41 | 42 | ['post', '/post4', (req, res) => { 43 | res.writeHead(201) 44 | res.end('post4') 45 | }], 46 | 47 | ['post', '/post5', (req, res, next) => { 48 | next(new Error('post5')) 49 | }], 50 | 51 | ['post', '/post6', (req, res) => { 52 | res.end(String(res.statusCode)) 53 | }], 54 | 55 | ['post', '/post7', (req, res, next) => { 56 | res.statusCode = 201 57 | next() 58 | }, (req, res) => { 59 | res.end(String(res.statusCode)) 60 | }], 61 | 62 | ['get', '/static', (req, res) => { 63 | require('fs').createReadStream(fixture('a.js')).pipe(res) 64 | }], 65 | 66 | ['get', '/send', (req, res) => { 67 | send(req, fixture('a.js')).pipe(res) 68 | }], 69 | 70 | ['use',, (req, res) => { 71 | res.end('middleware') 72 | }] 73 | ] 74 | 75 | const CASES = [ 76 | ['get', '/get', 'get'], 77 | ['get', '/get2', 'get2'], 78 | ['post', '/post', 'post'], 79 | ['post', '/post2', 'post2', 201], 80 | ['post', '/post3', 'post3', 200], 81 | ['post', '/post4', 'post4', 201], 82 | ['post', '/post5', 'Internal Server Error', 500], 83 | ['post', '/post6', '200', 200], 84 | ['post', '/post7', '201', 201], 85 | ['get', '/static', '// a.js\n', 200], 86 | ['get', '/send', '// a.js\n', 200], 87 | ['put', '/not-defined', 'middleware', 200] 88 | ] 89 | 90 | module.exports = (test, prefix, app, router, callback, wrapper) => { 91 | let request 92 | 93 | const shouldSkip = c => c.koa && prefix === 'express' 94 | 95 | test.before(t => { 96 | ROUTES.forEach(c => { 97 | if (shouldSkip(c)) { 98 | return 99 | } 100 | 101 | const [method, pathname, ...middlewares] = c 102 | 103 | middlewares.forEach(middleware => { 104 | const wrapped = wrapper 105 | ? wrapper(middleware) 106 | : middleware 107 | 108 | if (method === 'use') { 109 | app.use(wrapped) 110 | return 111 | } 112 | 113 | router[method](pathname, wrapped) 114 | }) 115 | }) 116 | 117 | request = supertest(callback) 118 | }) 119 | 120 | CASES.forEach(c => { 121 | if (shouldSkip(c)) { 122 | return 123 | } 124 | 125 | test(`${prefix}: ${c.join(', ')}`, async t => { 126 | const [ 127 | method, 128 | pathname, 129 | body, 130 | code = 200 131 | ] = c 132 | 133 | const { 134 | status, 135 | text 136 | } = await request[method](pathname) 137 | 138 | if (code !== 500) { 139 | t.is(text, body) 140 | } 141 | 142 | t.is(status, code, 'status code not match') 143 | }) 144 | }) 145 | } 146 | --------------------------------------------------------------------------------