-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
dfe0d52
commit d11550a
Showing
16 changed files
with
687 additions
and
644 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
|
||
|
@@ -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) => { | ||
|
@@ -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), | ||
|
@@ -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), | ||
|
@@ -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); | ||
} | ||
|
@@ -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); | ||
} | ||
|
@@ -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); | ||
|
@@ -211,3 +271,4 @@ const listenAndServe = (port = 3000, log = console.log) => { | |
}; | ||
|
||
export { handleRoute, listenAndServe, useMiddleware }; | ||
export type { Context }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
Oops, something went wrong.