diff --git a/build.ts b/build.ts new file mode 100644 index 0000000..a57c08f --- /dev/null +++ b/build.ts @@ -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 Output directory (default: "dist") + --minify Enable minification (or --minify.whitespace, --minify.syntax, etc) + --sourcemap Sourcemap type: none|linked|inline|external + --target Build target: browser|bun|node + --format Output format: esm|cjs|iife + --splitting Enable code splitting + --packages Package handling: bundle|external + --public-path Public path for assets + --env Environment handling: inline|disable|prefix* + --conditions Package.json export conditions (comma separated) + --external External packages (comma separated) + --banner Add banner text to output + --footer Add footer text to output + --define 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 & { [key: string]: any } { + const config: Partial & { [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`); diff --git a/bun.lock b/bun.lock index 0e8c7d4..3beb8e1 100644 --- a/bun.lock +++ b/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=="], diff --git a/package.json b/package.json index 9bd3846..01bedd0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/plugins/encodeClassTransformer.ts b/plugins/encodeClassTransformer.ts index e8c170a..c1e44a6 100644 --- a/plugins/encodeClassTransformer.ts +++ b/plugins/encodeClassTransformer.ts @@ -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) diff --git a/src/main.ts b/src/main.ts index 9130a40..51266cc 100644 --- a/src/main.ts +++ b/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( +const appHonoNest = await NestFactory.create( AppModule, new HonoAdapter(), { @@ -51,10 +51,26 @@ const app = await NestFactory.create( // 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), +} diff --git a/src/server/HonoAdapter/adapter.ts b/src/server/HonoAdapter/adapter.ts index fece5b7..54b3e08 100644 --- a/src/server/HonoAdapter/adapter.ts +++ b/src/server/HonoAdapter/adapter.ts @@ -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; -type ServerType = http.Server | http2.Http2Server | http2.Http2SecureServer; type Ctx = Context | (() => Promise); 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, + 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 = { [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 { - return this.instance.fetch(input, init); - } + public listen(...args: any[]) {} public getHonoInstance(): Hono<{ Bindings: HttpBindings }> { return this.instance; } diff --git a/src/server/HonoAdapter/interfaces.ts b/src/server/HonoAdapter/interfaces.ts index 77db5bc..9c0ca10 100644 --- a/src/server/HonoAdapter/interfaces.ts +++ b/src/server/HonoAdapter/interfaces.ts @@ -73,7 +73,7 @@ export interface NestHonoApplication< callback?: (err: Error, address: string) => void, ): Promise; fetch(input: RequestInfo, init?: RequestInit): Promise; - getHonoInstance(): Hono<{ Bindings: any }>; + getHonoInstance(): Hono; } export type HonoRequest = Request & { headers?: Record; diff --git a/src/server/HonoAdapter/test.ts b/src/server/HonoAdapter/test.ts new file mode 100644 index 0000000..891e1af --- /dev/null +++ b/src/server/HonoAdapter/test.ts @@ -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; +type ServerType = Server | http2.Http2Server | http2.Http2SecureServer; +type Ctx = Context | (() => Promise); +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 { + 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 { + 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 { + 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 { + 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 { + 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, + 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 = { + [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 { + 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); + }, + }); + } +} \ No newline at end of file diff --git a/src/server/app.module.ts b/src/server/app.module.ts index ec97235..feab157 100644 --- a/src/server/app.module.ts +++ b/src/server/app.module.ts @@ -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('*'); + } +} diff --git a/src/server/middleware.ts b/src/server/middleware.ts new file mode 100644 index 0000000..ac0aadb --- /dev/null +++ b/src/server/middleware.ts @@ -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`, + ) ; + }); + } +}