Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: honojs/node-server
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.3.3
Choose a base ref
...
head repository: honojs/node-server
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v1.3.4
Choose a head ref
  • 5 commits
  • 17 files changed
  • 1 contributor

Commits on Dec 13, 2023

  1. chore: add linter (#110)

    yusukebe authored Dec 13, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    1e5f648 View commit details
  2. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    021a2e6 View commit details

Commits on Dec 14, 2023

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    6fed45b View commit details

Commits on Dec 29, 2023

  1. fix: handle response that is not an instance of Response (#115)

    * fix: handle response that is not an instance of `Response`
    
    * fixed
    yusukebe authored Dec 29, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    24f9042 View commit details
  2. v1.3.4

    yusukebe committed Dec 29, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    18b4e2c View commit details
Showing with 1,289 additions and 54 deletions.
  1. +3 −0 .eslintrc.cjs
  2. +11 −0 .vscode/settings.json
  3. +7 −3 package.json
  4. +13 −3 src/listener.ts
  5. +3 −2 src/request.ts
  6. +1 −0 src/response.ts
  7. +2 −1 src/serve-static.ts
  8. +2 −1 src/server.ts
  9. +4 −4 src/types.ts
  10. +4 −3 src/utils.ts
  11. +1 −0 src/vercel.ts
  12. +1 −1 test/request.test.ts
  13. +2 −1 test/response.test.ts
  14. +2 −2 test/serve-static.test.ts
  15. +25 −14 test/server.test.ts
  16. +5 −5 test/vercel.test.ts
  17. +1,203 −14 yarn.lock
3 changes: 3 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
extends: ['@hono/eslint-config'],
}
11 changes: 11 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}
10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hono/node-server",
"version": "1.3.3",
"version": "1.3.4",
"description": "Node.js Adapter for Hono",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -43,7 +43,9 @@
"watch": "tsup --watch",
"postbuild": "publint",
"prerelease": "yarn build && yarn test",
"release": "np"
"release": "np",
"lint": "eslint --ext js,ts src test",
"lint:fix": "eslint --ext js,ts src test --fix"
},
"license": "MIT",
"repository": {
@@ -60,10 +62,12 @@
"node": ">=18.14.1"
},
"devDependencies": {
"@hono/eslint-config": "^0.0.2",
"@types/jest": "^29.5.3",
"@types/node": "^20.10.0",
"@types/supertest": "^2.0.12",
"hono": "^3.9.2",
"eslint": "^8.55.0",
"hono": "^3.11.7",
"jest": "^29.6.1",
"np": "^7.7.0",
"publint": "^0.1.16",
16 changes: 13 additions & 3 deletions src/listener.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { IncomingMessage, ServerResponse, OutgoingHttpHeaders } from 'node:http'
import type { Http2ServerRequest, Http2ServerResponse } from 'node:http2'
import type { FetchCallback } from './types'
import './globals'
import { cacheKey } from './response'
import { newRequest } from './request'
import { cacheKey } from './response'
import type { FetchCallback } from './types'
import { writeFromReadableStream, buildOutgoingHttpHeaders } from './utils'
import './globals'

const regBuffer = /^no$/i
const regContentType = /^(application\/json\b|text\/(?!event-stream\b))/i
@@ -25,6 +25,8 @@ const handleResponseError = (e: unknown, outgoing: ServerResponse | Http2ServerR
console.info('The user aborted a request.')
} else {
console.error(e)
if (!outgoing.headersSent) outgoing.writeHead(500, { 'Content-Type': 'text/plain' })
outgoing.end(`Error: ${err.message}`)
outgoing.destroy(err)
}
}
@@ -33,6 +35,7 @@ const responseViaCache = (
res: Response,
outgoing: ServerResponse | Http2ServerResponse
): undefined | Promise<undefined> => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [status, body, header] = (res as any)[cacheKey]
if (typeof body === 'string') {
header['Content-Length'] = Buffer.byteLength(body)
@@ -53,6 +56,13 @@ const responseViaResponseObject = async (
if (res instanceof Promise) {
res = await res.catch(handleFetchError)
}
if (!(res instanceof Response)) {
return handleResponseError(
// @ts-expect-error the object must have `toString()`
new Error(`The response is not an instance of Response, but ${res.toString()}`),
outgoing
)
}
if (cacheKey in res) {
try {
return responseViaCache(res as Response, outgoing)
5 changes: 3 additions & 2 deletions src/request.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// Define prototype for lightweight pseudo Request object

import { Readable } from 'node:stream'
import type { IncomingMessage } from 'node:http'
import type { Http2ServerRequest } from 'node:http2'
import { Readable } from 'node:stream'

const newRequestFromIncoming = (
method: string,
@@ -80,4 +81,4 @@ export const newRequest = (incoming: IncomingMessage | Http2ServerRequest) => {
const req = Object.create(requestPrototype)
req[incomingKey] = incoming
return req
};
}
1 change: 1 addition & 0 deletions src/response.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// Define lightweight pseudo Response class and replace global.Response with it.

import type { OutgoingHttpHeaders } from 'node:http'
3 changes: 2 additions & 1 deletion src/serve-static.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ReadStream} from 'fs'
import { createReadStream, existsSync, lstatSync } from 'fs'
import type { MiddlewareHandler } from 'hono'
import { ReadStream, createReadStream, existsSync, lstatSync } from 'fs'
import { getFilePath } from 'hono/utils/filepath'
import { getMimeType } from 'hono/utils/mime'

3 changes: 2 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { createServer as createServerHTTP } from 'node:http'
import type { AddressInfo } from 'node:net'
import type { Options, ServerType } from './types'
import { getRequestListener } from './listener'
import type { Options, ServerType } from './types'

export const createAdaptorServer = (options: Options): ServerType => {
const fetchCallback = options.fetch
const requestListener = getRequestListener(fetchCallback)
// ts will complain about createServerHTTP and createServerHTTP2 not being callable, which works just fine
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const createServer: any = options.createServer || createServerHTTP
const server: ServerType = createServer(options.serverOptions || {}, requestListener)
return server
8 changes: 4 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import type { createServer, Server, ServerOptions as HttpServerOptions } from 'node:http'
import type {
createServer as createHttpsServer,
ServerOptions as HttpsServerOptions,
} from 'node:https'
import type {
createSecureServer as createSecureHttp2Server,
createServer as createHttp2Server,
@@ -11,6 +7,10 @@ import type {
SecureServerOptions as SecureHttp2ServerOptions,
ServerOptions as Http2ServerOptions,
} from 'node:http2'
import type {
createServer as createHttpsServer,
ServerOptions as HttpsServerOptions,
} from 'node:https'

export type FetchCallback = (request: Request) => Promise<unknown> | unknown

7 changes: 4 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { Writable } from 'node:stream'
import type { OutgoingHttpHeaders } from 'node:http'
import type { Writable } from 'node:stream'

export function writeFromReadableStream(stream: ReadableStream<Uint8Array>, writable: Writable) {
if (stream.locked) {
throw new TypeError('ReadableStream is locked.')
} else if (writable.destroyed) {
stream.cancel();
stream.cancel()
return
}
const reader = stream.getReader()
@@ -16,6 +16,7 @@ export function writeFromReadableStream(stream: ReadableStream<Uint8Array>, writ
writable.off('close', cancel)
writable.off('error', cancel)
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function cancel(error?: any) {
reader.cancel(error).catch(() => {})
if (error) writable.destroy(error)
@@ -28,7 +29,7 @@ export function writeFromReadableStream(stream: ReadableStream<Uint8Array>, writ
if (done) {
writable.end()
} else if (!writable.write(value)) {
writable.once("drain", onDrain);
writable.once('drain', onDrain)
} else {
return reader.read().then(flow, cancel)
}
1 change: 1 addition & 0 deletions src/vercel.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Hono } from 'hono'
import { getRequestListener } from './listener'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const handle = (app: Hono<any, any, any>) => {
return getRequestListener(app.fetch)
}
2 changes: 1 addition & 1 deletion test/request.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IncomingMessage } from 'node:http'
import type { IncomingMessage } from 'node:http'
import { newRequest } from '../src/request'

describe('Request', () => {
3 changes: 2 additions & 1 deletion test/response.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createServer, type Server } from 'node:http'
import { AddressInfo } from 'node:net'
import type { AddressInfo } from 'node:net'
import { GlobalResponse } from '../src/response'

class NextResponse extends Response {}
@@ -56,6 +56,7 @@ describe('Response', () => {

// can only use new operator
expect(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(Response as any)()
}).toThrow()

4 changes: 2 additions & 2 deletions test/serve-static.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createAdaptorServer } from './../src/server'
import { Hono } from 'hono'
import request from 'supertest'
import { serveStatic } from './../src/serve-static'
import { createAdaptorServer } from './../src/server'

describe('Serve Static Middleware', () => {
const app = new Hono()
@@ -59,7 +59,7 @@ describe('Serve Static Middleware', () => {
it('Should return 404 for non-existent files', async () => {
const res = await request(server).get('/static/does-not-exist.html')
expect(res.status).toBe(404)
expect(res.headers['content-type']).toBe('text/plain;charset=UTF-8')
expect(res.headers['content-type']).toBe('text/plain; charset=UTF-8')
expect(res.text).toBe('404 Not Found')
})

39 changes: 25 additions & 14 deletions test/server.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { createAdaptorServer } from '../src/server'
import request from 'supertest'
import fs from 'node:fs'
import { createServer as createHttp2Server } from 'node:http2'
import { createServer as createHTTPSServer } from 'node:https'
import { Hono } from 'hono'
import { basicAuth } from 'hono/basic-auth'
import { compress } from 'hono/compress'
import { poweredBy } from 'hono/powered-by'
import { basicAuth } from 'hono/basic-auth'
import { createServer as createHTTPSServer } from 'node:https'
import { createServer as createHttp2Server } from 'node:http2'
import fs from 'node:fs'
import request from 'supertest'
import { createAdaptorServer } from '../src/server'

describe('Basic', () => {
const app = new Hono()
@@ -24,6 +24,10 @@ describe('Basic', () => {
app.delete('/posts/:id', (c) => {
return c.text(`DELETE ${c.req.param('id')}`)
})
// @ts-expect-error the response is string
app.get('/invalid', () => {
return '<h1>HTML</h1>'
})

const server = createAdaptorServer(app)

@@ -56,7 +60,15 @@ describe('Basic', () => {
it('Should return 201 response - DELETE /posts/123', async () => {
const res = await request(server).delete('/posts/123')
expect(res.status).toBe(200)
expect(res.text).toBe(`DELETE 123`)
expect(res.text).toBe('DELETE 123')
})

it('Should return 500 response - GET /invalid', async () => {
const res = await request(server).get('/invalid')
expect(res.status).toBe(500)
expect(res.headers['content-type']).toBe('text/plain')
// The error message might be changed.
expect(res.text).toBe('Error: The response is not an instance of Response, but <h1>HTML</h1>')
})
})

@@ -77,15 +89,15 @@ describe('Routing', () => {
it('Should return responses from `/book/*`', async () => {
let res = await request(server).get('/book')
expect(res.status).toBe(200)
expect(res.text).toBe(`get /book`)
expect(res.text).toBe('get /book')

res = await request(server).get('/book/123')
expect(res.status).toBe(200)
expect(res.text).toBe(`get /book/123`)
expect(res.text).toBe('get /book/123')

res = await request(server).post('/book')
expect(res.status).toBe(200)
expect(res.text).toBe(`post /book`)
expect(res.text).toBe('post /book')
})
})

@@ -106,11 +118,11 @@ describe('Routing', () => {
it('Should return responses from `/chained/*`', async () => {
let res = await request(server).get('/chained/abc')
expect(res.status).toBe(200)
expect(res.text).toBe(`GET for abc`)
expect(res.text).toBe('GET for abc')

res = await request(server).post('/chained/abc')
expect(res.status).toBe(200)
expect(res.text).toBe(`POST for abc`)
expect(res.text).toBe('POST for abc')

res = await request(server).put('/chained/abc')
expect(res.status).toBe(404)
@@ -478,7 +490,6 @@ describe('Hono compression', () => {
})
})


describe('set child response to c.res', () => {
const app = new Hono()
app.use('*', async (c, next) => {
@@ -497,4 +508,4 @@ describe('set child response to c.res', () => {
expect(res.status).toBe(200)
expect(res.headers['content-type']).toMatch(/application\/json/)
})
})
})
10 changes: 5 additions & 5 deletions test/vercel.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import request from 'supertest'
import { Hono } from 'hono'
import request from 'supertest'
import { handle } from '../src/vercel'

describe('Basic', () => {
@@ -40,7 +40,7 @@ describe('Basic', () => {
it('Should return 201 response - DELETE /api/posts/123', async () => {
const res = await request(server).delete('/api/posts/123')
expect(res.status).toBe(200)
expect(res.text).toBe(`DELETE 123`)
expect(res.text).toBe('DELETE 123')
})
})

@@ -63,15 +63,15 @@ describe('Routing', () => {
it('Should return responses from `/v2/api/*`', async () => {
let res = await request(server).get('/v2/api')
expect(res.status).toBe(200)
expect(res.text).toBe(`get /api`)
expect(res.text).toBe('get /api')

res = await request(server).get('/v2/api/123')
expect(res.status).toBe(200)
expect(res.text).toBe(`get /api/123`)
expect(res.text).toBe('get /api/123')

res = await request(server).post('/v2/api')
expect(res.status).toBe(200)
expect(res.text).toBe(`post /api`)
expect(res.text).toBe('post /api')
})
})
})
Loading