feat: integrate Hono with NestJS and add logging middleware
This commit is contained in:
152
build.ts
Normal file
152
build.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env bun
|
||||
import { existsSync } from "fs";
|
||||
import { rm } from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
||||
console.log(`
|
||||
🏗️ Bun Build Script
|
||||
|
||||
Usage: bun run build.ts [options]
|
||||
|
||||
Common Options:
|
||||
--outdir <path> Output directory (default: "dist")
|
||||
--minify Enable minification (or --minify.whitespace, --minify.syntax, etc)
|
||||
--sourcemap <type> Sourcemap type: none|linked|inline|external
|
||||
--target <target> Build target: browser|bun|node
|
||||
--format <format> Output format: esm|cjs|iife
|
||||
--splitting Enable code splitting
|
||||
--packages <type> Package handling: bundle|external
|
||||
--public-path <path> Public path for assets
|
||||
--env <mode> Environment handling: inline|disable|prefix*
|
||||
--conditions <list> Package.json export conditions (comma separated)
|
||||
--external <list> External packages (comma separated)
|
||||
--banner <text> Add banner text to output
|
||||
--footer <text> Add footer text to output
|
||||
--define <obj> Define global constants (e.g. --define.VERSION=1.0.0)
|
||||
--help, -h Show this help message
|
||||
|
||||
Example:
|
||||
bun run build.ts --outdir=dist --minify --sourcemap=linked --external=react,react-dom
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const toCamelCase = (str: string): string => str.replace(/-([a-z])/g, g => g[1].toUpperCase());
|
||||
|
||||
const parseValue = (value: string): any => {
|
||||
if (value === "true") return true;
|
||||
if (value === "false") return false;
|
||||
|
||||
if (/^\d+$/.test(value)) return parseInt(value, 10);
|
||||
if (/^\d*\.\d+$/.test(value)) return parseFloat(value);
|
||||
|
||||
if (value.includes(",")) return value.split(",").map(v => v.trim());
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
function parseArgs(): Partial<Bun.BuildConfig> & { [key: string]: any } {
|
||||
const config: Partial<Bun.BuildConfig> & { [key: string]: any } = {};
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg === undefined) continue;
|
||||
if (!arg.startsWith("--")) continue;
|
||||
|
||||
if (arg.startsWith("--no-")) {
|
||||
const key = toCamelCase(arg.slice(5));
|
||||
config[key] = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!arg.includes("=") && (i === args.length - 1 || args[i + 1]?.startsWith("--"))) {
|
||||
const key = toCamelCase(arg.slice(2));
|
||||
config[key] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
let key: string;
|
||||
let value: string;
|
||||
|
||||
if (arg.includes("=")) {
|
||||
[key, value] = arg.slice(2).split("=", 2) as [string, string];
|
||||
} else {
|
||||
key = arg.slice(2);
|
||||
value = args[++i] ?? "";
|
||||
}
|
||||
|
||||
key = toCamelCase(key);
|
||||
|
||||
if (key.includes(".")) {
|
||||
const [parentKey, childKey] = key.split(".");
|
||||
config[parentKey] = config[parentKey] || {};
|
||||
config[parentKey][childKey] = parseValue(value);
|
||||
} else {
|
||||
config[key] = parseValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
const units = ["B", "KB", "MB", "GB"];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
||||
};
|
||||
|
||||
console.log("\n🚀 Starting build process...\n");
|
||||
|
||||
const cliConfig = parseArgs();
|
||||
const outdir = cliConfig.outdir || path.join(process.cwd(), "dist");
|
||||
|
||||
// if (existsSync(outdir)) {
|
||||
// console.log(`🗑️ Cleaning previous build at ${outdir}`);
|
||||
// await rm(outdir, { recursive: true, force: true });
|
||||
// }
|
||||
|
||||
const start = performance.now();
|
||||
|
||||
// const entrypoints = [...new Bun.Glob("**.html").scanSync("src")]
|
||||
// .map(a => path.resolve("src", a))
|
||||
// .filter(dir => !dir.includes("node_modules"));
|
||||
const entrypoints = [path.resolve("dist/server/index.js")];
|
||||
console.log(`📄 Found ${entrypoints.length} HTML ${entrypoints.length === 1 ? "file" : "files"} to process\n`);
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints,
|
||||
outdir,
|
||||
plugins: [],
|
||||
minify: false,
|
||||
// target: "browser",
|
||||
target: "bun",
|
||||
// sourcemap: "linked",
|
||||
sourcemap: false,
|
||||
define: {
|
||||
"process.env.NODE_ENV": JSON.stringify("production"),
|
||||
},
|
||||
external: ["@nestjs/microservices",'@nestjs/platform-express', "@nestjs/websockets", "class-transformer", "class-validator"],
|
||||
...cliConfig,
|
||||
});
|
||||
|
||||
const end = performance.now();
|
||||
|
||||
const outputTable = result.outputs.map(output => ({
|
||||
File: path.relative(process.cwd(), output.path),
|
||||
Type: output.kind,
|
||||
Size: formatFileSize(output.size),
|
||||
}));
|
||||
|
||||
console.table(outputTable);
|
||||
const buildTime = (end - start).toFixed(2);
|
||||
|
||||
console.log(`\n✅ Build completed in ${buildTime}ms\n`);
|
||||
3
bun.lock
3
bun.lock
@@ -33,6 +33,7 @@
|
||||
"devDependencies": {
|
||||
"@hattip/adapter-node": "^0.0.49",
|
||||
"@hono-di/vite": "^0.0.15",
|
||||
"@hono/node-server": "^1.19.8",
|
||||
"@primevue/auto-import-resolver": "^4.5.4",
|
||||
"@types/bun": "^1.3.5",
|
||||
"@types/node": "^25.0.5",
|
||||
@@ -267,6 +268,8 @@
|
||||
|
||||
"@hono-di/vite": ["@hono-di/vite@0.0.15", "", { "dependencies": { "@hono-di/client": "0.0.15", "@hono-di/generate": "0.0.15", "sirv": "^2.0.4" }, "peerDependencies": { "vite": "^7.0.0" } }, "sha512-VzqcqkReVHWxMiGHdnze1I9efNZTSMBmm2qRsLU7XDtJGBBPu3WzlNvlf84NjkExeYb55dMhDc3Cln7bKhv/LQ=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.8", "", { "peerDependencies": { "hono": "^4" } }, "sha512-0/g2lIOPzX8f3vzW1ggQgvG5mjtFBDBHFAzI5SFAi2DzSqS9luJwqg9T6O/gKYLi+inS7eNxBeIFkkghIPvrMA=="],
|
||||
|
||||
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
||||
|
||||
"@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="],
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bunx --bun vite",
|
||||
"build": "bunx --bun vite build && bun build dist/server/index.js --target bun --minify --outdir dist",
|
||||
"build": "bunx --bun vite build && bun run build.ts",
|
||||
"preview": "bunx --bun vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -35,6 +35,7 @@
|
||||
"devDependencies": {
|
||||
"@hattip/adapter-node": "^0.0.49",
|
||||
"@hono-di/vite": "^0.0.15",
|
||||
"@hono/node-server": "^1.19.8",
|
||||
"@primevue/auto-import-resolver": "^4.5.4",
|
||||
"@types/bun": "^1.3.5",
|
||||
"@types/node": "^25.0.5",
|
||||
|
||||
@@ -64,7 +64,6 @@ export default function transformerClassnamesMinifier(options: CompileClassOptio
|
||||
enforce: 'pre',
|
||||
async transform(s, _id, { uno }) {
|
||||
if(s.original.includes('p-button') || s.original.includes('p-component') || s.original.includes('p-button-secondary')) {
|
||||
console.log("transforming:", _id);
|
||||
}
|
||||
const matches = [...s.original.matchAll(regexp)]
|
||||
if (!matches.length)
|
||||
|
||||
32
src/main.ts
32
src/main.ts
@@ -33,7 +33,7 @@ import { NestHonoApplication } from './server/HonoAdapter/interfaces';
|
||||
// app.get("/.well-known/*", (c) => {
|
||||
// return c.json({ ok: true });
|
||||
// });
|
||||
const app = await NestFactory.create<NestHonoApplication>(
|
||||
const appHonoNest = await NestFactory.create<NestHonoApplication>(
|
||||
AppModule,
|
||||
new HonoAdapter(),
|
||||
{
|
||||
@@ -51,10 +51,26 @@ const app = await NestFactory.create<NestHonoApplication>(
|
||||
// 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
|
||||
const app = appHonoNest.getHonoInstance();
|
||||
app.use(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();
|
||||
}, contextStorage());
|
||||
|
||||
appHonoNest.useStaticAssets("/*", { root: "./dist/client" });
|
||||
await appHonoNest.init();
|
||||
app.get("/.well-known/*", (c) => {
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
app.use(ssrRender);
|
||||
const httpAdapter = appHonoNest.get(HttpAdapterHost);
|
||||
// await app.listen(3000)
|
||||
// console.log("HTTP Adapter:", app.fetch.toString());
|
||||
export default {
|
||||
fetch: app.fetch.bind(app),
|
||||
}
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
// @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 { 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';
|
||||
@@ -20,15 +8,14 @@ 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';
|
||||
|
||||
// Define HttpBindings as an empty interface if you don't need specific bindings
|
||||
interface HttpBindings {}
|
||||
|
||||
type HonoHandler = RequestHandler<HonoRequest, Context>;
|
||||
type ServerType = http.Server | http2.Http2Server | http2.Http2SecureServer;
|
||||
type Ctx = Context | (() => Promise<Context>);
|
||||
type Method =
|
||||
| 'all'
|
||||
@@ -44,11 +31,11 @@ type Method =
|
||||
* Adapter for using Hono with NestJS.
|
||||
*/
|
||||
export class HonoAdapter extends AbstractHttpAdapter<
|
||||
ServerType,
|
||||
any,
|
||||
HonoRequest,
|
||||
Context
|
||||
> {
|
||||
private _isParserRegistered: boolean;
|
||||
private _isParserRegistered: boolean = false;
|
||||
|
||||
protected readonly instance: Hono<{ Bindings: HttpBindings }>;
|
||||
|
||||
@@ -74,7 +61,9 @@ export class HonoAdapter extends AbstractHttpAdapter<
|
||||
|
||||
private createRouteHandler(routeHandler: HonoHandler) {
|
||||
return async (ctx: Context, next: Next) => {
|
||||
ctx.req['params'] = ctx.req.param();
|
||||
// ctx.req['params'] = ctx.req.param();
|
||||
// ctx.req['params'] = ctx.req.param();
|
||||
Object.assign(ctx.req, { params: ctx.req.param() });
|
||||
|
||||
await routeHandler(ctx.req, ctx, next);
|
||||
|
||||
@@ -110,7 +99,7 @@ export class HonoAdapter extends AbstractHttpAdapter<
|
||||
responseContentType = 'application/json';
|
||||
}
|
||||
|
||||
await this.setHeader(ctx, 'Content-Type', responseContentType);
|
||||
await this.setHeader(ctx, 'Content-Type', String(responseContentType));
|
||||
}
|
||||
|
||||
if (responseContentType === 'application/json' && isObject(body)) {
|
||||
@@ -210,7 +199,10 @@ export class HonoAdapter extends AbstractHttpAdapter<
|
||||
}
|
||||
|
||||
public async end() {
|
||||
return RESPONSE_ALREADY_SENT;
|
||||
// return RESPONSE_ALREADY_SENT;
|
||||
new Response(null, {
|
||||
headers: { ['x-hono-already-sent']: "true" }
|
||||
})
|
||||
}
|
||||
|
||||
public render() {
|
||||
@@ -239,9 +231,14 @@ export class HonoAdapter extends AbstractHttpAdapter<
|
||||
});
|
||||
}
|
||||
|
||||
public useStaticAssets(path: string, options: ServeStaticOptions) {
|
||||
public async useStaticAssets(path: string, options: any) {
|
||||
Logger.log('Registering static assets middleware');
|
||||
this.instance.use(path, serveStatic(options));
|
||||
if ((process as any).versions?.bun && import.meta.env.PROD) {
|
||||
const { serveStatic } = await import("hono/bun");
|
||||
// app.use(serveStatic({ root: "./dist/client" }))
|
||||
// this.instance.use(serveStatic({ root: "./dist/client" }))
|
||||
this.instance.use(path, serveStatic(options));
|
||||
}
|
||||
}
|
||||
|
||||
public setViewEngine() {
|
||||
@@ -303,7 +300,9 @@ export class HonoAdapter extends AbstractHttpAdapter<
|
||||
Logger.log(
|
||||
`Registering body parser middleware for type: ${type} | bodyLimit: ${bodyLimit}`,
|
||||
);
|
||||
this.instance.use(this.bodyLimit(bodyLimit));
|
||||
if(bodyLimit !== undefined) {
|
||||
this.instance.use(this.bodyLimit(bodyLimit));
|
||||
}
|
||||
|
||||
// To avoid the Nest application init to override our custom
|
||||
// body parser, we mark the parsers as registered.
|
||||
@@ -325,7 +324,8 @@ export class HonoAdapter extends AbstractHttpAdapter<
|
||||
ctx.req.header('x-cluster-client-ip') ??
|
||||
ctx.req.header('x-forwarded') ??
|
||||
ctx.req.header('forwarded-for') ??
|
||||
ctx.req.header('via')
|
||||
ctx.req.header('via')??
|
||||
"unknown"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -358,31 +358,28 @@ export class HonoAdapter extends AbstractHttpAdapter<
|
||||
}
|
||||
|
||||
public initHttpServer(options: NestApplicationOptions) {
|
||||
Logger.log('Initializing Hono HTTP server adapter');
|
||||
this.instance.use(async (ctx, next) => {
|
||||
ctx.req['ip'] = this.extractClientIp(ctx);
|
||||
// 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);
|
||||
|
||||
// ctx.req['query'] = ctx.req.query() as any;
|
||||
// ctx.req['headers'] = Object.fromEntries(ctx.req.raw.headers);
|
||||
Object.assign(ctx.req, {
|
||||
ip: this.extractClientIp(ctx),
|
||||
query: ctx.req.query() as Record<string, string>,
|
||||
headers: Object.fromEntries(ctx.req.raw.headers as any),
|
||||
})
|
||||
const contentType = ctx.req.header('content-type');
|
||||
await this.parseRequestBody(ctx, contentType, options.rawBody);
|
||||
if (contentType) {
|
||||
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,
|
||||
});
|
||||
return this.httpServer;
|
||||
}
|
||||
|
||||
public getType(): string {
|
||||
return 'hono';
|
||||
}
|
||||
public getType(): string { return 'hono'; }
|
||||
|
||||
public registerParserMiddleware(_prefix?: string, rawBody?: boolean) {
|
||||
if (this._isParserRegistered) {
|
||||
@@ -400,7 +397,7 @@ export class HonoAdapter extends AbstractHttpAdapter<
|
||||
public async createMiddlewareFactory(requestMethod: RequestMethod) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
return (path: string, callback: Function) => {
|
||||
const routeMethodsMap = {
|
||||
const routeMethodsMap: Record<string, Function> = {
|
||||
[RequestMethod.ALL]: this.instance.all,
|
||||
[RequestMethod.DELETE]: this.instance.delete,
|
||||
[RequestMethod.GET]: this.instance.get,
|
||||
@@ -424,12 +421,7 @@ export class HonoAdapter extends AbstractHttpAdapter<
|
||||
}
|
||||
|
||||
// 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 listen(...args: any[]) {}
|
||||
public getHonoInstance(): Hono<{ Bindings: HttpBindings }> {
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export interface NestHonoApplication<
|
||||
callback?: (err: Error, address: string) => void,
|
||||
): Promise<TServer>;
|
||||
fetch(input: RequestInfo, init?: RequestInit): Promise<Response>;
|
||||
getHonoInstance(): Hono<{ Bindings: any }>;
|
||||
getHonoInstance(): Hono;
|
||||
}
|
||||
export type HonoRequest = Request & {
|
||||
headers?: Record<string, string>;
|
||||
|
||||
466
src/server/HonoAdapter/test.ts
Normal file
466
src/server/HonoAdapter/test.ts
Normal file
@@ -0,0 +1,466 @@
|
||||
// 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 http2 from 'http2';
|
||||
import { createServer as createServerHTTP, Server } from 'http';
|
||||
import { createServer as createServerHTTPS } from 'https';
|
||||
|
||||
import { HonoRequest, TypeBodyParser } from './interfaces';
|
||||
import { Data } from 'node_modules/hono/dist/types/context';
|
||||
|
||||
// Define HttpBindings as an empty interface if you don't need specific bindings
|
||||
interface HttpBindings {}
|
||||
|
||||
type HonoHandler = RequestHandler<HonoRequest, Context>;
|
||||
type ServerType = 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 = false;
|
||||
|
||||
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();
|
||||
// ctx.req['params'] = ctx.req.param();
|
||||
Object.assign(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', String(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;
|
||||
new Response(null, {
|
||||
headers: { ['x-hono-already-sent']: "true" }
|
||||
})
|
||||
}
|
||||
|
||||
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 async useStaticAssets(path: string, options: any) {
|
||||
Logger.log("Đã bị comment trong adapter")
|
||||
Logger.log('Registering static assets middleware');
|
||||
if ((process as any).versions?.bun) {
|
||||
const { serveStatic } = await import("hono/bun");
|
||||
// app.use(serveStatic({ root: "./dist/client" }))
|
||||
// this.instance.use(serveStatic({ root: "./dist/client" }))
|
||||
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}`,
|
||||
);
|
||||
if(bodyLimit !== undefined) {
|
||||
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')??
|
||||
"unknown"
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
Logger.log('Initializing Hono HTTP server adapter');
|
||||
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);
|
||||
Object.assign(ctx.req, {
|
||||
ip: this.extractClientIp(ctx),
|
||||
query: ctx.req.query() as Record<string, string>,
|
||||
headers: Object.fromEntries(ctx.req.raw.headers as any),
|
||||
})
|
||||
const contentType = ctx.req.header('content-type');
|
||||
if (contentType) {
|
||||
await this.parseRequestBody(ctx, contentType, options.rawBody!);
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
const isHttpsEnabled = options?.httpsOptions;
|
||||
const createServer = isHttpsEnabled
|
||||
? createServerHTTPS
|
||||
: createServerHTTP;
|
||||
// this.httpServer = createAdaptorServer({
|
||||
// fetch: this.instance.fetch,
|
||||
// createServer,
|
||||
// overrideGlobalObjects: false,
|
||||
// });
|
||||
// this.httpServer = isHttpsEnabled ?
|
||||
return this.httpServer;
|
||||
}
|
||||
|
||||
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: Record<string, Function> = {
|
||||
[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): Response | Promise<Response> {
|
||||
return this.instance.fetch(input as Request, 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);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { LoggerMiddleware } from './middleware';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -13,4 +14,9 @@ import { AppService } from './app.service';
|
||||
AppService, // hono-di:providers
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer
|
||||
.apply(LoggerMiddleware).forRoutes('*');
|
||||
}
|
||||
}
|
||||
|
||||
34
src/server/middleware.ts
Normal file
34
src/server/middleware.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// Source - https://stackoverflow.com/a
|
||||
// Posted by Stark Jeon
|
||||
// Retrieved 2026-01-10, License - CC BY-SA 4.0
|
||||
|
||||
import { Injectable, NestMiddleware, Logger } from "@nestjs/common";
|
||||
import { Context } from "hono";
|
||||
|
||||
@Injectable()
|
||||
export class LoggerMiddleware implements NestMiddleware {
|
||||
private logger = new Logger("HTTP");
|
||||
|
||||
async use(request: Request, response: Response, next: any) {
|
||||
const start = Date.now()
|
||||
// console.log("Request:", request.method, request.url, arguments[2].toString());
|
||||
// const { ip, method, originalUrl } = request;
|
||||
// const userAgent = request.get("user-agent") || "";
|
||||
const ctx = arguments[2] as Context;
|
||||
// ctx
|
||||
// response.on("finish", () => {
|
||||
// const { statusCode } = response;
|
||||
// const contentLength = response.get("content-length");
|
||||
|
||||
// this.logger.log(
|
||||
// `${method} ${originalUrl} ${statusCode} ${contentLength} - ${userAgent} ${ip}`,
|
||||
// );
|
||||
// });
|
||||
await next().finally(() => {
|
||||
const ms = Date.now() - start
|
||||
this.logger.log(
|
||||
`${request.method} ${request.url} - ${ms}ms`,
|
||||
) ;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user