change architecture

This commit is contained in:
2026-01-12 02:08:47 +07:00
parent f80ea881c6
commit 27bcb8bbef
76 changed files with 997 additions and 621 deletions

View File

@@ -11,7 +11,6 @@
"@hiogawa/tiny-rpc": "^0.2.3-pre.18", "@hiogawa/tiny-rpc": "^0.2.3-pre.18",
"@hiogawa/utils": "^1.7.0", "@hiogawa/utils": "^1.7.0",
"@hono-di/cli": "^0.0.15", "@hono-di/cli": "^0.0.15",
"@hono-di/core": "^0.0.15",
"@nestjs/common": "^11.1.11", "@nestjs/common": "^11.1.11",
"@nestjs/core": "^11.1.11", "@nestjs/core": "^11.1.11",
"@primeuix/themes": "^2.0.2", "@primeuix/themes": "^2.0.2",
@@ -21,6 +20,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"hono": "^4.11.3", "hono": "^4.11.3",
"is-mobile": "^5.0.0", "is-mobile": "^5.0.0",
"nestjs-zod": "^5.1.1",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"primevue": "^4.5.4", "primevue": "^4.5.4",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
@@ -640,6 +640,8 @@
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
@@ -762,6 +764,8 @@
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"nestjs-zod": ["nestjs-zod@5.1.1", "", { "dependencies": { "deepmerge": "^4.3.1" }, "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "@nestjs/swagger": "^7.4.2 || ^8.0.0 || ^11.0.0", "rxjs": "^7.0.0", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@nestjs/swagger"] }, "sha512-pXa9Jrdip7iedKvGxJTvvCFVRCoIcNENPCsHjpCefPH3PcFejRgkZkUcr3TYITRyxnUk7Zy5OsLpirZGLYBfBQ=="],
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],

48
components.d.ts vendored
View File

@@ -12,47 +12,47 @@ export {}
/* prettier-ignore */ /* prettier-ignore */
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
Add: typeof import('./src/components/icons/Add.vue')['default'] Add: typeof import('./src/client/components/icons/Add.vue')['default']
Bell: typeof import('./src/components/icons/Bell.vue')['default'] Bell: typeof import('./src/client/components/icons/Bell.vue')['default']
Button: typeof import('primevue/button')['default'] Button: typeof import('primevue/button')['default']
Checkbox: typeof import('primevue/checkbox')['default'] Checkbox: typeof import('primevue/checkbox')['default']
CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default'] CheckIcon: typeof import('./src/client/components/icons/CheckIcon.vue')['default']
Credit: typeof import('./src/components/icons/Credit.vue')['default'] Credit: typeof import('./src/client/components/icons/Credit.vue')['default']
DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default'] DashboardLayout: typeof import('./src/client/components/DashboardLayout.vue')['default']
Home: typeof import('./src/components/icons/Home.vue')['default'] Home: typeof import('./src/client/components/icons/Home.vue')['default']
InputText: typeof import('primevue/inputtext')['default'] InputText: typeof import('primevue/inputtext')['default']
Layout: typeof import('./src/components/icons/Layout.vue')['default'] Layout: typeof import('./src/client/components/icons/Layout.vue')['default']
Message: typeof import('primevue/message')['default'] Message: typeof import('primevue/message')['default']
Password: typeof import('primevue/password')['default'] Password: typeof import('primevue/password')['default']
RootLayout: typeof import('./src/components/RootLayout.vue')['default'] RootLayout: typeof import('./src/client/components/RootLayout.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
TestIcon: typeof import('./src/components/icons/TestIcon.vue')['default'] TestIcon: typeof import('./src/client/components/icons/TestIcon.vue')['default']
Upload: typeof import('./src/components/icons/Upload.vue')['default'] Upload: typeof import('./src/client/components/icons/Upload.vue')['default']
Video: typeof import('./src/components/icons/Video.vue')['default'] Video: typeof import('./src/client/components/icons/Video.vue')['default']
VueHead: typeof import('./src/components/VueHead.tsx')['default'] VueHead: typeof import('./src/client/components/VueHead.tsx')['default']
} }
} }
// For TSX support // For TSX support
declare global { declare global {
const Add: typeof import('./src/components/icons/Add.vue')['default'] const Add: typeof import('./src/client/components/icons/Add.vue')['default']
const Bell: typeof import('./src/components/icons/Bell.vue')['default'] const Bell: typeof import('./src/client/components/icons/Bell.vue')['default']
const Button: typeof import('primevue/button')['default'] const Button: typeof import('primevue/button')['default']
const Checkbox: typeof import('primevue/checkbox')['default'] const Checkbox: typeof import('primevue/checkbox')['default']
const CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default'] const CheckIcon: typeof import('./src/client/components/icons/CheckIcon.vue')['default']
const Credit: typeof import('./src/components/icons/Credit.vue')['default'] const Credit: typeof import('./src/client/components/icons/Credit.vue')['default']
const DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default'] const DashboardLayout: typeof import('./src/client/components/DashboardLayout.vue')['default']
const Home: typeof import('./src/components/icons/Home.vue')['default'] const Home: typeof import('./src/client/components/icons/Home.vue')['default']
const InputText: typeof import('primevue/inputtext')['default'] const InputText: typeof import('primevue/inputtext')['default']
const Layout: typeof import('./src/components/icons/Layout.vue')['default'] const Layout: typeof import('./src/client/components/icons/Layout.vue')['default']
const Message: typeof import('primevue/message')['default'] const Message: typeof import('primevue/message')['default']
const Password: typeof import('primevue/password')['default'] const Password: typeof import('primevue/password')['default']
const RootLayout: typeof import('./src/components/RootLayout.vue')['default'] const RootLayout: typeof import('./src/client/components/RootLayout.vue')['default']
const RouterLink: typeof import('vue-router')['RouterLink'] const RouterLink: typeof import('vue-router')['RouterLink']
const RouterView: typeof import('vue-router')['RouterView'] const RouterView: typeof import('vue-router')['RouterView']
const TestIcon: typeof import('./src/components/icons/TestIcon.vue')['default'] const TestIcon: typeof import('./src/client/components/icons/TestIcon.vue')['default']
const Upload: typeof import('./src/components/icons/Upload.vue')['default'] const Upload: typeof import('./src/client/components/icons/Upload.vue')['default']
const Video: typeof import('./src/components/icons/Video.vue')['default'] const Video: typeof import('./src/client/components/icons/Video.vue')['default']
const VueHead: typeof import('./src/components/VueHead.tsx')['default'] const VueHead: typeof import('./src/client/components/VueHead.tsx')['default']
} }

View File

@@ -13,7 +13,6 @@
"@hiogawa/tiny-rpc": "^0.2.3-pre.18", "@hiogawa/tiny-rpc": "^0.2.3-pre.18",
"@hiogawa/utils": "^1.7.0", "@hiogawa/utils": "^1.7.0",
"@hono-di/cli": "^0.0.15", "@hono-di/cli": "^0.0.15",
"@hono-di/core": "^0.0.15",
"@nestjs/common": "^11.1.11", "@nestjs/common": "^11.1.11",
"@nestjs/core": "^11.1.11", "@nestjs/core": "^11.1.11",
"@primeuix/themes": "^2.0.2", "@primeuix/themes": "^2.0.2",
@@ -23,6 +22,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"hono": "^4.11.3", "hono": "^4.11.3",
"is-mobile": "^5.0.0", "is-mobile": "^5.0.0",
"nestjs-zod": "^5.1.1",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"primevue": "^4.5.4", "primevue": "^4.5.4",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",

View File

@@ -115,8 +115,8 @@ export default function ssrPlugin(): Plugin[] {
return path.resolve( return path.resolve(
__dirname, __dirname,
options?.ssr options?.ssr
? pwd+"/src/api/httpClientAdapter.server.ts" ? pwd+"/src/client/api/httpClientAdapter.server.ts"
: pwd+"/src/api/httpClientAdapter.client.ts" : pwd+"/src/client/api/httpClientAdapter.client.ts"
); );
}, },
async configResolved(config) { async configResolved(config) {

View File

@@ -1,12 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import Add from "@/components/icons/Add.vue"; import Home from "@/client/components/icons/Home.vue";
import Bell from "@/components/icons/Bell.vue"; import Video from "@/client/components/icons/Video.vue";
import Home from "@/components/icons/Home.vue"; import Credit from "@/client/components/icons/Credit.vue";
import Video from "@/components/icons/Video.vue";
import Credit from "@/components/icons/Credit.vue";
import Upload from "./icons/Upload.vue"; import Upload from "./icons/Upload.vue";
import { cn } from "@/lib/utils"; import { cn } from "@/client/lib/utils";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/client/stores/auth";
import { createStaticVNode } from "vue"; import { createStaticVNode } from "vue";
const auth = useAuthStore(); const auth = useAuthStore();

View File

@@ -1,5 +1,5 @@
import 'uno.css'; import 'uno.css';
import createVueApp from './shared/createVueApp'; import createVueApp from '@/shared/createVueApp';
async function render() { async function render() {
const { app, router } = createVueApp(); const { app, router } = createVueApp();
router.isReady().then(() => { router.isReady().then(() => {

View File

@@ -84,7 +84,7 @@ export function buildBootstrapScript() {
assets: [], assets: [],
}, },
"1": { "1": {
file: "src/client.ts", file: "src/client/index.ts",
isEntry: true, isEntry: true,
css: [], css: [],
}, },

View File

@@ -8,5 +8,5 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { VueHead } from "@/components/VueHead"; import { VueHead } from "@/client/components/VueHead";
</script> </script>

View File

@@ -74,7 +74,7 @@ import { reactive } from 'vue';
import { Form, type FormSubmitEvent } from '@primevue/forms'; import { Form, type FormSubmitEvent } from '@primevue/forms';
import { zodResolver } from '@primevue/forms/resolvers/zod'; import { zodResolver } from '@primevue/forms/resolvers/zod';
import { z } from 'zod'; import { z } from 'zod';
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/client/stores/auth';
const auth = useAuthStore(); const auth = useAuthStore();
// const $form = Form.useFormContext(); // const $form = Form.useFormContext();

View File

@@ -6,7 +6,7 @@ import {
createWebHistory, createWebHistory,
type RouteRecordRaw, type RouteRecordRaw,
} from "vue-router"; } from "vue-router";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/client/stores/auth";
type RouteData = RouteRecordRaw & { type RouteData = RouteRecordRaw & {
meta?: ResolvableValue<ReactiveHead> & { requiresAuth?: boolean }; meta?: ResolvableValue<ReactiveHead> & { requiresAuth?: boolean };
@@ -15,7 +15,7 @@ type RouteData = RouteRecordRaw & {
const routes: RouteData[] = [ const routes: RouteData[] = [
{ {
path: "/", path: "/",
component: () => import("@/components/RootLayout.vue"), component: () => import("@/client/components/RootLayout.vue"),
children: [ children: [
{ {
path: "", path: "",
@@ -76,7 +76,7 @@ const routes: RouteData[] = [
}, },
{ {
path: "", path: "",
component: () => import("@/components/DashboardLayout.vue"), component: () => import("@/client/components/DashboardLayout.vue"),
meta: { requiresAuth: true }, meta: { requiresAuth: true },
children: [ children: [
{ {

View File

@@ -184,7 +184,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { Head } from '@unhead/vue/components' import { Head } from '@unhead/vue/components'
import { cn } from '@/lib/utils'; import { cn } from '@/client/lib/utils';
const pricing = { const pricing = {
title: "Simple, transparent pricing", title: "Simple, transparent pricing",
subtitle: "Choose the plan that fits your needs. No hidden fees.", subtitle: "Choose the plan that fits your needs. No hidden fees.",

View File

@@ -1,6 +1,6 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { client } from '@/api/rpcclient'; import { client } from '@/client/api/rpcclient';
import { ref } from 'vue'; import { ref } from 'vue';
interface User { interface User {

View File

@@ -1,76 +1,63 @@
import { Hono } from 'hono'; import { NestFactory } from "@nestjs/core";
import { contextStorage } from 'hono/context-storage'; import Bun from "bun";
import { cors } from "hono/cors"; import { Hono } from "hono";
import isMobile from 'is-mobile'; import { contextStorage } from "hono/context-storage";
import { rpcServer } from './api/rpc'; import isMobile from "is-mobile";
import { ssrRender } from './worker/ssrRender'; import { AppModule } from "./server/app.module";
import { HttpAdapterHost, NestFactory } from '@nestjs/core'; import { HonoAdapter, NestHonoApplication } from "./server/common/adapter/hono";
import { AppModule } from './server/app.module'; import { CustomZodValidationPipe } from "./server/common/pipes/CustomZodValidation.pipe";
import { HonoAdapter } from './server/HonoAdapter/adapter'; import { ssrRender } from "./server/HonoAdapter/ssrRender";
import { NestHonoApplication } from './server/HonoAdapter/interfaces'; import { TransformInterceptor } from "./server/common/interceptor/transform.interceptor";
// import { serveStatic } from "hono/bun"; let serve: Bun.Server<undefined> | any = {
// @ts-ignore stop: async () => {},
// const app = new Hono() }
// const isDev = import.meta.env.DEV; const hono = new Hono();
const app = await NestFactory.create<NestHonoApplication>(
// // 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 appHonoNest = await NestFactory.create<NestHonoApplication>(
AppModule, AppModule,
new HonoAdapter(), new HonoAdapter({
{ hono,
rawBody: true, close: () => {
console.log("Closing server");
return serve!.stop();
}, },
address: () => String(serve!.hostname),
listen({ port, hostname, hono, httpsOptions = {}, forceCloseConnections }) {
return new Promise<void>((resolve) => {
serve = Bun.serve({
port,
hostname,
fetch: hono.fetch.bind(hono),
});
console.log(`Server listening on http://${serve.hostname}:${serve.port}`);
resolve();
});
},
})
); );
// app.get('*', (c) => { app.setGlobalPrefix("api");
// console.log("Request URL:", c); app.enableShutdownHooks();
// // return next(); app.useGlobalPipes(new CustomZodValidationPipe());
// }); app.useGlobalInterceptors(new TransformInterceptor());
// app.use(ssrRender);
// app.use(function (req, next) { hono.use(async (c, next) => {
// console.log("Request:", arguments[1]); c.set("fetch", hono.request.bind(hono));
// return (c, next) => { const ua = c.req.header("User-Agent");
// next();
// }
// });
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) { if (!ua) {
return c.json({ error: "User-Agent header is missing" }, 400); return c.json({ error: "User-Agent header is missing" }, 400);
}; }
c.set("isMobile", isMobile({ ua })); c.set("isMobile", isMobile({ ua }));
await next(); await next();
}, contextStorage()); }, contextStorage());
appHonoNest.useStaticAssets("/*", { root: "./dist/client" }); app.useStaticAssets("/*", { root: "./dist/client" });
await appHonoNest.init(); await app.init();
app.get("/.well-known/*", (c) => { hono.get("/.well-known/*", (c) => {
return c.json({ ok: true }); return c.json({ ok: true });
}); });
app.use(ssrRender); hono.use(ssrRender);
const httpAdapter = appHonoNest.get(HttpAdapterHost); if (import.meta.env.PROD) {
// await app.listen(3000) await app.listen(3500);
// console.log("HTTP Adapter:", app.fetch.toString());
export default {
fetch: app.fetch.bind(app),
} }
const honoDev = import.meta.env.DEV ? hono : null;
export default honoDev;
// };

View File

@@ -298,7 +298,7 @@ export class HonoAdapter extends AbstractHttpAdapter<
bodyLimit?: number, bodyLimit?: number,
) { ) {
Logger.log( Logger.log(
`Registering body parser middleware for type: ${type} | bodyLimit: ${bodyLimit}`, `Registering body parser middleware for type: ${type} ${bodyLimit ? 'with limit ' + bodyLimit + ' bytes' : ''}`,
); );
if(bodyLimit !== undefined) { if(bodyLimit !== undefined) {
this.instance.use(this.bodyLimit(bodyLimit)); this.instance.use(this.bodyLimit(bodyLimit));

View File

@@ -3,9 +3,9 @@ import { renderSSRHead } from "@unhead/vue/server";
import { Context } from "hono"; import { Context } from "hono";
import { streamText } from "hono/streaming"; import { streamText } from "hono/streaming";
import { renderToWebStream } from "vue/server-renderer"; import { renderToWebStream } from "vue/server-renderer";
import { buildBootstrapScript } from "@/lib/manifest"; import { buildBootstrapScript } from "@/client/lib/manifest";
import { styleTags } from "@/lib/primePassthrough"; import { styleTags } from "@/client/lib/primePassthrough";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/client/stores/auth";
// @ts-ignore // @ts-ignore
import Base from "@primevue/core/base"; import Base from "@primevue/core/base";
import { BlankEnv, BlankInput } from "hono/types"; import { BlankEnv, BlankInput } from "hono/types";

View File

@@ -1,466 +0,0 @@
// 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);
},
});
}
}

View File

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

View File

@@ -1,22 +1,20 @@
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
import { AppController } from './app.controller'; import { APP_FILTER } from "@nestjs/core";
import { AppService } from './app.service'; import { HttpExceptionFilter } from "./common/filter/http-exception.filter";
import { LoggerMiddleware } from './middleware'; import { LoggerMiddleware } from "./middleware";
import { AuthModule } from "./modules/auth/auth.module";
@Module({ @Module({
imports: [ imports: [AuthModule],
// hono-di:imports
],
controllers: [
AppController, // hono-di:controllers
],
providers: [ providers: [
AppService, // hono-di:providers {
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
], ],
}) })
export class AppModule implements NestModule { export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) { configure(consumer: MiddlewareConsumer) {
consumer consumer.apply(LoggerMiddleware).forRoutes("*");
.apply(LoggerMiddleware).forRoutes('*');
} }
} }

View File

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

View File

@@ -0,0 +1,86 @@
import { Hono } from "hono";
import type { NestApplicationOptions } from "@nestjs/common/interfaces";
import type { Server as NodeHttpServer } from "node:http";
import type { NestHttpServerRequired } from "./_nest";
import { createNestRequiredHttpServer, isFakeHttpServer } from "./_util";
import { HonoRouterAdapter } from "./_adapter";
export type InitHttpServerConfig = Pick<NestApplicationOptions, "httpsOptions" | "forceCloseConnections"> & {
hono: Hono;
};
export interface CreateHonoAdapterOption {
/** Called during server initialization */
initHttpServer?(config: InitHttpServerConfig): NestHttpServerRequired;
/** Obtain the address information for HTTP Server listening */
address?: () => ReturnType<NodeHttpServer["address"]>;
/** Call when shutting down the server */
close?: () => Promise<void>;
listen?: (config: InitHttpServerConfig & { port: number; hostname?: string }) => Promise<void>;
/** Customize Hono instance */
hono?: Hono;
}
/**
* Adapter for using Hono with NestJS.
*/
export class HonoAdapter extends HonoRouterAdapter {
#honoAdapterConfig: CreateHonoAdapterOption;
constructor(config: CreateHonoAdapterOption = {}) {
super(config.hono ?? new Hono());
this.#honoAdapterConfig = config;
}
override listen(port: string | number, callback?: () => void): void;
override listen(port: string | number, hostname: string, callback?: () => void): void;
override async listen(port: string | number, ...args: any[]): Promise<void> {
port = +port;
let callback = args[args.length - 1];
if (typeof callback !== "function") callback = undefined;
const config = this.#honoAdapterConfig;
if (config.listen) {
try {
const hostname = typeof args[0] === "string" ? args[0] : undefined;
await config.listen({
...this.#initHttServerOption!,
port: +port,
hostname,
hono: this.instance,
});
} catch (error) {
callback(error);
return;
}
}
if (isFakeHttpServer(this.httpServer)) {
this.httpServer.address = config.address ?? (() => "127.0.0.1");
callback?.();
} else {
try {
port = +port;
return this.httpServer.listen!(port, ...args);
} catch (error) {
callback?.(error);
}
}
}
//implement
async close(): Promise<void> {
if (!isFakeHttpServer(this.httpServer) && this.httpServer.close) {
await new Promise<void>((resolve, reject) => {
return this.httpServer.close!((err) => (err ? reject(err) : resolve()));
});
}
return this.#honoAdapterConfig.close?.();
}
#initHttServerOption?: Omit<InitHttpServerConfig, "hono">;
//implement
initHttpServer(options: NestApplicationOptions = {}) {
const { forceCloseConnections, httpsOptions } = options;
this.#initHttServerOption = { forceCloseConnections, httpsOptions };
const httpServer = this.#honoAdapterConfig.initHttpServer?.({ ...this.#initHttServerOption!, hono: this.instance });
this.httpServer = httpServer ?? createNestRequiredHttpServer();
}
}

View File

@@ -0,0 +1,36 @@
import type { INestApplication } from "@nestjs/common";
import type { HonoApplicationExtra, HonoBodyParser } from "./hono.impl.ts";
import type { CORSOptions } from "./_adapter.ts";
import type { NestHttpServerRequired } from "./_nest.ts";
import type { MiddlewareHandler } from "hono";
export interface NestHonoApplication<TServer extends NestHttpServerRequired = NestHttpServerRequired>
extends INestApplication<TServer>, HonoApplicationExtra {
use(...handlers: MiddlewareHandler[]): this;
use(path: string, ...handlers: MiddlewareHandler[]): this;
// getHttpAdapter(): HonoAdapter;
/**
* By default, `application/json`, `application/x-www-form-urlencoded`, `multipart/form-data` and `text/plain` are automatically resolved
* You can customize the parser, for example to parse the `application/custom` request body unmapped
* ```ts
* const app = await NestFactory.create<NestHonoApplication>(AppModule, adapter);
* app.useBodyParser("application/custom", async (honoRequest) => {
* const json = await honoRequest.json();
* return new Map(json);
* });
*
* @Controller()
* class Test {
* @Get("data")
* method(@Body() body: Map) {
* return data;
* }
* }
*/
useBodyParser(contentType: string, parser: HonoBodyParser): void;
enableCors(options?: CORSOptions): void;
enableCors(options: any): void;
}
declare module "@nestjs/common" {
interface INestApplication<TServer = any> {}
}

View File

@@ -0,0 +1,298 @@
import type { Context, Env, Hono, Next, Schema } from "hono";
import type { ServeStaticOptions } from "hono/serve-static";
import { bodyLimit as bodyLimitMid } from "hono/body-limit";
import { cors } from "hono/cors";
import type { RedirectStatusCode, StatusCode } from "hono/utils/http-status";
import { RequestMethod } from "@nestjs/common";
import type { ErrorHandler, RequestHandler } from "@nestjs/common/interfaces";
import { AbstractHttpAdapter } from "@nestjs/core/adapters/http-adapter";
import type { BlankEnv, BlankSchema, MiddlewareHandler } from "hono/types";
import type { NestHandler, NestHttpServerRequired } from "./_nest";
import { createHonoReq, createHonoRes, InternalHonoReq, InternalHonoRes, sendResult } from "./_util";
import type { HonoApplicationExtra, HonoBodyParser } from "./hono.impl";
import type { CorsOptions, CorsOptionsDelegate } from "@nestjs/common/interfaces/external/cors-options.interface";
export type CORSOptions = Partial<NonNullable<Parameters<typeof cors>[0]>>;
const NEST_HEADERS = Symbol("nest_headers");
type NestHonoHandler = NestHandler<InternalHonoReq, InternalHonoRes>;
export interface HonoRouterAdapter<
E extends Env = BlankEnv,
S extends Schema = BlankSchema,
BasePath extends string = "/",
> extends AbstractHttpAdapter<NestHttpServerRequired, InternalHonoReq, InternalHonoRes> {
getInstance(): Hono<E, S, BasePath>;
getInstance<T = any>(): T;
getHeader(response: any, name: string): any;
appendHeader(response: any, name: string, value: string): any;
}
export abstract class HonoRouterAdapter
extends AbstractHttpAdapter<NestHttpServerRequired, InternalHonoReq, InternalHonoRes>
implements HonoApplicationExtra {
declare protected readonly instance: Hono;
declare protected httpServer: NestHttpServerRequired;
private createRouteHandler(routeHandler: NestHonoHandler) {
return async (ctx: Context, next?: Next): Promise<Response> => {
const nestHeaders: Record<string, string> = {};
Reflect.set(ctx, NEST_HEADERS, nestHeaders);
let body: any;
const contentType = ctx.req.header("Content-Type");
if (contentType) {
const parser = this.#bodyParsers.get(contentType);
if (parser) body = await parser(ctx.req);
}
await routeHandler(
createHonoReq(ctx, { body, params: ctx.req.param(), rawBody: undefined }),
createHonoRes(ctx),
next,
);
return sendResult(ctx, nestHeaders);
};
}
/**
* Router
*/
override all(handler: NestHonoHandler): void;
override all(path: string, handler: NestHonoHandler): void;
override all(pathOrHandler: string | NestHonoHandler, handler?: NestHonoHandler) {
const [routePath, routeHandler] = getRouteAndHandler(pathOrHandler, handler);
this.instance.all(routePath, this.createRouteHandler(routeHandler));
}
override get(pathOrHandler: string | NestHonoHandler, handler?: NestHonoHandler) {
const [routePath, routeHandler] = getRouteAndHandler(pathOrHandler, handler);
this.instance.get(routePath, this.createRouteHandler(routeHandler));
}
override post(pathOrHandler: string | NestHonoHandler, handler?: NestHonoHandler) {
const [routePath, routeHandler] = getRouteAndHandler(pathOrHandler, handler);
this.instance.post(routePath, this.createRouteHandler(routeHandler));
}
override put(pathOrHandler: string | NestHonoHandler, handler?: NestHonoHandler) {
const [routePath, routeHandler] = getRouteAndHandler(pathOrHandler, handler);
this.instance.put(routePath, this.createRouteHandler(routeHandler));
}
override delete(pathOrHandler: string | NestHonoHandler, handler?: NestHonoHandler) {
const [routePath, routeHandler] = getRouteAndHandler(pathOrHandler, handler);
this.instance.delete(routePath, this.createRouteHandler(routeHandler));
}
override patch(pathOrHandler: string | NestHonoHandler, handler?: NestHonoHandler) {
const [routePath, routeHandler] = getRouteAndHandler(pathOrHandler, handler);
this.instance.patch(routePath, this.createRouteHandler(routeHandler));
}
override options(pathOrHandler: string | NestHonoHandler, handler?: NestHonoHandler) {
const [routePath, routeHandler] = getRouteAndHandler(pathOrHandler, handler);
this.instance.options(routePath, this.createRouteHandler(routeHandler));
}
override use(...handler: [string | MiddlewareHandler, ...MiddlewareHandler[]]): void {
//@ts-ignore
this.instance.use(...handler);
}
useBodyLimit(bodyLimit: number) {
this.instance.use(
bodyLimitMid({
maxSize: bodyLimit,
onError: () => {
throw new Error("Body too large");
},
}),
);
}
#bodyParsers: Map<string | undefined, HonoBodyParser> = new Map();
useBodyParser(contentType: string, parser: HonoBodyParser): void;
/**
* @param rawBody When NestApplicationOptions.bodyParser is set to true, rawBody will be true
*/
useBodyParser(contentType: string, rawBody?: boolean | HonoBodyParser, parser?: HonoBodyParser) {
if (typeof rawBody === "function") {
parser = rawBody;
} else if (typeof parser !== "function") {
return;
}
this.#bodyParsers.set(contentType, parser);
}
//implement
// useStaticAssets(path: string, options: ServeStaticOptions): never {
// throw new Error("Method useStaticAssets not implemented."); //TODO useStaticAssets
// }
async useStaticAssets(path: string, options: ServeStaticOptions) {
// this.Logger.log('Registering static assets middleware');
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));
}
return this;
}
//implement
setViewEngine(options: any | string): never {
throw new Error("Method setViewEngine not implemented."); //TODO setViewEngine
}
//implement
getRequestHostname(request: InternalHonoReq): string {
return request.req.header("Host") ?? "";
}
//implement
getRequestMethod(request: InternalHonoReq): string {
return request.req.method;
}
//implement
getRequestUrl(request: InternalHonoReq): string {
return request.req.url;
}
// implement
status(res: InternalHonoRes, statusCode: StatusCode) {
res.status(statusCode);
}
//implement
/**
* 回复数据
*/
reply(res: InternalHonoRes, body: any, statusCode?: StatusCode) {
if (statusCode) res.status(statusCode);
res.send(body);
}
/**
* implement
* 没有响应数据时用于结束http响应
*/
end(res: InternalHonoRes, message?: string) {
res.send(message ?? null);
}
//implement
render(res: InternalHonoRes, view: string | Promise<string>, options: any) {
res.send(res.render(view));
}
//implement
redirect(res: InternalHonoRes, statusCode: RedirectStatusCode, url: string) {
res.send(res.redirect(url, statusCode));
}
//implement
setErrorHandler(handler: ErrorHandler<InternalHonoReq, InternalHonoRes>) {
this.instance.onError(async (err: Error, ctx: Context) => {
await handler(err, createHonoReq(ctx, { body: {}, params: {}, rawBody: undefined }), createHonoRes(ctx));
return sendResult(ctx, {});
});
}
//implement
setNotFoundHandler(handler: RequestHandler<InternalHonoReq, InternalHonoRes>) {
this.instance.notFound(async (ctx: Context) => {
await handler(createHonoReq(ctx, { body: {}, params: {}, rawBody: undefined }), createHonoRes(ctx));
return sendResult(ctx, {});
});
}
//implement
isHeadersSent(res: InternalHonoRes): boolean {
return res.finalized;
}
//implement
setHeader(res: InternalHonoRes, name: string, value: string) {
Reflect.get(res, NEST_HEADERS)[name] = value.toLowerCase();
}
//implement
getType(): string {
return "hono";
}
#isParserRegistered: boolean = false;
// implement nest 初始化时设定的一些默认解析器
registerParserMiddleware(prefix: string = "", rawBody?: boolean) {
if (this.#isParserRegistered) return;
this.useBodyParser("application/x-www-form-urlencoded", (honoReq) => honoReq.parseBody());
this.useBodyParser("multipart/form-data", (honoReq) => honoReq.parseBody());
this.useBodyParser("application/json", (honoReq) => honoReq.json());
this.useBodyParser("text/plain", (honoReq) => honoReq.text());
this.#isParserRegistered = true;
}
//implement
override enableCors(options?: CORSOptions): void;
override enableCors(options: any): void;
override enableCors(options?: any) {
this.instance.use(cors(options));
}
//implement
createMiddlewareFactory(requestMethod: RequestMethod): (path: string, callback: Function) => any {
return (path: string, callback: Function) => {
const nestMiddleware = callback as NestMiddlewareHandler;
async function handler(ctx: Context, next: () => Promise<void>) {
// let calledNext = false;
// await nestMiddleware(ctx, ctx, () => {
// calledNext = true;
// });
// if (calledNext) return next();
await nestMiddleware(
ctx.req,
ctx,
// createHonoReq(ctx, { body: {}, params: {}, rawBody: undefined }),
// createHonoRes(ctx),
next
);
}
if (requestMethod === RequestMethod.ALL || (requestMethod as number) === -1) {
this.instance.use(path, handler);
} else {
const method = RequestMethod[requestMethod];
this.instance.on(method, path, handler);
}
};
}
//implement
applyVersionFilter(): () => () => any {
throw new Error("Versioning not yet supported in Hono"); //TODO applyVersionFilter
}
//@ts-ignore nest 10 implement
override getHeader = undefined;
//@ts-ignore nest 10 implement
override appendHeader = undefined;
}
type NestMiddlewareHandler = (req: Context['req'], res: Context, next: Next) => Promise<void>;
function getRouteAndHandler(
pathOrHandler: string | NestHonoHandler,
handler?: NestHonoHandler,
): [string, NestHonoHandler] {
let path: string;
if (typeof pathOrHandler === "function") {
handler = pathOrHandler;
path = "";
} else {
path = pathOrHandler;
handler = handler!;
}
return [path, handler];
}
function transformsNestCrosOption(options: CorsOptions | CorsOptionsDelegate<InternalHonoReq> = {}): CORSOptions {
if (typeof options === "function") throw new Error("options must be an object");
let { origin } = options;
if (typeof origin === "function") origin = undefined; //TODO
return {
//@ts-ignore 需要转换
origin, //TODO
allowHeaders: toArray(options.allowedHeaders),
allowMethods: toArray(options.methods),
exposeHeaders: toArray(options.exposedHeaders),
credentials: options.credentials,
maxAge: options.maxAge,
};
}
function toArray<T>(item?: T | T[]): T[] | undefined {
if (!item) return undefined;
return item instanceof Array ? item : [item];
}

View File

@@ -0,0 +1,28 @@
import type { RequestHandler } from "@nestjs/common/interfaces";
import type { Server } from "node:http";
//see nest project: packages\core\router\route-params-factory.ts
export interface NestReqRequired {
rawBody?: any;
session?: any;
files?: any;
body: Record<string, any>;
params: Record<string, string | undefined>;
ip: string;
hosts?: Record<string, string | undefined>;
query: Record<string, string | undefined>;
headers: Record<string, string | undefined>;
}
export type NestHandler<Req extends NestReqRequired, Res extends object> = RequestHandler<Req, Res>;
//see nest project: packages/core/nest-application.ts
export interface NestHttpServerRequired {
once(event: "error", listener: (err: any) => void): this;
removeListener(event: "error", listener: (...args: any[]) => void): this;
// 必须返回一个对象或者字符串,否则 NestApplication.listen() 返回的 Promise 将用于不会解决
address(): ReturnType<Server["address"]>;
listen?(port: number, ...args: any[]): void;
close?(callback: (err?: any) => void): void;
}

View File

@@ -0,0 +1,154 @@
import type { NestHttpServerRequired, NestReqRequired } from "./_nest.ts";
import type { Context } from "hono";
export function createHonoReq(
ctx: Context,
info: {
body: Record<string, any>;
params: Record<string, string>;
rawBody: any;
},
): InternalHonoReq {
const { params, rawBody } = info;
let body = info.body ?? {};
const honoReq = ctx as InternalHonoReq;
if (Object.hasOwn(honoReq, IS_HONO_REQUEST)) return honoReq;
const nestReq: Omit<NestReqRequired, "headers" | "query" | "body" | "ip"> = {
rawBody,
session: ctx.get("session") ?? {},
//hosts: {}, //nest sets up hosts automatically
files: {}, //TODO files
params: params,
};
Object.assign(honoReq, nestReq);
let ip: string | undefined;
let headers: Record<string, any> | undefined;
let query: Record<string, any> | undefined;
Object.defineProperties(honoReq, {
headers: {
get(this: Context) {
return headers ?? (headers = Object.fromEntries(this.req.raw.headers as any));
},
enumerable: true,
},
query: {
get(this: Context) {
return query ?? (query = this.req.query());
},
enumerable: true,
},
// Hono Context already has a body attribute, so we need to use a Proxy to override it
body: {
value: new Proxy(ctx.body, {
get(rawBody, key: string) {
return body[key];
},
ownKeys(rawBody) {
return Object.keys(body);
},
apply(rawBody, thisArg, args) {
return rawBody.apply(ctx, args as any);
},
}),
enumerable: true,
writable: false,
},
ip: {
get() {
return "";
},
enumerable: true,
},
[IS_HONO_REQUEST]: {
value: true,
},
});
return honoReq;
}
const IS_HONO_REQUEST = Symbol("Is Hono Request");
function mountResponse(ctx: Context, data: any) {
Reflect.set(ctx, NEST_BODY, data);
}
export function createHonoRes(context: Context): InternalHonoRes {
Reflect.set(context, "send", function (this: Context, response: any = this.newResponse(null)) {
mountResponse(this, response);
});
return context as any as InternalHonoRes;
}
export function sendResult(ctx: Context, headers: Record<string, string>) {
const body = Reflect.get(ctx, NEST_BODY);
let response: Response;
switch (typeof body) {
case "string": {
response = ctx.text(body);
break;
}
case "object": {
if (body === null) response = ctx.body(null);
else if (body instanceof Response) response = body;
else if (body instanceof ReadableStream) response = ctx.body(body);
else if (body instanceof Uint8Array) response = ctx.body(body as any);
else if (body instanceof Blob) response = ctx.body(body.stream());
else response = ctx.json(body);
break;
}
case "undefined": {
response = ctx.body(null);
break;
}
default:
return ctx.text("HonoAdapter cannot convert unknown types", 500);
}
Object.entries(headers).forEach(([key, value]) => response.headers.set(key, value));
return response;
}
export type InternalHonoReq = NestReqRequired & Context;
export type InternalHonoRes = Context & {
send(data?: any): void;
};
const NEST_BODY = Symbol("nest_body");
const FakeHttpServer = Symbol("Fake HTTP Server");
export function isFakeHttpServer(server: any) {
return Reflect.get(server, FakeHttpServer);
}
export function createNestRequiredHttpServer(): NestHttpServerRequired {
return new Proxy(
{
once() {
return this;
},
removeListener() {
return this;
},
address() {
return null;
},
then: undefined,
[FakeHttpServer]: true,
},
{
get(target, key) {
if (typeof key === "symbol" || Object.hasOwn(target, key)) {
//@ts-ignore
return target[key];
}
console.trace("Nest Adapter: Nest uses undefined httpServer property", key);
const value = function () {
throw new Error(`Nest call undefined httpServer property '${String(key)}'`);
};
//@ts-ignore
target[key] = value;
return value;
},
},
);
}

View File

@@ -0,0 +1,18 @@
import type { HonoRequest } from "hono/request";
import type { ServeStaticOptions } from "hono/serve-static";
export type { InternalHonoRes as HonoResponse } from "./_util.ts";
export type { HonoRequest } from "hono/request";
export interface HonoApplicationExtra {
useBodyParser(contentType: string, parser: HonoBodyParser): void;
/**
* Sets a base directory for public assets.
* Example `app.useStaticAssets('public', { root: '/' })`
* @returns {this}
*/
useStaticAssets(path: string, options: ServeStaticOptions): Promise<this>;
}
export type HonoBodyParser = (context: HonoRequest) => Promise<any> | any;

View File

@@ -0,0 +1,3 @@
export * from "./hono.impl";
export * from "./HonoApplication";
export * from "./HonoAdapter";

View File

@@ -0,0 +1,71 @@
export const MSG = {
OBJECT: {
BILL: 'Bill',
COURSE: 'Course',
COMBO: 'Combo',
UNIT: 'Unit',
RESOURCE: 'Resource',
NOTI: 'Noti',
OBJECT: '[OBJECT]',
},
FRONTEND: {
DELETE_SUCCESS: 'Deleted successfully!',
UPDATE_SUCCESS: 'Updated successfully!',
USERNAME_DUPLICATED: 'Username has been used',
USERNAME_NOT_EXIST: 'Username not exists',
OTP_NOT_EXIST: 'Otp not exists',
USERNAME_INVALID: 'Username not valid format',
EMAIL_DUPLICATED: 'Duplicate email',
EMAIL_NOT_EXIST: 'Email not found',
EMAIL_INACTIVE_DUPLICATED: 'Email duplicate',
ACCOUNT_INACTIVE: 'Email not active',
WRONG_PASSWORD: 'Wrong password',
DOUPLICATE_PASSWORD: 'The same password',
TOO_MANY_REQUEST: 'Too many request',
ID_NOTFOUND: 'Id not found',
NO_PERMISSION: 'You might have not permission to do this action',
OBJECT_NOT_FOUND: '[OBJECT] does not exist!',
AUTH: {
UN_AUTH: 'unauthorized!',
LOGIN_SUCCESS: 'Đăng nhập thành công!',
LOGIN_ERR: 'Email hoặc mật khẩu không đúng!',
// AUTH_FAILED: 'Invalid login info!',
AUTH_FAILED: 'Thông tin đăng nhập không chính xác!',
AUTH_FAILED_EMAIL_NOT_EXIST: 'Not found Email!',
AUTH_FAILED_WRONG_PASSWORD: 'Wrong password!',
},
ACTIVE_USER: {
ERROR: 'Internal error',
TOKEN_INVALID: 'Token invalid',
EMAIL_VERIFIED: 'Email have been verify',
EMAIL_VERIFY_SUCCESS: 'Success verify email',
SUCCESS: 'Account success verify',
EXPIRY: 'Expiry account',
},
},
RESPONSE: {
SUCCESS: 'Thành công!',
GET_REQUEST_OK: 'GET OK',
POST_REQUEST_OK: 'POST OK',
PUT_REQUEST_OK: 'Cập nhật thành công!',
PATCH_REQUEST_OK: 'PATCH OK',
DELETE_REQUEST_OK: 'DELETE OK',
CREATED: 'Đã tạo thành công!',
REQUEST_GET_FAIL: 'GET OK',
POST_REQUEST_FAIL: 'POST FAIL',
PUT_REQUEST_FAIL: 'PUT FAIL',
PATCH_REQUEST_FAIL: 'PATCH FAIL',
DELETE_REQUEST_FAIL: 'DELETE FAIL',
DUPLICATED: 'DUPLICATED',
PROCESSING: 'Processing request',
BAD_REQUEST: 'Bad request',
INTERNAL_SERVER_ERROR: 'Internal Server Error',
METHOD_NOT_ALLOWED: 'Method Not Allowed Error',
DELETE_BILL_ERROR: 'Không thể xoá hoá đơn đã thanh toán!',
},
UPLOAD: {
IMAGE_FILE_TYPE_ONLY: 'Forbidden image type',
DOCUMENT_FILE_TYPE_ONLY: 'Forbidden file type',
},
};

View File

@@ -0,0 +1,105 @@
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import * as path from 'path';
import { MSG } from '../constant/messages';
import { Context } from 'hono';
import { HttpAdapterHost } from '@nestjs/core';
import { sendResult } from '../adapter/hono/_util';
import { HonoResponse } from '../adapter/hono';
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(HttpExceptionFilter.name);
constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
private convertStack(stack: string): string {
return stack
.split('\n')
.map((row) => row.trim())[0];
}
async catch(exception: any, host: ArgumentsHost) {
// console.log(host.getArgs());
this.logger.error(this.convertStack(exception.stack));
// this.logger.error(JSON.stringify(exception, null, 2));
const ctx = host.switchToHttp().getResponse<HonoResponse>();
// const request = ctx.getRequest<Request>();
// const response = ctx.getResponse();
let statusCode: any = HttpStatus.INTERNAL_SERVER_ERROR;
let message: any = MSG.RESPONSE.INTERNAL_SERVER_ERROR;
if (exception instanceof HttpException) {
[statusCode, message] = this.handleHttpException(exception);
}
if (
String(exception.name) === 'QueryFailedError' &&
exception.errno === 1062
) {
[statusCode, message] = this.handleConflict();
}
if (String(exception.name) === 'ValidationError') {
[statusCode, message] = this.handleValidatorError(exception);
}
if (String(exception.name) === 'BadRequestException') {
[statusCode, message] = this.handleValidatorError(exception);
}
if (String(exception.name) === 'UnauthorizedException') {
[statusCode, message] = this.handleAuthError(exception);
}
// const user = request.user as User;
const time = new Date().toLocaleString();
const routePath = `${ctx.req.method} ${ctx.req.url}`;
const errorObject = {
error: {
time,
message,
path: routePath,
detail: {
stack: this.convertStack(exception.stack),
},
},
exception: exception.code || exception.name,
statusCode,
};
// sendResult(ctx, {});
ctx.status(statusCode);
ctx.send(errorObject);
// mountResponse
// httpAdapter.reply(ctx, errorObject, statusCode);
// return ctx.json(errorObject);
// ctx.set("__nest_response", ctx.json(errorObject, statusCode));
// return response.status(500).json({
// statusCode: 500,
// message: "Internal server error",
// });
}
handleHttpException(exception: any) {
const statusCode = Number(exception.getStatus());
const message = exception.response.error;
return [statusCode, message];
}
handleAuthError(exception: any) {
const statusCode = Number(exception.getStatus());
const message = exception.response.message;
return [statusCode, message];
}
handleValidatorError(exception: any) {
const statusCode = HttpStatus.BAD_REQUEST;
const message = exception.response.message[0].constraints;
return [statusCode, message];
}
handleConflict() {
const statusCode = HttpStatus.CONFLICT;
const message = `${MSG.FRONTEND.USERNAME_DUPLICATED} OR ${MSG.FRONTEND.EMAIL_DUPLICATED}`;
return [statusCode, message];
}
}

View File

@@ -0,0 +1,24 @@
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { map } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, any>
{
intercept(
context: ExecutionContext,
next: CallHandler,
) {
return next.handle().pipe(
map((data) => ({
success: true,
data,
})),
);
}
}

View File

@@ -0,0 +1,14 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { ZodValidationPipe } from 'nestjs-zod';
import { ZodError } from 'zod';
import { MSG } from '../constant/messages';
@Injectable()
export class CustomZodValidationPipe extends ZodValidationPipe {
protected exceptionFactory(error: ZodError) {
return new BadRequestException(
error,
MSG.RESPONSE.BAD_REQUEST,
);
}
}

View File

@@ -0,0 +1,13 @@
import { Controller, Get } from "@nestjs/common";
@Controller('auth')
export class AuthController {
constructor() {}
@Get('/')
index() {
return { message: 'Auth Controller is working' };
// throw new Error('Not implemented');
// return 'Hello Auth';
}
}

View File

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

View File

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

View File

@@ -7,9 +7,9 @@ import ToastService from 'primevue/toastservice';
import Tooltip from 'primevue/tooltip'; import Tooltip from 'primevue/tooltip';
import { createSSRApp } from 'vue'; import { createSSRApp } from 'vue';
import { RouterView } from 'vue-router'; import { RouterView } from 'vue-router';
import { withErrorBoundary } from '@/lib/hoc/withErrorBoundary'; import { withErrorBoundary } from '@/client/lib/hoc/withErrorBoundary';
import { vueSWR } from '@/lib/swr/use-swrv'; import { vueSWR } from '@/client/lib/swr/use-swrv';
import createAppRouter from '@/routes'; import createAppRouter from '@/client/routes';
const bodyClass = ":uno: font-sans text-gray-800 antialiased flex flex-col min-h-screen bg-gray-50"; const bodyClass = ":uno: font-sans text-gray-800 antialiased flex flex-col min-h-screen bg-gray-50";
function createApp() { function createApp() {
const pinia = createPinia(); const pinia = createPinia();

View File

@@ -13,7 +13,7 @@
"jsxImportSource": "vue", "jsxImportSource": "vue",
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"],
}, },
"experimentalDecorators": true, "experimentalDecorators": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,

View File

@@ -1,6 +1,6 @@
import { defineConfig, presetAttributify, presetTypography, presetWind4, transformerCompileClass, transformerVariantGroup } from 'unocss' import { defineConfig, presetAttributify, presetTypography, presetWind4, transformerCompileClass, transformerVariantGroup } from 'unocss'
import { presetBootstrapBtn } from "./bootstrap_btn"; import { presetBootstrapBtn } from "./bootstrap_btn";
import transformerClassnamesMinifier from './plugins/encodeClassTransformer' // import transformerClassnamesMinifier from './plugins/encodeClassTransformer'
export default defineConfig({ export default defineConfig({
presets: [ presets: [
presetWind4() as any, presetWind4() as any,
@@ -106,9 +106,11 @@ export default defineConfig({
], ],
transformers: [transformerVariantGroup(), transformerCompileClass({ transformers: [transformerVariantGroup(), transformerCompileClass({
classPrefix: "_", classPrefix: "_",
}),transformerClassnamesMinifier({ }),
trigger: ':m:', // transformerClassnamesMinifier({
})], // trigger: ':m:',
// })
],
preflights: [ preflights: [
{ {
getCSS: (context) => { getCSS: (context) => {

View File

@@ -21,7 +21,7 @@ export default defineConfig((env) => {
dts: true, // Generate TypeScript declaration file dts: true, // Generate TypeScript declaration file
}), }),
Components({ Components({
dirs: ["src/components"], dirs: ["src/client/components"],
extensions: ["vue", "tsx"], extensions: ["vue", "tsx"],
dts: true, dts: true,
dtsTsx: true, dtsTsx: true,
@@ -44,7 +44,7 @@ export default defineConfig((env) => {
build: { build: {
outDir: "dist/client", outDir: "dist/client",
rollupOptions: { rollupOptions: {
input: { index: "/src/client.ts" }, input: { index: "/src/client/index.ts" },
}, },
}, },
}, },