├── examples
├── file_upload
│ ├── file
│ ├── README.md
│ └── main.ts
├── static
│ ├── assets
│ │ ├── read.txt
│ │ └── template.ts
│ └── main.ts
├── template
│ ├── index.html
│ └── main.ts
├── ultra_cat_app
│ ├── README.md
│ ├── .vscode
│ │ └── settings.json
│ ├── public
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── pages
│ │ │ ├── Home.tsx
│ │ │ ├── Login.tsx
│ │ │ └── List.tsx
│ │ ├── import_map.json
│ │ ├── index.html
│ │ ├── index.tsx
│ │ └── App.tsx
│ ├── user
│ │ ├── user.ts
│ │ └── group.ts
│ ├── cat
│ │ ├── cat.ts
│ │ └── group.ts
│ ├── deps.ts
│ ├── main.ts
│ ├── jsx_loader.ts
│ └── db.ts
├── cat_app
│ ├── main.ts
│ ├── cat.ts
│ └── handler.ts
├── jsx
│ └── main.jsx
├── websocket
│ ├── server.ts
│ └── index.html
└── test.ts
├── .gitattributes
├── vendor
└── https
│ └── deno.land
│ ├── std
│ ├── ws
│ │ └── mod.ts
│ ├── io
│ │ ├── buffer.ts
│ │ ├── bufio.ts
│ │ └── util.ts
│ ├── path
│ │ └── mod.ts
│ ├── fmt
│ │ └── colors.ts
│ ├── http
│ │ ├── cookie.ts
│ │ ├── server.ts
│ │ └── http_status.ts
│ ├── mime
│ │ └── multipart.ts
│ ├── testing
│ │ ├── bench.ts
│ │ └── asserts.ts
│ └── textproto
│ │ └── mod.ts
│ └── x
│ ├── dejs
│ └── mod.ts
│ ├── mysql
│ └── mod.ts
│ └── router
│ └── mod.ts
├── .gitignore
├── constants.ts
├── middleware
├── skipper.ts
├── logger.ts
├── logger_test.ts
├── cors_test.ts
└── cors.ts
├── _http_method.ts
├── mod.ts
├── benchmarks
├── paths.ts
└── app.ts
├── docs
├── table_of_contents.md
├── static_files.md
├── cors.md
├── context.md
├── logger.md
├── style_guide.md
├── router.md
├── middleware.md
├── getting_started.md
└── exception_filter.md
├── .github
└── workflows
│ └── ci.yml
├── test_util.ts
├── util.ts
├── types.ts
├── router_test.ts
├── LICENSE
├── dem.json
├── README.md
├── util_test.ts
├── router.ts
├── CHANGELOG.md
├── _header.ts
├── _mime.ts
├── group_test.ts
├── group.ts
├── context_test.ts
├── context.ts
├── app_test.ts
├── http_exception.ts
└── app.ts
/examples/file_upload/file:
--------------------------------------------------------------------------------
1 | Hello World!
--------------------------------------------------------------------------------
/examples/static/assets/read.txt:
--------------------------------------------------------------------------------
1 | static
--------------------------------------------------------------------------------
/examples/static/assets/template.ts:
--------------------------------------------------------------------------------
1 | export const template = "abc";
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Use Unix line endings in all text files.
2 | * text=auto eol=lf
3 |
--------------------------------------------------------------------------------
/examples/template/index.html:
--------------------------------------------------------------------------------
1 | <% if (name) { %>
2 |
hello, <%= name %>!
3 | <% } %>
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/ultra_cat_app/README.md:
--------------------------------------------------------------------------------
1 | ## Usage
2 |
3 | ```
4 | deno run -A --unstable main.ts
5 | ```
6 |
--------------------------------------------------------------------------------
/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/path/mod.ts:
--------------------------------------------------------------------------------
1 | export * from "https://deno.land/std@0.110.0/path/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 |
--------------------------------------------------------------------------------
/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/server.ts:
--------------------------------------------------------------------------------
1 | export * from "https://deno.land/std@0.110.0/http/server.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/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/http/http_status.ts:
--------------------------------------------------------------------------------
1 | export * from "https://deno.land/std@0.110.0/http/http_status.ts";
2 |
--------------------------------------------------------------------------------
/vendor/https/deno.land/std/testing/asserts.ts:
--------------------------------------------------------------------------------
1 | export * from "https://deno.land/std@0.110.0/testing/asserts.ts";
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /types
2 | .DS_Store
3 | /.vscode
4 | /.idea
5 | node_modules
6 | /package*.json
7 | /deno.d.ts
8 | typedoc
9 |
--------------------------------------------------------------------------------
/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/public/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "deno.enable": true,
3 | "deno.import_map": "./import_map.json"
4 | }
5 |
--------------------------------------------------------------------------------
/examples/ultra_cat_app/user/user.ts:
--------------------------------------------------------------------------------
1 | export interface User {
2 | id?: number;
3 | username?: string;
4 | password?: string;
5 | }
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/ultra_cat_app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/websocket/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | WebSocket
4 |
5 |
6 |
7 |
8 |
9 |
34 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Abc
2 |
3 | > **A** **b**etter Deno framework to **c**reate web application
4 |
5 | [](https://github.com/zhmushan/abc)
6 | [](https://github.com/zhmushan/abc/actions)
7 | [](https://github.com/zhmushan/abc)
8 | [](https://github.com/denoland/deno)
9 | [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 | Cats List
80 |
81 |
82 | | ID |
83 | Name |
84 | Age |
85 | Action |
86 |
87 |
88 |
89 | {cats.map((c) => (
90 |
91 | | {c.id} |
92 | {c.name} |
93 | {c.age} |
94 |
95 |
96 | |
97 |
98 | ))}
99 |
100 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app_test.ts:
--------------------------------------------------------------------------------
1 | import type { HandlerFunc } 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 | import { NotFoundHandler } from "./util.ts";
7 | import { NotFoundException } from "./http_exception.ts";
8 | import { HttpMethod } from "./constants.ts";
9 | const { readFile, test } = Deno;
10 |
11 | const decoder = new TextDecoder();
12 | const addr = `http://localhost:8081`;
13 |
14 | test("app static", async function (): Promise {
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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*