Skip to content

Commit 7568b1e

Browse files
committed
initial commit
0 parents  commit 7568b1e

File tree

10 files changed

+1504
-0
lines changed

10 files changed

+1504
-0
lines changed

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2018-2022 the oak authors
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+
SOFTWARE.

README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# nat
2+
3+
A server side rendering framework for Deno CLI and Deploy.
4+
5+
Incorporating [acorn](https://deno.land/x/acorn/),
6+
[nano-jsx](https://nanojsx.io/), and [twind](https://twind.dev/), it provides
7+
the tooling to provide a server centric framework for providing dynamic websites
8+
served from the edge.
9+
10+
## Getting started
11+
12+
nat as a setup script which makes it easy to scaffold out a project and is the
13+
easiest way to get started. You will need the Deno CLI [installed]() locally and
14+
will want to run the following command within the current directory you want to
15+
setup:
16+
17+
```
18+
> deno run https://deno.land/x/nat/setup.ts
19+
```
20+
21+
The script will prompt you for read and write permissions to the current
22+
directory as well as ask for your confirmation to write out the initial setup
23+
files for the project.
24+
25+
Once the project is setup, edit the `main.tsx` and use `deno task start` to run
26+
your server locally.
27+
28+
You can also deploy the project to [Deno Deploy](https://dash.deno.com/new).
29+
30+
## Concepts
31+
32+
The framework includes the acorn
33+
[Router](https://doc.deno.land/https://deno.land/x/acorn/mod.ts/~/Router). An
34+
instance of the router is returned from the
35+
[init()](https://doc.deno.land/https://deno.land/x/nat/mod.ts/~/init) function.
36+
The acorn router is based of web standard [URLPattern]() API which allows
37+
matching URLs and parsing out values to variables. Those variables are available
38+
in the handler's
39+
[context `.params` property](https://doc.deno.land/https://deno.land/x/acorn/mod.ts/~/Context#params).
40+
41+
The framework comes with [nano-jsx](https://nanojsx.io/) built in, which makes
42+
it easy to server-side render JSX/TSX as a response.
43+
44+
The framework also comes with [twind](https://twind.dev/) integrated which is
45+
well suited to server side rendering of tailwind's functional CSS styles in a
46+
super efficient way.
47+
48+
---
49+
50+
Copyright 2022 the oak authors. All rights reserved. MIT License.

_examples/server.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/** @jsx h */
2+
/** @jsxFrag Fragment */
3+
import { Fragment, h, init, render, tw } from "../mod.ts";
4+
5+
const router = init();
6+
7+
const App = ({ name }: { name: string }) => (
8+
<>
9+
<div class={tw`text-xl`}>Hello {name}!</div>
10+
</>
11+
);
12+
13+
router.get("/", render(<App name="world" />));
14+
15+
router.listen();

deno.jsonc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"importMap": "./import-map.json",
3+
"tasks": {
4+
"example": "deno run --check --allow-net --allow-hrtime _examples/server.tsx"
5+
}
6+
}

handlers.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright 2022 the oak authors. All rights reserved. MIT License.
2+
3+
/**
4+
* Handlers,
5+
* @module
6+
*/
7+
8+
import { Helmet } from "nano-jsx/helmet";
9+
import { renderSSR } from "nano-jsx/ssr";
10+
import { contentType } from "std/media_types";
11+
import { getStyleTag, virtualSheet } from "twind/sheets";
12+
13+
export const sheet = virtualSheet();
14+
15+
/** A handler function which renders JSX and send it to the client.
16+
*
17+
* ### Example
18+
*
19+
* ```ts
20+
* import { h, init, render } from "https://deno.land/x/nat/mod.ts";
21+
* import { App } from "./App.tsx";
22+
*
23+
* const router = init();
24+
* router.get("/", render(<App />));
25+
* router.listen();
26+
* ```
27+
*/
28+
export function render(jsx: unknown) {
29+
return function handler() {
30+
sheet.reset();
31+
const page = renderSSR(jsx);
32+
const styles = getStyleTag(sheet);
33+
const {
34+
body,
35+
head,
36+
footer,
37+
attributes: { body: bodyAttributes, html: htmlAttributes },
38+
} = Helmet.SSR(page);
39+
return new Response(
40+
`<!DOCTYPE html>
41+
<html ${htmlAttributes.toString()}>
42+
<head>
43+
${styles}
44+
${head.join("\n")}
45+
</head>
46+
<body ${bodyAttributes.toString()}>
47+
${body}
48+
${footer.join("\n")}
49+
</body>
50+
</html>`,
51+
{
52+
headers: {
53+
"content-type": contentType("text/html")!,
54+
},
55+
},
56+
);
57+
};
58+
}

import-map.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"imports": {
3+
"acorn": "https://deno.land/x/[email protected]/mod.ts",
4+
"nano-jsx/core": "https://deno.land/x/[email protected]/core.ts",
5+
"nano-jsx/fragment": "https://deno.land/x/[email protected]/fragment.ts",
6+
"nano-jsx/helmet": "https://deno.land/x/[email protected]/components/helmet.ts",
7+
"nano-jsx/ssr": "https://deno.land/x/[email protected]/ssr.ts",
8+
"oak_commons/types": "https://deno.land/x/[email protected]/types.d.ts",
9+
"std/media_types": "https://deno.land/[email protected]/media_types/mod.ts",
10+
"twind": "https://esm.sh/[email protected]?pin=v82",
11+
"twind/css": "https://esm.sh/[email protected]/css?pin=v82",
12+
"twind/sheets": "https://esm.sh/[email protected]/sheets?pin=v82"
13+
}
14+
}

init.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright 2022 the oak authors. All rights reserved. MIT License.
2+
3+
/** Contains initialization and setup functionality.
4+
*
5+
* @module
6+
*/
7+
8+
import { Router } from "acorn";
9+
import { type Configuration as TwindConfig, setup } from "twind";
10+
import { type KeyRing } from "oak_commons/types";
11+
12+
import { sheet } from "./handlers.ts";
13+
14+
export interface StartOptions extends TwindConfig {
15+
/** A key ring which will be used for signing and validating cookies. */
16+
keys?: KeyRing;
17+
/** When providing internal responses, like on unhandled errors, prefer JSON
18+
* responses to HTML responses. When set to `false` HTML will be preferred
19+
* when responding, but content type negotiation will still be respected.
20+
* Defaults to `true`. */
21+
preferJson?: boolean;
22+
/** When `true` skip setting up default logging on the router. */
23+
quiet?: boolean;
24+
}
25+
26+
/** Initialize the environment optionally using the provided options, returning
27+
* an instance of {@linkcode Router}.
28+
*
29+
* ### Example
30+
*
31+
* ```ts
32+
* import { init, render } from "https://deno.land/x/nat/mod.ts";
33+
* import { App } from "./App.tsx";
34+
*
35+
* const router = init();
36+
* router.get("/", render(<App />));
37+
*
38+
* router.listen();
39+
* ```
40+
*/
41+
export function init(options: StartOptions = {}): Router {
42+
options.sheet = sheet;
43+
setup(options);
44+
const router = new Router(options);
45+
if (!options.quiet) {
46+
router.addEventListener(
47+
"listen",
48+
({ secure, hostname, port }) =>
49+
console.log(
50+
`%cListening: %c${
51+
secure ? "https://" : "http://"
52+
}${hostname}:${port}`,
53+
"color:green;font-weight:bold;",
54+
"color:yellow",
55+
),
56+
);
57+
router.addEventListener(
58+
"handled",
59+
(
60+
{
61+
response: { status },
62+
route,
63+
request: { url, method },
64+
measure: { duration },
65+
},
66+
) => {
67+
const responseColor = status < 400
68+
? "color:green"
69+
: status < 500
70+
? "color:yellow"
71+
: "color:red";
72+
let path = route?.route;
73+
if (!path) {
74+
try {
75+
path = new URL(url).pathname;
76+
} catch {
77+
// just swallow errors here
78+
}
79+
}
80+
console.log(
81+
`%c${method} ${path} - [${status}] ${duration.toFixed(2)}ms`,
82+
responseColor,
83+
);
84+
},
85+
);
86+
}
87+
return router;
88+
}

0 commit comments

Comments
 (0)