Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: middleware for compatibility with express/connect for Hono #928

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/pink-trainers-destroy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hono/connect': major
---

Connect middleware for Hono
Empty file added packages/connect/CHANGELOG.md
Empty file.
26 changes: 26 additions & 0 deletions packages/connect/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Connect middleware for Hono

An middleware for compatibility with express/connect for Hono

## Usage

```ts
import { connect } from '@hono/connect'
import { Hono } from 'hono'
import helmet from 'helmet'

const app = new Hono()

app.use('*', connect(helmet()))
app.get('/', (c) => c.text('foo'))

export default app
```

## Author

EdamAme-x <https://github.com/EdamAme-x>

## License

MIT
55 changes: 55 additions & 0 deletions packages/connect/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"name": "@hono/connect",
"version": "1.0.0",
"description": "An middleware for compatibility with express/connect for Hono",
"type": "module",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"test": "vitest --run",
"build": "tsup ./src/index.ts --format esm,cjs --dts",
"publint": "publint",
"release": "yarn build && yarn test && yarn publint && yarn publish"
},
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"license": "MIT",
"publishConfig": {
"registry": "https://registry.npmjs.org",
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/honojs/middleware.git"
},
"homepage": "https://github.com/honojs/middleware",
"peerDependencies": {
"hono": "*"
},
"devDependencies": {
"@types/connect": "^3",
"@types/express": "^4",
"helmet": "^8.0.0",
"hono": "^4.4.12",
"tsup": "^8.1.0",
"vitest": "^1.6.0"
},
"dependencies": {
"connect": "^3.7.0",
"express": "^4.21.2",
"node-mocks-http": "^1.16.2"
}
}
58 changes: 58 additions & 0 deletions packages/connect/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Hono } from 'hono'
import { poweredBy } from 'hono/powered-by'
import { connect } from '../src'
import helmet from 'helmet'

describe('Connect middleware', () => {
const app = new Hono()

app.use('/connect/*', connect(helmet()))
app.get('/connect/foo', (c) => c.text('foo'))

app.use('/connect-with/*', async (c, next) => {
c.header("x-powered-by", "Hono Connect Middleware")
await next()
}, connect(helmet()))
app.get('/connect-with/foo', (c) => c.text('foo'))

app.use('/connect-multi/*', connect((_req, res, next) => {
res.header("x-powered-by", "Hono Connect Middleware")
res.status(201)
next()
}, helmet()))
app.get('/connect-multi/foo', (c) => c.text('foo'))

app.use('/connect-only/*', connect((_req, res) => {
res.send("Hono is cool")
res.end()
}))
app.get('/connect-only/foo', (c) => c.text('foo'))

it('Should change header', async () => {
const res = await app.request('http://localhost/connect/foo')
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(res.headers.get('content-security-policy')).not.toBeNull()
})

it('Should remove powered-by header', async () => {
const res = await app.request('http://localhost/connect-with/foo')
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(res.headers.get('x-powered-by')).toBeNull()
})

it('Should remove powered-by header and change status-code with multi connect middleware', async () => {
const res = await app.request('http://localhost/connect-multi/foo')
expect(res).not.toBeNull()
expect(res.status).toBe(201)
expect(res.headers.get('x-powered-by')).toBeNull()
})

it('Should run only connect middleware', async () => {
const res = await app.request('http://localhost/connect-only/foo')
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(await res.text()).toBe("Hono is cool")
})
})
66 changes: 66 additions & 0 deletions packages/connect/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { createMiddleware } from 'hono/factory'
import { ConnectMiddleware } from './types'
import { createResponse } from "node-mocks-http";
import Connect, { HandleFunction } from "connect";
import { transformRequestToIncomingMessage, transformResponseToServerResponse } from './utils';
import { StatusCode } from 'hono/utils/http-status';

export const connect = (...middlewares: ConnectMiddleware[]) => {
const connectApp = Connect();

for (const middleware of middlewares) {
connectApp.use(middleware as HandleFunction);
}

return createMiddleware(async (c, next) => {
const res = await new Promise<Response | undefined>((resolve) => {
const request = transformRequestToIncomingMessage(c.req.raw);
// @ts-expect-error
request.app = connectApp

const response = createResponse();
const end = response.end;

// @ts-expect-error
response.end = (...args: Parameters<typeof response.end>) => {
const call = end.call(response, ...args);

const connectResponse = transformResponseToServerResponse(response);

if (response.writableEnded) {
resolve(connectResponse)
}

return call;
};

connectApp.handle(request, response, () => {
const connectResponse = transformResponseToServerResponse(response);
const preparedHeaders = (c.newResponse(null, 204, {})).headers;
const connectHeaders = connectResponse.headers;

for (const key of [...preparedHeaders.keys()]) {
c.header(key, undefined);
}

for (const [key, value] of [...connectHeaders.entries()]) {
c.header(key, value);
}

c.status(connectResponse.status as StatusCode);

if (connectResponse.body) {
resolve(c.body(connectResponse.body));
} else {
resolve(undefined);
}
});
});

if (res) {
c.res = res;
}

await next();
});
}
8 changes: 8 additions & 0 deletions packages/connect/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { NextFunction, Request, Response } from "express";
import type { MockRequest, MockResponse } from "node-mocks-http";

export type ConnectMiddleware = (
req: MockRequest<Request>,
res: MockResponse<Response>,
next: NextFunction,
) => unknown;
45 changes: 45 additions & 0 deletions packages/connect/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { ServerResponse } from "node:http";
import {
type MockResponse,
type RequestOptions,
createRequest,
RequestMethod,
} from "node-mocks-http";

export function transformRequestToIncomingMessage(
request: Request,
options?: RequestOptions,
) {
const parsedURL = new URL(request.url, "http://localhost");

const query: Record<string, unknown> = {};
for (const [key, value] of parsedURL.searchParams.entries()) {
query[key] = value;
}

const message = createRequest({
method: request.method.toUpperCase() as RequestMethod,
url: parsedURL.pathname,
headers: Object.fromEntries(request.headers.entries()),
query,
...(request.body && {
body: request.body
}),
...options,
});

return message;
}

export function transformResponseToServerResponse(
serverResponse: MockResponse<ServerResponse>,
) {
return new Response(
serverResponse._getData() || serverResponse._getBuffer(),
{
status: serverResponse.statusCode,
statusText: serverResponse.statusMessage,
headers: serverResponse.getHeaders() as HeadersInit,
},
);
}
10 changes: 10 additions & 0 deletions packages/connect/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
},
"include": [
"src/**/*.ts"
],
}
8 changes: 8 additions & 0 deletions packages/connect/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/// <reference types="vitest" />
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
globals: true,
},
})
Loading