diff --git a/deno-router/check-env.ts b/deno-router/check-env.ts new file mode 100644 index 0000000..ef41317 --- /dev/null +++ b/deno-router/check-env.ts @@ -0,0 +1,6 @@ +const isDeno = typeof Deno !== "undefined" && !!Deno.version; +const isNode = typeof process !== "undefined" && !!process.versions?.node; + +console.log('process', process.version); +console.log("isDeno", isDeno); +console.log("isNode", isNode); \ No newline at end of file diff --git a/deno-router/deno.json b/deno-router/deno.json new file mode 100644 index 0000000..404eac3 --- /dev/null +++ b/deno-router/deno.json @@ -0,0 +1,42 @@ +{ + "tasks": { + "start": "deno run --allow-all index.ts" + }, + "imports": { + "@kevisual/router": "https://esm.xiongxiao.me/@kevisual/router" + }, + "compilerOptions": { + "types": [ + "./types", + "https://esm.xiongxiao.me/@kevisual/router@0.0.10/dist/router.d.ts" + ] + }, + "fmt": { + "files": { + + "include": ["src/"], + "exclude": [] + }, + "options": { + "lineWidth": 80, + "indentWidth": 2, + "useTabs": false, + "singleQuote": false, + "proseWrap": "preserve" + } + }, + "lint": { + "include": [ + "./src/**/*.ts", + "index.ts" + ], + "exclude": [ + "./types" + ], + "rules": { + "tags": ["recommended"], + "include": [], + "exclude": ["require-await"] + } + } +} \ No newline at end of file diff --git a/deno-router/deno.lock b/deno-router/deno.lock new file mode 100644 index 0000000..9684b23 --- /dev/null +++ b/deno-router/deno.lock @@ -0,0 +1,38 @@ +{ + "version": "4", + "specifiers": { + "npm:@types/node@*": "22.12.0" + }, + "npm": { + "@types/node@22.12.0": { + "integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==", + "dependencies": [ + "undici-types" + ] + }, + "undici-types@6.20.0": { + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + } + }, + "redirects": { + "http://esm.xiongxiao.me/@types/ws@~8.18.1/index.d.mts": "http://esm.xiongxiao.me/@types/ws@8.18.1/index.d.mts", + "http://esm.xiongxiao.me/bufferutil@^4.0.1?target=denonext": "http://esm.xiongxiao.me/bufferutil@4.0.9?target=denonext", + "http://esm.xiongxiao.me/node-gyp-build@^4.3.0?target=denonext": "http://esm.xiongxiao.me/node-gyp-build@4.8.4?target=denonext", + "http://esm.xiongxiao.me/utf-8-validate@%3E=5.0.2?target=denonext": "http://esm.xiongxiao.me/utf-8-validate@6.0.5?target=denonext", + "http://esm.xiongxiao.me/ws@^8.18.1?target=denonext": "http://esm.xiongxiao.me/ws@8.18.1?target=denonext", + "https://esm.xiongxiao.me/@kevisual/router": "http://esm.xiongxiao.me/@kevisual/router@0.0.10", + "https://esm.xiongxiao.me/@kevisual/router/router": "http://esm.xiongxiao.me/@kevisual/router@0.0.10/router" + }, + "remote": { + "http://esm.xiongxiao.me/@kevisual/router@0.0.10": "f7e3ea672cef9b532b62cc557f3369afbe7bdfd35001a62cd246033b303af048", + "http://esm.xiongxiao.me/@kevisual/router@0.0.10/denonext/router.mjs": "ee734dfbbe30415695d67e9d735e968e0e4554dc77b14368603a51273641a580", + "http://esm.xiongxiao.me/bufferutil@4.0.9/denonext/bufferutil.mjs": "13dca4d5bb2c68cbe119f880fa3bd785b9a81a8e02e0834dae604b4b85295cd8", + "http://esm.xiongxiao.me/bufferutil@4.0.9?target=denonext": "e32574569ab438facfcc3f412c659b0719bbf05477136ca176938c9a3ac45125", + "http://esm.xiongxiao.me/node-gyp-build@4.8.4/denonext/node-gyp-build.mjs": "9a86f2d044fc77bd60aaa3d697c2ba1b818da5fb1b9aaeedec59a40b8e908803", + "http://esm.xiongxiao.me/node-gyp-build@4.8.4?target=denonext": "261a6cedf1fdbf159798141ba1e2311ac1510682c5c8b55dacc8cf5fdee4aa06", + "http://esm.xiongxiao.me/utf-8-validate@6.0.5/denonext/utf-8-validate.mjs": "66b8ea532a0c745068f5b96ddb1bae332c3036703243541d2e89e66331974d98", + "http://esm.xiongxiao.me/utf-8-validate@6.0.5?target=denonext": "071bc33ba1a58297e23a34d69dd589fd06df04b0f373b382ff5da544a623f271", + "http://esm.xiongxiao.me/ws@8.18.1/denonext/ws.mjs": "9c6726c262f19d2dbbd5b5224d9f118cdbeb793ea475ecd40de2b778886a5e16", + "http://esm.xiongxiao.me/ws@8.18.1?target=denonext": "e99b670fc49b38e15a7576ddcd5bb01e123fe9b3a017db7f97898127811b4e27" + } +} diff --git a/deno-router/index.ts b/deno-router/index.ts new file mode 100644 index 0000000..2559e87 --- /dev/null +++ b/deno-router/index.ts @@ -0,0 +1,24 @@ +// @ts-types="https://esm.xiongxiao.me/@kevisual/router/dist/router.d.ts?raw" +import { App } from '@kevisual/router'; + +console.log('meta', import.meta.url) +// deno run --allow-all index.ts +// deno run --config ./deno.json --allow-all index.ts +console.log('hello world'); + +const app = new App(); + +app + .route({ + path: 'demo', + }) + .define(async (ctx) => { + ctx.body = 'hello world'; + }) + .addTo(app); + +app.listen(3000, () => { + console.log('Server is running on http://localhost:3000'); +}); + +console.log(`http://localhost:3000/api/router?path=demo`); \ No newline at end of file diff --git a/deno-router/types/router.d.ts b/deno-router/types/router.d.ts new file mode 100644 index 0000000..f032635 --- /dev/null +++ b/deno-router/types/router.d.ts @@ -0,0 +1,618 @@ +import { Schema } from 'http://esm.xiongxiao.me/zod@3.24.3/index.d.ts'; +export { Schema } from 'http://esm.xiongxiao.me/zod@3.24.3/index.d.ts'; +import http, { IncomingMessage, ServerResponse } from 'node:http'; +import https from 'node:https'; +import http2 from 'node:http2'; +import * as cookie from 'http://esm.xiongxiao.me/cookie@1.0.2/dist/index.d.ts'; +import { WebSocketServer, WebSocket } from 'http://esm.xiongxiao.me/@types/ws@~8.18.1/index.d.mts'; + +type BaseRule = { + value?: any; + required?: boolean; + message?: string; +}; +type RuleString = { + type: 'string'; + minLength?: number; + maxLength?: number; + regex?: string; +} & BaseRule; +type RuleNumber = { + type: 'number'; + min?: number; + max?: number; +} & BaseRule; +type RuleBoolean = { + type: 'boolean'; +} & BaseRule; +type RuleArray = { + type: 'array'; + items: Rule; + minItems?: number; + maxItems?: number; +} & BaseRule; +type RuleObject = { + type: 'object'; + properties: { + [key: string]: Rule; + }; +} & BaseRule; +type RuleAny = { + type: 'any'; +} & BaseRule; +type Rule = RuleString | RuleNumber | RuleBoolean | RuleArray | RuleObject | RuleAny; +declare const createSchema: (rule: Rule) => Schema; + +type RouterContextT = { + code?: number; + [key: string]: any; +}; +type RouteContext = { + query?: { + [key: string]: any; + }; + /** return body */ + body?: number | string | Object; + /** return code */ + code?: number; + /** return msg */ + message?: string; + state?: S; + currentPath?: string; + currentKey?: string; + currentRoute?: Route; + progress?: [[string, string]][]; + nextQuery?: { + [key: string]: any; + }; + end?: boolean; + queryRouter?: QueryRouter; + error?: any; + /** 请求 route的返回结果,包函ctx */ + call?: (message: { + path: string; + key?: string; + payload?: any; + [key: string]: any; + } | { + id: string; + apyload?: any; + [key: string]: any; + }, ctx?: RouteContext & { + [key: string]: any; + }) => Promise; + /** 请求 route的返回结果,不包函ctx */ + queryRoute?: (message: { + path: string; + key?: string; + payload?: any; + }, ctx?: RouteContext & { + [key: string]: any; + }) => Promise; + index?: number; + throw?: (code?: number | string, message?: string, tips?: string) => void; + /** 是否需要序列化 */ + needSerialize?: boolean; +} & T; +type Run = (ctx: RouteContext) => Promise; +type NextRoute = Pick; +type RouteOpts = { + path?: string; + key?: string; + id?: string; + run?: Run; + nextRoute?: NextRoute; + description?: string; + metadata?: { + [key: string]: any; + }; + middleware?: Route[] | string[]; + type?: 'route' | 'middleware'; + /** + * validator: { + * packageName: { + * type: 'string', + * required: true, + * }, + * } + */ + validator?: { + [key: string]: Rule; + }; + schema?: { + [key: string]: Schema; + }; + isVerify?: boolean; + verify?: (ctx?: RouteContext, dev?: boolean) => boolean; + verifyKey?: (key: string, ctx?: RouteContext, dev?: boolean) => boolean; + /** + * $#$ will be used to split path and key + */ + idUsePath?: boolean; + isDebug?: boolean; +}; +type DefineRouteOpts = Omit; +declare const pickValue: readonly ["path", "key", "id", "description", "type", "validator", "middleware"]; +type RouteInfo = Pick; +declare class Route { + /** + * 一级路径 + */ + path?: string; + /** + * 二级路径 + */ + key?: string; + id?: string; + share?: boolean; + run?: Run; + nextRoute?: NextRoute; + description?: string; + metadata?: { + [key: string]: any; + }; + middleware?: (Route | string)[]; + type?: string; + private _validator?; + schema?: { + [key: string]: Schema; + }; + data?: any; + /** + * 是否需要验证 + */ + isVerify?: boolean; + /** + * 是否开启debug,开启后会打印错误信息 + */ + isDebug?: boolean; + constructor(path: string, key?: string, opts?: RouteOpts); + private createSchema; + /** + * set validator and create schema + * @param validator + */ + set validator(validator: { + [key: string]: Rule; + }); + get validator(): { + [key: string]: Rule; + }; + /** + * has code, body, message in ctx, return ctx if has error + * @param ctx + * @param dev + * @returns + */ + verify(ctx: RouteContext, dev?: boolean): void; + /** + * Need to manully call return ctx fn and configure body, code, message + * @param key + * @param ctx + * @param dev + * @returns + */ + verifyKey(key: string, ctx: RouteContext, dev?: boolean): { + message: string; + path: string; + key: string; + error: any; + } | { + message: string; + path: string; + key: string; + error?: undefined; + }; + setValidator(validator: { + [key: string]: Rule; + }): this; + define(opts: DefineRouteOpts): this; + define(fn: Run): this; + define(key: string, fn: Run): this; + define(path: string, key: string, fn: Run): this; + addTo(router: QueryRouter | { + add: (route: Route) => void; + [key: string]: any; + }): void; + setData(data: any): this; + throw(code?: number | string, message?: string, tips?: string): void; +} +declare class QueryRouter { + routes: Route[]; + maxNextRoute: number; + context?: RouteContext; + constructor(); + add(route: Route): void; + /** + * remove route by path and key + * @param route + */ + remove(route: Route | { + path: string; + key?: string; + }): void; + /** + * remove route by id + * @param uniqueId + */ + removeById(unique: string): void; + /** + * 执行route + * @param path + * @param key + * @param ctx + * @returns + */ + runRoute(path: string, key: string, ctx?: RouteContext): any; + /** + * 第一次执行 + * @param message + * @param ctx + * @returns + */ + parse(message: { + path: string; + key?: string; + payload?: any; + }, ctx?: RouteContext & { + [key: string]: any; + }): Promise; + /** + * 返回的数据包含所有的context的请求返回的内容,可做其他处理 + * @param message + * @param ctx + * @returns + */ + call(message: { + id?: string; + path?: string; + key?: string; + payload?: any; + }, ctx?: RouteContext & { + [key: string]: any; + }): Promise; + /** + * 请求 result 的数据 + * @param message + * @param ctx + * @returns + */ + queryRoute(message: { + path: string; + key?: string; + payload?: any; + }, ctx?: RouteContext & { + [key: string]: any; + }): Promise<{ + code: any; + data: any; + message: any; + }>; + setContext(ctx: RouteContext): Promise; + getList(): RouteInfo[]; + getHandle(router: QueryRouter, wrapperFn?: HandleFn, ctx?: RouteContext): (msg: { + path: string; + key?: string; + [key: string]: any; + }, handleContext?: RouteContext) => Promise<{ + [key: string]: any; + code: string; + data?: any; + message?: string; + } | { + code: any; + data: any; + message: any; + } | { + code: number; + message: any; + data?: undefined; + }>; + exportRoutes(): Route<{ + [key: string]: any; + }>[]; + importRoutes(routes: Route[]): void; + importRouter(router: QueryRouter): void; + throw(code?: number | string, message?: string, tips?: string): void; + hasRoute(path: string, key?: string): Route<{ + [key: string]: any; + }>; +} +type QueryRouterServerOpts = { + handleFn?: HandleFn; + context?: RouteContext; +}; +interface HandleFn { + (msg: { + path: string; + [key: string]: any; + }, ctx?: any): { + code: string; + data?: any; + message?: string; + [key: string]: any; + }; + (res: RouteContext): any; +} +/** + * QueryRouterServer + * @description 移除server相关的功能,只保留router相关的功能,和http.createServer不相关,独立 + */ +declare class QueryRouterServer extends QueryRouter { + handle: any; + constructor(opts?: QueryRouterServerOpts); + setHandle(wrapperFn?: HandleFn, ctx?: RouteContext): void; + use(path: string, fn: (ctx: any) => any, opts?: RouteOpts): void; + addRoute(route: Route): void; + Route: typeof Route; + route(opts: RouteOpts): Route>; + route(path: string, key?: string): Route>; + route(path: string, opts?: RouteOpts): Route>; + route(path: string, key?: string, opts?: RouteOpts): Route>; + /** + * 等于queryRoute,但是调用了handle + * @param param0 + * @returns + */ + run({ path, key, payload }: { + path: string; + key?: string; + payload?: any; + }): Promise; +} + +declare class Connect { + path: string; + key?: string; + _fn?: (ctx?: RouteContext) => Promise; + description?: string; + connects: { + path: string; + key?: string; + }[]; + share: boolean; + constructor(path: string); + use(path: string): void; + useList(paths: string[]): void; + useConnect(connect: Connect): void; + useConnectList(connects: Connect[]): void; + getPathList(): string[]; + set fn(fn: (ctx?: RouteContext) => Promise); + get fn(): (ctx?: RouteContext) => Promise; +} +declare class QueryConnect { + connects: Connect[]; + constructor(); + add(connect: Connect): void; + remove(connect: Connect): void; + getList(): { + path: string; + key: string; + }[]; +} + +type Listener = (...args: any[]) => void; +type CookieFn = (name: string, value: string, options?: cookie.SerializeOptions, end?: boolean) => void; +type HandleCtx = { + req: IncomingMessage & { + cookies: Record; + }; + res: ServerResponse & { + /** + * cookie 函数, end 参数用于设置是否立即设置到响应头,设置了后面的cookie再设置会覆盖前面的 + */ + cookie: CookieFn; + }; +}; +type Cors = { + /** + * @default '*'' + */ + origin?: string | undefined; +}; +type ServerOpts = { + /**path default `/api/router` */ + path?: string; + /**handle Fn */ + handle?: (msg?: { + path: string; + key?: string; + [key: string]: any; + }, ctx?: { + req: http.IncomingMessage; + res: http.ServerResponse; + }) => any; + cors?: Cors; + httpType?: 'http' | 'https' | 'http2'; + httpsKey?: string; + httpsCert?: string; +}; +declare class Server { + path: string; + private _server; + handle: ServerOpts['handle']; + private _callback; + private cors; + private hasOn; + private httpType; + private options; + constructor(opts?: ServerOpts); + listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void): void; + listen(port: number, hostname?: string, listeningListener?: () => void): void; + listen(port: number, backlog?: number, listeningListener?: () => void): void; + listen(port: number, listeningListener?: () => void): void; + listen(path: string, backlog?: number, listeningListener?: () => void): void; + listen(path: string, listeningListener?: () => void): void; + listen(handle: any, backlog?: number, listeningListener?: () => void): void; + listen(handle: any, listeningListener?: () => void): void; + createServer(): http.Server | https.Server | http2.Http2SecureServer; + setHandle(handle?: any): void; + /** + * get callback + * @returns + */ + createCallback(): (req: IncomingMessage, res: ServerResponse) => Promise; + get handleServer(): any; + set handleServer(fn: any); + /** + * 兜底监听,当除开 `/api/router` 之外的请求,框架只监听一个api,所以有其他的请求都执行其他的监听 + * @description 主要是为了兼容其他的监听 + * @param listener + */ + on(listener: Listener | Listener[]): void; + get callback(): any; + get server(): http.Server | https.Server | http2.Http2SecureServer; +} + +/** + * get params and body + * 优先原则 + * 1. 请求参数中的 payload 的token 优先 + * 2. 请求头中的 authorization 优先 + * 3. 请求头中的 cookie 优先 + * @param req + * @param res + * @returns + */ +declare const handleServer: (req: IncomingMessage, res: ServerResponse) => Promise<{ + cookies: Record; + token: string; +}>; + +/** 自定义错误 */ +declare class CustomError extends Error { + code?: number; + data?: any; + message: string; + tips?: string; + constructor(code?: number | string, message?: string, tips?: string); + static fromCode(code?: number): CustomError; + static fromErrorData(code?: number, data?: any): CustomError; + static parseError(e: CustomError): { + code: number; + data: any; + message: string; + tips: string; + }; + parse(e?: CustomError): { + code: number; + data: any; + message: string; + tips: string; + }; +} + +type WsServerBaseOpts = { + wss?: WebSocketServer; + path?: string; +}; +type ListenerFn = (message: { + data: Record; + ws: WebSocket; + end: (data: any) => any; +}) => Promise; +declare class WsServerBase { + wss: WebSocketServer; + path: string; + listeners: { + type: string; + listener: ListenerFn; + }[]; + listening: boolean; + constructor(opts: WsServerBaseOpts); + setPath(path: string): void; + listen(): void; + addListener(type: string, listener: ListenerFn): void; + removeListener(type: string): void; +} +declare class WsServer extends WsServerBase { + server: Server; + constructor(server: Server, opts?: any); + initListener(): void; + listen(): void; +} + +type RouterHandle = (msg: { + path: string; + [key: string]: any; +}) => { + code: string; + data?: any; + message?: string; + [key: string]: any; +}; +type AppOptions = { + router?: QueryRouter; + server?: Server; + /** handle msg 关联 */ + routerHandle?: RouterHandle; + routerContext?: RouteContext; + serverOptions?: ServerOpts; + io?: boolean; + ioOpts?: { + routerHandle?: RouterHandle; + routerContext?: RouteContext; + path?: string; + }; +}; +type AppReqRes = HandleCtx; +/** + * 封装了 Router 和 Server 的 App 模块,处理http的请求和响应,内置了 Cookie 和 Token 和 res 的处理 + */ +declare class App { + router: QueryRouter; + server: Server; + io: WsServer; + constructor(opts?: AppOptions); + listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void): void; + listen(port: number, hostname?: string, listeningListener?: () => void): void; + listen(port: number, backlog?: number, listeningListener?: () => void): void; + listen(port: number, listeningListener?: () => void): void; + listen(path: string, backlog?: number, listeningListener?: () => void): void; + listen(path: string, listeningListener?: () => void): void; + listen(handle: any, backlog?: number, listeningListener?: () => void): void; + listen(handle: any, listeningListener?: () => void): void; + use(path: string, fn: (ctx: any) => any, opts?: RouteOpts): void; + addRoute(route: Route): void; + add: (route: Route) => void; + Route: typeof Route; + route(opts: RouteOpts): Route; + route(path: string, key?: string): Route; + route(path: string, opts?: RouteOpts): Route; + route(path: string, key?: string, opts?: RouteOpts): Route; + call(message: { + path: string; + key?: string; + payload?: any; + }, ctx?: RouteContext & { + [key: string]: any; + }): Promise; + queryRoute(path: string, key?: string, payload?: any, ctx?: RouteContext & { + [key: string]: any; + }): Promise<{ + code: any; + data: any; + message: any; + }>; + exportRoutes(): Route<{ + [key: string]: any; + }>[]; + importRoutes(routes: any[]): void; + importApp(app: App): void; + throw(code?: number | string, message?: string, tips?: string): void; +} + +export { App, Connect, CustomError, QueryConnect, QueryRouter, QueryRouterServer, Route, Server, createSchema, handleServer }; +export type { RouteContext, RouteOpts, Rule, Run };