Skip to content

Commit

Permalink
v0.3.0
Browse files Browse the repository at this point in the history
  • Loading branch information
dragonwocky committed May 11, 2022
1 parent dfe0d52 commit d11550a
Show file tree
Hide file tree
Showing 16 changed files with 687 additions and 644 deletions.
29 changes: 0 additions & 29 deletions CHANGELOG.md

This file was deleted.

67 changes: 3 additions & 64 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,67 +2,6 @@

**nadder** is an opinionated web server framework for Deno.

It includes [URL Pattern](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API)
routing, post-route middleware, helpers for creating/reading/manipulating cookies and responses,
upgrading HTTP connections to WebSocket connections (inc. sorting into channels),
PostgreSQL (or in-memory) session storage (inc. garbage collection and expiry), a React-free
JSX transformer and atomic CSS with [Uno](https://github.com/unocss/unocss).

## Quick start

```tsx
/**
* @jsx h
* @jsxFrag jsxFrag
*/

import 'https://deno.land/x/[email protected]/load.ts';

import nadder, {
Document,
h,
jsxFrag,
postgresConnection,
postgresSession,
} from 'https://deno.land/x/[email protected]/mod.ts';

const postgres = postgresConnection({
password: Deno.env.get('POSTGRES_PWD'),
hostname: Deno.env.get('POSTGRES_HOST'),
}),
session = await postgresSession(postgres);

nadder.handleRoute('GET', '/{index.html}?', async (ctx) => {
const count = (((await session.get(ctx, 'count')) as number) ?? -1) + 1;
await session.set(ctx, 'count', count);

ctx.res.body = await Document(
'Home',
<>
<h1 class="text-green-600">Hello world!</h1>
<p>
Load count: <span class="font-bold">{count}</span>
</p>
</>
);
ctx.res.inferContentType('html');
});

nadder.listenAndServe();
```

All features are made available as exports of the `mod.ts` file
and documented in the `types.ts` file.

For convenience, the following dependencies are re-exported:

- `setCookie`, `deleteCookie`, `Cookie`, `HTTPStatus` and `HTTPStatusText` from [`std/http`](https://deno.land/std/http).

---

Changes to this project are recorded in the [CHANGELOG](CHANGELOG.md).

This project is licensed under the [MIT License](LICENSE).

To support future development of this project, please consider
[sponsoring the author](https://github.com/sponsors/dragonwocky).
It is mostly for personal use and so as of yet is undocumented.
It includes utility classes, server-side jsx rendering, asset minification,
WebSocket channels, HTTP middleware, markdown rendering, and etc.
46 changes: 33 additions & 13 deletions deps.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,47 @@
export * as path from "https://deno.land/[email protected]/path/mod.ts";
export { readableStreamFromReader } from "https://deno.land/[email protected]/streams/mod.ts";
/*! mit license (c) dragonwocky <[email protected]> (https://dragonwocky.me/) */

export { serve as stdServe } from "https://deno.land/std@0.125.0/http/server.ts";
export { serve } from "https://deno.land/std@0.138.0/http/server.ts";
export {
Status as HTTPStatus,
STATUS_TEXT as HTTPStatusText,
} from "https://deno.land/[email protected]/http/http_status.ts";
export { contentType } from "https://deno.land/x/[email protected]/mod.ts";
} from "https://deno.land/[email protected]/http/http_status.ts";
export { contentType } from "https://deno.land/x/[email protected]/mod.ts";

export {
deleteCookie,
getCookies,
setCookie,
} from "https://deno.land/[email protected]/http/cookie.ts";
export type { Cookie } from "https://deno.land/[email protected]/http/cookie.ts";
} from "https://deno.land/[email protected]/http/cookie.ts";
export type { Cookie } from "https://deno.land/[email protected]/http/cookie.ts";

export { readableStreamFromReader } from "https://deno.land/[email protected]/streams/mod.ts";
export { basename } from "https://deno.land/[email protected]/path/mod.ts";

export const modernNormalize =
`/*! modern-normalize v1.1.0 | MIT License | https://github.com/sindresorhus/modern-normalize */*,::after,::before{box-sizing:border-box}html{-moz-tab-size:4;tab-size:4}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}body{font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji'}hr{height:0;color:inherit}abbr[title]{text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Consolas,'Liberation Mono',Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}::-moz-focus-inner{border-style:none;padding:0}:-moz-focusring{outline:1px dotted ButtonText}:-moz-ui-invalid{box-shadow:none}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}`;
export * as swc from "https://deno.land/x/[email protected]/mod.ts";

export { createGenerator as unoGenerator } from "https://esm.sh/@unocss/[email protected]";
export { default as unoPreset } from "https://esm.sh/@unocss/[email protected]";
export { presetTypography as unoTypography } from "https://esm.sh/@unocss/[email protected]";
export { default as postcss } from "https://deno.land/x/[email protected]/mod.js";
export { default as autoprefixer } from "https://esm.sh/[email protected]";
export { default as selectorParser } from "https://esm.sh/[email protected]";
export { default as valueParser } from "https://esm.sh/[email protected]";

export { micromark } from "https://esm.sh/[email protected]";
export { gfm, gfmHtml } from "https://esm.sh/[email protected]";
export { math, mathHtml } from "https://esm.sh/[email protected]";
export { default as hljs } from "https://esm.sh/[email protected]";
export type { HtmlExtension } from "https://esm.sh/[email protected]/index.d.ts";

export { createGenerator } from "https://cdn.skypack.dev/@unocss/[email protected]";
export { default as presetWind } from "https://cdn.skypack.dev/@unocss/[email protected]";
export { default as presetTypography } from "https://cdn.skypack.dev/@unocss/[email protected]";
export { default as presetIcons } from "https://cdn.skypack.dev/@unocss/[email protected]";

export * as postgres from "https://deno.land/x/[email protected]/mod.ts";
export const iconifyCollections = async (...sets: string[]) => {
const reqs = [], sourceUrl = "https://esm.sh/@iconify/[email protected]/json";
for (const set of sets) {
reqs.push(
fetch(`${sourceUrl}/${set}.json`)
.then((res) => res.json()).then((json) => [set, json]),
);
}
return { collections: Object.fromEntries(await Promise.all(reqs)) };
};
119 changes: 90 additions & 29 deletions server.ts → listen.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,75 @@
/**
* nadder
* (c) 2022 dragonwocky <[email protected]> (https://dragonwocky.me/)
* (https://github.com/dragonwocky/nadder) under the MIT license
*/

import type { Callback, Context, Mutable, RequestMethod } from "./types.ts";
import { RequestMethods } from "./types.ts";
/*! mit license (c) dragonwocky <[email protected]> (https://dragonwocky.me/) */

import {
basename,
contentType,
getCookies,
HTTPStatus,
HTTPStatusText,
path,
readableStreamFromReader,
stdServe,
serve,
} from "./deps.ts";

type Mutable<T> = { -readonly [P in keyof T]: T[P] };

// https://www.iana.org/assignments/http-methods/http-methods.xhtml
const RequestMethods = [
"POST", // create
"GET", // read
"PUT", // replace
"PATCH", // update
"DELETE", // delete
"*",
] as const;
type RequestMethod = typeof RequestMethods[number];
type RequestBody =
| string
| number
| boolean
| Record<string, unknown>
| unknown[]
| null
| undefined
| FormData
| Blob
| ArrayBuffer;

type Callback = (ctx: Context) => void | Promise<void>;
interface Context {
readonly req: {
method: RequestMethod;
ip: string | null;
url: URL;
body: RequestBody;
bodyType: undefined | "json" | "text" | "formData" | "blob";
queryParams: URLSearchParams;
pathParams: Record<string, string>;
cookies: Record<string, string>;
headers: Headers;
};
res: {
body: BodyInit;
status: number;
headers: Headers;
readonly sent: boolean;
sendStatus: (status: HTTPStatus) => void;
sendJSON: (data: unknown) => void;
sendFile: (filepath: string) => Promise<void>;
sendFileStream: (filepath: string) => Promise<void>;
inferContentType: (lookup: string) => void;
markForDownload: (filename?: string) => void;
};
upgrade: {
readonly available: boolean;
readonly socket: () => WebSocket | undefined;
channel: {
readonly name: string;
join: (name: string) => void;
broadcast: (message: unknown) => void;
};
};
}

const middleware: Callback[] = [],
useMiddleware = (callback: Callback) => middleware.push(callback);

Expand All @@ -24,17 +78,17 @@ const routes: [RequestMethod, URLPattern, Callback][] = [],
method: RequestMethod,
path: string,
callback: Callback,
) => routes.push([method, new URLPattern({ pathname: path }), callback]);

const getRoute = (method: RequestMethod, href: string) => {
for (const route of routes) {
if (!["*", route[0]].includes(method)) continue;
if (!route[1].test(href)) continue;
const pathParams = route[1].exec(href)?.pathname?.groups ?? {};
return { callback: route[2], pathParams };
}
return undefined;
};
) => routes.push([method, new URLPattern({ pathname: path }), callback]),
getRoute = (method: RequestMethod, href: string) => {
href = href.replaceAll(/\/$/g, "");
for (const route of routes) {
if (!["*", route[0]].includes(method)) continue;
if (!route[1].test(href)) continue;
const pathParams = route[1].exec(href)?.pathname?.groups ?? {};
return { callback: route[2], pathParams };
}
return undefined;
};

const activeSocketConnections: Map<string, Set<WebSocket>> = new Map(),
removeSocketFromChannel = (name: string, socket: WebSocket) => {
Expand All @@ -53,7 +107,7 @@ const listenAndServe = (port = 3000, log = console.log) => {
log(`✨ server started at http://localhost:${port}/`);
log("listening for requests...");
log("");
stdServe(async (req, conn) => {
serve(async (req, conn) => {
let override: Response | undefined, socket: WebSocket | undefined;

const url = new URL(req.url),
Expand All @@ -63,6 +117,7 @@ const listenAndServe = (port = 3000, log = console.log) => {
ip: (conn.remoteAddr as Deno.NetAddr).hostname,
url,
body: undefined,
bodyType: undefined,
queryParams: new URLSearchParams(url.search),
pathParams: {},
cookies: getCookies(req.headers),
Expand All @@ -82,13 +137,13 @@ const listenAndServe = (port = 3000, log = console.log) => {
if (data instanceof Map || data instanceof Set) data = [...data];
ctx.res.body = JSON.stringify(data, null, 2);
ctx.res.inferContentType("json");
ctx.res.sendStatus(HTTPStatus.OK);
ctx.res.status = HTTPStatus.OK;
},
sendFile: async (filepath) => {
try {
ctx.res.body = await Deno.readTextFile(filepath);
ctx.res.inferContentType(path.basename(filepath));
ctx.res.sendStatus(HTTPStatus.OK);
ctx.res.inferContentType(basename(filepath));
ctx.res.status = HTTPStatus.OK;
} catch {
ctx.res.sendStatus(HTTPStatus.NotFound);
}
Expand All @@ -97,9 +152,8 @@ const listenAndServe = (port = 3000, log = console.log) => {
try {
const file = await Deno.open(filepath, { read: true });
ctx.res.body = readableStreamFromReader(file);
file.close();
ctx.res.inferContentType(path.basename(filepath));
ctx.res.sendStatus(HTTPStatus.OK);
ctx.res.inferContentType(basename(filepath));
ctx.res.status = HTTPStatus.OK;
} catch {
ctx.res.sendStatus(HTTPStatus.NotFound);
}
Expand Down Expand Up @@ -181,11 +235,17 @@ const listenAndServe = (port = 3000, log = console.log) => {
contentType.includes("multipart/form-data");
if (isJSON) {
ctx.req.body = await req.json();
ctx.req.bodyType = "json";
} else if (isText) {
ctx.req.body = await req.text();
ctx.req.bodyType = "text";
} else if (isFormData) {
ctx.req.body = await req.formData();
} else ctx.req.body = await req.blob();
ctx.req.bodyType = "formData";
} else {
ctx.req.body = await req.blob();
ctx.req.bodyType = "blob";
}
}

const route = getRoute(ctx.req.method, ctx.req.url.href);
Expand All @@ -211,3 +271,4 @@ const listenAndServe = (port = 3000, log = console.log) => {
};

export { handleRoute, listenAndServe, useMiddleware };
export type { Context };
44 changes: 26 additions & 18 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
/**
* nadder
* (c) 2022 dragonwocky <[email protected]> (https://dragonwocky.me/)
* (https://github.com/dragonwocky/nadder) under the MIT license
*/
/*! mit license (c) dragonwocky <[email protected]> (https://dragonwocky.me/) */

export * as default from "./server.ts";
import { handleRoute, listenAndServe, useMiddleware } from "./listen.ts";
export default { handleRoute, listenAndServe, useMiddleware };
export type { Context } from "./listen.ts";

export { HTTPStatus, HTTPStatusText } from "./deps.ts";
export { deleteCookie, setCookie } from "./deps.ts";

export { postgresConnection } from "./storage/drivers/postgres.ts";
export { memorySession } from "./storage/sessions/memory.ts";
export { postgresSession } from "./storage/sessions/postgres.ts";

export { h, jsxFrag, jsxToString } from "./ssr/jsx.tsx";
export { createUnoGenerator, expandUtilityGroups } from "./ssr/uno.ts";
export { Document } from "./ssr/document.tsx";

export type { Context, Session } from "./types.ts";
export {
contentType,
deleteCookie,
getCookies,
HTTPStatus,
HTTPStatusText,
setCookie,
} from "./deps.ts";
export type { Cookie } from "./deps.ts";

export {
asyncJsxToSync,
escapeHtml,
h,
isElement,
jsxFrag,
jsxToString,
primitiveToString,
renderDocument,
setTheme,
} from "./render.tsx";
export { Skeleton, Spinner, Stream } from "./stream.tsx";
export { transform, transformFile } from "./transform.ts";
Loading

0 comments on commit d11550a

Please sign in to comment.