chore: update dependencies and refactor application structure

- Bump AWS SDK packages to version 3.966.0
- Update @hono-di packages to version 0.0.15
- Update @types/node to version 25.0.5
- Refactor main application entry point to integrate NestJS with Hono
- Remove unused gRPC and user module files
- Add HonoAdapter for NestJS integration
- Implement basic AppController and AppService
- Configure Vite server to run on port 3000
This commit is contained in:
2026-01-10 14:14:29 +07:00
parent 3dcbbeacef
commit e1e1d9cb7b
14 changed files with 701 additions and 109 deletions

View File

@@ -4,30 +4,57 @@ import { cors } from "hono/cors";
import isMobile from 'is-mobile';
import { rpcServer } from './api/rpc';
import { ssrRender } from './worker/ssrRender';
import { HttpAdapterHost, NestFactory } from '@nestjs/core';
import { AppModule } from './server/app.module';
import { HonoAdapter } from './server/HonoAdapter/adapter';
import { NestHonoApplication } from './server/HonoAdapter/interfaces';
// import { serveStatic } from "hono/bun";
// @ts-ignore
const app = new Hono()
const isDev = import.meta.env.DEV;
// const app = new Hono()
// const isDev = import.meta.env.DEV;
// app.use(renderer)
app.use('*', contextStorage());
app.use(cors(), async (c, next) => {
c.set("fetch", app.request.bind(app));
const ua = c.req.header("User-Agent")
if (!ua) {
return c.json({ error: "User-Agent header is missing" }, 400);
};
c.set("isMobile", isMobile({ ua }));
await next();
}, rpcServer);
if (!isDev) {
if ((process as any).versions?.bun) {
const { serveStatic } = await import("hono/bun");
app.use(serveStatic({ root: "./dist/client" }))
}
}
app.get("/.well-known/*", (c) => {
return c.json({ ok: true });
});
app.get("*", ssrRender);
// // app.use(renderer)
// app.use('*', contextStorage());
// app.use(cors(), async (c, next) => {
// c.set("fetch", app.request.bind(app));
// const ua = c.req.header("User-Agent")
// if (!ua) {
// return c.json({ error: "User-Agent header is missing" }, 400);
// };
// c.set("isMobile", isMobile({ ua }));
// await next();
// }, rpcServer);
// if (!isDev) {
// if ((process as any).versions?.bun) {
// const { serveStatic } = await import("hono/bun");
// app.use(serveStatic({ root: "./dist/client" }))
// }
// }
// app.get("/.well-known/*", (c) => {
// return c.json({ ok: true });
// });
const app = await NestFactory.create<NestHonoApplication>(
AppModule,
new HonoAdapter(),
{
rawBody: true,
},
);
// app.get('*', (c) => {
// console.log("Request URL:", c);
// // return next();
// });
// app.use(ssrRender);
// app.use(function (req, next) {
// console.log("Request:", arguments[1]);
// return (c, next) => {
// next();
// }
// });
const honoInstance = app.getHonoInstance();
honoInstance.use(contextStorage());
honoInstance.get("*", ssrRender);
// app.use('*', contextStorage());
await app.init();
const httpAdapter = app.get(HttpAdapterHost);
export default app

View File

@@ -0,0 +1,445 @@
// @ts-ignore
// @ts-nocheck
import { HttpBindings, createAdaptorServer } from '@hono/node-server';
import {
ServeStaticOptions,
serveStatic,
} from '@hono/node-server/serve-static';
import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response';
import { RequestMethod } from '@nestjs/common';
import { HttpStatus, Logger } from '@nestjs/common';
import {
ErrorHandler,
NestApplicationOptions,
RequestHandler,
} from '@nestjs/common/interfaces';
import { isObject } from '@nestjs/common/utils/shared.utils';
import { AbstractHttpAdapter } from '@nestjs/core/adapters/http-adapter';
import { Context, Next, Hono } from 'hono';
import { bodyLimit } from 'hono/body-limit';
import { cors } from 'hono/cors';
// import { Data } from 'hono/dist/types/context';
import { RedirectStatusCode, StatusCode } from 'hono/utils/http-status';
import * as http from 'http';
import http2 from 'http2';
import * as https from 'https';
import { HonoRequest, TypeBodyParser } from './interfaces';
import { Data } from 'node_modules/hono/dist/types/context';
type HonoHandler = RequestHandler<HonoRequest, Context>;
type ServerType = http.Server | http2.Http2Server | http2.Http2SecureServer;
type Ctx = Context | (() => Promise<Context>);
type Method =
| 'all'
| 'get'
| 'post'
| 'put'
| 'delete'
| 'use'
| 'patch'
| 'options';
/**
* Adapter for using Hono with NestJS.
*/
export class HonoAdapter extends AbstractHttpAdapter<
ServerType,
HonoRequest,
Context
> {
private _isParserRegistered: boolean;
protected readonly instance: Hono<{ Bindings: HttpBindings }>;
constructor() {
const honoInstance = new Hono<{ Bindings: HttpBindings }>();
super(honoInstance);
this.instance = honoInstance;
}
get isParserRegistered(): boolean {
return !!this._isParserRegistered;
}
private getRouteAndHandler(
pathOrHandler: string | HonoHandler,
handler?: HonoHandler,
): [string, HonoHandler] {
const path = typeof pathOrHandler === 'function' ? '' : pathOrHandler;
handler = typeof pathOrHandler === 'function' ? pathOrHandler : handler;
return [path, handler!];
}
private createRouteHandler(routeHandler: HonoHandler) {
return async (ctx: Context, next: Next) => {
ctx.req['params'] = ctx.req.param();
await routeHandler(ctx.req, ctx, next);
return this.getBody(ctx);
};
}
private async normalizeContext(ctx: Ctx): Promise<Context> {
if (typeof ctx === 'function') {
return await ctx();
}
return ctx;
}
private async getBody(ctx: Ctx, body?: Data) {
ctx = await this.normalizeContext(ctx);
if (body === undefined && ctx.res && ctx.res.body !== null) {
return ctx.res;
}
let responseContentType = await this.getHeader(ctx, 'Content-Type');
if (!responseContentType || responseContentType.startsWith('text/plain')) {
if (
body instanceof Buffer ||
body instanceof Uint8Array ||
body instanceof ArrayBuffer ||
body instanceof ReadableStream
) {
responseContentType = 'application/octet-stream';
} else if (isObject(body)) {
responseContentType = 'application/json';
}
await this.setHeader(ctx, 'Content-Type', responseContentType);
}
if (responseContentType === 'application/json' && isObject(body)) {
return ctx.json(body);
} else if (body === undefined) {
return ctx.newResponse(null);
}
return ctx.body(body);
}
private registerRoute(
method: Method,
pathOrHandler: string | HonoHandler,
handler?: HonoHandler,
) {
const [routePath, routeHandler] = this.getRouteAndHandler(
pathOrHandler,
handler,
);
const routeHandler2 = this.createRouteHandler(routeHandler);
switch (method) {
case 'all':
this.instance.all(routePath, routeHandler2);
break;
case 'get':
this.instance.get(routePath, routeHandler2);
break;
case 'post':
this.instance.post(routePath, routeHandler2);
break;
case 'put':
this.instance.put(routePath, routeHandler2);
break;
case 'delete':
this.instance.delete(routePath, routeHandler2);
break;
case 'use':
this.instance.use(routePath, routeHandler2);
break;
case 'patch':
this.instance.patch(routePath, routeHandler2);
break;
case 'options':
this.instance.options(routePath, routeHandler2);
break;
}
}
public all(pathOrHandler: string | HonoHandler, handler?: HonoHandler) {
this.registerRoute('all', pathOrHandler, handler);
}
public get(pathOrHandler: string | HonoHandler, handler?: HonoHandler) {
this.registerRoute('get', pathOrHandler, handler);
}
public post(pathOrHandler: string | HonoHandler, handler?: HonoHandler) {
this.registerRoute('post', pathOrHandler, handler);
}
public put(pathOrHandler: string | HonoHandler, handler?: HonoHandler) {
this.registerRoute('put', pathOrHandler, handler);
}
public delete(pathOrHandler: string | HonoHandler, handler?: HonoHandler) {
this.registerRoute('delete', pathOrHandler, handler);
}
public use(pathOrHandler: string | HonoHandler, handler?: HonoHandler) {
this.registerRoute('use', pathOrHandler, handler);
}
public patch(pathOrHandler: string | HonoHandler, handler?: HonoHandler) {
this.registerRoute('patch', pathOrHandler, handler);
}
public options(pathOrHandler: string | HonoHandler, handler?: HonoHandler) {
this.registerRoute('options', pathOrHandler, handler);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public async reply(ctx: Ctx, body: any, statusCode?: StatusCode) {
ctx = await this.normalizeContext(ctx);
if (statusCode) {
ctx.status(statusCode);
}
ctx.res = await this.getBody(ctx, body);
}
public async status(ctx: Ctx, statusCode: StatusCode) {
ctx = await this.normalizeContext(ctx);
return ctx.status(statusCode);
}
public async end() {
return RESPONSE_ALREADY_SENT;
}
public render() {
throw new Error('Method not implemented.');
}
public async redirect(ctx: Ctx, statusCode: RedirectStatusCode, url: string) {
ctx = await this.normalizeContext(ctx);
ctx.res = ctx.redirect(url, statusCode);
}
public setErrorHandler(handler: ErrorHandler) {
this.instance.onError(async (err: Error, ctx: Context) => {
await handler(err, ctx.req, ctx);
return this.getBody(ctx);
});
}
public setNotFoundHandler(handler: RequestHandler) {
this.instance.notFound(async (ctx: Context) => {
await handler(ctx.req, ctx);
await this.status(ctx, HttpStatus.NOT_FOUND);
return this.getBody(ctx, 'Not Found');
});
}
public useStaticAssets(path: string, options: ServeStaticOptions) {
Logger.log('Registering static assets middleware');
this.instance.use(path, serveStatic(options));
}
public setViewEngine() {
throw new Error('Method not implemented.');
}
public async isHeadersSent(ctx: Ctx): Promise<boolean> {
ctx = await this.normalizeContext(ctx);
return ctx.finalized;
}
public async getHeader(ctx: Ctx, name: string) {
ctx = await this.normalizeContext(ctx);
return ctx.res.headers.get(name);
}
public async setHeader(ctx: Ctx, name: string, value: string) {
ctx = await this.normalizeContext(ctx);
ctx.res.headers.set(name, value);
}
public async appendHeader(ctx: Ctx, name: string, value: string) {
ctx = await this.normalizeContext(ctx);
ctx.res.headers.append(name, value);
}
public async getRequestHostname(ctx: Ctx): Promise<string> {
ctx = await this.normalizeContext(ctx);
return ctx.req.header().host;
}
public getRequestMethod(request: HonoRequest): string {
return request.method;
}
public getRequestUrl(request: HonoRequest): string {
return request.url;
}
public enableCors(options: {
origin:
| string
| string[]
| ((origin: string, c: Context) => string | undefined | null);
allowMethods?: string[];
allowHeaders?: string[];
maxAge?: number;
credentials?: boolean;
exposeHeaders?: string[];
}) {
this.instance.use(cors(options));
}
public useBodyParser(
type: TypeBodyParser,
rawBody?: boolean,
bodyLimit?: number,
) {
Logger.log(
`Registering body parser middleware for type: ${type} | bodyLimit: ${bodyLimit}`,
);
this.instance.use(this.bodyLimit(bodyLimit));
// To avoid the Nest application init to override our custom
// body parser, we mark the parsers as registered.
this._isParserRegistered = true;
}
public close(): Promise<void> {
return new Promise((resolve) => this.httpServer.close(() => resolve()));
}
private extractClientIp(ctx: Context): string {
return (
ctx.req.header('cf-connecting-ip') ??
ctx.req.header('x-forwarded-for') ??
ctx.req.header('x-real-ip') ??
ctx.req.header('forwarded') ??
ctx.req.header('true-client-ip') ??
ctx.req.header('x-client-ip') ??
ctx.req.header('x-cluster-client-ip') ??
ctx.req.header('x-forwarded') ??
ctx.req.header('forwarded-for') ??
ctx.req.header('via')
);
}
private async parseRequestBody(
ctx: Context,
contentType: string,
rawBody: boolean,
): Promise<void> {
if (
contentType?.startsWith('multipart/form-data') ||
contentType?.startsWith('application/x-www-form-urlencoded')
) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(ctx.req as any).body = await ctx.req
.parseBody({
all: true,
})
.catch(() => {});
} else if (
contentType?.startsWith('application/json') ||
contentType?.startsWith('text/plain')
) {
if (rawBody) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(ctx.req as any).rawBody = Buffer.from(await ctx.req.text());
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(ctx.req as any).body = await ctx.req.json().catch(() => {});
}
}
public initHttpServer(options: NestApplicationOptions) {
this.instance.use(async (ctx, next) => {
ctx.req['ip'] = this.extractClientIp(ctx);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ctx.req['query'] = ctx.req.query() as any;
ctx.req['headers'] = Object.fromEntries(ctx.req.raw.headers);
const contentType = ctx.req.header('content-type');
await this.parseRequestBody(ctx, contentType, options.rawBody);
await next();
});
const isHttpsEnabled = options?.httpsOptions;
const createServer = isHttpsEnabled
? https.createServer
: http.createServer;
this.httpServer = createAdaptorServer({
fetch: this.instance.fetch,
createServer,
overrideGlobalObjects: false,
});
}
public getType(): string {
return 'hono';
}
public registerParserMiddleware(_prefix?: string, rawBody?: boolean) {
if (this._isParserRegistered) {
return;
}
Logger.log('Registering parser middleware');
this.useBodyParser('application/x-www-form-urlencoded', rawBody);
this.useBodyParser('application/json', rawBody);
this.useBodyParser('text/plain', rawBody);
this._isParserRegistered = true;
}
public async createMiddlewareFactory(requestMethod: RequestMethod) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
return (path: string, callback: Function) => {
const routeMethodsMap = {
[RequestMethod.ALL]: this.instance.all,
[RequestMethod.DELETE]: this.instance.delete,
[RequestMethod.GET]: this.instance.get,
[RequestMethod.OPTIONS]: this.instance.options,
[RequestMethod.PATCH]: this.instance.patch,
[RequestMethod.POST]: this.instance.post,
[RequestMethod.PUT]: this.instance.put,
};
const routeMethod = (
routeMethodsMap[requestMethod] || this.instance.get
).bind(this.instance);
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
routeMethod(path, async (ctx: Context, next: Function) => {
await callback(ctx.req, ctx, next);
});
};
}
public applyVersionFilter(): () => () => unknown {
throw new Error('Versioning not yet supported in Hono');
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public listen(port: string | number, ...args: any[]): ServerType {
return this.httpServer.listen(port, ...args);
}
public fetch(input: RequestInfo, init?: RequestInit): Promise<Response> {
return this.instance.fetch(input, init);
}
public getHonoInstance(): Hono<{ Bindings: HttpBindings }> {
return this.instance;
}
public bodyLimit(maxSize: number) {
return bodyLimit({
maxSize,
onError: (ctx) => {
const errorMessage = `Body size exceeded: ${maxSize} bytes. Size: ${ctx.req.header('Content-Length')} bytes. Method: ${ctx.req.method}. Path: ${ctx.req.path}`;
throw new Error(errorMessage);
},
});
}
}

View File

@@ -0,0 +1,80 @@
import { HonoRequest as Request } from 'hono';
// import { ServeStaticOptions } from '@hono/node-server/serve-static';
import { HttpServer, INestApplication } from '@nestjs/common';
import { Context, Hono, MiddlewareHandler } from 'hono';
import { ServeStaticOptions } from 'hono/serve-static';
export type TypeBodyParser =
| 'application/json'
| 'text/plain'
| 'application/x-www-form-urlencoded';
interface HonoViewOptions {
engine: string;
templates: string;
}
/**
* @publicApi
*/
export interface NestHonoApplication<
TServer extends Hono = Hono,
> extends INestApplication<TServer> {
/**
* Returns the underlying HTTP adapter bounded to a Hono app.
*
* @returns {HttpServer}
*/
getHttpAdapter(): HttpServer<Context, MiddlewareHandler, Hono>;
/**
* Register Hono body parsers on the fly.
*
* @example
* // enable the json parser with a parser limit of 50mb
* app.useBodyParser('application/json', 50 * 1024 * 1024);
*
* @returns {this}
*/
useBodyParser(type: TypeBodyParser, bodyLimit?: number): this;
/**
* Sets a base directory for public assets.
* Example `app.useStaticAssets('public', { root: '/' })`
* @returns {this}
*/
useStaticAssets(path: string, options: ServeStaticOptions): this;
/**
* Sets a view engine for templates (views), for example: `pug`, `handlebars`, or `ejs`.
*
* Don't pass in a string. The string type in the argument is for compatibility reason and will cause an exception.
* @returns {this}
*/
setViewEngine(options: HonoViewOptions | string): this;
/**
* Starts the application.
* @returns A Promise that, when resolved, is a reference to the underlying HttpServer.
*/
listen(
port: number | string,
callback?: (err: Error, address: string) => void,
): Promise<TServer>;
listen(
port: number | string,
address: string,
callback?: (err: Error, address: string) => void,
): Promise<TServer>;
listen(
port: number | string,
address: string,
backlog: number,
callback?: (err: Error, address: string) => void,
): Promise<TServer>;
fetch(input: RequestInfo, init?: RequestInit): Promise<Response>;
getHonoInstance(): Hono<{ Bindings: any }>;
}
export type HonoRequest = Request & {
headers?: Record<string, string>;
};

View File

@@ -0,0 +1,13 @@
// import { Controller, Get } from '@hono-di/core';
import { Controller, Get } from "@nestjs/common";
@Controller('app')
export class AppController {
constructor() {}
@Get('/')
index() {
return 'Hello App';
}
}

16
src/server/app.module.ts Normal file
View File

@@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
// hono-di:imports
],
controllers: [
AppController, // hono-di:controllers
],
providers: [
AppService, // hono-di:providers
],
})
export class AppModule {}

View File

@@ -0,0 +1,6 @@
import { Injectable } from "@nestjs/common";
@Injectable()
export class AppService {
constructor() {}
}

View File

@@ -1 +0,0 @@
export {}

View File

@@ -1,11 +0,0 @@
import { Controller, Get } from '@hono-di/core';
@Controller('user')
export class UserController {
constructor() {}
@Get('/')
index() {
return 'Hello User';
}
}

View File

@@ -1,14 +0,0 @@
import { Module } from '@hono-di/core';
@Module({
imports: [
// hono-di:imports
],
controllers: [
// hono-di:controllers
],
providers: [
// hono-di:providers
],
})
export class UserModule {}

View File

@@ -1,6 +0,0 @@
import { Injectable } from '@hono-di/core';
@Injectable()
export class UserService {
constructor() {}
}

View File

@@ -1 +0,0 @@
export {}