From 2ba7b9f0a9b93267d9ea455679e1bc664510e39c Mon Sep 17 00:00:00 2001 From: Matheus Sampaio Queiroga Date: Thu, 26 Oct 2023 03:48:51 +0000 Subject: [PATCH] Update code Signed-off-by: GitHub --- package.json | 3 +- src/application.ts | 64 ++++++++++++++++++++----------- src/handler.ts | 82 +++++++++++++++++++++++++++++++--------- src/index.ts | 7 ++++ src/middles/bodyParse.ts | 1 + src/util.ts | 5 ++- tsconfig.json | 2 +- 7 files changed, 122 insertions(+), 42 deletions(-) create mode 100644 src/index.ts diff --git a/package.json b/package.json index 3ec43c5..50d6b3e 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "busboy": "^1.6.0", "cookie": "^0.5.0", "path-to-regexp": "^6.2.1", - "ws": "^8.14.2" + "ws": "^8.14.2", + "yaml": "^2.3.3" } } diff --git a/src/application.ts b/src/application.ts index 32e4b61..efa4671 100644 --- a/src/application.ts +++ b/src/application.ts @@ -1,11 +1,23 @@ import { IncomingMessage, Server, ServerResponse } from "http"; import { AddressInfo, ListenOptions } from "net"; +import path from "path"; import { parse } from "url"; import util from "util"; import { WebSocket, WebSocketServer } from "ws"; import { ErrorRequestHandler, Handlers, Layer, NextFunction, RequestHandler, WsRequestHandler, assignRequest, assignResponse, assignWsResponse } from "./handler.js"; import { Methods, methods } from "./util.js"; +export type RouterSettingsConfig = Record; +export class RouterSettings extends Map { + constructor(sets?: RouterSettingsConfig) { + super(); + if (sets) for (const k in sets) this.set(k, sets[k]); + } + toJSON(): RouterSettingsConfig { + return Array.from(this.entries()).reduce((acc, [key, value]) => Object.assign(acc, {[key]: value}), {}); + } +} + export interface Router { (req: IncomingMessage, res: ServerResponse, next?: (err?: any) => void): void; (req: IncomingMessage, socket: WebSocket, next?: (err?: any) => void): void; @@ -21,16 +33,25 @@ export interface Router { } export class Router extends Function { - constructor(opts?: any) { + constructor(routeOpts?: RouterSettingsConfig) { super("if (typeof this.handler === 'function') { this.handler.apply(this, arguments); } else if (typeof arguments.callee === 'function') { arguments.callee.apply(arguments.callee, arguments); } else { throw new Error('Cannot get Router class'); }"); + this.settings = new RouterSettings(routeOpts); + this.settings.set("env", process.env.NODE_ENV || "development"); + this.settings.set("path resolve", true).set("json space", 2); + this.settings.set("json replacer", (_key: string, value: any) => { + if (value instanceof BigInt || typeof value === "bigint") return { type: "bigint", value: value.toString() }; + else if (value && typeof value.toJSON === "function") return value.toJSON(); + return value; + }); } layers: Layer[] = []; wsRooms: Map = new Map(); + settings: RouterSettings; handler(req: IncomingMessage, res: WebSocket|ServerResponse, next?: NextFunction) { - if (typeof next !== "function") next = (err) => { - if (err && !(err === "router" || err === "route")) console.error(err); + if (typeof next !== "function") next = (err?: any) => { + if (err && !(err === "router" || err === "route") && this.settings.get("env") !== "production") console.error(err); if (res instanceof WebSocket) { res.send("Close connection!"); res.close(); @@ -45,12 +66,13 @@ export class Router extends Function { } } - if (!req["path"]) req["path"] = (parse(req.url)).pathname; - const { layers } = this, method = (res instanceof WebSocket ? "ws" : (String(req.method||"").toLowerCase())), saveParms = Object.freeze(req["params"] || {}), originalPath = req["path"];; + const { layers } = this, method = (res instanceof WebSocket ? "ws" : (String(req.method||"").toLowerCase())), saveParms = Object.freeze(req["params"] || {}); + let originalPath: string = req["path"]||(parse(req.url)).pathname; + if (this.settings.get("path resolve")) originalPath = path.posix.resolve("/", originalPath); + if (this.settings.has("app path") && typeof this.settings.get("app path") === "string" && originalPath.startsWith(this.settings.get("app path"))) originalPath = path.posix.resolve("/", originalPath.slice(path.posix.resolve("/", this.settings.get("app path")).length)); let layersIndex = 0; - nextHandler().catch(next); - async function nextHandler(err?: any) { + const nextHandler = async (err?: any) => { req["path"] = originalPath; req["params"] = Object.assign({}, saveParms); if (err && err === "route") return next(); @@ -60,37 +82,39 @@ export class Router extends Function { else if (layer.method && layer.method !== method) return nextHandler(err); const layerMatch = layer.match(req["path"]); if (!layerMatch) return nextHandler(err); + req["path"] = layerMatch.path; if (err && layer.handler.length !== 4) return nextHandler(err); try { if (err) { if (res instanceof WebSocket) return nextHandler(err); const fn = layer.handler as ErrorRequestHandler; - await fn(err, assignRequest(req, method, Object.assign({}, saveParms, layerMatch.params)), assignResponse(res), nextHandler); + await fn(err, assignRequest(this, req, method, Object.assign({}, saveParms, layerMatch.params)), assignResponse(this, res), nextHandler); } else { if (res instanceof WebSocket) { const fn = layer.handler as WsRequestHandler; - await fn(assignRequest(req, method, Object.assign({}, saveParms, layerMatch.params)), assignWsResponse(res, this), nextHandler); + await fn(assignRequest(this, req, method, Object.assign({}, saveParms, layerMatch.params)), assignWsResponse(res), nextHandler); } else { const fn = layer.handler as RequestHandler; - await fn(assignRequest(req, method, Object.assign({}, saveParms, layerMatch.params)), assignResponse(res), nextHandler); + await fn(assignRequest(this, req, method, Object.assign({}, saveParms, layerMatch.params)), assignResponse(this, res), nextHandler); } } } catch (err) { nextHandler(err); } } + nextHandler().catch(next); } - use(...fn: Handlers[]): this; - use(path: string|RegExp, ...fn: Handlers[]): this; + use(...fn: RequestHandler[]): this; + use(path: string|RegExp, ...fn: RequestHandler[]): this; use() { let p: [string|RegExp, Handlers[]]; - if (!(arguments[0] instanceof RegExp || typeof arguments[0] === "string" && arguments[0].trim())) p = ["/", Array.from(arguments)]; + if (!(arguments[0] instanceof RegExp || typeof arguments[0] === "string" && arguments[0].trim())) p = ["(.*)", Array.from(arguments)]; else p = [arguments[0], Array.from(arguments).slice(1)]; for (const fn of p[1]) { if (typeof fn !== "function") throw new Error(util.format("Invalid middleare, require function, recived %s", typeof fn)); - this.layers.push(new Layer(p[0], fn, { strict: false, end: false })); + this.layers.push(new Layer(p[0], fn, { isRoute: true, strict: false, end: false })); } return this; } @@ -109,7 +133,7 @@ export class Router extends Function { methods.forEach(method => Router.prototype[method] = function(this: Router) { return this.__method.apply(this, ([method] as any[]).concat(Array.from(arguments))) } as any) export class Neste extends Router { - httpServer: Server; + httpServer?: Server; listen(port?: number, hostname?: string, backlog?: number, listeningListener?: () => void): this; listen(port?: number, hostname?: string, listeningListener?: () => void): this; listen(port?: number, backlog?: number, listeningListener?: () => void): this; @@ -144,7 +168,7 @@ export class Neste extends Router { const wsServer = new WebSocketServer({ noServer: true }); this.httpServer.on("upgrade", (req, sock, head) => wsServer.handleUpgrade(req, sock, head, (client) => this.handler(req, client))); return this.httpServer; - })()).address() + })()).address(); } closeAllConnections(): void { @@ -153,7 +177,7 @@ export class Neste extends Router { const wsServer = new WebSocketServer({ noServer: true }); this.httpServer.on("upgrade", (req, sock, head) => wsServer.handleUpgrade(req, sock, head, (client) => this.handler(req, client))); return this.httpServer; - })()).closeAllConnections() + })()).closeAllConnections(); } closeIdleConnections(): void { @@ -187,8 +211,4 @@ export class Neste extends Router { })()).setTimeout.apply(this.httpServer, arguments); return this; } -} - -const app = new Neste(); -app.listen(3000, () => console.log("Http 3000")) -app.get("/", ({headers, Cookies, hostname}, res) => res.json({ req: { headers, Cookies: Cookies.toJSON(), hostname } })) \ No newline at end of file +} \ No newline at end of file diff --git a/src/handler.ts b/src/handler.ts index ccb974b..567f72e 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -6,8 +6,10 @@ import { parse } from "url"; import { isIP } from "net"; import { defineProperties, mixin } from "./util.js"; import stream from "stream"; +import yaml from "yaml"; import * as ranger from "./ranger.js"; import { Router } from "./application.js"; +import { posix } from "path"; export class CookieManeger extends Map { constructor(public initialCookies: string) { @@ -39,6 +41,7 @@ export class CookieManeger extends Map { req: Request; + app: Router; } export class Response { set(key: string, value: string|string[]|number) { @@ -248,8 +252,46 @@ export class Response { * res.json({ user: 'tj' }); */ json(obj: any) { - if (!(this.has("Content-Type"))) this.set("Content-Type", "application/json") - return this.send(JSON.stringify(obj, null, 2)); + if (!(obj && typeof obj === "object")) throw new TypeError("Require body object"); + else if (!(this.has("Content-Type"))) this.set("Content-Type", "application/json") + + // settings + const escape = this.app.settings.get("json escape") + const replacer = this.app.settings.get("json replacer"); + const spaces = this.app.settings.get("json space"); + + // JSON Body + let jsonBody = JSON.stringify(obj, replacer, spaces); + if (escape && typeof jsonBody === "string") { + jsonBody = jsonBody.replace(/[<>&]/g, function (c) { + switch (c.charCodeAt(0)) { + case 0x3c: + return "\\u003c" + case 0x3e: + return "\\u003e" + case 0x26: + return "\\u0026" + /* istanbul ignore next: unreachable default */ + default: + return c + } + }); + } + return this.send(jsonBody); + } + + /** + * Send YAML response. + * + * Examples: + * + * res.yaml(null); + * res.yaml({ user: 'tj' }); + */ + yaml(obj: any) { + if (!(obj && typeof obj === "object")) throw new TypeError("Require body object"); + else if (!(this.has("Content-Type"))) this.set("Content-Type", "application/x-yaml; text/yaml") + return this.send(yaml.stringify(obj, null, 2)); } /** @@ -280,8 +322,9 @@ export class Response { * res.send({ some: 'json' }); * res.send('

some html

'); */ - send(body: any) { - if (body === undefined || body === null) throw new TypeError("Require body"); + send(body: any): this { + if (body === undefined) throw new TypeError("Require body"); + else if (body === null) body = ""; let encoding: BufferEncoding = "utf8"; const bodyType = typeof body; if (bodyType === "string") { @@ -370,34 +413,38 @@ export type Handlers = WsRequestHandler|RequestHandler|ErrorRequestHandler; export class Layer { method?: string; + isRoute?: boolean; handler: Handlers; matchFunc: MatchFunction; match(path: string): undefined|{ path: string, params: Record } { const value = this.matchFunc(path); if (!value) return undefined; return { - path: value.path, + path: this.isRoute ? (value.path !== path ? posix.join("/", path.slice(value.path.length)) : value.path) : value.path, params: value.params as any, }; } - constructor(path: string|RegExp, fn: Handlers, options?: Omit) { + constructor(path: string|RegExp, fn: Handlers, options?: Omit) { if (!(typeof fn === "function")) throw new Error("Register function"); if (!(options)) options = {}; if (path === "*") path = "(.*)"; + this.isRoute = !!options.isRoute; this.handler = fn; - this.matchFunc = regexMatch(path, {...options, decode: decodeURIComponent }); + this.matchFunc = regexMatch(path, {...options, decode: decodeURIComponent, encode: encodeURI }); } } -export function assignRequest(req: IncomingMessage, method: string, params: Record): Request { +export function assignRequest(app: Router, req: IncomingMessage, method: string, params: Record): Request { + if (req["__Neste"]) return req as any; const parseQuery = new URLSearchParams(parse(req.url).query); mixin(req, Request.prototype, false); defineProperties(req, { - method: { configurable: false, enumerable: false, writable: false, value: method }, - Cookies: { configurable: false, enumerable: false, writable: false, value: new CookieManeger((req.headers||{}).cookie||"") }, - query: { configurable: true, enumerable: true, writable: true, value: Object.assign(Array.from(parseQuery.keys()).reduce>((acc, key) => { acc[key] = parseQuery.get(key); return acc; }, {}), req["query"]) }, - params: { configurable: false, enumerable: false, writable: false, value: params }, + app: { writable: true, configurable: true, enumerable: true, value: app }, + method: { writable: true, configurable: true, enumerable: true, value: method }, + Cookies: { writable: true, configurable: true, enumerable: true, value: new CookieManeger((req.headers||{}).cookie||"") }, + query: { writable: true, configurable: true, enumerable: true, value: Object.assign(Array.from(parseQuery.keys()).reduce>((acc, key) => { acc[key] = parseQuery.get(key); return acc; }, {}), req["query"]) }, + params: { writable: true, configurable: true, enumerable: true, value: params }, protocol: { configurable: true, enumerable: true, @@ -464,15 +511,16 @@ export function assignRequest(req: IncomingMessage, method: string, params: Reco return req as any; } -export function assignResponse(res: ServerResponse): Response { +export function assignResponse(app: Router, res: ServerResponse): Response { + if (res["__Neste"]) return res as any; mixin(res, Response.prototype, false); - defineProperties(res, {}); + defineProperties(res, { + app: { writable: true, configurable: true, enumerable: true, value: app } + }); return res as any; } -export function assignWsResponse(res: WebSocket, router: Router): WsResponse { +export function assignWsResponse(res: WebSocket): WsResponse { mixin(res, WsResponse.prototype, false); - router.wsRooms - defineProperties(res, {}); return res as any; } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..5a436bd --- /dev/null +++ b/src/index.ts @@ -0,0 +1,7 @@ +import { CookieManeger, Handlers, ErrorRequestHandler, RequestHandler, WsRequestHandler, Layer, NextFunction, Request, Response, WsResponse } from "./handler.js"; +import { Neste, Router, RouterSettings, RouterSettingsConfig } from "./application.js"; + +function router() { return new Router(); } +function neste() { return new Neste(); } +export { neste, router, CookieManeger, Handlers, ErrorRequestHandler, RequestHandler, WsRequestHandler, Layer, NextFunction, Request, Response, WsResponse, Neste, Router, RouterSettings, RouterSettingsConfig }; +export default neste; \ No newline at end of file diff --git a/src/middles/bodyParse.ts b/src/middles/bodyParse.ts index 948b53f..79b5c0f 100644 --- a/src/middles/bodyParse.ts +++ b/src/middles/bodyParse.ts @@ -13,6 +13,7 @@ export type FileStorage = { restore(fn: (err: any, stream?: stream.Readable) => void): void; delete(fn: (err?: any) => void): void; } + export class LocalFile extends Map { rootDir?: string; async deleteFile(file: string) { diff --git a/src/util.ts b/src/util.ts index 36edb81..22e1944 100644 --- a/src/util.ts +++ b/src/util.ts @@ -27,6 +27,9 @@ export function mixin(dest: T, src: C, redefine?: boolean): T & C { } export function defineProperties(obj: any, config: Record>) { - for (let key in config) Object.defineProperty(obj, key, config[key]); + for (let key in config) { + if (config[key].value !== undefined) obj[key] = config[key].value; + else Object.defineProperty(obj, key, config[key]); + } return obj; } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 713c647..156e399 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,7 @@ "declaration": true, "strict": false, "noUnusedLocals": true, - "isolatedModules": true, + "isolatedModules": false, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true,