From b6487dfce701ed824a163a4a91609c4dcee642a4 Mon Sep 17 00:00:00 2001 From: Chukwuemeka Ajima Date: Sun, 24 Jul 2022 01:45:09 +0200 Subject: [PATCH] feat(roter): add Router class implementation - add router class with necessary methods - add example folders to show Router usage - refactor and fixes --- .gitignore | 2 +- README.md | 88 +++++++++- clean-dist | 7 +- examples/note-app/app.js | 63 ++++++++ .../note-app/controller/NoteController.js | 65 ++++++++ examples/note-app/controller/index.js | 2 + examples/note-app/dataStore/data.json | 20 +++ examples/note-app/middleware/index.js | 2 + examples/note-app/middleware/logger.js | 4 + examples/note-app/middleware/requestID.js | 6 + examples/note-app/models/index.js | 1 + examples/note-app/models/notes.js | 32 ++++ .../note-app/repository/NoteRepository.js | 49 ++++++ examples/note-app/repository/index.js | 2 + examples/note-app/routes/index.ts | 2 + examples/note-app/routes/notes.js | 15 ++ index.ts | 10 +- package.json | 2 +- postman.jpeg | Bin 0 -> 9080 bytes postman_collection.json | 151 ++++++++++++++++++ src/colston.ts | 54 ++++--- src/context.ts | 5 + src/params.ts | 7 +- src/routeRegister.ts | 11 +- src/router.ts | 58 +++++++ src/types.d.ts | 7 +- todo.md | 2 +- tsconfig.json | 1 + 28 files changed, 616 insertions(+), 52 deletions(-) create mode 100644 examples/note-app/app.js create mode 100644 examples/note-app/controller/NoteController.js create mode 100644 examples/note-app/controller/index.js create mode 100644 examples/note-app/dataStore/data.json create mode 100644 examples/note-app/middleware/index.js create mode 100644 examples/note-app/middleware/logger.js create mode 100644 examples/note-app/middleware/requestID.js create mode 100644 examples/note-app/models/index.js create mode 100644 examples/note-app/models/notes.js create mode 100644 examples/note-app/repository/NoteRepository.js create mode 100644 examples/note-app/repository/index.js create mode 100644 examples/note-app/routes/index.ts create mode 100644 examples/note-app/routes/notes.js create mode 100644 postman.jpeg create mode 100644 postman_collection.json create mode 100644 src/router.ts diff --git a/.gitignore b/.gitignore index ecb7d9c..e289779 100644 --- a/.gitignore +++ b/.gitignore @@ -132,4 +132,4 @@ dist # custom generate.sh *.http -example* +example diff --git a/README.md b/README.md index 62aa295..649c875 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Fast, lightweight and zero dependency framework for [bunjs](https://bun.sh) 🚀 -![npm](https://img.shields.io/npm/v/colstonjs?color=blue&style=plastic) +![GitHub Workflow Status](https://img.shields.io/github/workflow/status/ajimae/colstonjs/publish?style=plastic) ![npm](https://img.shields.io/npm/v/colstonjs?color=blue&style=plastic) ![GitHub](https://img.shields.io/github/license/ajimae/colstonjs?style=plastic) ![npm](https://img.shields.io/npm/dt/colstonjs?style=plastic) @@ -13,21 +13,26 @@ Fast, lightweight and zero dependency framework for [bunjs](https://bun.sh) 🚀 - [Install](#installation) - [Usage](#usage) - [Examples](#examples) - - [Hellow Bun](#hello-bun) + - [Hello Bun](#hello-bun) - [Read request body as json or text](#read-request-body-as-json-or-text) - [Using named parameters](#using-named-parameters) - [Using query parameters](#using-query-parameters) - [Method Chaining](#method-chaining) + - [Running the demo note-app](#running-the-demo-note-app) - [Middleware](#middleware) - [Application-Level Middleware](#application-level-middleware) - [Route-Level Middleware](#route-level-middleware) + - [Context `locals`](#context-locals) + - [Router](#router) + - [Instantiating Router class](#instantiating-router-class) + - [Injecting Router instance into the app](#injecting-router-instance-into-the-app) - [Application instance cache](#application-instance-cache) - [Error Handler](#error-handler) - [Benchmark](#benchmark) - [Contribute](#contribute) - [License](#license) - [Author](#author) -- [Note](#note:) +- [DevNote](#devnote) ## Background @@ -198,6 +203,17 @@ app app.start(8000); ``` +#### Running the demo `note-app` +Follow the steps below to run the `demo note-taking api application` in the `examples`directory. +- Clone this repository +- Change directory into the note-app folder by running `cd examples/note-app` +- Start the http server to listen on port `8000` by running `bun app.js` +- User your favourite `http client` (e.g Postman) to make requests to the `listening http server`. + + + + + ### Middleware Colstonjs support both `route` level middleware as well as `app` level middleware. @@ -267,7 +283,71 @@ app.get("/", middleware-1, middleware-2, middleware-3, ..., middleware-k, (ctx: }); ... ``` +#### Context locals +`ctx.locals` is a plain javascript object that is specifically added to allow sharing of data amongst the chain of middlewares and/or handler functions. + +```ts +// server.ts +... +let requestCount = 0; +app.post("/request-count", (ctx, next) => { + /** + * req.locals can be used to pass + * data from one middleware to another + */ + ctx.locals.requestCount = requestCount; + next(); +}, (ctx, next) => { + ++ctx.locals.requestCount; + next(); +}, (ctx) => { + let count = ctx.locals.requestCount; + return ctx.status(200).text(count); // 1 +}); +``` + +### Router +#### Instantiating Router class +Router class provide a way to separate router specific declaration/blocks from the app logic, by providing that extra abstraction layer for your project. +```typescript +// router.ts +import Router from "Router"; + +// instantiate the router class +const router1 = new Router(); +const router2 = new Router(); + +// define user routes - can be in a separate file or module. +router1.post('/user', (ctx) => { return ctx.status(200).json({ user }) }); +router1.get('/users', (ctx) => { return ctx.json({ users }) }); +router1.delete('/user?id', (ctx) => { return ctx.status(204).head() }); + +// define the notes route - can also be in separate module. +router2.get('/note/:id', (ctx) => { return ctx.json({ note }) }); +router2.get('/notes', (ctx) => { return ctx.json({ notes }) }); +router2.post('/note', (ctx) => { return ctx.status(201).json({ note }) }); + +export { router1, router2 }; +``` + +#### Injecting Router instance into the app +```typescript +// server.ts +import Colston from "colstonjs"; +import { router1, router2 } from "./router"; + +const app: Colston = new Colston(); +app.all(router1, router2); + +// other routes can still be defined here +app.get("/", (ctx) => { + return ctx.status(200).text("Welcome to colstonjs framework for bun"); +}); + +app.start(8000) +``` +The `app.all()` method takes in k numbers of router instance objects e.g `app.all(router-1, router-2, ..., router-k);`. The [example](example) folder contains a full note taking backend app that utilizes this pattern. ## Application instance cache We can cache simple data which will leave throughout the application instance lifecycle. @@ -445,5 +525,5 @@ See the TODO doc [here](todo.md), feel free to also add to the list by editing t ## Author Coded with 💙 by [Chukwuemeka Ajima](https://github.com/ajimae) -## Note: +## DevNote: Although this version is fairly stable, it is actively still under development so also is [bunjs](https://bun.sh) and might contain some bugs, hence, not ideal for a production app. diff --git a/clean-dist b/clean-dist index 589dbea..fd058ae 100755 --- a/clean-dist +++ b/clean-dist @@ -11,12 +11,7 @@ cat << EOF > dist/index.d.ts import Colston from "./declarations/colston"; export default Colston; export * from "./declarations/types.d"; -EOF - -# update imports in index.js -cat << EOF > dist/index.js - import Colston from "./src/colston"; - export default Colston; + export { default as Router } from "./src/router"; EOF echo "✨ Done" diff --git a/examples/note-app/app.js b/examples/note-app/app.js new file mode 100644 index 0000000..66b68ed --- /dev/null +++ b/examples/note-app/app.js @@ -0,0 +1,63 @@ +import Colston from "../../index"; +import router from "./routes"; +import { logger, requestID } from "./middleware"; + +const app = new Colston(); + +let requestCount = 0; + +app + .get("/one", (ctx) => { + requestCount++; + return ctx.status(200).text("One"); + }) + .get("/two", ctx => ctx.text("two")) + .post("/two", (ctx) => { + requestCount++; + return ctx.status(200).text("Two"); + }) + .patch("/three", (ctx) => { + requestCount++; + return ctx.status(200).text("Three"); + }); + +app.post("/requestCount", (ctx, next) => { + /** + * req.locals can be used to pass + * data from one middleware to another + */ + ctx.locals.requestCount = requestCount; + next(); +}, (ctx, next) => { + ++ctx.locals.requestCount; + next(); +}, (ctx) => { + return ctx.status(200).text(ctx.locals.requestCount.toString()); +}); + +app.get("/request-id", requestID, (ctx) => { + return ctx.status(200).json({ + message: "This will give every request a unique ID and in the header too.", + requestID: ctx.request.id + }); +}); + +/** + * the app.all(...route: Router) mehtod + * accepts k-numbers of router instance objects + * where each router instance object are + * @example + * + * router-1 = new Router().get(path, ...middlewares) + * router-2 = new Router().post(path, ...niddlewares) + * ... + * router-k = new Router().(path, ...middlewares) + * + * app.all(router-1, router-2, ..., router-k) + */ +app.use(logger); +app.all(router); + +app.start(8000, function () { + console.log(`server running on port {8000}`); +}); diff --git a/examples/note-app/controller/NoteController.js b/examples/note-app/controller/NoteController.js new file mode 100644 index 0000000..1eda446 --- /dev/null +++ b/examples/note-app/controller/NoteController.js @@ -0,0 +1,65 @@ +/** + * NoteController class + * @class NoteController + * @methods getNotes + * @methods postNotes + */ +class NoteController { + constructor(noteRepository) { + this.noteRepository = noteRepository; + } + + /** + * retrieve all available notes + * @param {*} ctx + * @returns bun Response instance + */ + async getNotes(ctx) { + const notes = await this.noteRepository.getNotes(ctx); + + return ctx.status(200).json({ + success: true, + data: notes + }); + } + + /** + * retrieve all available notes + * @param {*} ctx + * @returns bun Response instance + */ + async getAllNotes(ctx) { + const notes = await this.noteRepository.getAllNotes(); + + return ctx.status(200).json({ + success: true, + data: notes + }); + } + + /** + * post a single note data + * @param {*} ctx + * @returns bun Response instance + */ + async postNote(ctx) { + const notes = await this.noteRepository.postNote(ctx); + + return ctx.status(201).json({ + success: true, + data: notes + }); + } + + /** + * delte a sinlge note data + * @param {*} ctx + * @returns bun Response instance + */ + async deleteNote(ctx) { + await this.noteRepository.deleteNote(ctx); + return ctx.status(204).head(); + } +} + +export default NoteController diff --git a/examples/note-app/controller/index.js b/examples/note-app/controller/index.js new file mode 100644 index 0000000..17f2ec8 --- /dev/null +++ b/examples/note-app/controller/index.js @@ -0,0 +1,2 @@ +import NoteController from "./NoteController"; +export default NoteController; diff --git a/examples/note-app/dataStore/data.json b/examples/note-app/dataStore/data.json new file mode 100644 index 0000000..df5da66 --- /dev/null +++ b/examples/note-app/dataStore/data.json @@ -0,0 +1,20 @@ +[ + { + "id": "L4JPI2tLmXMpwmaPcUZZlfuLljW7JYPP2hfmYcmSQ", + "note": "this is my first note", + "createdAt": "1970-01-01T00:00:00.005Z", + "updatedAt": "1970-01-01T00:00:00.005Z" + }, + { + "id": "jyBuT1zpMLc0uIQGDOKNkJJmI9YCzg5LLWU2OgkgE", + "note": "this is my second note", + "createdAt": "1980-01-01T00:00:00.035Z", + "updatedAt": "1980-01-01T00:00:00.035Z" + }, + { + "id": "QbSMatSi9YVxKQBQny9yBfAh8E3VWkkWg8GMHwQHdOA", + "note": "this is my third note", + "createdAt": "1990-01-01T00:00:02.000Z", + "updatedAt": "1990-01-01T00:00:02.000Z" + } +] diff --git a/examples/note-app/middleware/index.js b/examples/note-app/middleware/index.js new file mode 100644 index 0000000..e543579 --- /dev/null +++ b/examples/note-app/middleware/index.js @@ -0,0 +1,2 @@ +export { logger } from "./logger"; +export { requestID } from './requestID'; diff --git a/examples/note-app/middleware/logger.js b/examples/note-app/middleware/logger.js new file mode 100644 index 0000000..a4a9678 --- /dev/null +++ b/examples/note-app/middleware/logger.js @@ -0,0 +1,4 @@ +export const logger = (ctx) => { + const { pathname } = new URL(ctx.request.url); + console.info("- - " + [new Date()], "- - " + ctx.request.method + " " + pathname + " HTTP 1.1" + " - "); +} diff --git a/examples/note-app/middleware/requestID.js b/examples/note-app/middleware/requestID.js new file mode 100644 index 0000000..4ceacf9 --- /dev/null +++ b/examples/note-app/middleware/requestID.js @@ -0,0 +1,6 @@ +import crypto from "crypto"; // built into bun + +export const requestID = (ctx) => { + ctx.request.id = crypto.randomBytes(18).toString('hex'); + ctx.setHeader('request-id', ctx.request.id); +} diff --git a/examples/note-app/models/index.js b/examples/note-app/models/index.js new file mode 100644 index 0000000..e59ebfa --- /dev/null +++ b/examples/note-app/models/index.js @@ -0,0 +1 @@ +export * from "./notes"; \ No newline at end of file diff --git a/examples/note-app/models/notes.js b/examples/note-app/models/notes.js new file mode 100644 index 0000000..da3d165 --- /dev/null +++ b/examples/note-app/models/notes.js @@ -0,0 +1,32 @@ +import crypto from 'crypto'; +const data = require('../dataStore/data.json') + +export function find(id) { + return data.find((v) => v.id == id); +} + +export function findAll() { + return data; +} + +export function save(datum) { + const id = crypto. + randomBytes(32) + .toString('base64') + .replace(/[_+=\/]/gi, ''); + + const _data = { + id, + ...datum, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + } + + data.push(_data); + return _data; +} + +export function Delete(id) { + const idx = data.findIndex(v => v.id == id); + return data.splice(idx, 1); +} diff --git a/examples/note-app/repository/NoteRepository.js b/examples/note-app/repository/NoteRepository.js new file mode 100644 index 0000000..d1c0184 --- /dev/null +++ b/examples/note-app/repository/NoteRepository.js @@ -0,0 +1,49 @@ +/** + * NoteRepository class + * @class NotRepositorye + * @methods getNotes + * @methods postNote + */ +class NoteRepository { + constructor(noteModels) { + this.noteModels = noteModels; + } + + /** + * retrieve all available notes + * @param {*} ctx + * @returns bun Response instance + */ + async getNotes(ctx) { + const { id } = await ctx.request.params; + return this.noteModels.find(id) ?? {}; + } + + async getAllNotes() { + return this.noteModels.findAll(); + } + + /** + * post a single note data + * @param {*} ctx + * @returns bun Response instance + */ + async postNote(ctx) { + const note = await ctx.request.body; + const notes = await this.noteModels.save(note); + + return notes; + } + + /** + * delete a single note data + * @param {*} ctx + * @returns bun Reponse instance + */ + async deleteNote(ctx) { + const { id } = await ctx.request.query; + return this.noteModels.Delete(id); + } +} + +export default NoteRepository; diff --git a/examples/note-app/repository/index.js b/examples/note-app/repository/index.js new file mode 100644 index 0000000..3451a97 --- /dev/null +++ b/examples/note-app/repository/index.js @@ -0,0 +1,2 @@ +import NoteRepository from "./NoteRepository"; +export default NoteRepository; diff --git a/examples/note-app/routes/index.ts b/examples/note-app/routes/index.ts new file mode 100644 index 0000000..b86ca5e --- /dev/null +++ b/examples/note-app/routes/index.ts @@ -0,0 +1,2 @@ +import router from './notes'; +export default router; diff --git a/examples/note-app/routes/notes.js b/examples/note-app/routes/notes.js new file mode 100644 index 0000000..4284f8b --- /dev/null +++ b/examples/note-app/routes/notes.js @@ -0,0 +1,15 @@ +import { Router } from '../../../index'; +import * as model from '../models'; +import NoteRepository from '../repository'; +import NoteController from '../controller'; + +const router = new Router(); +const noteRepository = new NoteRepository(model); +const noteController = new NoteController(noteRepository); + +router.get('/note/:id', noteController.getNotes.bind(noteController)); +router.get('/notes', noteController.getAllNotes.bind(noteController)); +router.post('/note', noteController.postNote.bind(noteController)); +router.delete('/?id', noteController.deleteNote.bind(noteController)); + +export default router; diff --git a/index.ts b/index.ts index 0de31df..41d26ce 100644 --- a/index.ts +++ b/index.ts @@ -1,4 +1,12 @@ import Colston from "./src/colston"; -export * from "./src/types.d" export default Colston; +/** + * uncomment (the exported types.d) + * only when you are sure you are + * importing the root `index.ts` file + * into your project else leave it commented. + * + * export * from './src/types.d' + */ +export { default as Router } from "./src/router"; diff --git a/package.json b/package.json index 8f3cd83..e28647c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "colstonjs", - "version": "0.1.0-beta.2", + "version": "0.1.0-beta.3", "author": "Chukwuemeka Ajima ", "repository": "https://github.com/ajimae/colstonjs.git", "main": "dist/index.js", diff --git a/postman.jpeg b/postman.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..117b03e635ca2b306e3d7e8487ddd5083a74d4bc GIT binary patch literal 9080 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 = []; readonly middleware: Array = []; readonly cache = new Map(); @@ -107,57 +108,64 @@ export default class Colston implements IColston { } /** - * + * @description HTTP DELETE method + * @param path + * @param cb + * @returns {this} */ public delete(path: string, ...cb: Array): Colston { - routeRegister(path, "DELETE", cb, this.routeTable) + routeRegister(path, "DELETE", cb, this.routeTable); return this; } /** * @description add level route - * @param {string} path - * @param {Function} handler + * @param {Array} callbacks */ public use(...cb: Array<(ctx: Context) => Response | Promise | void>): void { this.middleware.push(...cb); } + public all(...routes: Array): Colston { + for (let i = 0; i < routes.length; i++) + this.routeTable.push(...routes[i].routeTable); + + return this; + } + /** * @description bun fetch function * @param {Request} request bun request object * @returns {Response} bun response object */ - // @ts-ignore async fetch(request: Request): Promise { + // https://github.com/oven-sh/bun/issues/677 + if (!request.method) request.verb = 'DELETE'; const context = new Context(request); /** * invoke all app level middlewares */ this.middleware.forEach((cb, _) => { - if (typeof cb == "function") { - cb(context); - } + if (typeof cb == "function") cb(context); }); - + let exists: boolean = false; - let routes: Array = []; - - routes = Object.keys(this.routeTable); - - // temporal fix for "/" path matching all routes before it. - const index = routes.indexOf("/"); - if (index > -1) routes.push(routes.splice(index, 1)[0]); - + let routes: Array> = []; + + routes = this.routeTable.map(v => Object.keys(v)); + + const idx = routes.findIndex(v => v[0] == "/"); + if (idx > -1) routes.push(routes.splice(idx, 1)[0]); + for (let i = 0; i < routes.length; i++) { const route = routes[i]; - let parsedRoute = parse(route); - + let parsedRoute = parse(route[0]); + if ( new RegExp(parsedRoute).test(request.url) && - this.routeTable[route][request.method.toLowerCase()] + this.routeTable[i][route[0]]?.[request.method.toLowerCase() || request.verb.toLowerCase()] ) { - const middleware = this.routeTable[route][request.method.toLowerCase()]; + const middleware = this.routeTable[i][route[0]][request.method.toLowerCase() || request.verb.toLowerCase()]; const m = request.url.match(new RegExp(parsedRoute)); const _middleware = middleware.slice(); diff --git a/src/context.ts b/src/context.ts index f351d04..4f062eb 100644 --- a/src/context.ts +++ b/src/context.ts @@ -22,6 +22,11 @@ export default class Context { return this; } + public setHeader(key: string, value: any) { + this.headers[key] = value.toString(); + return this; + } + /** * @warning method might behave unexpectedly * @param raw diff --git a/src/params.ts b/src/params.ts index acc7e97..54d499a 100644 --- a/src/params.ts +++ b/src/params.ts @@ -29,17 +29,12 @@ function parse(url: string): string { } } - /** - * TODO: - * fix issue with route not matching exact value - */ if (isQuery) { return str; } - // add end border to query string str += "$"; - return str + return str; } export default parse; diff --git a/src/routeRegister.ts b/src/routeRegister.ts index 4657339..ed2a4d6 100644 --- a/src/routeRegister.ts +++ b/src/routeRegister.ts @@ -1,17 +1,16 @@ -import Context from "./context"; import { methods } from "./methods"; import type { Middleware, MethodType } from "./types.d"; -export default function register(path: string, method: MethodType, callback: Array, routeTable: object = {}): void | never { - routeTable[path] = validate(path, method, callback); +export default function register(path: string, method: MethodType, callback: Array, routeTable: Array = []): void | never { + routeTable.push({ [path]: validate(path, method, callback) }); } function validate(path: string, method: MethodType, callback: Array): { [path: string]: Array } { if (methods.indexOf(method) === -1) throw new Error("Invalid HTTP method, Accepted methods are: " + methods.join(" ")); if (path.charAt(0) !== "/") throw new Error("Invalid path, path must start with '/'"); - + for (const i in callback) - if (typeof callback[i] !== "function") throw new Error("Invalid handler function, handler must be a function"); - + if (typeof callback[i] !== "function") throw new Error("Invalid handler function, handler must be a function"); + return { [method.toLowerCase()]: callback }; } diff --git a/src/router.ts b/src/router.ts new file mode 100644 index 0000000..ee0b99b --- /dev/null +++ b/src/router.ts @@ -0,0 +1,58 @@ +import type { Middleware } from "./types.d"; +import routeRegister from "./routeRegister"; + +export default class Router { + routeTable: Array = []; + // constructor(routeTable?: RouteTable) { + // this.routeTable = routeTable; + // } + + /** + * @description register HTTP GET method + * @param path + * @returns void + */ + public get(path: string, ...cb: Array): any { + routeRegister(path, "GET", cb, this.routeTable); + } + + /** + * @description register HTTP POST method + * @param path + * @param cb + * @returns {this} + */ + public post(path: string, ...cb: Array): void { + routeRegister(path, "POST", cb, this.routeTable); + } + + /** + * @description register HTTP PATCH method + * @param path + * @param cb + * @returns {this} + */ + public patch(path: string, ...cb: Array): void { + routeRegister(path, "PATCH", cb, this.routeTable); + } + + /** + * @description register HTTP PUT method + * @param path + * @param cb + * @returns {this} + */ + public put(path: string, ...cb: Array): void { + routeRegister(path, "PUT", cb, this.routeTable); + } + + /** + * @description register HTTP DELETE method + * @param path + * @param cb + * @returns {this} + */ + public delete(path: string, ...cb: Array): void { + routeRegister(path, "DELETE", cb, this.routeTable); + } +} diff --git a/src/types.d.ts b/src/types.d.ts index 6542073..a96f102 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -5,6 +5,7 @@ declare global { query: Record; params: Record; body: object | string; + verb: string; } } export declare type Options = { @@ -26,7 +27,7 @@ export type MethodType = export declare type Context = _Context; export declare type Next = () => Promise | void; -export declare type Middleware = (context: Context, next: Next ) => Response | void | Promise; +export declare type Middleware = (context: Context, next: Next) => Response | void | Promise; /** * @class Jade * @description add route to routeTable, match and process request @@ -38,7 +39,7 @@ export interface IColston { readonly routeTable: object; readonly middleware: Array; readonly cache: Map; - + /** * @description internal error handler * @param error @@ -108,4 +109,4 @@ export interface IColston { * @returns bun server instance */ start(port?: number, cb?: Function): Server; -} \ No newline at end of file +} diff --git a/todo.md b/todo.md index 04e554f..ec4debb 100644 --- a/todo.md +++ b/todo.md @@ -5,5 +5,5 @@ - [x] strip out radix3 and ufo for custom router table and parser - [ ] allow middleware to return response and exit the application lifecycle at anytime. - [ ] add unit and integration tests -- [ ] add example folder with example(s) source file +- [x] add example folder with example(s) source file - [ ] rewrite the route implementation to allow wild cards and route paths (string) pattern matching diff --git a/tsconfig.json b/tsconfig.json index 5b5710b..c06481a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "target": "ES5", "declaration": true, "esModuleInterop": true, "isolatedModules": true,