diff --git a/bun.config.ts b/bun.config.ts index c24904f..3d959da 100644 --- a/bun.config.ts +++ b/bun.config.ts @@ -1,15 +1,16 @@ import { buildWithBun } from '@kevisual/code-builder'; -await buildWithBun({ naming: 'app', entry: 'agent/main.ts', dts: true }); +const external: any[] = [] +await buildWithBun({ naming: 'app', entry: 'agent/main.ts', dts: true, external }); -await buildWithBun({ naming: 'router', entry: 'src/index.ts', dts: true }); +await buildWithBun({ naming: 'router', entry: 'src/index.ts', dts: true, external }); -await buildWithBun({ naming: 'router-browser', entry: 'src/app-browser.ts', target: 'browser', dts: true }); +await buildWithBun({ naming: 'router-browser', entry: 'src/app-browser.ts', target: 'browser', dts: true, external }); -await buildWithBun({ naming: 'router-define', entry: 'src/router-define.ts', target: 'browser', dts: true }); +await buildWithBun({ naming: 'router-define', entry: 'src/router-define.ts', target: 'browser', dts: true, external }); -await buildWithBun({ naming: 'router-simple', entry: 'src/router-simple.ts', dts: true }); +await buildWithBun({ naming: 'router-simple', entry: 'src/router-simple.ts', dts: true, external }); -await buildWithBun({ naming: 'opencode', entry: 'src/opencode.ts', dts: true }); +await buildWithBun({ naming: 'opencode', entry: 'src/opencode.ts', dts: true, external }); -await buildWithBun({ naming: 'ws', entry: 'src/ws.ts', dts: true }); +await buildWithBun({ naming: 'ws', entry: 'src/ws.ts', dts: true, external }); diff --git a/demo/simple/src/app-ws.ts b/demo/simple/src/app-ws.ts index 5b00af1..5199e4b 100644 --- a/demo/simple/src/app-ws.ts +++ b/demo/simple/src/app-ws.ts @@ -31,6 +31,7 @@ app.route({ }, }).define(async (ctx) => { ctx.body = '03'; + ctx.args.test return ctx; }).addTo(app); // app.server.on({ diff --git a/demo/simple/src/apps-https/app.ts b/demo/simple/src/apps-https/app.ts index ba75fa2..1a3b0ba 100644 --- a/demo/simple/src/apps-https/app.ts +++ b/demo/simple/src/apps-https/app.ts @@ -18,15 +18,6 @@ route01.run = async (ctx) => { ctx.body = '01'; return ctx; }; -app.use( - 'demo', - async (ctx) => { - ctx.body = '01'; - return ctx; - }, - { key: '01' }, -); - const route02 = new Route('demo', '02'); route02.run = async (ctx) => { ctx.body = '02'; diff --git a/demo/simple/src/apps-https/create-sign.ts b/demo/simple/src/apps-https/create-sign.ts deleted file mode 100644 index b8d2029..0000000 --- a/demo/simple/src/apps-https/create-sign.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createCert } from '@kevisual/router/sign'; -import { writeFileSync } from 'fs'; -const { key, cert } = createCert(); - -writeFileSync('https-key.pem', key); -writeFileSync('https-cert.pem', cert); \ No newline at end of file diff --git a/demo/simple/src/browser/app.ts b/demo/simple/src/browser/app.ts index e39929d..feed969 100644 --- a/demo/simple/src/browser/app.ts +++ b/demo/simple/src/browser/app.ts @@ -20,7 +20,7 @@ router .define(async (ctx) => { ctx.body = 'Hello, world!'; // throw new CustomError('error'); - throw new CustomError(5000, 'error'); + ctx.throw(5000, 'error'); }) .addTo(router); diff --git a/demo/simple/src/cert/index.ts b/demo/simple/src/cert/index.ts deleted file mode 100644 index b28f878..0000000 --- a/demo/simple/src/cert/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { createCert } from '@kevisual/router/src/sign.ts'; -import fs from 'node:fs'; - -const cert = createCert(); - -fs.writeFileSync('pem/https-private-key.pem', cert.key); -fs.writeFileSync('pem/https-cert.pem', cert.cert); -fs.writeFileSync( - 'pem/https-config.json', - JSON.stringify( - { - createTime: new Date().getTime(), - expireDate: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).getTime(), - expireTime: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(), - }, - null, - 2, - ), -); diff --git a/demo/simple/src/check-handle-context/app.ts b/demo/simple/src/check-handle-context/app.ts index f6c9d71..2755d8b 100644 --- a/demo/simple/src/check-handle-context/app.ts +++ b/demo/simple/src/check-handle-context/app.ts @@ -16,10 +16,16 @@ const queryApp = new QueryRouterServer(); app .route({ path: 'hello', + metadata: { + args: { + name: 'string', + }, + } }) .define(async (ctx) => { // console.log('hello', ctx); // console.log('hello', ctx.res); + ctx.query.name; console.log('hello', ctx.query.cookies); // ctx.res?.cookie?.('token', 'abc', { // domain: '*', // 设置为顶级域名,允许跨子域共享 diff --git a/demo/simple/src/check-schema/index.ts b/demo/simple/src/check-schema/index.ts deleted file mode 100644 index 09d2566..0000000 --- a/demo/simple/src/check-schema/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { createSchema } from '@kevisual/router'; - -const a = createSchema({ - type: 'string', - minLength: 1, - maxLength: 10, - regex: '^[a-zA-Z0-9_]+$', - required: false, -}); - -console.log(a.safeParse('1234567890')); -console.log(a.safeParse('').error); -console.log(a.safeParse(undefined)); -console.log(a.safeParse(null).error); diff --git a/demo/simple/src/simple-router/a.ts b/demo/simple/src/simple-router/a.ts deleted file mode 100644 index 64b231b..0000000 --- a/demo/simple/src/simple-router/a.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { SimpleRouter } from '@kevisual/router/src/router-simple.ts'; - -export const router = new SimpleRouter(); - -router.get('/', async (req, res) => { - console.log('get /'); -}); - -router.post('/post', async (req, res) => { - console.log('post /post'); - console.log('req body:', req, res); - res.end('post response'); -}); - -router.get('/user/:id', async (req, res) => { - console.log('get /user/:id', req.params); - res.end(`user id is ${req.params.id}`); -}); - -router.post('/user/:id', async (req, res) => { - console.log('post /user/:id params', req.params); - const body = await router.getBody(req); - console.log('post body:', body); - res.end(`post user id is ${req.params.id}`); -}); - -router.post('/user/:id/a', async (req, res) => { - console.log('post /user/:id', req.params); - res.end(`post user id is ${req.params.id} a`); -}); - -// router.parse({ url: 'http://localhost:3000/', method: 'GET' } as any, {} as any); -// router.parse({ url: 'http://localhost:3000/post', method: 'POST' } as any, {} as any); -// router.parse({ url: 'http://localhost:3000/user/1/a', method: 'GET' } as any, {} as any); -// router.parse({ url: 'http://localhost:3000/user/1/a', method: 'POST' } as any, {} as any); diff --git a/demo/simple/src/simple-router/b.ts b/demo/simple/src/simple-router/b.ts deleted file mode 100644 index 8da9093..0000000 --- a/demo/simple/src/simple-router/b.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { App } from '@kevisual/router/src/app.ts'; - -import { router } from './a.ts'; - -export const app = new App(); - -app.server.on([{ - fun: async (req, res) => { - console.log('Received request:', req.method, req.url); - const p = await router.parse(req, res); - if (p) { - console.log('Router parse result:', p); - } - } -}, { - id: 'abc', - path: '/ws', - io: true, - fun: async (data, end) => { - console.log('Custom middleware for /ws'); - console.log('Data received:', data); - end({ message: 'Hello from /ws middleware' }); - } -}]); - -app.server.listen(3004, () => { - console.log('Server is running on http://localhost:3004'); - - // fetch('http://localhost:3004/', { method: 'GET' }).then(async (res) => { - // const text = await res.text(); - // console.log('Response for GET /:', text); - // }); - - // fetch('http://localhost:3004/post', { - // method: 'POST', - // headers: { 'Content-Type': 'application/json' }, - // body: JSON.stringify({ message: 'Hello, server!' }), - // }).then(async (res) => { - // const text = await res.text(); - // console.log('Response for POST /post:', text); - // }); - - // fetch('http://localhost:3004/user/123', { method: 'GET' }).then(async (res) => { - // const text = await res.text(); - // console.log('Response for GET /user/123:', text); - // }); - - fetch('http://localhost:3004/user/456', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: 'User456' }), - }).then(async (res) => { - const text = await res.text(); - console.log('Response for POST /user/456:', text); - }); -}); \ No newline at end of file diff --git a/demo/simple/src/test-two-app/app.ts b/demo/simple/src/test-two-app/app.ts index fb31b65..ac9ded1 100644 --- a/demo/simple/src/test-two-app/app.ts +++ b/demo/simple/src/test-two-app/app.ts @@ -65,8 +65,6 @@ app.importRoutes(app1.exportRoutes()); app.importRoutes(app2.exportRoutes()); -app.importApp(app3); - app.listen(4003, () => { console.log(`http://localhost:4003/api/router?path=app1&key=02`); console.log(`http://localhost:4003/api/router?path=app1&key=01`); diff --git a/demo/simple/src/validator/app.ts b/demo/simple/src/validator/app.ts index 8ecff38..46a5a1f 100644 --- a/demo/simple/src/validator/app.ts +++ b/demo/simple/src/validator/app.ts @@ -26,41 +26,6 @@ qr.add( description: 'get project detail2', run: async (ctx: RouteContext) => { ctx!.body = 'project detail2'; - return ctx; - }, - validator: { - id: { - type: 'number', - required: true, - message: 'id is required', - }, - data: { - // @ts-ignore - type: 'object', - message: 'data query is error', - properties: { - name: { - type: 'string', - required: true, - message: 'name is required', - }, - age: { - type: 'number', - required: true, - message: 'age is error', - }, - friends: { - type: 'object', - properties: { - hair: { - type: 'string', - required: true, - message: 'hair is required', - }, - }, - }, - }, - }, }, }), ); @@ -73,7 +38,7 @@ const main = async () => { id: 4, data: { name: 'john', - age: 's'+13, + age: 's' + 13, friends: { hair: 'black', messages: 'hello', diff --git a/package.json b/package.json index fb0b98e..9a9bf1d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package", "name": "@kevisual/router", - "version": "0.0.83", + "version": "0.0.84", "description": "", "type": "module", "main": "./dist/router.js", @@ -37,7 +37,7 @@ "@types/xml2js": "^0.4.14", "eventemitter3": "^5.0.4", "fast-glob": "^3.3.3", - "hono": "^4.12.0", + "hono": "^4.12.2", "nanoid": "^5.1.6", "path-to-regexp": "^8.3.0", "send": "^1.2.1", diff --git a/src/app.ts b/src/app.ts index 173e49e..c46c8cb 100644 --- a/src/app.ts +++ b/src/app.ts @@ -19,7 +19,7 @@ type AppOptions = { appId?: string; }; -export type AppRouteContext = HandleCtx & RouteContext & { app: App }; +export type AppRouteContext = HandleCtx & RouteContext & { app: App }; /** * 封装了 Router 和 Server 的 App 模块,处理http的请求和响应,内置了 Cookie 和 Token 和 res 的处理 @@ -29,6 +29,7 @@ export class App extends QueryRouterServer> { declare appId: string; router: QueryRouterServer; server: ServerType; + declare context: AppRouteContext; constructor(opts?: AppOptions) { super({ initHandle: false, context: { needSerialize: true, ...opts?.routerContext } as any }); const router = this; diff --git a/src/route.ts b/src/route.ts index 1d2e8f1..15982e2 100644 --- a/src/route.ts +++ b/src/route.ts @@ -23,7 +23,7 @@ type BuildRouteContext = M extends { args?: infer A } : RouteContext : RouteContext; -export type RouteContext = { +export type RouteContext = { /** * 本地自己调用的时候使用,可以标识为当前自调用,那么 auth 就不许重复的校验 * 或者不需要登录的,直接调用 @@ -77,19 +77,19 @@ export type RouteContext = { ctx?: RouteContext & { [key: string]: any }, ) => Promise; /** 请求 route的返回结果,解析了body为data,就类同于 query.post获取的数据*/ - run?: (message: { path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) => Promise; + run?: (message: { path: string; key?: string; payload?: any }, ctx?: RouteContext) => Promise; index?: number; throw?: throwError['throw']; /** 是否需要序列化, 使用JSON.stringify和JSON.parse */ needSerialize?: boolean; -} & T; +} & T & U; export type SimpleObject = Record; export type Run = (ctx: Required>) => Promise; export type RunMessage = { path?: string; key?: string; id?: string; payload?: any; }; export type NextRoute = Pick; export type RouteMiddleware = | { - path: string; + path?: string; key?: string; id?: string; } @@ -148,7 +148,11 @@ export type RouteInfo = Pick; type ExtractMetadata = M extends { metadata?: infer Meta } ? Meta extends SimpleObject ? Meta : SimpleObject : SimpleObject; -export class Route implements throwError { +/** + * @M 是 route的 metadate的类型,默认是 SimpleObject + * @U 是 RouteContext 里 state的类型 + */ +export class Route implements throwError { /** * 一级路径 */ @@ -287,11 +291,11 @@ export const fromJSONSchema = schema.fromJSONSchema; * @parmas overwrite 是否覆盖已存在的route,默认true */ export type AddOpts = { overwrite?: boolean }; -export class QueryRouter implements throwError { +export class QueryRouter implements throwError { appId: string = ''; routes: Route[]; maxNextRoute = 40; - context?: RouteContext = {}; // default context for call + context?: RouteContext = {} as RouteContext; // default context for call constructor() { this.routes = []; } @@ -334,10 +338,10 @@ export class QueryRouter implements throwError { * @param ctx * @returns */ - async runRoute(path: string, key: string, ctx?: RouteContext) { + async runRoute(path: string, key: string, ctx?: RouteContext): Promise> { const route = this.routes.find((r) => r.path === path && r.key === key); const maxNextRoute = this.maxNextRoute; - ctx = (ctx || {}) as RouteContext; + ctx = (ctx || {}) as RouteContext; ctx.currentPath = path; ctx.currentId = route?.id; ctx.currentKey = key; @@ -353,7 +357,7 @@ export class QueryRouter implements throwError { ctx.code = 500; ctx.message = 'Too many nextRoute'; ctx.body = null; - return; + return ctx; } // run middleware if (route && route.middleware && route.middleware.length > 0) { @@ -408,7 +412,7 @@ export class QueryRouter implements throwError { const middleware = routeMiddleware[i]; if (middleware) { try { - await middleware.run(ctx as Required); + await middleware.run(ctx as Required>); } catch (e) { if (route?.isDebug) { console.error('=====debug====:middlerware error'); @@ -430,6 +434,7 @@ export class QueryRouter implements throwError { return ctx; } if (ctx.end) { + return ctx; } } } @@ -438,7 +443,7 @@ export class QueryRouter implements throwError { if (route) { if (route.run) { try { - await route.run(ctx as Required); + await route.run(ctx as Required>); } catch (e) { if (route?.isDebug) { console.error('=====debug====:route error'); @@ -493,7 +498,7 @@ export class QueryRouter implements throwError { } } // 如果没有找到route,返回404,这是因为出现了错误 - return Promise.resolve({ code: 404, body: 'Not found' }); + return Promise.resolve({ code: 404, body: 'Not found' } as RouteContext); } /** * 第一次执行 @@ -501,12 +506,12 @@ export class QueryRouter implements throwError { * @param ctx * @returns */ - async parse(message: { path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) { + async parse(message: { path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) { if (!message?.path) { - return Promise.resolve({ code: 404, body: null, message: 'Not found path' }); + return Promise.resolve({ code: 404, body: null, message: 'Not found path' } as RouteContext); } const { path, key = '', payload = {}, ...query } = message; - ctx = ctx || {}; + ctx = ctx || {} as RouteContext; ctx.query = { ...ctx.query, ...query, ...payload }; ctx.args = ctx.query; ctx.state = { ...ctx?.state }; @@ -540,7 +545,7 @@ export class QueryRouter implements throwError { * @param ctx * @returns */ - async call(message: { id?: string; path?: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) { + async call(message: { id?: string; path?: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) { let path = message.path; let key = message.key; // 优先 path + key @@ -581,7 +586,7 @@ export class QueryRouter implements throwError { * @param ctx * @returns */ - async run(message: { id?: string; path?: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) { + async run(message: { id?: string; path?: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) { const res = await this.call(message, { ...this.context, ...ctx }); return { code: res.code, @@ -595,7 +600,7 @@ export class QueryRouter implements throwError { * @param ctx */ setContext(ctx: RouteContext) { - this.context = ctx; + this.context = ctx as RouteContext; } getList(filter?: (route: Route) => boolean): RouteInfo[] { return this.routes.filter(filter || (() => true)).map((r) => { @@ -606,11 +611,11 @@ export class QueryRouter implements throwError { /** * 获取handle函数, 这里会去执行parse函数 */ - getHandle(router: QueryRouter, wrapperFn?: HandleFn, ctx?: RouteContext) { - return async (msg: { id?: string; path?: string; key?: string;[key: string]: any }, handleContext?: RouteContext) => { + getHandle(router: QueryRouter, wrapperFn?: HandleFn, ctx?: RouteContext) { + return async (msg: { id?: string; path?: string; key?: string;[key: string]: any }, handleContext?: RouteContext) => { try { const context = { ...ctx, ...handleContext }; - const res = await router.call(msg, context); + const res = await router.call(msg, context) as any; if (wrapperFn) { res.data = res.body; return wrapperFn(res, context); @@ -663,7 +668,7 @@ export class QueryRouter implements throwError { description: '列出当前应用下的所有的路由信息', middleware: opts?.middleware || [], run: async (ctx: RouteContext) => { - const tokenUser = ctx.state.tokenUser; + const tokenUser = ctx.state as unknown as { tokenUser?: any }; let isUser = !!tokenUser; const list = this.getList(opts?.filter).filter((item) => { if (item.id === 'auth' || item.id === 'auth-can' || item.id === 'check-auth-admin' || item.id === 'auth-admin') { @@ -724,9 +729,10 @@ interface HandleFn { * @description 移除server相关的功能,只保留router相关的功能,和http.createServer不相关,独立 * @template C 自定义 RouteContext 类型 */ -export class QueryRouterServer extends QueryRouter { +export class QueryRouterServer extends QueryRouter { declare appId: string; handle: any; + declare context: RouteContext; constructor(opts?: QueryRouterServerOpts) { super(); const initHandle = opts?.initHandle ?? true; @@ -767,18 +773,21 @@ export class QueryRouterServer extends Qu } return new Route>>(path, key, opts); } + prompt(description: string) { + return new Route(undefined, undefined, { description }); + } /** * 调用了handle * @param param0 * @returns */ - async run(msg: { id?: string; path?: string; key?: string; payload?: any }, ctx?: Partial> & { [key: string]: any }) { + async run(msg: { id?: string; path?: string; key?: string; payload?: any }, ctx?: Partial>) { const handle = this.handle; if (handle) { return handle(msg, ctx); } - return super.run(msg, ctx); + return super.run(msg, ctx as RouteContext); } } diff --git a/src/test/app-type.ts b/src/test/app-type.ts index 22ccfdb..af47945 100644 --- a/src/test/app-type.ts +++ b/src/test/app-type.ts @@ -1,10 +1,15 @@ -import { App } from "@/app.ts"; -import { QueryRouterServer } from "@/route.ts"; +import { App, AppRouteContext } from "@/app.ts"; +import { QueryRouterServer, RouteContext } from "@/app.ts"; import z from "zod"; +const route: RouteContext<{ customField: string }> = {} as any; +route.customField +const appRoute: AppRouteContext<{ customField: string }> = {} as any; +appRoute.customField // 示例 1: 使用 App,它会自动使用 AppRouteContext 作为 ctx 类型 const app = new App<{ customField: string; }>(); +app.context.customField = "customValue"; // 可以在 app.context 中添加自定义字段,这些字段会在 ctx 中可用 app.route({ path: 'test1', metadata: { @@ -15,23 +20,25 @@ app.route({ }).define(async (ctx) => { // ctx.app 是 App 类型 const appName = ctx.app.appId; - + // ctx.customField 来自自定义泛型参数 + const customField: string | undefined = ctx.customField; + // ctx.req 和 ctx.res 来自 HandleCtx const req = ctx.req; const res = ctx.res; - + // ctx.args 从 metadata.args 推断 const name: string = ctx.args.name; - - // ctx.customField 来自自定义泛型参数 - const customField: string | undefined = ctx.customField; - + const name2: string = ctx.query.name; + + ctx.body = `Hello ${name}!`; }); // 示例 2: 使用 QueryRouterServer,它可以传递自定义的 Context 类型 const router = new QueryRouterServer<{ routerContextField: number; }>(); +router.context.routerContextField router.route({ path: 'router-test', metadata: { @@ -42,7 +49,7 @@ router.route({ }).define(async (ctx) => { const value: number = ctx.args.value; const field: number | undefined = ctx.routerContextField; - + ctx.body = value; }); // 示例 3: 不带泛型参数的 QueryRouterServer,使用默认的 RouteContext @@ -56,7 +63,7 @@ defaultRouter.route({ }, }).define(async (ctx) => { const id: string = ctx.args.id; - + ctx.body = id; }); export { app, router, defaultRouter }; \ No newline at end of file