From 574c91dfd204eb9b6a076f631dfb067a9d65ae0d Mon Sep 17 00:00:00 2001 From: Tobias Schneider Date: Sun, 26 Jun 2022 00:14:24 +0200 Subject: [PATCH] clean up --- .gitignore | 6 +- package.json | 6 +- src/index.d.ts | 270 ++++++++++++++++++++++++++++++++++++++++++ src/index.js | 310 +++++++++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 2 +- 5 files changed, 589 insertions(+), 5 deletions(-) create mode 100644 src/index.d.ts create mode 100644 src/index.js diff --git a/.gitignore b/.gitignore index 60122e6..611dc7d 100644 --- a/.gitignore +++ b/.gitignore @@ -141,4 +141,8 @@ dist .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz -.pnp.* \ No newline at end of file +.pnp.* + +# Custom +/index.js +/index.d.ts \ No newline at end of file diff --git a/package.json b/package.json index b55f940..7c1ed53 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,10 @@ "name": "@tsndr/cloudflare-worker-router", "version": "2.0.0-pre.9", "description": "", - "main": "dist/index.js", - "types": "dist/index.d.ts", + "main": "index.js", + "types": "index.d.ts", "scripts": { - "build": "rm -rf dist/* && tsc" + "build": "tsc" }, "repository": { "type": "git", diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100644 index 0000000..545c877 --- /dev/null +++ b/src/index.d.ts @@ -0,0 +1,270 @@ +/// +/** + * Route Object + * + * @typedef Route + * @property {string} method HTTP request method + * @property {string} url URL String + * @property {RouterHandler[]} handlers Array of handler functions + */ +interface Route { + method: string; + url: string; + handlers: RouterHandler[]; +} +/** + * Router Context + * + * @typedef RouterContext + * @property {Object} env Environment + * @property {RouterRequest} req Request Object + * @property {RouterResponse} res Response Object + * @property {RouterNext} next Next Handler + */ +interface RouterContext { + env: any; + req: RouterRequest; + res: RouterResponse; + next: RouterNext; +} +/** + * Request Object + * + * @typedef RouterRequest + * @property {string} url URL + * @property {string} method HTTP request method + * @property {RouterRequestParams} params Object containing all parameters defined in the url string + * @property {RouterRequestQuery} query Object containing all query parameters + * @property {Headers} headers Request headers object + * @property {any} body Only available if method is `POST`, `PUT`, `PATCH` or `DELETE`. Contains either the received body string or a parsed object if valid JSON was sent. + * @property {IncomingRequestCfProperties=} cf object containing custom Cloudflare properties. (https://developers.cloudflare.com/workers/examples/accessing-the-cloudflare-object) + */ +interface RouterRequest { + url: string; + method: string; + params: RouterRequestParams; + query: RouterRequestQuery; + headers: Headers; + body: any; + cf?: IncomingRequestCfProperties; +} +interface RouterRequestParams { + [key: string]: string; +} +interface RouterRequestQuery { + [key: string]: string; +} +/** + * Response Object + * + * @typedef RouterResponse + * @property {Headers} headers Response headers object + * @property {number=204} status Return status code (default: `204`) + * @property {any=} body Either an `object` (will be converted to JSON) or a string + * @property {Response=} raw A response object that is to be returned, this will void all other res properties and return this as is. + */ +interface RouterResponse { + headers: Headers; + status?: number; + body?: any; + raw?: Response; + webSocket?: WebSocket; +} +/** + * Next Function + * + * @callback RouterNext + * @returns {Promise} + */ +interface RouterNext { + (): Promise; +} +/** + * Handler Function + * + * @callback RouterHandler + * @param {RouterContext} ctx + * @returns {Promise | void} + */ +interface RouterHandler { + (ctx: RouterContext): Promise | void; +} +/** + * CORS Config + * + * @typedef RouterCorsConfig + * @property {string} allowOrigin Access-Control-Allow-Origin (default: `*`) + * @property {string} allowMethods Access-Control-Allow-Methods (default: `*`) + * @property {string} allowHeaders Access-Control-Allow-Headers (default: `*`) + * @property {number} maxAge Access-Control-Max-Age (default: `86400`) + * @property {number} optionsSuccessStatus Return status code for OPTIONS request (default: `204`) + */ +interface RouterCorsConfig { + allowOrigin: string; + allowMethods: string; + allowHeaders: string; + maxAge: number; + optionsSuccessStatus: number; +} +/** + * Router + * + * @class + * @constructor + * @public + */ +declare class Router { + /** + * Router Array + * + * @protected + * @type {Route[]} + */ + protected routes: Route[]; + /** + * Global Handlers + */ + protected globalHandlers: RouterHandler[]; + /** + * Debug Mode + * + * @protected + * @type {boolean} + */ + protected debugMode: boolean; + /** + * CORS Config + * + * @protected + * @type {RouterCorsConfig} + */ + protected corsConfig: RouterCorsConfig; + /** + * Register global handlers + * + * @param {RouterHandler[]} handlers + * @returns {Router} + */ + use(...handlers: RouterHandler[]): Router; + /** + * Register CONNECT route + * + * @param {string} url + * @param {RouterHandler[]} handlers + * @returns {Router} + */ + connect(url: string, ...handlers: RouterHandler[]): Router; + /** + * Register DELETE route + * + * @param {string} url + * @param {RouterHandler[]} handlers + * @returns {Router} + */ + delete(url: string, ...handlers: RouterHandler[]): Router; + /** + * Register GET route + * + * @param {string} url + * @param {RouterHandler[]} handlers + * @returns {Router} + */ + get(url: string, ...handlers: RouterHandler[]): Router; + /** + * Register HEAD route + * + * @param {string} url + * @param {RouterHandler[]} handlers + * @returns {Router} + */ + head(url: string, ...handlers: RouterHandler[]): Router; + /** + * Register OPTIONS route + * + * @param {string} url + * @param {RouterHandler[]} handlers + * @returns {Router} + */ + options(url: string, ...handlers: RouterHandler[]): Router; + /** + * Register PATCH route + * + * @param {string} url + * @param {RouterHandler[]} handlers + * @returns {Router} + */ + patch(url: string, ...handlers: RouterHandler[]): Router; + /** + * Register POST route + * + * @param {string} url + * @param {RouterHandler[]} handlers + * @returns {Router} + */ + post(url: string, ...handlers: RouterHandler[]): Router; + /** + * Register PUT route + * + * @param {string} url + * @param {RouterHandler[]} handlers + * @returns {Router} + */ + put(url: string, ...handlers: RouterHandler[]): Router; + /** + * Register TRACE route + * + * @param {string} url + * @param {RouterHandler[]} handlers + * @returns {Router} + */ + trace(url: string, ...handlers: RouterHandler[]): Router; + /** + * Register route, ignoring method + * + * @param {string} url + * @param {RouterHandler[]} handlers + * @returns {Router} + */ + any(url: string, ...handlers: RouterHandler[]): Router; + /** + * Debug Mode + * + * @param {boolean} [state=true] Whether to turn on or off debug mode (default: true) + */ + debug(state?: boolean): void; + /** + * Enable CORS support + * + * @param {RouterCorsConfig} config + * @returns {Router} + */ + cors(config: RouterCorsConfig): Router; + /** + * Register route + * + * @private + * @param {string} method HTTP request method + * @param {string} url URL String + * @param {RouterHandler[]} handlers Arrar of handler functions + * @returns {Router} + */ + private register; + /** + * Get Route by request + * + * @private + * @param {Request} request + * @returns {RouterRequest | undefined} + */ + private getRoute; + /** + * Handle requests + * + * @param {any} env + * @param {Request} request + * @param {any=} extend + * @returns {Response} + */ + handle(env: any, request: Request, extend?: any): Promise; +} +export default Router; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..86e71c2 --- /dev/null +++ b/src/index.js @@ -0,0 +1,310 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +/** + * Router + * + * @class + * @constructor + * @public + */ +class Router { + /** + * Router Array + * + * @protected + * @type {Route[]} + */ + routes = []; + /** + * Global Handlers + */ + globalHandlers = []; + /** + * Debug Mode + * + * @protected + * @type {boolean} + */ + debugMode = false; + /** + * CORS Config + * + * @protected + * @type {RouterCorsConfig} + */ + corsConfig = { + allowOrigin: '*', + allowMethods: '*', + allowHeaders: '*', + maxAge: 86400, + optionsSuccessStatus: 204 + }; + /** + * Register global handlers + * + * @param {RouterHandler[]} handlers + * @returns {Router} + */ + use(...handlers) { + for (let handler of handlers) { + this.globalHandlers.push(handler); + } + return this; + } + /** + * Register CONNECT route + * + * @param {string} url + * @param {RouterHandler[]} handlers + * @returns {Router} + */ + connect(url, ...handlers) { + return this.register('CONNECT', url, handlers); + } + /** + * Register DELETE route + * + * @param {string} url + * @param {RouterHandler[]} handlers + * @returns {Router} + */ + delete(url, ...handlers) { + return this.register('DELETE', url, handlers); + } + /** + * Register GET route + * + * @param {string} url + * @param {RouterHandler[]} handlers + * @returns {Router} + */ + get(url, ...handlers) { + return this.register('GET', url, handlers); + } + /** + * Register HEAD route + * + * @param {string} url + * @param {RouterHandler[]} handlers + * @returns {Router} + */ + head(url, ...handlers) { + return this.register('HEAD', url, handlers); + } + /** + * Register OPTIONS route + * + * @param {string} url + * @param {RouterHandler[]} handlers + * @returns {Router} + */ + options(url, ...handlers) { + return this.register('OPTIONS', url, handlers); + } + /** + * Register PATCH route + * + * @param {string} url + * @param {RouterHandler[]} handlers + * @returns {Router} + */ + patch(url, ...handlers) { + return this.register('PATCH', url, handlers); + } + /** + * Register POST route + * + * @param {string} url + * @param {RouterHandler[]} handlers + * @returns {Router} + */ + post(url, ...handlers) { + return this.register('POST', url, handlers); + } + /** + * Register PUT route + * + * @param {string} url + * @param {RouterHandler[]} handlers + * @returns {Router} + */ + put(url, ...handlers) { + return this.register('PUT', url, handlers); + } + /** + * Register TRACE route + * + * @param {string} url + * @param {RouterHandler[]} handlers + * @returns {Router} + */ + trace(url, ...handlers) { + return this.register('TRACE', url, handlers); + } + /** + * Register route, ignoring method + * + * @param {string} url + * @param {RouterHandler[]} handlers + * @returns {Router} + */ + any(url, ...handlers) { + return this.register('*', url, handlers); + } + /** + * Debug Mode + * + * @param {boolean} [state=true] Whether to turn on or off debug mode (default: true) + */ + debug(state = true) { + this.debugMode = state; + } + /** + * Enable CORS support + * + * @param {RouterCorsConfig} config + * @returns {Router} + */ + cors(config) { + config = config || {}; + this.corsConfig = { + allowOrigin: config.allowOrigin || '*', + allowMethods: config.allowMethods || '*', + allowHeaders: config.allowHeaders || '*, Authorization', + maxAge: config.maxAge || 86400, + optionsSuccessStatus: config.optionsSuccessStatus || 204 + }; + return this; + } + /** + * Register route + * + * @private + * @param {string} method HTTP request method + * @param {string} url URL String + * @param {RouterHandler[]} handlers Arrar of handler functions + * @returns {Router} + */ + register(method, url, handlers) { + this.routes.push({ + method, + url, + handlers + }); + return this; + } + /** + * Get Route by request + * + * @private + * @param {Request} request + * @returns {RouterRequest | undefined} + */ + getRoute(request) { + const url = new URL(request.url); + const pathArr = url.pathname.split('/').filter(i => i); + return this.routes.find(r => { + const routeArr = r.url.split('/').filter(i => i); + if (![request.method, '*'].includes(r.method) || routeArr.length !== pathArr.length) + return false; + const params = {}; + for (let i = 0; i < routeArr.length; i++) { + if (routeArr[i] !== pathArr[i] && routeArr[i][0] !== ':') + return false; + if (routeArr[i][0] === ':') + params[routeArr[i].substring(1)] = pathArr[i]; + } + request.params = params; + const query = {}; + for (const [k, v] of url.searchParams.entries()) { + query[k] = v; + } + request.query = query; + return true; + }) || this.routes.find(r => r.url === '*' && [request.method, '*'].includes(r.method)); + } + /** + * Handle requests + * + * @param {any} env + * @param {Request} request + * @param {any=} extend + * @returns {Response} + */ + async handle(env, request, extend = {}) { + try { + const req = { + ...extend, + method: request.method, + headers: request.headers, + url: request.url, + cf: request.cf, + params: {}, + query: {}, + body: '' + }; + if (req.method === 'OPTIONS' && Object.keys(this.corsConfig).length) { + return new Response(null, { + headers: { + 'Access-Control-Allow-Origin': this.corsConfig.allowOrigin, + 'Access-Control-Allow-Methods': this.corsConfig.allowMethods, + 'Access-Control-Allow-Headers': this.corsConfig.allowHeaders, + 'Access-Control-Max-Age': this.corsConfig.maxAge.toString() + }, + status: this.corsConfig.optionsSuccessStatus + }); + } + if (['POST', 'PUT', 'PATCH'].includes(req.method)) { + if (req.headers.has('Content-Type') && req.headers.get('Content-Type').includes('json')) { + try { + req.body = await request.json(); + } + catch { + req.body = {}; + } + } + else { + try { + req.body = await request.text(); + } + catch { + req.body = ''; + } + } + } + const route = this.getRoute(req); + if (!route) + return new Response(this.debugMode ? 'Route not found!' : null, { status: 404 }); + const res = { headers: new Headers() }; + if (Object.keys(this.corsConfig).length) { + res.headers.set('Access-Control-Allow-Origin', this.corsConfig.allowOrigin); + res.headers.set('Access-Control-Allow-Methods', this.corsConfig.allowMethods); + res.headers.set('Access-Control-Allow-Headers', this.corsConfig.allowHeaders); + res.headers.set('Access-Control-Max-Age', this.corsConfig.maxAge.toString()); + } + const handlers = [...this.globalHandlers, ...route.handlers]; + let prevIndex = -1; + const runner = async (index) => { + if (index === prevIndex) + throw new Error('next() called multiple times'); + prevIndex = index; + if (typeof handlers[index] === 'function') + await handlers[index]({ env, req, res, next: async () => await runner(index + 1) }); + }; + await runner(0); + if (typeof res.body === 'object') { + if (!res.headers.has('Content-Type')) + res.headers.set('Content-Type', 'application/json'); + res.body = JSON.stringify(res.body); + } + if (res.raw) + return res.raw; + return new Response([101, 204, 205, 304].includes(res.status || (res.body ? 200 : 204)) ? null : res.body, { status: res.status, headers: res.headers, webSocket: res.webSocket || null }); + } + catch (err) { + console.error(err); + return new Response(this.debugMode && err instanceof Error ? err.stack : '', { status: 500 }); + } + } +} +exports.default = Router; diff --git a/tsconfig.json b/tsconfig.json index afd7594..491d1f5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "outDir": "./dist", + "outDir": ".", "module": "commonjs", "target": "esnext", "lib": ["esnext"],