├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── _header.ts ├── _http_method.ts ├── _mime.ts ├── app.ts ├── app_test.ts ├── benchmarks ├── app.ts └── paths.ts ├── constants.ts ├── context.ts ├── context_test.ts ├── dem.json ├── docs ├── context.md ├── cors.md ├── exception_filter.md ├── getting_started.md ├── logger.md ├── middleware.md ├── router.md ├── static_files.md ├── style_guide.md └── table_of_contents.md ├── examples ├── cat_app │ ├── cat.ts │ ├── handler.ts │ └── main.ts ├── file_upload │ ├── README.md │ ├── file │ └── main.ts ├── jsx │ └── main.jsx ├── static │ ├── assets │ │ ├── read.txt │ │ └── template.ts │ └── main.ts ├── template │ ├── index.html │ └── main.ts ├── test.ts ├── ultra_cat_app │ ├── .vscode │ │ └── settings.json │ ├── README.md │ ├── cat │ │ ├── cat.ts │ │ └── group.ts │ ├── db.ts │ ├── deps.ts │ ├── jsx_loader.ts │ ├── main.ts │ ├── public │ │ ├── .vscode │ │ │ └── settings.json │ │ ├── App.tsx │ │ ├── import_map.json │ │ ├── index.html │ │ ├── index.tsx │ │ └── pages │ │ │ ├── Home.tsx │ │ │ ├── List.tsx │ │ │ └── Login.tsx │ └── user │ │ ├── group.ts │ │ └── user.ts └── websocket │ ├── index.html │ └── server.ts ├── group.ts ├── group_test.ts ├── http_exception.ts ├── middleware ├── cors.ts ├── cors_test.ts ├── logger.ts ├── logger_test.ts └── skipper.ts ├── mod.ts ├── router.ts ├── router_test.ts ├── test_util.ts ├── types.ts ├── util.ts ├── util_test.ts └── vendor └── https └── deno.land ├── std ├── fmt │ └── colors.ts ├── http │ ├── cookie.ts │ ├── http_status.ts │ └── server.ts ├── io │ ├── buffer.ts │ ├── bufio.ts │ └── util.ts ├── mime │ └── multipart.ts ├── path │ └── mod.ts ├── testing │ ├── asserts.ts │ └── bench.ts ├── textproto │ └── mod.ts └── ws │ └── mod.ts └── x ├── dejs └── mod.ts ├── mysql └── mod.ts └── router └── mod.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Use Unix line endings in all text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: denolib/setup-deno@v2 12 | with: 13 | deno-version: 1.14.3 14 | 15 | - run: deno --version 16 | - run: deno fmt --check 17 | - run: deno test -A 18 | 19 | - name: Benchmarks 20 | run: deno run --allow-net ./benchmarks/app.ts 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /types 2 | .DS_Store 3 | /.vscode 4 | /.idea 5 | node_modules 6 | /package*.json 7 | /deno.d.ts 8 | typedoc 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### v1.3.3 / 2021.06.23 4 | 5 | - feat: Add ".ico" to mime 6 | - upgrade: deno_std to 0.99.0 7 | 8 | ### v1.3.2 / 2021.06.15 9 | 10 | - upgrade: deno_std to 0.98.0 11 | 12 | ### v1.3.1 / 2021.04.14 13 | 14 | - upgrade: deno_std to 0.92.0 15 | 16 | ### v1.3.0 / 2021.03.08 17 | 18 | - feat: Make "group.use" available anywhere 19 | - feat: Add ".mjs", ".css" to mime 20 | - upgrade: deno_std to 0.89.0 21 | 22 | ### v1.2.6 / 2021.02.19 23 | 24 | - fix(middleware/cors): Fix the matching error of `allowOrigins` 25 | - upgrade: deno_std 0.87.0 26 | 27 | ### v1.2.5 **deprecated** 28 | 29 | ### v1.2.4 / 2020.12.16 30 | 31 | - fix: add wasm in mimetypes 32 | - upgrade: deno_std 0.81.0 33 | 34 | ### v1.2.3 / 2020.12.02 35 | 36 | - refactor: remove namespace 37 | 38 | ### v1.2.2 / 2020.11.24 39 | 40 | - upgrade: deno_std 0.79.0 41 | 42 | ### v1.2.1 / 2020.11.08 43 | 44 | - fix: the default content type should carry utf8 45 | - upgrade: deno_std 0.76.0 46 | 47 | ### v1.2.0 / 2020.10.30 48 | 49 | - upgrade: deno_std 0.75.0 50 | - upgrade: router v2.0.0 51 | 52 | ### v1.1.0 / 2020.09.04 53 | 54 | - BREAKING: context.body use "get" accessor 55 | - fix: context.body cannot be read multiple times 56 | - upgrade: deno_std 0.67.0 57 | 58 | ### v1.0.3 / 2020.08.16 59 | 60 | - feat: Support get & set data to context 61 | - fix: Not serving static files correctly 62 | - upgrade: deno_std 0.65.0 63 | 64 | ### v1.0.2 / 2020.08.02 65 | 66 | - upgrade: deno_std 0.63.0 67 | - upgrade: router v1 68 | 69 | ### v1.0.1 / 2020.07.19 70 | 71 | - upgrade: deno_std 0.61.0 72 | 73 | ### v1.0.0 / 2020.07.05 74 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2020 木杉 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Abc 2 | 3 | > **A** **b**etter Deno framework to **c**reate web application 4 | 5 | [![tag](https://img.shields.io/github/tag/zhmushan/abc.svg)](https://github.com/zhmushan/abc) 6 | [![Build Status](https://github.com/zhmushan/abc/workflows/ci/badge.svg?branch=master)](https://github.com/zhmushan/abc/actions) 7 | [![license](https://img.shields.io/github/license/zhmushan/abc.svg)](https://github.com/zhmushan/abc) 8 | [![tag](https://img.shields.io/badge/deno->=1.0.0-green.svg)](https://github.com/denoland/deno) 9 | [![tag](https://img.shields.io/badge/std-0.98.0-green.svg)](https://github.com/denoland/deno) 10 | 11 | #### Quick links 12 | 13 | - [API Reference](https://doc.deno.land/https/deno.land/x/abc/mod.ts) 14 | - [Guides](https://deno.land/x/abc/docs/table_of_contents.md) 15 | - [Examples](https://deno.land/x/abc/examples) 16 | - [Changelog](https://deno.land/x/abc/CHANGELOG.md) 17 | 18 | ## Hello World 19 | 20 | ```ts 21 | import { Application } from "https://deno.land/x/abc@v1.3.3/mod.ts"; 22 | 23 | const app = new Application(); 24 | 25 | console.log("http://localhost:8080/"); 26 | 27 | app 28 | .get("/hello", (c) => { 29 | return "Hello, Abc!"; 30 | }) 31 | .start({ port: 8080 }); 32 | ``` 33 | -------------------------------------------------------------------------------- /_header.ts: -------------------------------------------------------------------------------- 1 | export const Accept = "Accept", 2 | AcceptEncoding = "Accept-Encoding", 3 | Allow = "Allow", 4 | Authorization = "Authorization", 5 | ContentDisposition = "Content-Disposition", 6 | ContentEncoding = "Content-Encoding", 7 | ContentLength = "Content-Length", 8 | ContentType = "Content-Type", 9 | Cookie = "Cookie", 10 | SetCookie = "Set-Cookie", 11 | IfModifiedSince = "If-Modified-Since", 12 | LastModified = "Last-Modified", 13 | Location = "Location", 14 | Upgrade = "Upgrade", 15 | Vary = "Vary", 16 | WWWAuthenticate = "WWW-Authenticate", 17 | XForwardedFor = "X-Forwarded-For", 18 | XForwardedProto = "X-Forwarded-Proto", 19 | XForwardedProtocol = "X-Forwarded-Protocol", 20 | XForwardedSsl = "X-Forwarded-Ssl", 21 | XUrlScheme = "X-Url-Scheme", 22 | XHTTPMethodOverride = "X-HTTP-Method-Override", 23 | XRealIP = "X-Real-IP", 24 | XRequestID = "X-Request-ID", 25 | XRequestedWith = "X-Requested-With", 26 | Server = "Server", 27 | Origin = "Origin", // Access control 28 | AccessControlRequestMethod = "Access-Control-Request-Method", 29 | AccessControlRequestHeaders = "Access-Control-Request-Headers", 30 | AccessControlAllowOrigin = "Access-Control-Allow-Origin", 31 | AccessControlAllowMethods = "Access-Control-Allow-Methods", 32 | AccessControlAllowHeaders = "Access-Control-Allow-Headers", 33 | AccessControlAllowCredentials = "Access-Control-Allow-Credentials", 34 | AccessControlExposeHeaders = "Access-Control-Expose-Headers", 35 | AccessControlMaxAge = "Access-Control-Max-Age", // Security 36 | StrictTransportSecurity = "Strict-Transport-Security", 37 | XContentTypeOptions = "X-Content-Type-Options", 38 | XXSSProtection = "X-XSS-Protection", 39 | XFrameOptions = "X-Frame-Options", 40 | ContentSecurityPolicy = "Content-Security-Policy", 41 | ContentSecurityPolicyReportOnly = "Content-Security-Policy-Report-Only", 42 | XCSRFToken = "X-CSRF-Token", 43 | ReferrerPolicy = "Referrer-Policy"; 44 | -------------------------------------------------------------------------------- /_http_method.ts: -------------------------------------------------------------------------------- 1 | export const Get = "GET", 2 | Head = "HEAD", 3 | Post = "POST", 4 | Put = "PUT", 5 | Patch = "PATCH", 6 | Delete = "DELETE", 7 | Connect = "CONNECT", 8 | Options = "OPTIONS", 9 | Trace = "TRACE"; 10 | -------------------------------------------------------------------------------- /_mime.ts: -------------------------------------------------------------------------------- 1 | const charsetUTF8 = "charset=UTF-8"; 2 | 3 | export const ApplicationGZip = "application/gzip", 4 | ApplicationJSON = "application/json", 5 | ApplicationJSONCharsetUTF8 = ApplicationJSON + "; " + charsetUTF8, 6 | ApplicationJavaScript = "application/javascript", 7 | ApplicationJavaScriptCharsetUTF8 = ApplicationJavaScript + "; " + 8 | charsetUTF8, 9 | ApplicationXML = "application/xml", 10 | ApplicationXMLCharsetUTF8 = ApplicationXML + "; " + charsetUTF8, 11 | TextMarkdown = "text/markdown", 12 | TextMarkdownCharsetUTF8 = TextMarkdown + "; " + charsetUTF8, 13 | TextXML = "text/xml", 14 | TextXMLCharsetUTF8 = TextXML + "; " + charsetUTF8, 15 | ApplicationForm = "application/x-www-form-urlencoded", 16 | ApplicationProtobuf = "application/protobuf", 17 | ApplicationMsgpack = "application/msgpack", 18 | TextHTML = "text/html", 19 | TextHTMLCharsetUTF8 = TextHTML + "; " + charsetUTF8, 20 | TextPlain = "text/plain", 21 | TextPlainCharsetUTF8 = TextPlain + "; " + charsetUTF8, 22 | TextCSS = "text/css", 23 | TextCSSCharsetUTF8 = TextCSS + "; " + charsetUTF8, 24 | MultipartForm = "multipart/form-data", 25 | OctetStream = "application/octet-stream", 26 | ImageSVG = "image/svg+xml", 27 | ImageXIcon = "image/x-icon", 28 | ApplicationWASM = "application/wasm"; 29 | 30 | export const DB: Record = { 31 | ".md": TextMarkdownCharsetUTF8, 32 | ".html": TextHTMLCharsetUTF8, 33 | ".htm": TextHTMLCharsetUTF8, 34 | ".json": ApplicationJSON, 35 | ".map": ApplicationJSON, 36 | ".txt": TextPlainCharsetUTF8, 37 | ".ts": ApplicationJavaScriptCharsetUTF8, 38 | ".tsx": ApplicationJavaScriptCharsetUTF8, 39 | ".js": ApplicationJavaScriptCharsetUTF8, 40 | ".jsx": ApplicationJavaScriptCharsetUTF8, 41 | ".gz": ApplicationGZip, 42 | ".svg": ImageSVG, 43 | ".wasm": ApplicationWASM, 44 | ".mjs": ApplicationJavaScriptCharsetUTF8, 45 | ".css": TextCSSCharsetUTF8, 46 | ".ico": ImageXIcon, 47 | }; 48 | -------------------------------------------------------------------------------- /app.ts: -------------------------------------------------------------------------------- 1 | import type { HandlerFunc, MiddlewareFunc, Renderer } from "./types.ts"; 2 | import type { Handler } from "./vendor/https/deno.land/std/http/server.ts"; 3 | 4 | import { serve, Server } from "./vendor/https/deno.land/std/http/server.ts"; 5 | import { join } from "./vendor/https/deno.land/std/path/mod.ts"; 6 | import { yellow } from "./vendor/https/deno.land/std/fmt/colors.ts"; 7 | import { Context } from "./context.ts"; 8 | import { Router } from "./router.ts"; 9 | import { Group } from "./group.ts"; 10 | import { 11 | createHttpExceptionBody, 12 | HttpException, 13 | InternalServerErrorException, 14 | } from "./http_exception.ts"; 15 | 16 | const { listen, listenTls } = Deno; 17 | 18 | export function NotImplemented(): Error { 19 | return new Error("Not Implemented"); 20 | } 21 | 22 | /** 23 | * Hello World. 24 | * 25 | * const app = new Application(); 26 | * 27 | * app 28 | * .get("/hello", (c) => { 29 | * return "Hello, Abc!"; 30 | * }) 31 | * .start({ port: 8080 }); 32 | */ 33 | export class Application { 34 | server?: Server; 35 | renderer?: Renderer; 36 | router = new Router(); 37 | middleware: MiddlewareFunc[] = []; 38 | premiddleware: MiddlewareFunc[] = []; 39 | 40 | #process?: Promise; 41 | #groups: Group[] = []; 42 | #closed = false; 43 | 44 | /** Unstable */ 45 | get θprocess(): Promise | undefined { 46 | console.warn(yellow("`Application#θprocess` is UNSTABLE!")); 47 | return this.#process; 48 | } 49 | 50 | async #start(listener: Deno.Listener): Promise { 51 | const handler: Handler = (req) => { 52 | const c = new Context({ 53 | r: req, 54 | app: this, 55 | }); 56 | let h: HandlerFunc; 57 | 58 | for (const i of this.#groups) { 59 | i.θapplyMiddleware(); 60 | } 61 | 62 | if (this.premiddleware.length === 0) { 63 | h = this.router.find(req.method, c); 64 | h = this.#applyMiddleware(h, ...this.middleware); 65 | } else { 66 | h = (c) => { 67 | h = this.router.find(req.method, c); 68 | h = this.#applyMiddleware(h, ...this.middleware); 69 | return h(c); 70 | }; 71 | h = this.#applyMiddleware(h, ...this.premiddleware); 72 | } 73 | 74 | return this.#transformResult(c, h).then(() => c.res); 75 | }; 76 | 77 | const s = this.server = new Server({ handler }); 78 | await s.serve(listener); 79 | } 80 | 81 | #applyMiddleware = (h: HandlerFunc, ...m: MiddlewareFunc[]): HandlerFunc => { 82 | for (let i = m.length - 1; i >= 0; --i) { 83 | h = m[i](h); 84 | } 85 | 86 | return h; 87 | }; 88 | 89 | /** 90 | * Start an HTTP server. 91 | * 92 | * app.start({ port: 8080 }); 93 | */ 94 | start(listenOptions: Deno.ListenOptions): void { 95 | this.#process = this.#start(listen(listenOptions)); 96 | } 97 | 98 | /** Start an HTTPS server. */ 99 | startTLS(listenOptions: Deno.ListenTlsOptions): void { 100 | this.#process = this.#start(listenTls(listenOptions)); 101 | } 102 | 103 | /** 104 | * Stop the server immediately. 105 | * 106 | * await app.close(); 107 | */ 108 | async close(): Promise { 109 | // console.log(this.listener); 110 | if (this.server) { 111 | this.server.close(); 112 | } 113 | await this.#process; 114 | } 115 | 116 | /** `pre` adds middleware which is run before router. */ 117 | pre(...m: MiddlewareFunc[]): Application { 118 | this.premiddleware.push(...m); 119 | return this; 120 | } 121 | 122 | /** `use` adds middleware which is run after router. */ 123 | use(...m: MiddlewareFunc[]): Application { 124 | this.middleware.push(...m); 125 | return this; 126 | } 127 | 128 | connect(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): Application { 129 | return this.add("CONNECT", path, h, ...m); 130 | } 131 | 132 | delete(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): Application { 133 | return this.add("DELETE", path, h, ...m); 134 | } 135 | 136 | get(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): Application { 137 | return this.add("GET", path, h, ...m); 138 | } 139 | 140 | head(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): Application { 141 | return this.add("HEAD", path, h, ...m); 142 | } 143 | 144 | options(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): Application { 145 | return this.add("OPTIONS", path, h, ...m); 146 | } 147 | 148 | patch(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): Application { 149 | return this.add("PATCH", path, h, ...m); 150 | } 151 | 152 | post(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): Application { 153 | return this.add("POST", path, h, ...m); 154 | } 155 | 156 | put(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): Application { 157 | return this.add("PUT", path, h, ...m); 158 | } 159 | 160 | trace(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): Application { 161 | return this.add("TRACE", path, h, ...m); 162 | } 163 | 164 | any(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): Application { 165 | const methods = [ 166 | "CONNECT", 167 | "DELETE", 168 | "GET", 169 | "HEAD", 170 | "OPTIONS", 171 | "PATCH", 172 | "POST", 173 | "PUT", 174 | "TRACE", 175 | ]; 176 | for (const method of methods) { 177 | this.add(method, path, h, ...m); 178 | } 179 | return this; 180 | } 181 | 182 | match( 183 | methods: string[], 184 | path: string, 185 | h: HandlerFunc, 186 | ...m: MiddlewareFunc[] 187 | ): Application { 188 | for (const method of methods) { 189 | this.add(method, path, h, ...m); 190 | } 191 | return this; 192 | } 193 | 194 | add( 195 | method: string, 196 | path: string, 197 | handler: HandlerFunc, 198 | ...middleware: MiddlewareFunc[] 199 | ): Application { 200 | this.router.add(method, path, (c: Context): unknown => { 201 | let h = handler; 202 | for (const m of middleware) { 203 | h = m(h); 204 | } 205 | return h(c); 206 | }); 207 | return this; 208 | } 209 | 210 | /** `group` creates a new router group with prefix and optional group level middleware. */ 211 | group(prefix: string, ...m: MiddlewareFunc[]): Group { 212 | const g = new Group({ app: this, prefix }); 213 | this.#groups.push(g); 214 | g.use(...m); 215 | return g; 216 | } 217 | 218 | /** 219 | * Register a new route with path prefix to serve static files from the provided root directory. 220 | * For example, a request to `/static/js/main.js` will fetch and serve `assets/js/main.js` file. 221 | * 222 | * app.static("/static", "assets"); 223 | */ 224 | static(prefix: string, root: string, ...m: MiddlewareFunc[]): Application { 225 | if (prefix[prefix.length - 1] === "/") { 226 | prefix = prefix.slice(0, prefix.length - 1); 227 | } 228 | const h: HandlerFunc = (c) => { 229 | const filepath = c.path.substr(prefix.length); 230 | return c.file(join(root, filepath)); 231 | }; 232 | return this.get(`${prefix}/*`, h, ...m); 233 | } 234 | 235 | /** 236 | * Register a new route with path to serve a static file with optional route-level middleware. 237 | * 238 | * app.file("/", "public/index.html"); 239 | */ 240 | file(path: string, filepath: string, ...m: MiddlewareFunc[]): Application { 241 | return this.get(path, (c) => c.file(filepath), ...m); 242 | } 243 | 244 | async #transformResult(c: Context, h: HandlerFunc): Promise { 245 | let result: unknown; 246 | try { 247 | result = await h(c); 248 | } catch (e) { 249 | if (e instanceof HttpException) { 250 | result = c.json( 251 | typeof e.response === "object" 252 | ? e.response 253 | : createHttpExceptionBody(e.response, undefined, e.status), 254 | e.status, 255 | ); 256 | } else { 257 | console.log(e); 258 | e = new InternalServerErrorException(e.message); 259 | result = c.json( 260 | (e as InternalServerErrorException).response, 261 | (e as InternalServerErrorException).status, 262 | ); 263 | } 264 | } 265 | if (c.response.status == undefined) { 266 | switch (typeof result) { 267 | case "object": 268 | if (result instanceof Uint8Array) { 269 | c.blob(result); 270 | } else { 271 | c.json(result as Record); 272 | } 273 | break; 274 | case "string": 275 | /^\s* { 15 | const app = createApplication(); 16 | app.static("/examples", "./examples/template"); 17 | 18 | let res = await fetch(`${addr}/examples/main.ts`); 19 | assertEquals(res.status, Status.OK); 20 | assertEquals( 21 | await res.text(), 22 | decoder.decode(await readFile("./examples/template/main.ts")), 23 | ); 24 | 25 | res = await fetch(`${addr}/examples/`); 26 | assertEquals(res.status, Status.NotFound); 27 | assertEquals( 28 | await res.text(), 29 | JSON.stringify(new NotFoundException().response), 30 | ); 31 | res = await fetch(`${addr}/examples/index.html`); 32 | assertEquals(res.status, Status.OK); 33 | assertEquals( 34 | await res.text(), 35 | decoder.decode(await readFile("./examples/template/index.html")), 36 | ); 37 | 38 | res = await fetch(`${addr}/examples/empty`); 39 | assertEquals(res.status, Status.NotFound); 40 | assertEquals( 41 | await res.text(), 42 | JSON.stringify(new NotFoundException().response), 43 | ); 44 | await app.close(); 45 | }); 46 | 47 | test("app file", async function (): Promise { 48 | const app = createApplication(); 49 | app.file("ci", "./.github/workflows/ci.yml"); 50 | app.file("fileempty", "./fileempty"); 51 | 52 | let res = await fetch(`${addr}/ci`); 53 | assertEquals(res.status, Status.OK); 54 | assertEquals( 55 | await res.text(), 56 | decoder.decode(await readFile("./.github/workflows/ci.yml")), 57 | ); 58 | 59 | res = await fetch(`${addr}/fileempty`); 60 | assertEquals(res.status, Status.NotFound); 61 | assertEquals( 62 | await res.text(), 63 | JSON.stringify(new NotFoundException().response), 64 | ); 65 | await app.close(); 66 | }); 67 | 68 | test("app middleware", async function (): Promise { 69 | const app = createApplication(); 70 | let str = ""; 71 | app 72 | .pre((next) => 73 | (c) => { 74 | str += "0"; 75 | return next(c); 76 | } 77 | ) 78 | .use( 79 | (next) => 80 | (c) => { 81 | str += "1"; 82 | return next(c); 83 | }, 84 | (next) => 85 | (c) => { 86 | str += "2"; 87 | return next(c); 88 | }, 89 | (next) => 90 | (c) => { 91 | str += "3"; 92 | return next(c); 93 | }, 94 | ) 95 | .get("/middleware", () => str); 96 | 97 | const res = await fetch(`${addr}/middleware`); 98 | assertEquals(res.status, Status.OK); 99 | assertEquals(await res.text(), str); 100 | assertEquals(str, "0123"); 101 | await app.close(); 102 | }); 103 | 104 | test("app middleware error", async function (): Promise { 105 | const app = createApplication(); 106 | const errMsg = "err"; 107 | app.get("/middlewareerror", NotFoundHandler, function (): HandlerFunc { 108 | return function (): HandlerFunc { 109 | throw new NotFoundException(errMsg); 110 | }; 111 | }); 112 | 113 | const res = await fetch(`${addr}/middlewareerror`); 114 | assertEquals(res.status, Status.NotFound); 115 | assertEquals( 116 | await res.text(), 117 | JSON.stringify(new NotFoundException(errMsg).response), 118 | ); 119 | await app.close(); 120 | }); 121 | 122 | test("app handler", async function (): Promise { 123 | const app = createApplication(); 124 | app.get("/ok", (): string => "ok"); 125 | 126 | const res = await fetch(`${addr}/ok`); 127 | assertEquals(res.status, Status.OK); 128 | assertEquals(await res.text(), "ok"); 129 | await app.close(); 130 | }); 131 | 132 | test("app http methods", async function (): Promise { 133 | const app = createApplication(); 134 | app 135 | .delete("/delete", (): string => "delete") 136 | .get("/get", (): string => "get") 137 | .post("/post", (): string => "post") 138 | .put("/put", (): string => "put") 139 | .any("/any", (): string => "any") 140 | .match(Object.values(HttpMethod), "/match", (): string => "match"); 141 | 142 | let res = await fetch(`${addr}/delete`, { method: HttpMethod.Delete }); 143 | assertEquals(res.status, Status.OK); 144 | assertEquals(await res.text(), "delete"); 145 | 146 | res = await fetch(`${addr}/get`, { method: HttpMethod.Get }); 147 | assertEquals(res.status, Status.OK); 148 | assertEquals(await res.text(), "get"); 149 | 150 | res = await fetch(`${addr}/post`, { method: HttpMethod.Post }); 151 | assertEquals(res.status, Status.OK); 152 | assertEquals(await res.text(), "post"); 153 | 154 | res = await fetch(`${addr}/put`, { method: HttpMethod.Put }); 155 | assertEquals(res.status, Status.OK); 156 | assertEquals(await res.text(), "put"); 157 | 158 | for (const i of ["GET", "PUT", "POST", "PATCH", "DELETE"]) { 159 | res = await fetch(`${addr}/any`, { method: i }); 160 | assertEquals(res.status, Status.OK); 161 | assertEquals(await res.text(), "any"); 162 | res = await fetch(`${addr}/match`, { method: i }); 163 | assertEquals(res.status, Status.OK); 164 | assertEquals(await res.text(), "match"); 165 | } 166 | await app.close(); 167 | }); 168 | 169 | test("app not found", async function (): Promise { 170 | const app = createApplication(); 171 | app.get("/not_found_handler", NotFoundHandler); 172 | 173 | const res = await fetch(`${addr}/not_found_handler`); 174 | assertEquals(res.status, Status.NotFound); 175 | assertEquals( 176 | await res.text(), 177 | JSON.stringify(new NotFoundException().response), 178 | ); 179 | await app.close(); 180 | }); 181 | 182 | test("app query string", async function (): Promise { 183 | const app = createApplication(); 184 | app.get("/qs", (c) => c.queryParams); 185 | const res = await fetch(`${addr}/qs?foo=bar`); 186 | assertEquals(res.status, Status.OK); 187 | assertEquals(await res.json(), { foo: "bar" }); 188 | await app.close(); 189 | }); 190 | 191 | test("app use after router", async function (): Promise { 192 | const app = createApplication(); 193 | let preUname: string | undefined, 194 | useUname: string | undefined, 195 | handlerUname: string | undefined; 196 | app.get("/:uname", (c) => { 197 | handlerUname = c.params.uname; 198 | }); 199 | app.pre((next) => 200 | (c) => { 201 | preUname = c.params.uname; 202 | return next(c); 203 | } 204 | ); 205 | app.use((next) => 206 | (c) => { 207 | useUname = c.params.uname; 208 | return next(c); 209 | } 210 | ); 211 | 212 | await fetch(`${addr}/zhmushan`).then((resp) => resp.text()); 213 | assertEquals(preUname, undefined); 214 | assertEquals(useUname, "zhmushan"); 215 | assertEquals(handlerUname, "zhmushan"); 216 | await app.close(); 217 | }); 218 | -------------------------------------------------------------------------------- /benchmarks/app.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "../vendor/https/deno.land/std/testing/asserts.ts"; 2 | import { 3 | bench, 4 | runBenchmarks, 5 | } from "../vendor/https/deno.land/std/testing/bench.ts"; 6 | import { Application } from "../mod.ts"; 7 | import paths from "./paths.ts"; 8 | 9 | const app = new Application(); 10 | 11 | for (const i of paths) { 12 | app.any(i, async (c) => c.path); 13 | } 14 | 15 | app.start({ port: 8080 }); 16 | 17 | bench({ 18 | name: "simple app", 19 | runs: 8, 20 | async func(b): Promise { 21 | b.start(); 22 | const conns = []; 23 | for (let i = 0; i < 50; ++i) { 24 | conns.push(fetch("http://localhost:8080/").then((resp) => resp.text())); 25 | } 26 | await Promise.all(conns); 27 | for await (const i of conns) { 28 | assertEquals(i, "/"); 29 | } 30 | b.stop(); 31 | }, 32 | }); 33 | 34 | runBenchmarks().finally(() => { 35 | app.close(); 36 | }); 37 | -------------------------------------------------------------------------------- /benchmarks/paths.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | "/", 3 | "/cmd/:tool/:sub", 4 | "/cmd/:tool/", 5 | "/src/*filepath", 6 | "/search/", 7 | "/search/:query", 8 | "/user_:name", 9 | "/user_:name/about", 10 | "/files/:dir/*filepath", 11 | "/doc/", 12 | "/doc/go_faq.html", 13 | "/doc/go1.html", 14 | "/info/:user/public", 15 | "/info/:user/project/:project", 16 | ]; 17 | -------------------------------------------------------------------------------- /constants.ts: -------------------------------------------------------------------------------- 1 | export * as HttpMethod from "./_http_method.ts"; 2 | export * as Header from "./_header.ts"; 3 | export * as MIME from "./_mime.ts"; 4 | -------------------------------------------------------------------------------- /context.ts: -------------------------------------------------------------------------------- 1 | import type { Cookie } from "./vendor/https/deno.land/std/http/cookie.ts"; 2 | import type { Application } from "./app.ts"; 3 | import type { ContextOptions } from "./types.ts"; 4 | 5 | import { Status } from "./vendor/https/deno.land/std/http/http_status.ts"; 6 | import { join } from "./vendor/https/deno.land/std/path/mod.ts"; 7 | import { 8 | getCookies, 9 | setCookie, 10 | } from "./vendor/https/deno.land/std/http/cookie.ts"; 11 | import { Header, MIME } from "./constants.ts"; 12 | import { contentType, NotFoundHandler } from "./util.ts"; 13 | 14 | const { cwd, readFile } = Deno; 15 | 16 | const encoder = new TextEncoder(); 17 | 18 | export class Context { 19 | app!: Application; 20 | #request!: Request; 21 | url!: URL; 22 | 23 | response: { 24 | body?: BodyInit; 25 | headers: Headers; 26 | status?: number; 27 | statusText?: string; 28 | } = { headers: new Headers() }; 29 | params: Record = {}; 30 | customContext: any; 31 | 32 | #store?: Map; 33 | 34 | get cookies(): Record { 35 | return getCookies(this.#request.headers); 36 | } 37 | 38 | get path(): string { 39 | return this.url.pathname; 40 | } 41 | 42 | get method(): string { 43 | return this.#request.method; 44 | } 45 | 46 | get queryParams(): Record { 47 | const params: Record = {}; 48 | for (const [k, v] of this.url.searchParams) { 49 | params[k] = v; 50 | } 51 | return params; 52 | } 53 | 54 | get req(): Request { 55 | return this.#request; 56 | } 57 | 58 | get res(): Response { 59 | const { body, headers, status, statusText } = this.response; 60 | return new Response(body, { headers, status, statusText }); 61 | } 62 | 63 | get(key: string | symbol): unknown { 64 | return this.#store?.get(key); 65 | } 66 | 67 | set(key: string | symbol, val: unknown): void { 68 | if (this.#store === undefined) { 69 | this.#store = new Map(); 70 | } 71 | 72 | this.#store.set(key, val); 73 | } 74 | 75 | constructor(opts: ContextOptions); 76 | constructor(c: Context); 77 | constructor(optionsOrContext: ContextOptions | Context) { 78 | if (optionsOrContext instanceof Context) { 79 | Object.assign(this, optionsOrContext); 80 | this.customContext = this; 81 | return; 82 | } 83 | 84 | const opts = optionsOrContext; 85 | this.app = opts.app; 86 | this.#request = opts.r; 87 | 88 | this.url = new URL(this.#request.url, `http://0.0.0.0`); 89 | } 90 | 91 | #writeContentType = (v: string): void => { 92 | if (!this.response.headers.has(Header.ContentType)) { 93 | this.response.headers.set(Header.ContentType, v); 94 | } 95 | }; 96 | 97 | string(v: string, code: Status = Status.OK): void { 98 | this.#writeContentType(MIME.TextPlainCharsetUTF8); 99 | this.response.status = code; 100 | this.response.body = encoder.encode(v); 101 | } 102 | 103 | json(v: Record | string, code: Status = Status.OK): void { 104 | this.#writeContentType(MIME.ApplicationJSONCharsetUTF8); 105 | this.response.status = code; 106 | this.response.body = encoder.encode( 107 | typeof v === "object" ? JSON.stringify(v) : v, 108 | ); 109 | } 110 | 111 | /** Sends an HTTP response with status code. */ 112 | html(v: string, code: Status = Status.OK): void { 113 | this.#writeContentType(MIME.TextHTMLCharsetUTF8); 114 | this.response.status = code; 115 | this.response.body = encoder.encode(v); 116 | } 117 | 118 | /** Sends an HTTP blob response with status code. */ 119 | htmlBlob(b: Uint8Array, code: Status = Status.OK): void { 120 | this.blob(b, MIME.TextHTMLCharsetUTF8, code); 121 | } 122 | 123 | /** 124 | * Renders a template with data and sends a text/html response with status code. 125 | * renderer must be registered first. 126 | */ 127 | async render( 128 | name: string, 129 | data: T = {} as T, 130 | code: Status = Status.OK, 131 | ): Promise { 132 | if (!this.app.renderer) { 133 | throw new Error(); 134 | } 135 | const r = await this.app.renderer.render(name, data); 136 | this.htmlBlob(r, code); 137 | } 138 | 139 | /** Sends a blob response with content type and status code. */ 140 | blob( 141 | b: Uint8Array, 142 | contentType?: string, 143 | code: Status = Status.OK, 144 | ): void { 145 | if (contentType) { 146 | this.#writeContentType(contentType); 147 | } 148 | this.response.status = code; 149 | this.response.body = b; 150 | } 151 | 152 | async file(filepath: string): Promise { 153 | filepath = join(cwd(), filepath); 154 | try { 155 | this.blob(await readFile(filepath), contentType(filepath)); 156 | } catch { 157 | NotFoundHandler(); 158 | } 159 | } 160 | 161 | /** append a `Set-Cookie` header to the response */ 162 | setCookie(c: Cookie): void { 163 | setCookie(this.response.headers, c); 164 | } 165 | 166 | /** Redirects a response to a specific URL. the `code` defaults to `302` if omitted */ 167 | redirect(url: string, code = Status.Found): void { 168 | this.response.headers.set(Header.Location, url); 169 | this.response.status = code; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /context_test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | assertStringIncludes, 4 | } from "./vendor/https/deno.land/std/testing/asserts.ts"; 5 | import { Status } from "./vendor/https/deno.land/std/http/http_status.ts"; 6 | import { createMockRequest } from "./test_util.ts"; 7 | import { Context } from "./context.ts"; 8 | import { Header } from "./constants.ts"; 9 | const { test } = Deno; 10 | 11 | test("context string resp", function (): void { 12 | const options = { app: undefined!, r: createMockRequest() }; 13 | const results = [ 14 | `{foo: "bar"}`, 15 | `

Title

`, 16 | `foo`, 17 | `foo=bar`, 18 | `undefined`, 19 | `null`, 20 | `0`, 21 | `true`, 22 | ``, 23 | ]; 24 | const c = new Context(options); 25 | for (const r of results) { 26 | c.string(r); 27 | assertEquals(c.response.status, 200); 28 | assertEquals(c.response.body, new TextEncoder().encode(r)); 29 | assertStringIncludes( 30 | c.response.headers!.get("Content-Type") ?? "", 31 | "text/plain", 32 | ); 33 | } 34 | }); 35 | 36 | test("context json resp", function (): void { 37 | const options = { app: undefined!, r: createMockRequest() }; 38 | const results = [{ foo: "bar" }, `{foo: "bar"}`, [1, 2], {}, [], `[]`]; 39 | const c = new Context(options); 40 | for (const r of results) { 41 | c.json(r); 42 | assertEquals(c.response.status, 200); 43 | assertEquals( 44 | c.response.body, 45 | new TextEncoder().encode(typeof r === "object" ? JSON.stringify(r) : r), 46 | ); 47 | assertStringIncludes( 48 | c.response.headers!.get("Content-Type") ?? "", 49 | "application/json", 50 | ); 51 | } 52 | }); 53 | 54 | test("context html resp", function (): void { 55 | const options = { app: undefined!, r: createMockRequest() }; 56 | const results = [ 57 | `{foo: "bar"}`, 58 | `

Title

`, 59 | `foo`, 60 | `foo=bar`, 61 | `undefined`, 62 | `null`, 63 | `0`, 64 | `true`, 65 | ``, 66 | ]; 67 | const c = new Context(options); 68 | for (const r of results) { 69 | c.html(r); 70 | assertEquals(c.response.status, 200); 71 | assertEquals(c.response.body, new TextEncoder().encode(r)); 72 | assertStringIncludes( 73 | c.response.headers!.get("Content-Type") ?? "", 74 | "text/html", 75 | ); 76 | } 77 | }); 78 | 79 | test("context file resp", async function (): Promise { 80 | const options = { app: undefined!, r: createMockRequest() }; 81 | const c = new Context(options); 82 | await c.file("./mod.ts"); 83 | assertEquals( 84 | c.response.headers!.get("Content-Type"), 85 | "application/javascript; charset=UTF-8", 86 | ); 87 | }); 88 | 89 | test("context req with cookies", function RequestWithCookies(): void { 90 | const options = { app: undefined!, r: createMockRequest() }; 91 | const c = new Context(options); 92 | c.req.headers.append("Cookie", "PREF=al=en-GB&f1=123; wide=1; SID=123"); 93 | assertEquals(c.cookies, { 94 | PREF: "al=en-GB&f1=123", 95 | wide: "1", 96 | SID: "123", 97 | }); 98 | c.setCookie({ 99 | name: "hello", 100 | value: "world", 101 | }); 102 | assertEquals(c.response.headers?.get("Set-Cookie"), "hello=world"); 103 | }); 104 | 105 | test("context redirect", function (): void { 106 | const options = { app: undefined!, r: createMockRequest() }; 107 | const c = new Context(options); 108 | c.redirect("https://a.com"); 109 | assertEquals(c.response.headers?.get(Header.Location), "https://a.com"); 110 | assertEquals(c.response.status, Status.Found); 111 | c.redirect("https://b.com", Status.UseProxy); 112 | assertEquals(c.response.headers?.get(Header.Location), "https://b.com"); 113 | assertEquals(c.response.status, Status.UseProxy); 114 | }); 115 | 116 | test("context custom", function (): void { 117 | class CustomContext extends Context { 118 | constructor(c: Context) { 119 | super(c); 120 | } 121 | 122 | hello(): string { 123 | return "hello"; 124 | } 125 | } 126 | 127 | const options = { app: undefined!, r: createMockRequest() }; 128 | const c: Context = new CustomContext(new Context(options)); 129 | const cc = c.customContext; 130 | 131 | assertEquals(cc.hello(), "hello"); 132 | }); 133 | 134 | test("context get set", function (): void { 135 | const c = new Context({ app: undefined!, r: createMockRequest() }); 136 | 137 | c.set("Hello", "World"); 138 | assertEquals(c.get("hello"), undefined); 139 | assertEquals(c.get("Hello"), "World"); 140 | 141 | const key = Symbol("Hello"); 142 | c.set(key, "World"); 143 | assertEquals(c.get(Symbol("Hello")), undefined); 144 | assertEquals(c.get(key), "World"); 145 | }); 146 | -------------------------------------------------------------------------------- /dem.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": [ 3 | { 4 | "protocol": "https", 5 | "path": "deno.land/std", 6 | "version": "0.110.0", 7 | "files": [ 8 | "/fmt/colors.ts", 9 | "/http/cookie.ts", 10 | "/http/http_status.ts", 11 | "/http/server.ts", 12 | "/io/buffer.ts", 13 | "/io/bufio.ts", 14 | "/io/util.ts", 15 | "/mime/multipart.ts", 16 | "/path/mod.ts", 17 | "/testing/asserts.ts", 18 | "/testing/bench.ts", 19 | "/textproto/mod.ts", 20 | "/ws/mod.ts" 21 | ] 22 | }, 23 | { 24 | "protocol": "https", 25 | "path": "deno.land/x/dejs", 26 | "version": "0.10.1", 27 | "files": [ 28 | "/mod.ts" 29 | ] 30 | }, 31 | { 32 | "protocol": "https", 33 | "path": "deno.land/x/mysql", 34 | "version": "v2.6.0", 35 | "files": [ 36 | "/mod.ts" 37 | ] 38 | }, 39 | { 40 | "protocol": "https", 41 | "path": "deno.land/x/router", 42 | "version": "v2.0.0", 43 | "files": [ 44 | "/mod.ts" 45 | ] 46 | } 47 | ], 48 | "aliases": {} 49 | } 50 | -------------------------------------------------------------------------------- /docs/context.md: -------------------------------------------------------------------------------- 1 | ## Context 2 | 3 | ### Data sharing 4 | 5 | ```ts 6 | app.use((next) => 7 | (c) => { 8 | c.set("Name", "Mu Shan"); 9 | return next(c); 10 | } 11 | ); 12 | 13 | app.get("/", (c) => { 14 | return `Hello ${c.get("Name")}`; 15 | }); 16 | ``` 17 | 18 | ### Use a custom context 19 | 20 | ```ts 21 | // Define `CustomContext` 22 | class CustomContext extends Context { 23 | constructor(c: Context) { 24 | super(c); 25 | } 26 | 27 | hello() { 28 | this.string("Hello World!"); 29 | } 30 | } 31 | 32 | // Replace the original `Context` 33 | app.pre((next) => 34 | (c) => { 35 | const cc = new CustomContext(c); 36 | return next(cc); 37 | } 38 | ); 39 | 40 | app.get("/", (c) => { 41 | const cc: CustomContext = c.customContext!; 42 | cc.hello(); 43 | }); 44 | 45 | app.start({ port: 8080 }); 46 | ``` 47 | 48 | Browse to http://localhost:8080 and you should see "Hello World!" on the page. 49 | -------------------------------------------------------------------------------- /docs/cors.md: -------------------------------------------------------------------------------- 1 | ## CORS 2 | 3 | `CORS` is a mechanism that allows resources to be requested from another domain, 4 | which enable secure cross-domain data transfers. 5 | 6 | ### Usage 7 | 8 | ```ts 9 | const config: CORSConfig = { 10 | allowOrigins: ["https://a.com", "https://b.com", "https://c.com"], 11 | allowMethods: [HttpMethod.Get], 12 | }; 13 | const app = new Application(); 14 | app.use(cors(config)); 15 | ``` 16 | 17 | ### Default Configuration 18 | 19 | ```ts 20 | export const DefaultCORSConfig: CORSConfig = { 21 | skipper: DefaultSkipper, 22 | allowOrigins: ["*"], 23 | allowMethods: [ 24 | HttpMethod.Delete, 25 | HttpMethod.Get, 26 | HttpMethod.Head, 27 | HttpMethod.Patch, 28 | HttpMethod.Post, 29 | HttpMethod.Put, 30 | ], 31 | }; 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/exception_filter.md: -------------------------------------------------------------------------------- 1 | ## Exception Filter 2 | 3 | Abc comes with a built-in `exceptions layer` that handles all unhandled 4 | exceptions in the application and then automatically sends an appropriate 5 | user-friendly response. 6 | 7 | ### Usage 8 | 9 | ```ts 10 | const app = new Application(); 11 | app.post("/admin", (c) => { 12 | throw new HttpException("Forbidden", Status.Forbidden); 13 | }); 14 | ``` 15 | 16 | When the client calls this endpoint, the response looks like this: 17 | 18 | ```json 19 | { 20 | "statusCode": 403, 21 | "message": "Forbidden" 22 | } 23 | ``` 24 | 25 | You can also customize the content of the response body. 26 | 27 | ```ts 28 | const app = new Application(); 29 | app.post("/admin", (c) => { 30 | throw new HttpException( 31 | { 32 | status: Status.Forbidden, 33 | error: "This is a custom message", 34 | }, 35 | Status.Forbidden, 36 | ); 37 | }); 38 | ``` 39 | 40 | Using the above, this is how the response would look: 41 | 42 | ```json 43 | { 44 | "status": 403, 45 | "error": "This is a custom message" 46 | } 47 | ``` 48 | 49 | ### Custom Exception 50 | 51 | Once you inherit `HttpException`, Abc will recognize your exception and 52 | automatically take care of the error response. 53 | 54 | ```ts 55 | export class ForbiddenException extends HttpException { 56 | constructor() { 57 | super("Forbidden", Status.Forbidden); 58 | } 59 | } 60 | 61 | const app = new Application(); 62 | app.post("/admin", (c) => { 63 | throw new ForbiddenException(); 64 | }); 65 | ``` 66 | 67 | Response: 68 | 69 | ```json 70 | { 71 | "statusCode": 403, 72 | "message": "Forbidden" 73 | } 74 | ``` 75 | 76 | ## Http Exceptions 77 | 78 | Abc has a set of exceptions inherited from `HttpException`: 79 | 80 | - BadGatewayException 81 | - BadRequestException 82 | - ConflictException 83 | - ForbiddenException 84 | - GatewayTimeoutException 85 | - GoneException 86 | - TeapotException 87 | - MethodNotAllowedException 88 | - NotAcceptableException 89 | - NotFoundException 90 | - NotImplementedException 91 | - RequestEntityTooLargeException 92 | - RequestTimeoutException 93 | - ServiceUnavailableException 94 | - UnauthorizedException 95 | - UnprocessableEntityException 96 | - InternalServerErrorException 97 | - UnsupportedMediaTypeException 98 | -------------------------------------------------------------------------------- /docs/getting_started.md: -------------------------------------------------------------------------------- 1 | ## Hello World 2 | 3 | Create `server.ts` 4 | 5 | ```ts 6 | import { Application } from "https://deno.land/x/abc@v1.3.3/mod.ts"; 7 | 8 | const app = new Application(); 9 | 10 | app 11 | .get("/hello", (c) => { 12 | return "Hello, Abc!"; 13 | }) 14 | .start({ port: 8080 }); 15 | ``` 16 | 17 | Start server 18 | 19 | ```sh 20 | $ deno run --allow-net ./server.ts 21 | ``` 22 | 23 | Browse to http://localhost:8080/hello and you should see Hello, Abc! on the 24 | page. 25 | 26 | ## Routing 27 | 28 | ```ts 29 | app 30 | .get("/users/", findAll) 31 | .get("/users/:id", findOne) 32 | .post("/users/", create) 33 | .delete("/users/:id", deleteOne); 34 | ``` 35 | 36 | ## Path Parameters 37 | 38 | ```ts 39 | const findOne: HandlerFunc = (c) => { 40 | // User ID from path `users/:id` 41 | const { id } = c.params; 42 | return id; 43 | }; 44 | // app.get("/users/:id", findOne); 45 | ``` 46 | 47 | Browse to http://localhost:8080/users/zhmushan and you should see "zhmushan" on 48 | the page. 49 | 50 | ## Query Parameters 51 | 52 | `/list?page=0&size=5` 53 | 54 | ```ts 55 | const paging: HandlerFunc = (c) => { 56 | // Get page and size from the query string 57 | const { page, size } = c.queryParams; 58 | return `page: ${page}, size: ${size}`; 59 | }; 60 | // app.get("/list", paging); 61 | ``` 62 | 63 | Browse to http://localhost:8080/list?page=0&size=5 and you should see "page: 0, 64 | size: 5" on the page. 65 | 66 | ## Static Content 67 | 68 | Serve any file from `./folder/sample` directory for path `/sample/*`. 69 | 70 | ```ts 71 | app.static("/sample", "./folder/sample"); 72 | ``` 73 | 74 | ## Middleware 75 | 76 | ```ts 77 | const track: MiddlewareFunc = (next) => 78 | (c) => { 79 | console.log(`request to ${c.path}`); 80 | return next(c); 81 | }; 82 | 83 | // Root middleware 84 | app.use(logger()); 85 | 86 | // Group level middleware 87 | const g = app.group("/admin"); 88 | g.use(track); 89 | 90 | // Route level middleware 91 | app.get( 92 | "/users", 93 | (c) => { 94 | return "/users"; 95 | }, 96 | track, 97 | ); 98 | ``` 99 | -------------------------------------------------------------------------------- /docs/logger.md: -------------------------------------------------------------------------------- 1 | ## Logger 2 | 3 | Logger logs the information about each HTTP request. 4 | 5 | ### Usage 6 | 7 | ```ts 8 | import { Application } from "https://deno.land/x/abc@v1.3.3/mod.ts"; 9 | import { logger } from "https://deno.land/x/abc@v1.3.3/middleware/logger.ts"; 10 | 11 | const app = new Application(); 12 | app.use(logger()); 13 | ``` 14 | 15 | ### Default Configuration 16 | 17 | ```ts 18 | export const DefaultLoggerConfig: LoggerConfig = { 19 | skipper: DefaultSkipper, 20 | formatter: DefaultFormatter, 21 | output: Deno.stdout, 22 | }; 23 | ``` 24 | 25 | ### Default Formatter 26 | 27 | ```ts 28 | export const DefaultFormatter: Formatter = (c) => { 29 | const req = c.request; 30 | 31 | const time = new Date().toISOString(); 32 | const method = req.method; 33 | const url = req.url || "/"; 34 | const protocol = c.request.proto; 35 | 36 | return `${time} ${method} ${url} ${protocol}\n`; 37 | }; 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/middleware.md: -------------------------------------------------------------------------------- 1 | ## Middleware 2 | 3 | Middleware is a function which is called around the route handler. Middleware 4 | functions have access to the `request` and `response` objects. 5 | 6 | Let's start by implementing a simple middleware feature. 7 | 8 | ```ts 9 | const track: MiddlewareFunc = (next) => 10 | (c) => { 11 | console.log(`request to ${c.path}`); 12 | return next(c); 13 | }; 14 | ``` 15 | 16 | ### Levels 17 | 18 | - Root Level: 19 | 20 | - `pre` can register middleware which executed before the router processes the 21 | request. 22 | - `use` can register middleware which executed after the router processes the 23 | request. 24 | 25 | - Group Level: When creating a new group, we can register middleware just for 26 | that group. 27 | 28 | - Route Level: When defining a new route, we can optionally register middleware 29 | just for it. 30 | 31 | **Note: Once the `next` function is not returned, the middleware call will be 32 | interrupted!** 33 | 34 | There are always people who like to recite the calling order of middleware: 35 | 36 | ```ts 37 | app.get( 38 | "/", 39 | () => { 40 | console.log(1); 41 | }, 42 | (next) => { 43 | console.log(2); 44 | return (c) => { 45 | console.log(3); 46 | return next(c); 47 | }; 48 | }, 49 | (next) => { 50 | console.log(4); 51 | return (c) => { 52 | console.log(5); 53 | return next(c); 54 | }; 55 | }, 56 | ); 57 | 58 | // output: 2, 4, 5, 3, 1 59 | ``` 60 | 61 | ### Skipper 62 | 63 | There are cases when you would like to skip a middleware based on some 64 | conditions, for that each middleware has an option to define a function 65 | `skipper(c: Context): boolean`. 66 | 67 | ```ts 68 | const app = new Application(); 69 | app.use( 70 | logger({ 71 | skipper: (c) => { 72 | return c.path.startsWith("/skipper"); 73 | }, 74 | }), 75 | ); 76 | ``` 77 | -------------------------------------------------------------------------------- /docs/router.md: -------------------------------------------------------------------------------- 1 | ## Router 2 | 3 | The router module based on 4 | [zhmushan/router](https://github.com/zhmushan/router). 5 | 6 | We will always match according to the rules of **Static > Param > Any**. For 7 | static routes, we always match strictly equal strings. 8 | 9 | **_Pattern: /\* ,/user/:name, /user/zhmushan_** 10 | 11 | | path | route | 12 | | :-------------: | :------------: | 13 | | /zhmushan | /\* | 14 | | /users/zhmushan | /\* | 15 | | /user/zhmushan | /user/zhmushan | 16 | | /user/other | /user/:name | 17 | 18 | ### Basic route 19 | 20 | ```ts 21 | import { Application } from "https://deno.land/x/abc@v1.3.3/mod.ts"; 22 | 23 | const app = new Application(); 24 | 25 | app.get("/user/:name", (c) => { 26 | const { name } = c.params; 27 | return `Hello ${name}!`; 28 | }); 29 | ``` 30 | 31 | ### Group route 32 | 33 | ```ts 34 | // user_group.ts 35 | import type { Group } from "https://deno.land/x/abc@v1.3.3/mod.ts"; 36 | 37 | export default function (g: Group) { 38 | g.get("/:name", (c) => { 39 | const { name } = c.params; 40 | return `Hello ${name}!`; 41 | }); 42 | } 43 | ``` 44 | 45 | ```ts 46 | import { Application } from "https://deno.land/x/abc@v1.3.3/mod.ts"; 47 | import userGroup from "./user_group.ts"; 48 | 49 | const app = new Application(); 50 | 51 | userGroup(app.group("user")); 52 | ``` 53 | -------------------------------------------------------------------------------- /docs/static_files.md: -------------------------------------------------------------------------------- 1 | ## Static Files 2 | 3 | `static` registers a new route with path prefix to serve static files from the 4 | provided root directory. For example, a request to `/static/js/main.js` will 5 | fetch and serve `assets/js/main.js` file. 6 | 7 | ```ts 8 | app.static("/static", "assets"); 9 | ``` 10 | 11 | `abc.file()` registers a new route with path to serve a static file. 12 | 13 | ```ts 14 | app.file("/", "public/index.html"); 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/style_guide.md: -------------------------------------------------------------------------------- 1 | # Abc Style Guide 2 | 3 | ## Names 4 | 5 | - Use PascalCase for type names. 6 | - Use PascalCase for enum values. 7 | - Use PascalCase for global constants. 8 | - Use camelCase for function names. 9 | - Use camelCase for property names and local variables. 10 | 11 | ```ts 12 | // Bad 13 | export type myType = string; 14 | 15 | // Good 16 | export type MyType = string; 17 | ``` 18 | 19 | ```ts 20 | // Bad 21 | enum Color { 22 | RED, 23 | BLACK, 24 | } 25 | 26 | // Good 27 | enum Color { 28 | Red, 29 | Black, 30 | } 31 | ``` 32 | 33 | ```ts 34 | // Bad 35 | export function notFoundHandler(_?: Context): never { 36 | throw new Error(); 37 | } 38 | 39 | // Good 40 | export function NotFoundHandler(_?: Context): never { 41 | throw new Error(); 42 | } 43 | ``` 44 | 45 | ```ts 46 | // Bad 47 | export function NotFoundHandler(flag: boolean): void | never { 48 | if (flag) { 49 | throw new Error(); 50 | } 51 | } 52 | 53 | // Good 54 | export function notFoundHandler(flag: boolean): void | never { 55 | if (flag) { 56 | throw new Error(); 57 | } 58 | } 59 | ``` 60 | 61 | ```ts 62 | // Bad 63 | const GLOBAL_CONFIG = {}; 64 | 65 | // Good 66 | const GlobalConfig = {}; 67 | ``` 68 | 69 | ## `null` & `undefined` 70 | 71 | - Always use `undefined`. 72 | -------------------------------------------------------------------------------- /docs/table_of_contents.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | - [Getting Started](./getting_started.md) 4 | - [Context](./context.md) 5 | - [Router](./router.md) 6 | - [Middleware](./middleware.md) 7 | - [Static Files](./static_files.md) 8 | - [Exception Filter](./exception_filter.md) 9 | 10 | ## Middleware 11 | 12 | - [Logger](./logger.md) 13 | - [CORS](./cors.md) 14 | 15 | [Style Guide](./style_guide.md) 16 | -------------------------------------------------------------------------------- /examples/cat_app/cat.ts: -------------------------------------------------------------------------------- 1 | let catId = 1; 2 | function genCatId(): number { 3 | return catId++; 4 | } 5 | 6 | export class Cat { 7 | id: number; 8 | name: string; 9 | age: number; 10 | constructor(cat: CatDTO) { 11 | this.id = genCatId(); 12 | this.name = cat.name; 13 | this.age = cat.age; 14 | } 15 | } 16 | 17 | export interface CatDTO { 18 | name: string; 19 | age: number; 20 | } 21 | -------------------------------------------------------------------------------- /examples/cat_app/handler.ts: -------------------------------------------------------------------------------- 1 | import type { HandlerFunc } from "../../mod.ts"; 2 | import { CatDTO } from "./cat.ts"; 3 | 4 | import { Cat } from "./cat.ts"; 5 | 6 | const cats: Cat[] = []; 7 | 8 | export const findAll: HandlerFunc = () => cats; 9 | export const findOne: HandlerFunc = (c) => { 10 | const { id } = c.params as { id: string }; 11 | return cats.find((cat) => cat.id.toString() === id); 12 | }; 13 | export const create: HandlerFunc = async ({ req }) => { 14 | const { name, age } = await req.json() as CatDTO; 15 | const cat = new Cat({ name, age }); 16 | cats.push(cat); 17 | return cat; 18 | }; 19 | -------------------------------------------------------------------------------- /examples/cat_app/main.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "../../mod.ts"; 2 | import { create, findAll, findOne } from "./handler.ts"; 3 | 4 | const app = new Application(); 5 | 6 | app 7 | .get("/", findAll) 8 | .get("/:id", findOne) 9 | .post("/", create) 10 | .start({ port: 8080 }); 11 | 12 | console.log(`server listening on http://localhost:8080`); 13 | -------------------------------------------------------------------------------- /examples/file_upload/README.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | ``` 4 | deno run --allow-net ./main.ts 5 | 6 | # in another terminal 7 | curl http://localhost:8080/file -F "file=@./file" 8 | ``` 9 | -------------------------------------------------------------------------------- /examples/file_upload/file: -------------------------------------------------------------------------------- 1 | Hello World! -------------------------------------------------------------------------------- /examples/file_upload/main.ts: -------------------------------------------------------------------------------- 1 | import type { FormFile } from "../../vendor/https/deno.land/std/mime/multipart.ts"; 2 | import { Application } from "../../mod.ts"; 3 | 4 | const decoder = new TextDecoder(); 5 | 6 | const app = new Application(); 7 | 8 | app.start({ port: 8080 }); 9 | 10 | console.log(`server listening on http://localhost:8080`); 11 | 12 | app.post("/file", async ({ req }) => { 13 | const { file } = await c.body as { file: FormFile }; 14 | return { 15 | name: file.filename, 16 | content: decoder.decode(file.content), 17 | }; 18 | }); 19 | -------------------------------------------------------------------------------- /examples/jsx/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "https://dev.jspm.io/react"; 2 | import ReactDOMServer from "https://dev.jspm.io/react-dom/server"; 3 | import { Application } from "../../mod.ts"; 4 | 5 | const app = new Application(); 6 | 7 | app.use((next) => 8 | (c) => { 9 | let e = next(c); 10 | if (React.isValidElement(e)) { 11 | e = ReactDOMServer.renderToString(e); 12 | } 13 | 14 | return e; 15 | } 16 | ); 17 | 18 | app.get("/", () => { 19 | return

Hello

; 20 | }) 21 | .start({ port: 8080 }); 22 | 23 | console.log(`server listening on http://localhost:8080`); 24 | -------------------------------------------------------------------------------- /examples/static/assets/read.txt: -------------------------------------------------------------------------------- 1 | static -------------------------------------------------------------------------------- /examples/static/assets/template.ts: -------------------------------------------------------------------------------- 1 | export const template = "abc"; 2 | -------------------------------------------------------------------------------- /examples/static/main.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "../../mod.ts"; 2 | import { cors } from "../../middleware/cors.ts"; 3 | 4 | const app = new Application(); 5 | const port = 8080; 6 | app.static("/", "./assets", cors()).start({ port }); 7 | 8 | console.log(`server listening on http://localhost:${port}`); 9 | -------------------------------------------------------------------------------- /examples/template/index.html: -------------------------------------------------------------------------------- 1 | <% if (name) { %> 2 |

hello, <%= name %>!

3 | <% } %> 4 | -------------------------------------------------------------------------------- /examples/template/main.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "../../mod.ts"; 2 | import { renderFile } from "../../vendor/https/deno.land/x/dejs/mod.ts"; 3 | import { readAll } from "../../vendor/https/deno.land/std/io/util.ts"; 4 | 5 | const app = new Application(); 6 | 7 | app.renderer = { 8 | async render(name: string, data: T): Promise { 9 | return renderFile(name, data).then(readAll); 10 | }, 11 | }; 12 | 13 | app 14 | .get("/", async (c) => { 15 | await c.render("./index.html", { name: "zhmushan" }); 16 | }) 17 | .start({ port: 8080 }); 18 | 19 | console.log(`server listening on http://localhost:8080`); 20 | -------------------------------------------------------------------------------- /examples/test.ts: -------------------------------------------------------------------------------- 1 | import { join } from "../vendor/https/deno.land/std/path/mod.ts"; 2 | import { 3 | assert, 4 | assertEquals, 5 | } from "../vendor/https/deno.land/std/testing/asserts.ts"; 6 | import { BufReader } from "../vendor/https/deno.land/std/io/bufio.ts"; 7 | import { TextProtoReader } from "../vendor/https/deno.land/std/textproto/mod.ts"; 8 | const { run, test, execPath, chdir, cwd } = Deno; 9 | 10 | const dir = join(import.meta.url, ".."); 11 | const addr = "http://localhost:8080"; 12 | let server: Deno.Process; 13 | 14 | async function startServer(fpath: string): Promise { 15 | server = run({ 16 | cmd: [execPath(), "run", "--allow-net", "--allow-read", fpath], 17 | stdout: "piped", 18 | }); 19 | assert(server.stdout != null); 20 | 21 | const r = new TextProtoReader(new BufReader(server.stdout)); 22 | const s = await r.readLine(); 23 | assert(s !== null && s.includes("server listening")); 24 | } 25 | 26 | function killServer(): void { 27 | server.close(); 28 | server.stdout?.close(); 29 | } 30 | 31 | test("exmaples cat app", async function () { 32 | await startServer(join(dir, "./cat_app/main.ts")); 33 | try { 34 | const cat = { name: "zhmushan", age: 18 }; 35 | const createdCat = await fetch(addr, { 36 | method: "POST", 37 | body: JSON.stringify(cat), 38 | headers: { 39 | "content-type": "application/json", 40 | }, 41 | }).then((resp) => resp.json()); 42 | const foundCats = await fetch(addr).then((resp) => resp.json()); 43 | const foundCat = await fetch(`${addr}/1`).then((resp) => resp.json()); 44 | 45 | assertEquals(createdCat, { id: 1, ...cat }); 46 | assertEquals(foundCat, createdCat); 47 | assertEquals(foundCats, [foundCat]); 48 | } finally { 49 | killServer(); 50 | } 51 | }); 52 | 53 | test("exmaples jsx", async function () { 54 | await startServer(join(dir, "./jsx/main.jsx")); 55 | try { 56 | const text = await fetch(addr).then((resp) => resp.text()); 57 | assertEquals(text, `

Hello

`); 58 | } finally { 59 | killServer(); 60 | } 61 | }); 62 | 63 | test("exmaples template", async function () { 64 | chdir(join(cwd(), "./examples/template")); 65 | await startServer(join(dir, "./template/main.ts")); 66 | try { 67 | const text = await fetch(addr).then((resp) => resp.text()); 68 | assert(text.includes("hello, zhmushan!")); 69 | } finally { 70 | killServer(); 71 | chdir("../../"); 72 | } 73 | }); 74 | -------------------------------------------------------------------------------- /examples/ultra_cat_app/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": false, 4 | "deno.unstable": true 5 | } 6 | -------------------------------------------------------------------------------- /examples/ultra_cat_app/README.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | ``` 4 | deno run -A --unstable main.ts 5 | ``` 6 | -------------------------------------------------------------------------------- /examples/ultra_cat_app/cat/cat.ts: -------------------------------------------------------------------------------- 1 | export class Cat { 2 | id?: number; 3 | name?: string; 4 | age?: number; 5 | } 6 | 7 | export interface CatDTO { 8 | name: string; 9 | age: number; 10 | } 11 | -------------------------------------------------------------------------------- /examples/ultra_cat_app/cat/group.ts: -------------------------------------------------------------------------------- 1 | import type { Group } from "../deps.ts"; 2 | import type { Cat } from "./cat.ts"; 3 | 4 | import DB from "../db.ts"; 5 | 6 | export default function (g: Group) { 7 | g.get("", (c) => { 8 | const cats = DB.query(`select * from cats`); 9 | 10 | return cats; 11 | }) 12 | .post("", async (c) => { 13 | const { name, age } = await c.body as Cat; 14 | 15 | if (name && age) { 16 | const result = await DB.execute( 17 | `INSERT INTO cats(name, age) VALUES(?, ?)`, 18 | [name, age], 19 | ); 20 | 21 | if (result.affectedRows === 1) { 22 | return { id: result.lastInsertId, name, age }; 23 | } 24 | } 25 | }) 26 | .delete("/:id", async (c) => { 27 | const id = Number(c.params.id); 28 | 29 | if (id) { 30 | const result = await DB.execute(`DELETE FROM cats WHERE id = ?`, [id]); 31 | 32 | return result; 33 | } 34 | }) 35 | .put("/:id", async (c) => { 36 | const id = Number(c.params.id); 37 | const { name, age } = await c.body as Cat; 38 | 39 | if (id) { 40 | const result = await DB.execute( 41 | `UPDATE cats SET name = ?, age = ? where id = ?`, 42 | [name, age, id], 43 | ); 44 | 45 | return result; 46 | } 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /examples/ultra_cat_app/db.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "./deps.ts"; 2 | 3 | const DB = await new Client().connect({ 4 | hostname: "127.0.0.1", 5 | username: "root", 6 | password: "", 7 | }); 8 | const dbname = "ultra_cat_app"; 9 | 10 | await DB.execute(`CREATE DATABASE IF NOT EXISTS ${dbname}`); 11 | await DB.execute(`USE ${dbname}`); 12 | 13 | await DB.execute(` 14 | CREATE TABLE IF NOT EXISTS users ( 15 | id int(11) NOT NULL AUTO_INCREMENT, 16 | username varchar(20) NOT NULL UNIQUE, 17 | password varchar(32) NOT NULL, 18 | created_at timestamp not null default current_timestamp, 19 | PRIMARY KEY (id) 20 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 21 | `); 22 | 23 | await DB.execute(` 24 | CREATE TABLE IF NOT EXISTS cats ( 25 | id int(11) NOT NULL AUTO_INCREMENT, 26 | name varchar(20) NOT NULL, 27 | age int NOT NULL, 28 | created_at timestamp not null default current_timestamp, 29 | PRIMARY KEY (id) 30 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 31 | `); 32 | 33 | export default DB; 34 | -------------------------------------------------------------------------------- /examples/ultra_cat_app/deps.ts: -------------------------------------------------------------------------------- 1 | export * from "../../mod.ts"; 2 | export * from "../../constants.ts"; 3 | export { 4 | decode, 5 | encode, 6 | } from "../../vendor/https/deno.land/std/encoding/utf8.ts"; 7 | export * as path from "../../vendor/https/deno.land/std/path/mod.ts"; 8 | export * from "../../vendor/https/deno.land/std/hash/md5.ts"; 9 | export * from "../../vendor/https/deno.land/x/mysql/mod.ts"; 10 | -------------------------------------------------------------------------------- /examples/ultra_cat_app/jsx_loader.ts: -------------------------------------------------------------------------------- 1 | import { decode, Header, MiddlewareFunc, MIME } from "./deps.ts"; 2 | const { transpileOnly, readFile } = Deno; 3 | 4 | export const jsxLoader: MiddlewareFunc = (next) => 5 | async (c) => { 6 | const filepath = c.get("realpath") as string | undefined; 7 | 8 | if (filepath && /\.[j|t]sx?$/.test(filepath)) { 9 | c.response.headers.set( 10 | Header.ContentType, 11 | MIME.ApplicationJavaScriptCharsetUTF8, 12 | ); 13 | const f = await readFile(filepath); 14 | return ( 15 | await transpileOnly( 16 | { 17 | [filepath]: decode(f), 18 | }, 19 | { jsx: "react" }, 20 | ) 21 | )[filepath].source; 22 | } 23 | 24 | return next(c); 25 | }; 26 | -------------------------------------------------------------------------------- /examples/ultra_cat_app/main.ts: -------------------------------------------------------------------------------- 1 | import { Application, path } from "./deps.ts"; 2 | import userGroup from "./user/group.ts"; 3 | import catGroup from "./cat/group.ts"; 4 | import { jsxLoader } from "./jsx_loader.ts"; 5 | 6 | const app = new Application(); 7 | app.start({ port: 8080 }); 8 | 9 | const staticRoot = "./public"; 10 | 11 | app 12 | .get("/", (c) => c.file("./public/index.html")) 13 | .static("/", staticRoot, jsxLoader, (n) => 14 | (c) => { 15 | c.set("realpath", path.join(staticRoot, c.path)); 16 | return n(c); 17 | }); 18 | 19 | userGroup(app.group("/user")); 20 | catGroup(app.group("/cat")); 21 | 22 | console.log(`server listening on http://localhost:8080`); 23 | -------------------------------------------------------------------------------- /examples/ultra_cat_app/public/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.import_map": "./import_map.json" 4 | } 5 | -------------------------------------------------------------------------------- /examples/ultra_cat_app/public/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link, Route, Switch } from "react-router-dom"; 3 | import HomePage from "./pages/Home.tsx"; 4 | import LoginPage from "./pages/Login.tsx"; 5 | import ListPage from "./pages/List.tsx"; 6 | 7 | export default () => ( 8 | <> 9 | Home 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /examples/ultra_cat_app/public/import_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "react-router-dom": "https://cdn.skypack.dev/react-router-dom@5.2.0?dts", 4 | "react-dom": "https://cdn.skypack.dev/react-dom@v16.13.1?dts", 5 | "react": "https://cdn.skypack.dev/react@16.13.1?dts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/ultra_cat_app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/ultra_cat_app/public/index.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import React from "react"; 4 | import * as ReactDOM from "react-dom"; 5 | import { BrowserRouter as Router } from "react-router-dom"; 6 | import App from "./App.tsx"; 7 | 8 | function Index() { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | } 15 | 16 | ReactDOM.render(, document.getElementById("root")); 17 | -------------------------------------------------------------------------------- /examples/ultra_cat_app/public/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | export default () => ( 4 | <> 5 | Login 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /examples/ultra_cat_app/public/pages/List.tsx: -------------------------------------------------------------------------------- 1 | import type { Cat } from "../../cat/cat.ts"; 2 | 3 | import React, { useEffect, useState } from "react"; 4 | import { Link, useLocation } from "react-router-dom"; 5 | 6 | export default () => { 7 | const [cats, setCats] = useState([]); 8 | const [id, setId] = useState(0); 9 | const [name, setName] = useState(""); 10 | const [age, setAge] = useState(0); 11 | const [] = useState([]); 12 | const location = useLocation<{ username: string }>(); 13 | const username = location.state.username; 14 | 15 | useEffect(() => { 16 | fetch("/cat").then((resp) => resp.json()).then((data) => { 17 | if (data.length > 0) { 18 | console.log(data); 19 | setCats(data); 20 | } 21 | }); 22 | }, []); 23 | 24 | function add() { 25 | fetch("/cat", { 26 | method: "POST", 27 | headers: { 28 | "content-type": "application/json", 29 | }, 30 | body: JSON.stringify({ name, age }), 31 | }).then((resp) => resp.json()).then((data) => { 32 | if (data.id) { 33 | setCats([...cats, data]); 34 | } else { 35 | throw new Error(); 36 | } 37 | }).catch(() => { 38 | alert("failed"); 39 | }); 40 | } 41 | 42 | function del(id: number) { 43 | fetch(`/cat/${id}`, { 44 | method: "DELETE", 45 | }).then((resp) => resp.json()).then((data) => { 46 | if (data.affectedRows === 1) { 47 | setCats(cats.filter((c) => c.id !== id)); 48 | } 49 | }); 50 | } 51 | 52 | function update(id: number) { 53 | const n = name; 54 | const a = age; 55 | fetch(`/cat/${id}`, { 56 | method: "PUT", 57 | headers: { 58 | "content-type": "application/json", 59 | }, 60 | body: JSON.stringify({ name: n, age: a }), 61 | }).then((resp) => resp.json()).then((data) => { 62 | if (data.affectedRows === 1) { 63 | setCats(cats.map((c) => { 64 | if (c.id === id) { 65 | return { id: c.id, name: n, age: a }; 66 | } 67 | return c; 68 | })); 69 | } 70 | }); 71 | } 72 | 73 | return ( 74 | <> 75 | Hello, {username}! Logout 76 |
77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | {cats.map((c) => ( 90 | 91 | 92 | 93 | 94 | 97 | 98 | ))} 99 | 100 |
Cats List
IDNameAgeAction
{c.id}{c.name}{c.age} 95 | 96 |
101 | 102 | 103 | setId(Number(e.target.value))} 107 | /> 108 |   109 | 110 | setName(e.target.value)} 115 | /> 116 |   117 | 118 | setAge(Number(e.target.value))} 122 | /> 123 |   124 | 125 | 126 | ); 127 | }; 128 | -------------------------------------------------------------------------------- /examples/ultra_cat_app/public/pages/Login.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | 4 | export default () => { 5 | const history = useHistory(); 6 | 7 | const [username, setUsername] = useState(""); 8 | const [password, setPassword] = useState(""); 9 | 10 | function login() { 11 | fetch("/user/login", { 12 | method: "POST", 13 | headers: { 14 | "content-type": "application/json", 15 | }, 16 | body: JSON.stringify({ username, password }), 17 | }).then((resp) => resp.json()).then((data) => { 18 | if (data.username === username) { 19 | history.push({ pathname: "/list", state: { username } }); 20 | } else { 21 | throw new Error(); 22 | } 23 | }).catch(() => { 24 | alert("failed"); 25 | }); 26 | } 27 | 28 | function signup() { 29 | fetch("/user/signup", { 30 | method: "POST", 31 | headers: { 32 | "content-type": "application/json", 33 | }, 34 | body: JSON.stringify({ username, password }), 35 | }).then((resp) => resp.json()).then((data) => { 36 | if (data.username === username) { 37 | alert("success"); 38 | } else { 39 | throw new Error(); 40 | } 41 | }).catch(() => { 42 | alert("failed"); 43 | }); 44 | } 45 | 46 | return ( 47 | <> 48 | 49 | setUsername(e.target.value)} 53 | autoComplete="off" 54 | /> 55 |
56 | 57 | setPassword(e.target.value)} 61 | /> 62 |
63 | 64 | 65 | 66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /examples/ultra_cat_app/user/group.ts: -------------------------------------------------------------------------------- 1 | import type { Group } from "../deps.ts"; 2 | import type { User } from "./user.ts"; 3 | 4 | import { Md5 } from "../deps.ts"; 5 | import DB from "../db.ts"; 6 | 7 | export default function (g: Group) { 8 | g.post("/login", async (c) => { 9 | let { username, password } = await c.body as User; 10 | if (username && password) { 11 | const user = ((await DB.query( 12 | `SELECT password FROM users WHERE username = ?`, 13 | [username], 14 | )) as { password: string }[])[0]; 15 | if (user.password === new Md5().update(password).toString()) { 16 | return { username }; 17 | } 18 | } 19 | }).post("/signup", async (c) => { 20 | let { username, password } = await c.body as User; 21 | if (username && password) { 22 | const user = await DB.transaction(async (conn) => { 23 | await conn.execute( 24 | `INSERT INTO users(username, password) VALUES(?, ?)`, 25 | [username, new Md5().update(password!).toString()], 26 | ); 27 | const result = (await conn.query( 28 | `SELECT username FROM users WHERE username = ?`, 29 | [username], 30 | )) as { username: string }[]; 31 | 32 | return result[0]; 33 | }).catch((e) => { 34 | console.log(e); 35 | }); 36 | 37 | return user; 38 | } 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /examples/ultra_cat_app/user/user.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id?: number; 3 | username?: string; 4 | password?: string; 5 | } 6 | -------------------------------------------------------------------------------- /examples/websocket/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | WebSocket 4 | 5 | 6 | 7 |

8 | 9 | 34 | -------------------------------------------------------------------------------- /examples/websocket/server.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "../../mod.ts"; 2 | import { HandlerFunc } from "../../types.ts"; 3 | import { acceptWebSocket } from "../../vendor/https/deno.land/std/ws/mod.ts"; 4 | 5 | const app = new Application(); 6 | 7 | const hello: HandlerFunc = async (c) => { 8 | const { conn, headers, r: bufReader, w: bufWriter } = c.request; 9 | const ws = await acceptWebSocket({ 10 | conn, 11 | headers, 12 | bufReader, 13 | bufWriter, 14 | }); 15 | 16 | for await (const e of ws) { 17 | console.log(e); 18 | await ws.send("Hello, Client!"); 19 | } 20 | }; 21 | 22 | app.get("/ws", hello).file("/", "./index.html").start({ port: 8080 }); 23 | 24 | console.log(`server listening on http://localhost:8080`); 25 | -------------------------------------------------------------------------------- /group.ts: -------------------------------------------------------------------------------- 1 | import type { HandlerFunc, MiddlewareFunc } from "./types.ts"; 2 | import type { Application } from "./app.ts"; 3 | 4 | import { join } from "./vendor/https/deno.land/std/path/mod.ts"; 5 | 6 | export class Group { 7 | prefix: string; 8 | middleware: MiddlewareFunc[]; 9 | app: Application; 10 | 11 | #willBeAdded: Array< 12 | [method: string, path: string, h: HandlerFunc, m: MiddlewareFunc[]] 13 | >; 14 | 15 | constructor(opts: { app: Application; prefix: string }) { 16 | this.prefix = opts.prefix || ""; 17 | this.app = opts.app || ({} as Application); 18 | 19 | this.middleware = []; 20 | this.#willBeAdded = []; 21 | } 22 | 23 | use(...m: MiddlewareFunc[]): Group { 24 | this.middleware.push(...m); 25 | return this; 26 | } 27 | 28 | connect(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): Group { 29 | this.#willBeAdded.push(["CONNECT", path, h, m]); 30 | return this; 31 | } 32 | delete(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): Group { 33 | this.#willBeAdded.push(["DELETE", path, h, m]); 34 | return this; 35 | } 36 | get(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): Group { 37 | this.#willBeAdded.push(["GET", path, h, m]); 38 | return this; 39 | } 40 | head(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): Group { 41 | this.#willBeAdded.push(["HEAD", path, h, m]); 42 | return this; 43 | } 44 | options(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): Group { 45 | this.#willBeAdded.push(["OPTIONS", path, h, m]); 46 | return this; 47 | } 48 | patch(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): Group { 49 | this.#willBeAdded.push(["PATCH", path, h, m]); 50 | return this; 51 | } 52 | post(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): Group { 53 | this.#willBeAdded.push(["POST", path, h, m]); 54 | return this; 55 | } 56 | put(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): Group { 57 | this.#willBeAdded.push(["PUT", path, h, m]); 58 | return this; 59 | } 60 | trace(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): Group { 61 | this.#willBeAdded.push(["TRACE", path, h, m]); 62 | return this; 63 | } 64 | any(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): Group { 65 | const methods = [ 66 | "CONNECT", 67 | "DELETE", 68 | "GET", 69 | "HEAD", 70 | "OPTIONS", 71 | "PATCH", 72 | "POST", 73 | "PUT", 74 | "TRACE", 75 | ]; 76 | for (const method of methods) { 77 | this.#willBeAdded.push([method, path, h, m]); 78 | } 79 | return this; 80 | } 81 | match( 82 | methods: string[], 83 | path: string, 84 | h: HandlerFunc, 85 | ...m: MiddlewareFunc[] 86 | ): Group { 87 | for (const method of methods) { 88 | this.#willBeAdded.push([method, path, h, m]); 89 | } 90 | return this; 91 | } 92 | add( 93 | method: string, 94 | path: string, 95 | handler: HandlerFunc, 96 | ...middleware: MiddlewareFunc[] 97 | ): Group { 98 | this.#willBeAdded.push([method, path, handler, middleware]); 99 | return this; 100 | } 101 | 102 | static(prefix: string, root: string): Group { 103 | this.app.static(join(this.prefix, prefix), root); 104 | return this; 105 | } 106 | 107 | file(p: string, filepath: string, ...m: MiddlewareFunc[]): Group { 108 | this.app.file(join(this.prefix, p), filepath, ...m); 109 | return this; 110 | } 111 | 112 | group(prefix: string, ...m: MiddlewareFunc[]): Group { 113 | const g = this.app.group(this.prefix + prefix, ...this.middleware, ...m); 114 | return g; 115 | } 116 | 117 | θapplyMiddleware(): void { 118 | for (const [method, path, handler, middleware] of this.#willBeAdded) { 119 | this.app.add( 120 | method, 121 | this.prefix + path, 122 | handler, 123 | ...this.middleware, 124 | ...middleware, 125 | ); 126 | } 127 | this.#willBeAdded = []; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /group_test.ts: -------------------------------------------------------------------------------- 1 | import type { HandlerFunc, MiddlewareFunc } from "./types.ts"; 2 | 3 | import { assertEquals } from "./vendor/https/deno.land/std/testing/asserts.ts"; 4 | import { Status } from "./vendor/https/deno.land/std/http/http_status.ts"; 5 | import { createApplication } from "./test_util.ts"; 6 | const { test } = Deno; 7 | 8 | const addr = `http://localhost:8081`; 9 | 10 | test("group middleware", async function (): Promise { 11 | const app = createApplication(); 12 | const g = app.group("group"); 13 | const h: HandlerFunc = function (): void { 14 | return; 15 | }; 16 | const m1: MiddlewareFunc = (next) => (c) => next(c); 17 | const m2: MiddlewareFunc = (next) => (c) => next(c); 18 | const m3: MiddlewareFunc = (next) => (c) => next(c); 19 | const m4: MiddlewareFunc = () => 20 | (c) => { 21 | c.response.status = 404; 22 | }; 23 | const m5: MiddlewareFunc = () => 24 | (c) => { 25 | c.response.status = 405; 26 | }; 27 | 28 | g.use(m1, m2, m3); 29 | g.get("/404", h, m4); 30 | g.get("/405", h, m5); 31 | let res = await fetch(`${addr}/group/404`); 32 | assertEquals(res.status, Status.NotFound); 33 | assertEquals(await res.text(), ""); 34 | res = await fetch(`${addr}/group/405`); 35 | assertEquals(res.status, Status.MethodNotAllowed); 36 | assertEquals(await res.text(), ""); 37 | 38 | const u = app.group("user"); 39 | 40 | const check: MiddlewareFunc = (next) => { 41 | return function (c) { 42 | const { id } = c.params as { id: string }; 43 | if (id === "zhmushan") { 44 | c.set("role", "admin"); 45 | } else { 46 | c.set("role", "user"); 47 | } 48 | return next(c); 49 | }; 50 | }; 51 | 52 | u.get("/", (_) => "/"); 53 | u.get("/:id", (c) => { 54 | const role = c.get("role") as string; 55 | return role; 56 | }); 57 | 58 | u.use(check); 59 | 60 | res = await fetch(`${addr}/user/zhmushan`); 61 | assertEquals(res.status, Status.OK); 62 | assertEquals(await res.text(), "admin"); 63 | res = await fetch(`${addr}/user/MuShan`); 64 | assertEquals(res.status, Status.OK); 65 | assertEquals(await res.text(), "user"); 66 | await app.close(); 67 | }); 68 | -------------------------------------------------------------------------------- /http_exception.ts: -------------------------------------------------------------------------------- 1 | import { Status } from "./vendor/https/deno.land/std/http/http_status.ts"; 2 | 3 | export interface HttpExceptionBody { 4 | message?: string; 5 | error?: string; 6 | statusCode?: number; 7 | } 8 | 9 | export function createHttpExceptionBody( 10 | message: string, 11 | error?: string, 12 | statusCode?: number, 13 | ): HttpExceptionBody; 14 | export function createHttpExceptionBody>( 15 | body: T, 16 | ): T; 17 | export function createHttpExceptionBody>( 18 | msgOrBody: string | T, 19 | error?: string, 20 | statusCode?: number, 21 | ): HttpExceptionBody | T { 22 | if (typeof msgOrBody === "object" && !Array.isArray(msgOrBody)) { 23 | return msgOrBody; 24 | } else if (typeof msgOrBody === "string") { 25 | return { statusCode, error, message: msgOrBody }; 26 | } 27 | return { statusCode, error }; 28 | } 29 | 30 | export class HttpException extends Error { 31 | readonly message: any; 32 | constructor( 33 | readonly response: string | Record, 34 | readonly status: number, 35 | ) { 36 | super(); 37 | this.message = response; 38 | } 39 | } 40 | 41 | export class BadGatewayException extends HttpException { 42 | constructor( 43 | message?: string | Record | any, 44 | error = "Bad Gateway", 45 | ) { 46 | super( 47 | createHttpExceptionBody(message, error, Status.BadGateway), 48 | Status.BadGateway, 49 | ); 50 | } 51 | } 52 | 53 | export class BadRequestException extends HttpException { 54 | constructor( 55 | message?: string | Record | any, 56 | error = "Bad Request", 57 | ) { 58 | super( 59 | createHttpExceptionBody(message, error, Status.BadRequest), 60 | Status.BadRequest, 61 | ); 62 | } 63 | } 64 | 65 | export class ConflictException extends HttpException { 66 | constructor( 67 | message?: string | Record | any, 68 | error = "Conflict", 69 | ) { 70 | super( 71 | createHttpExceptionBody(message, error, Status.Conflict), 72 | Status.Conflict, 73 | ); 74 | } 75 | } 76 | 77 | export class ForbiddenException extends HttpException { 78 | constructor( 79 | message?: string | Record | any, 80 | error = "Forbidden", 81 | ) { 82 | super( 83 | createHttpExceptionBody(message, error, Status.Forbidden), 84 | Status.Forbidden, 85 | ); 86 | } 87 | } 88 | 89 | export class GatewayTimeoutException extends HttpException { 90 | constructor( 91 | message?: string | Record | any, 92 | error = "Gateway Timeout", 93 | ) { 94 | super( 95 | createHttpExceptionBody(message, error, Status.GatewayTimeout), 96 | Status.GatewayTimeout, 97 | ); 98 | } 99 | } 100 | 101 | export class GoneException extends HttpException { 102 | constructor(message?: string | Record | any, error = "Gone") { 103 | super(createHttpExceptionBody(message, error, Status.Gone), Status.Gone); 104 | } 105 | } 106 | 107 | export class TeapotException extends HttpException { 108 | constructor(message?: string | Record | any, error = "Teapot") { 109 | super( 110 | createHttpExceptionBody(message, error, Status.Teapot), 111 | Status.Teapot, 112 | ); 113 | } 114 | } 115 | 116 | export class MethodNotAllowedException extends HttpException { 117 | constructor( 118 | message?: string | Record | any, 119 | error = "Method Not Allowed", 120 | ) { 121 | super( 122 | createHttpExceptionBody(message, error, Status.MethodNotAllowed), 123 | Status.MethodNotAllowed, 124 | ); 125 | } 126 | } 127 | 128 | export class NotAcceptableException extends HttpException { 129 | constructor( 130 | message?: string | Record | any, 131 | error = "Not Acceptable", 132 | ) { 133 | super( 134 | createHttpExceptionBody(message, error, Status.NotAcceptable), 135 | Status.NotAcceptable, 136 | ); 137 | } 138 | } 139 | 140 | export class NotFoundException extends HttpException { 141 | constructor( 142 | message?: string | Record | any, 143 | error = "Not Found", 144 | ) { 145 | super( 146 | createHttpExceptionBody(message, error, Status.NotFound), 147 | Status.NotFound, 148 | ); 149 | } 150 | } 151 | 152 | export class NotImplementedException extends HttpException { 153 | constructor( 154 | message?: string | Record | any, 155 | error = "Not Implemented", 156 | ) { 157 | super( 158 | createHttpExceptionBody(message, error, Status.NotImplemented), 159 | Status.NotImplemented, 160 | ); 161 | } 162 | } 163 | 164 | export class RequestEntityTooLargeException extends HttpException { 165 | constructor( 166 | message?: string | Record | any, 167 | error = "Request Entity Too Large", 168 | ) { 169 | super( 170 | createHttpExceptionBody(message, error, Status.RequestEntityTooLarge), 171 | Status.RequestEntityTooLarge, 172 | ); 173 | } 174 | } 175 | 176 | export class RequestTimeoutException extends HttpException { 177 | constructor( 178 | message?: string | Record | any, 179 | error = "Request Timeout", 180 | ) { 181 | super( 182 | createHttpExceptionBody(message, error, Status.RequestTimeout), 183 | Status.RequestTimeout, 184 | ); 185 | } 186 | } 187 | 188 | export class ServiceUnavailableException extends HttpException { 189 | constructor( 190 | message?: string | Record | any, 191 | error = "Service Unavailable", 192 | ) { 193 | super( 194 | createHttpExceptionBody(message, error, Status.ServiceUnavailable), 195 | Status.ServiceUnavailable, 196 | ); 197 | } 198 | } 199 | 200 | export class UnauthorizedException extends HttpException { 201 | constructor( 202 | message?: string | Record | any, 203 | error = "Unauthorized", 204 | ) { 205 | super( 206 | createHttpExceptionBody(message, error, Status.Unauthorized), 207 | Status.Unauthorized, 208 | ); 209 | } 210 | } 211 | 212 | export class UnprocessableEntityException extends HttpException { 213 | constructor( 214 | message?: string | Record | any, 215 | error = "Unprocessable Entity", 216 | ) { 217 | super( 218 | createHttpExceptionBody(message, error, Status.UnprocessableEntity), 219 | Status.UnprocessableEntity, 220 | ); 221 | } 222 | } 223 | 224 | export class InternalServerErrorException extends HttpException { 225 | constructor( 226 | message?: string | Record | any, 227 | error = "Internal Server Error", 228 | ) { 229 | super( 230 | createHttpExceptionBody(message, error, Status.InternalServerError), 231 | Status.InternalServerError, 232 | ); 233 | } 234 | } 235 | 236 | export class UnsupportedMediaTypeException extends HttpException { 237 | constructor( 238 | message?: string | Record | any, 239 | error = "Unsupported Media Type", 240 | ) { 241 | super( 242 | createHttpExceptionBody(message, error, Status.UnsupportedMediaType), 243 | Status.UnsupportedMediaType, 244 | ); 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /middleware/cors.ts: -------------------------------------------------------------------------------- 1 | import type { HandlerFunc, MiddlewareFunc } from "../types.ts"; 2 | import type { Skipper } from "./skipper.ts"; 3 | 4 | import { Status } from "../vendor/https/deno.land/std/http/http_status.ts"; 5 | import { DefaultSkipper } from "./skipper.ts"; 6 | import { Header, HttpMethod } from "../constants.ts"; 7 | 8 | export const DefaultCORSConfig: CORSConfig = { 9 | skipper: DefaultSkipper, 10 | allowOrigins: ["*"], 11 | allowMethods: [ 12 | HttpMethod.Delete, 13 | HttpMethod.Get, 14 | HttpMethod.Head, 15 | HttpMethod.Patch, 16 | HttpMethod.Post, 17 | HttpMethod.Put, 18 | ], 19 | }; 20 | 21 | export function cors(config: CORSConfig = DefaultCORSConfig): MiddlewareFunc { 22 | if (config.skipper == null) { 23 | config.skipper = DefaultCORSConfig.skipper; 24 | } 25 | if (!config.allowOrigins || config.allowOrigins.length == 0) { 26 | config.allowOrigins = DefaultCORSConfig.allowOrigins; 27 | } 28 | if (!config.allowMethods || config.allowMethods.length == 0) { 29 | config.allowMethods = DefaultCORSConfig.allowMethods; 30 | } 31 | 32 | return function (next: HandlerFunc): HandlerFunc { 33 | return (c) => { 34 | if (config.skipper!(c)) { 35 | return next(c); 36 | } 37 | const req = c.req; 38 | const resp = c.response; 39 | const origin = req.headers!.get(Header.Origin)!; 40 | if (!resp.headers) resp.headers = new Headers(); 41 | 42 | let allowOrigin: string | null = null; 43 | for (const o of config.allowOrigins!) { 44 | if (o == "*" && config.allowCredentials) { 45 | allowOrigin = origin; 46 | break; 47 | } 48 | if (o == "*" || o == origin) { 49 | allowOrigin = o; 50 | break; 51 | } 52 | if (origin === null) { 53 | break; 54 | } 55 | if (origin.startsWith(o)) { 56 | allowOrigin = origin; 57 | break; 58 | } 59 | } 60 | 61 | resp.headers.append(Header.Vary, Header.Origin); 62 | if (config.allowCredentials) { 63 | resp.headers.set(Header.AccessControlAllowCredentials, "true"); 64 | } 65 | 66 | if (req.method != HttpMethod.Options) { 67 | if (allowOrigin) { 68 | resp.headers.set(Header.AccessControlAllowOrigin, allowOrigin); 69 | } 70 | if (config.exposeHeaders && config.exposeHeaders.length != 0) { 71 | resp.headers.set( 72 | Header.AccessControlExposeHeaders, 73 | config.exposeHeaders.join(","), 74 | ); 75 | } 76 | 77 | return next(c); 78 | } 79 | resp.headers.append(Header.Vary, Header.AccessControlAllowMethods); 80 | resp.headers.append(Header.Vary, Header.AccessControlAllowHeaders); 81 | if (allowOrigin) { 82 | resp.headers.set(Header.AccessControlAllowOrigin, allowOrigin); 83 | } 84 | resp.headers.set( 85 | Header.AccessControlAllowMethods, 86 | config.allowMethods!.join(","), 87 | ); 88 | if (config.allowHeaders && config.allowHeaders.length != 0) { 89 | resp.headers.set( 90 | Header.AccessControlAllowHeaders, 91 | config.allowHeaders.join(","), 92 | ); 93 | } else { 94 | const h = req.headers.get(Header.AccessControlRequestHeaders); 95 | if (h) { 96 | resp.headers.set(Header.AccessControlRequestHeaders, h); 97 | } 98 | } 99 | if (config.maxAge! > 0) { 100 | resp.headers.set(Header.AccessControlMaxAge, String(config.maxAge)); 101 | } 102 | 103 | resp.status = Status.NoContent; 104 | }; 105 | }; 106 | } 107 | 108 | export interface CORSConfig { 109 | skipper?: Skipper; 110 | allowOrigins?: string[]; 111 | allowMethods?: string[]; 112 | allowHeaders?: string[]; 113 | allowCredentials?: boolean; 114 | exposeHeaders?: string[]; 115 | maxAge?: number; 116 | } 117 | -------------------------------------------------------------------------------- /middleware/cors_test.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../context.ts"; 2 | 3 | import { assertEquals } from "../vendor/https/deno.land/std/testing/asserts.ts"; 4 | import { cors } from "./cors.ts"; 5 | import { Header } from "../constants.ts"; 6 | const { test } = Deno; 7 | 8 | test("middleware cors", function (): void { 9 | let headers = new Headers(); 10 | let ctx = { 11 | req: { 12 | headers: new Headers(), 13 | }, 14 | response: { 15 | headers, 16 | }, 17 | } as Context; 18 | cors()((c) => c)(ctx); 19 | assertEquals(headers.get(Header.Vary), Header.Origin); 20 | assertEquals(headers.get(Header.AccessControlAllowOrigin), "*"); 21 | 22 | headers = new Headers(); 23 | ctx = { 24 | req: { 25 | headers: new Headers(), 26 | }, 27 | response: { 28 | headers, 29 | }, 30 | } as Context; 31 | cors({ 32 | allowOrigins: ["http://foo.com", "http://bar.com"], 33 | })((c) => c)(ctx); 34 | assertEquals(headers.get(Header.AccessControlAllowOrigin), null); 35 | 36 | headers = new Headers(); 37 | ctx = { 38 | req: { 39 | headers: new Headers({ [Header.Origin]: "http://bar.com" }), 40 | }, 41 | response: { 42 | headers, 43 | }, 44 | } as Context; 45 | cors({ 46 | allowOrigins: ["http://foo.com", "http://bar.com"], 47 | })((c) => c)(ctx); 48 | assertEquals(headers.get(Header.AccessControlAllowOrigin), "http://bar.com"); 49 | 50 | headers = new Headers(); 51 | ctx = { 52 | req: { 53 | headers: new Headers({ [Header.Origin]: "http://bar.com/xyz/" }), 54 | }, 55 | response: { 56 | headers, 57 | }, 58 | } as Context; 59 | cors({ 60 | allowOrigins: ["http://foo.com", "http://bar.com"], 61 | })((c) => c)(ctx); 62 | assertEquals( 63 | headers.get(Header.AccessControlAllowOrigin), 64 | "http://bar.com/xyz/", 65 | ); 66 | }); 67 | -------------------------------------------------------------------------------- /middleware/logger.ts: -------------------------------------------------------------------------------- 1 | import type { MiddlewareFunc } from "../types.ts"; 2 | import type { Context } from "../context.ts"; 3 | import type { Skipper } from "./skipper.ts"; 4 | 5 | import { DefaultSkipper } from "./skipper.ts"; 6 | const { writeSync, stdout } = Deno; 7 | 8 | export type Formatter = (c: Context) => string; 9 | 10 | const encoder = new TextEncoder(); 11 | 12 | export const DefaultFormatter: Formatter = ({ method, path }) => { 13 | return `${new Date().toISOString()} ${method} ${path}\n`; 14 | }; 15 | 16 | export const DefaultLoggerConfig: LoggerConfig = { 17 | skipper: DefaultSkipper, 18 | formatter: DefaultFormatter, 19 | output: stdout, 20 | }; 21 | 22 | export function logger( 23 | config: LoggerConfig = DefaultLoggerConfig, 24 | ): MiddlewareFunc { 25 | if (config.formatter == null) { 26 | config.formatter = DefaultLoggerConfig.formatter; 27 | } 28 | if (config.skipper == null) { 29 | config.skipper = DefaultLoggerConfig.skipper; 30 | } 31 | if (config.output == null) { 32 | config.output = stdout; 33 | } 34 | return (next) => 35 | (c) => { 36 | if (config.skipper!(c)) { 37 | return next(c); 38 | } 39 | writeSync(config.output!.rid, encoder.encode(config.formatter!(c))); 40 | return next(c); 41 | }; 42 | } 43 | 44 | export interface LoggerConfig { 45 | skipper?: Skipper; 46 | formatter?: Formatter; 47 | 48 | // Default is Deno.stdout. 49 | output?: { rid: number }; 50 | } 51 | -------------------------------------------------------------------------------- /middleware/logger_test.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../context.ts"; 2 | 3 | import { 4 | assert, 5 | assertEquals, 6 | } from "../vendor/https/deno.land/std/testing/asserts.ts"; 7 | import { DefaultFormatter, logger } from "./logger.ts"; 8 | const { test, makeTempFileSync, readFileSync, openSync, removeSync } = Deno; 9 | 10 | const dt = new Date(); 11 | const ctx = { 12 | method: "GET", 13 | path: "/", 14 | } as Context; 15 | const decoder = new TextDecoder(); 16 | 17 | test("middleware logger", function (): void { 18 | const fpath = makeTempFileSync(); 19 | const f = openSync(fpath, { write: true }); 20 | logger({ 21 | output: f, 22 | })((c) => c)(ctx); 23 | const out = decoder.decode(readFileSync(fpath)); 24 | assert(out.includes(" GET /\n")); 25 | assert(new Date(out.split(" ")[0]).getTime() >= dt.getTime()); 26 | f.close(); 27 | removeSync(fpath); 28 | }); 29 | 30 | test("middleware logger default formatter", function (): void { 31 | const logInfo = DefaultFormatter(ctx); 32 | assert(logInfo.endsWith(" GET /\n")); 33 | assert(new Date(logInfo.split(" ")[0]).getTime() >= dt.getTime()); 34 | }); 35 | 36 | test("middleware logger custom formatter", function (): void { 37 | const fpath = makeTempFileSync(); 38 | const f = openSync(fpath, { write: true }); 39 | const info = "Hello, 你好!"; 40 | logger({ 41 | output: f, 42 | formatter: (): string => info, 43 | })((c) => c)(ctx); 44 | const out = decoder.decode(readFileSync(fpath)); 45 | assertEquals(out, info); 46 | f.close(); 47 | removeSync(fpath); 48 | }); 49 | -------------------------------------------------------------------------------- /middleware/skipper.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../context.ts"; 2 | 3 | export type Skipper = (c?: Context) => boolean; 4 | 5 | export const DefaultSkipper: Skipper = function (): boolean { 6 | return false; 7 | }; 8 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export { Application } from "./app.ts"; 2 | export { Group } from "./group.ts"; 3 | export { Context } from "./context.ts"; 4 | export { Router } from "./router.ts"; 5 | 6 | export * from "./types.ts"; 7 | export * from "./http_exception.ts"; 8 | -------------------------------------------------------------------------------- /router.ts: -------------------------------------------------------------------------------- 1 | import type { HandlerFunc } from "./types.ts"; 2 | import type { Context } from "./context.ts"; 3 | 4 | import { Node } from "./vendor/https/deno.land/x/router/mod.ts"; 5 | import { hasTrailingSlash, NotFoundHandler } from "./util.ts"; 6 | 7 | export class Router { 8 | trees: Record = {}; 9 | 10 | add(method: string, path: string, h: HandlerFunc): void { 11 | if (path[0] !== "/") { 12 | path = `/${path}`; 13 | } 14 | 15 | if (hasTrailingSlash(path)) { 16 | path = path.slice(0, path.length - 1); 17 | } 18 | 19 | let root = this.trees[method]; 20 | if (!root) { 21 | root = new Node(); 22 | this.trees[method] = root; 23 | } 24 | 25 | root.add(path, h); 26 | } 27 | 28 | find(method: string, c: Context): HandlerFunc { 29 | const node = this.trees[method]; 30 | let path = c.path; 31 | if (hasTrailingSlash(path)) { 32 | path = path.slice(0, path.length - 1); 33 | } 34 | let h: HandlerFunc | undefined; 35 | if (node) { 36 | const [handle, params] = node.find(path); 37 | if (params) { 38 | for (const [k, v] of params) { 39 | c.params[k] = v; 40 | } 41 | } 42 | 43 | if (handle) { 44 | h = handle as HandlerFunc; 45 | } 46 | } 47 | 48 | return h ?? NotFoundHandler; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /router_test.ts: -------------------------------------------------------------------------------- 1 | import { HandlerFunc } from "./types.ts"; 2 | 3 | import { assertEquals } from "./vendor/https/deno.land/std/testing/asserts.ts"; 4 | import { createMockRequest } from "./test_util.ts"; 5 | import { Router } from "./router.ts"; 6 | import { Context } from "./context.ts"; 7 | import { HttpMethod } from "./constants.ts"; 8 | const { test } = Deno; 9 | 10 | test("router basic", function (): void { 11 | const r = new Router(); 12 | const h: HandlerFunc = (c) => c.path; 13 | const c = new Context({ 14 | app: undefined!, 15 | r: createMockRequest({ url: "https://example.com/get" }), 16 | }); 17 | r.add(HttpMethod.Get, "/get", h); 18 | assertEquals(r.find(HttpMethod.Get, c), h); 19 | }); 20 | -------------------------------------------------------------------------------- /test_util.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "./app.ts"; 2 | 3 | const encoder = new TextEncoder(); 4 | 5 | export function createApplication(): Application { 6 | const app = new Application(); 7 | app.start({ port: 8081 }); 8 | return app; 9 | } 10 | 11 | export function createMockRequest( 12 | options: { url?: string } & RequestInit = {}, 13 | ): Request { 14 | options.url = options.url ?? "https://example.com/"; 15 | options.headers = options.headers ?? new Headers(); 16 | options.body = options.body ?? undefined; 17 | options.method = options.method ?? "POST"; 18 | 19 | return new Request(options.url, { ...options }); 20 | } 21 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "./context.ts"; 2 | import type { Application } from "./app.ts"; 3 | 4 | /** `Renderer` is the interface that wraps the `render` function. */ 5 | export type Renderer = { 6 | templates?: string; 7 | render(name: string, data: T): Promise; 8 | }; 9 | 10 | /* `HandlerFunc` defines a function to serve HTTP requests. */ 11 | export type HandlerFunc = (c: Context) => Promise | unknown; 12 | 13 | /* `MiddlewareFunc` defines a function to process middleware. */ 14 | export type MiddlewareFunc = (next: HandlerFunc) => HandlerFunc; 15 | 16 | export type ContextOptions = { app: Application; r: Request }; 17 | -------------------------------------------------------------------------------- /util.ts: -------------------------------------------------------------------------------- 1 | import { extname } from "./vendor/https/deno.land/std/path/mod.ts"; 2 | import { MIME } from "./constants.ts"; 3 | import { NotFoundException } from "./http_exception.ts"; 4 | 5 | /** Returns the content-type based on the extension of a path. */ 6 | export function contentType(filepath: string): string | undefined { 7 | return MIME.DB[extname(filepath)]; 8 | } 9 | 10 | export function hasTrailingSlash(str: string): boolean { 11 | if (str.length > 1 && str[str.length - 1] === "/") { 12 | return true; 13 | } 14 | 15 | return false; 16 | } 17 | 18 | export function NotFoundHandler(): never { 19 | throw new NotFoundException(); 20 | } 21 | -------------------------------------------------------------------------------- /util_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "./vendor/https/deno.land/std/testing/asserts.ts"; 2 | import { contentType } from "./util.ts"; 3 | import { MIME } from "./constants.ts"; 4 | const { test } = Deno; 5 | 6 | test("util content type", function (): void { 7 | assertEquals(contentType("/path/to/file"), undefined); 8 | assertEquals(contentType("/path/to/file.md"), MIME.TextMarkdownCharsetUTF8); 9 | assertEquals(contentType("/path/to/file.html"), MIME.TextHTMLCharsetUTF8); 10 | assertEquals(contentType("/path/to/file.htm"), MIME.TextHTMLCharsetUTF8); 11 | assertEquals(contentType("/path/to/file.json"), MIME.ApplicationJSON); 12 | assertEquals(contentType("/path/to/file.map"), MIME.ApplicationJSON); 13 | assertEquals(contentType("/path/to/file.txt"), MIME.TextPlainCharsetUTF8); 14 | assertEquals( 15 | contentType("/path/to/file.ts"), 16 | MIME.ApplicationJavaScriptCharsetUTF8, 17 | ); 18 | assertEquals( 19 | contentType("/path/to/file.tsx"), 20 | MIME.ApplicationJavaScriptCharsetUTF8, 21 | ); 22 | assertEquals( 23 | contentType("/path/to/file.js"), 24 | MIME.ApplicationJavaScriptCharsetUTF8, 25 | ); 26 | assertEquals( 27 | contentType("/path/to/file.jsx"), 28 | MIME.ApplicationJavaScriptCharsetUTF8, 29 | ); 30 | assertEquals(contentType("/path/to/file.gz"), MIME.ApplicationGZip); 31 | }); 32 | -------------------------------------------------------------------------------- /vendor/https/deno.land/std/fmt/colors.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/std@0.110.0/fmt/colors.ts"; 2 | -------------------------------------------------------------------------------- /vendor/https/deno.land/std/http/cookie.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/std@0.110.0/http/cookie.ts"; 2 | -------------------------------------------------------------------------------- /vendor/https/deno.land/std/http/http_status.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/std@0.110.0/http/http_status.ts"; 2 | -------------------------------------------------------------------------------- /vendor/https/deno.land/std/http/server.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/std@0.110.0/http/server.ts"; 2 | -------------------------------------------------------------------------------- /vendor/https/deno.land/std/io/buffer.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/std@0.110.0/io/buffer.ts"; 2 | -------------------------------------------------------------------------------- /vendor/https/deno.land/std/io/bufio.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/std@0.110.0/io/bufio.ts"; 2 | -------------------------------------------------------------------------------- /vendor/https/deno.land/std/io/util.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/std@0.110.0/io/util.ts"; 2 | -------------------------------------------------------------------------------- /vendor/https/deno.land/std/mime/multipart.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/std@0.110.0/mime/multipart.ts"; 2 | -------------------------------------------------------------------------------- /vendor/https/deno.land/std/path/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/std@0.110.0/path/mod.ts"; 2 | -------------------------------------------------------------------------------- /vendor/https/deno.land/std/testing/asserts.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/std@0.110.0/testing/asserts.ts"; 2 | -------------------------------------------------------------------------------- /vendor/https/deno.land/std/testing/bench.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/std@0.110.0/testing/bench.ts"; 2 | -------------------------------------------------------------------------------- /vendor/https/deno.land/std/textproto/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/std@0.110.0/textproto/mod.ts"; 2 | -------------------------------------------------------------------------------- /vendor/https/deno.land/std/ws/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/std@0.110.0/ws/mod.ts"; 2 | -------------------------------------------------------------------------------- /vendor/https/deno.land/x/dejs/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/x/dejs@0.10.1/mod.ts"; 2 | -------------------------------------------------------------------------------- /vendor/https/deno.land/x/mysql/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/x/mysql@v2.6.0/mod.ts"; 2 | -------------------------------------------------------------------------------- /vendor/https/deno.land/x/router/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/x/router@v2.0.0/mod.ts"; 2 | --------------------------------------------------------------------------------