diff --git a/eslint.config.mjs b/eslint.config.mjs index c3bc6af..645cdde 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,3 +1,23 @@ import baseConfig from '@hono/eslint-config' -export default [...baseConfig] +export default [ + ...baseConfig, + { + files: ['src/**/*.ts'], + rules: { + 'no-restricted-imports': [ + 'error', + { + paths: [ + // Don't allow imports from 'ws` in src to prevent leaking ws types into the public API + { + name: 'ws', + message: + 'Import websocket types from src/websocket-types.ts instead of from `ws`, see src/websocket-types.ts and https://github.com/honojs/node-server/issues/353 for more details.', + }, + ], + }, + ], + }, + }, +] diff --git a/src/index.ts b/src/index.ts index c083a0e..d41b69a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,3 +3,4 @@ export { upgradeWebSocket } from './websocket' export { getRequestListener } from './listener' export { RequestError } from './request' export type { HttpBindings, Http2Bindings, ServerType } from './types' +export type { WebSocketData, WebSocketLike, WebSocketServerLike } from './websocket-types' diff --git a/src/listener.ts b/src/listener.ts index 8845b6c..3c4bea7 100644 --- a/src/listener.ts +++ b/src/listener.ts @@ -87,6 +87,7 @@ const drainIncoming = (incoming: IncomingMessage | Http2ServerRequest): void => const makeCloseHandler = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any req: any, incoming: IncomingMessage | Http2ServerRequest, outgoing: ServerResponse | Http2ServerResponse, diff --git a/src/types.ts b/src/types.ts index 82b0d8a..2b03d56 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,3 @@ -import type { WebSocketServer } from 'ws' import type { createServer, IncomingMessage, @@ -20,6 +19,7 @@ import type { createServer as createHttpsServer, ServerOptions as HttpsServerOptions, } from 'node:https' +import type { WebSocketServerLike } from './websocket-types' export type HttpBindings = { incoming: IncomingMessage @@ -75,7 +75,7 @@ export type Options = { port?: number hostname?: string websocket?: { - server: WebSocketServer + server: WebSocketServerLike } } & ServerOptions diff --git a/src/websocket-types.ts b/src/websocket-types.ts new file mode 100644 index 0000000..048a81e --- /dev/null +++ b/src/websocket-types.ts @@ -0,0 +1,46 @@ +/** + * This is the minimal public interface for WebSocket that is compatible with `ws` (`@types/ws`) + * https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/ws/index.d.ts + * + * If you need more methods, copy the extra types over from the types file linked above. + * Don't import types from `ws` directly, as it will cause issues with users who have `skipLibCheck` enabled. + * See https://github.com/honojs/node-server/issues/353 for more details. + */ +import type { IncomingMessage } from 'node:http' +import type { Duplex } from 'node:stream' + +type WSReadyState = 0 | 1 | 2 | 3 + +export type WebSocketData = string | ArrayBuffer | Uint8Array | readonly Uint8Array[] + +export type WebSocketSendOptions = { + compress?: boolean +} + +export interface WebSocketLike { + protocol: string + readyState: WSReadyState + close(code?: number, reason?: string): void + send(data: string | ArrayBuffer | ArrayBufferView, options?: WebSocketSendOptions): void + on(event: 'message', listener: (data: WebSocketData, isBinary: boolean) => void): this + on(event: 'close', listener: (code: number, reason: Uint8Array) => void): this + on(event: 'error', listener: (error: unknown) => void): this + off(event: 'message', listener: (data: WebSocketData, isBinary: boolean) => void): this +} + +export interface WebSocketServerLike { + options: { + noServer?: boolean + } + on(event: 'connection', listener: (ws: WebSocketLike, request: IncomingMessage) => void): this + on(event: 'headers', listener: (headers: string[]) => void): this + off(event: 'headers', listener: (headers: string[]) => void): this + emit(event: 'connection', ws: WebSocketLike, request: IncomingMessage): boolean + handleUpgrade( + request: IncomingMessage, + socket: Duplex, + head: Buffer, + callback: (ws: WebSocketLike) => void + ): void + close(): void +} diff --git a/src/websocket.ts b/src/websocket.ts index 69c778c..eff1103 100644 --- a/src/websocket.ts +++ b/src/websocket.ts @@ -1,10 +1,10 @@ import type { UpgradeWebSocket, WSContext } from 'hono/ws' import { defineWebSocketHelper } from 'hono/ws' -import type { RawData, WebSocket, WebSocketServer } from 'ws' import type { IncomingMessage } from 'node:http' import { STATUS_CODES } from 'node:http' import type { Duplex } from 'node:stream' import type { FetchCallback, ServerType } from './types' +import type { WebSocketData, WebSocketLike, WebSocketServerLike } from './websocket-types' interface CloseEventInit extends EventInit { code?: number @@ -40,7 +40,10 @@ export const CloseEvent: typeof globalThis.CloseEvent = const generateConnectionSymbol = () => Symbol('connection') -type WaitForWebSocket = (request: IncomingMessage, connectionSymbol: symbol) => Promise +type WaitForWebSocket = ( + request: IncomingMessage, + connectionSymbol: symbol +) => Promise const CONNECTION_SYMBOL_KEY: unique symbol = Symbol('CONNECTION_SYMBOL_KEY') const WAIT_FOR_WEBSOCKET_SYMBOL: unique symbol = Symbol('WAIT_FOR_WEBSOCKET_SYMBOL') @@ -48,7 +51,7 @@ const WAIT_FOR_WEBSOCKET_SYMBOL: unique symbol = Symbol('WAIT_FOR_WEBSOCKET_SYMB export type UpgradeBindings = { incoming: IncomingMessage outgoing: undefined - wss: WebSocketServer + wss: WebSocketServerLike [CONNECTION_SYMBOL_KEY]?: symbol [WAIT_FOR_WEBSOCKET_SYMBOL]?: WaitForWebSocket } @@ -119,13 +122,13 @@ const createUpgradeRequest = (request: IncomingMessage): Request => { export const setupWebSocket = (options: { server: ServerType fetchCallback: FetchCallback - wss: WebSocketServer + wss: WebSocketServerLike }): void => { const { server, fetchCallback, wss } = options const waiterMap = new Map< IncomingMessage, - { resolve: (ws: WebSocket) => void; connectionSymbol: symbol } + { resolve: (ws: WebSocketLike) => void; connectionSymbol: symbol } >() wss.on('connection', (ws, request) => { @@ -137,7 +140,7 @@ export const setupWebSocket = (options: { }) const waitForWebSocket: WaitForWebSocket = (request, connectionSymbol) => { - return new Promise((resolve) => { + return new Promise((resolve) => { waiterMap.set(request, { resolve, connectionSymbol }) }) } @@ -203,7 +206,7 @@ export const setupWebSocket = (options: { }) } -export const upgradeWebSocket: UpgradeWebSocket = +export const upgradeWebSocket: UpgradeWebSocket = defineWebSocketHelper(async (c, events, options) => { if (c.req.header('upgrade')?.toLowerCase() !== 'websocket') { return @@ -221,13 +224,13 @@ export const upgradeWebSocket: UpgradeWebSocket { const ws = await waitForWebSocket(env.incoming, connectionSymbol) - const messagesReceivedInStarting: [data: RawData, isBinary: boolean][] = [] - const bufferMessage = (data: RawData, isBinary: boolean) => { + const messagesReceivedInStarting: [data: WebSocketData, isBinary: boolean][] = [] + const bufferMessage = (data: WebSocketData, isBinary: boolean) => { messagesReceivedInStarting.push([data, isBinary]) } ws.on('message', bufferMessage) - const ctx: WSContext = { + const ctx: WSContext = { binaryType: 'arraybuffer', close(code, reason) { ws.close(code, reason) @@ -251,7 +254,7 @@ export const upgradeWebSocket: UpgradeWebSocket { + const handleMessage = (data: WebSocketData, isBinary: boolean) => { const datas = Array.isArray(data) ? data : [data] for (const data of datas) { try { @@ -261,7 +264,9 @@ export const upgradeWebSocket: UpgradeWebSocket