├── .gitignore
├── .prettierrc
├── server
├── component-map.ts
├── utils.ts
├── dev.ts
├── router.tsx
└── load-page-component.ts
├── app
├── page.tsx
├── db
│ ├── get.ts
│ └── data
│ │ ├── bjork-post.json
│ │ ├── glass-animals-how-to-be.json
│ │ └── lady-gaga-the-fame.json
├── components
│ ├── SearchBox.tsx
│ └── SearchableAlbumList.tsx
├── document.tsx
├── utils
│ └── dev
│ │ ├── live-reload.ts
│ │ └── DevPanel.tsx
└── _router.tsx
├── .vscode
├── settings.json
└── launch.json
├── deno.jsonc
├── README.md
└── deno.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "all",
3 | "semi": false
4 | }
5 |
--------------------------------------------------------------------------------
/server/component-map.ts:
--------------------------------------------------------------------------------
1 | export type ComponentMap = {
2 | [key: string]: {
3 | id: string
4 | name: string
5 | chunks: unknown[]
6 | async: boolean
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react"
2 | import SearchableAlbumList from "./components/SearchableAlbumList.tsx"
3 | import { getAll } from "./db/get.ts"
4 |
5 | export default function ServerRoot({ search }: { search: string }) {
6 | return (
7 | <>
8 |
AbraMix
9 | Loading...}>
10 | {/* @ts-expect-error */}
11 |
12 |
13 | >
14 | )
15 | }
16 |
17 | async function Albums({ search }: { search: string }) {
18 | const albums = await getAll()
19 | return
20 | }
21 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "deno.enable": true,
3 | "deno.unstable": true,
4 | "eslint.enable": false,
5 | "[typescript]": {
6 | "editor.defaultFormatter": "denoland.vscode-deno"
7 | },
8 | "[typescriptreact]": {
9 | "editor.defaultFormatter": "denoland.vscode-deno"
10 | },
11 | "[javascript]": {
12 | "editor.defaultFormatter": "denoland.vscode-deno"
13 | },
14 | "[javascriptreact]": {
15 | "editor.defaultFormatter": "denoland.vscode-deno"
16 | },
17 | "[json]": {
18 | "editor.defaultFormatter": "denoland.vscode-deno"
19 | },
20 | "[jsonc]": {
21 | "editor.defaultFormatter": "denoland.vscode-deno"
22 | },
23 | "[markdown]": {
24 | "editor.defaultFormatter": "denoland.vscode-deno"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/db/get.ts:
--------------------------------------------------------------------------------
1 | import bjorkPost from "./data/bjork-post.json" assert { type: "json" }
2 | import glassAnimalsHowToBeAMHumanBeing from "./data/glass-animals-how-to-be.json" assert {
3 | type: "json",
4 | }
5 | import ladyGagaTheFame from "./data/lady-gaga-the-fame.json" assert {
6 | type: "json",
7 | }
8 |
9 | const albums = [bjorkPost, ladyGagaTheFame, glassAnimalsHowToBeAMHumanBeing]
10 | export type Album = typeof albums[number]
11 |
12 | const artificialWait = (ms = 200) =>
13 | new Promise((resolve) => setTimeout(resolve, ms))
14 |
15 | export async function getAll() {
16 | await artificialWait()
17 | return albums
18 | }
19 |
20 | export async function getById(id: string) {
21 | await artificialWait()
22 | return albums.find((album) => album.id === id)
23 | }
24 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "request": "launch",
9 | "name": "Launch Program",
10 | "type": "node",
11 | "program": "${workspaceFolder}/server/dev.ts",
12 | "cwd": "${workspaceFolder}",
13 | "runtimeExecutable": "deno",
14 | "windows": {
15 | "runtimeExecutable": "deno.exe"
16 | },
17 | "runtimeArgs": [
18 | "run",
19 | "--unstable",
20 | "--inspect-wait",
21 | "--allow-all"
22 | ],
23 | "attachSimplePort": 9229
24 | }
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/app/components/SearchBox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { nanoid } from "npm:nanoid"
3 | import { useTransition } from "react"
4 |
5 | console.log(nanoid())
6 |
7 | export default function SearchBox(props: { search: string }) {
8 | const [isPending, startTransition] = useTransition()
9 |
10 | function onChange(e: React.ChangeEvent) {
11 | startTransition(() => {
12 | window.router.navigate(`?search=${e.target.value}`)
13 | })
14 | }
15 | return (
16 | <>
17 |
23 |
31 | Loading...
32 |
33 | >
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/app/components/SearchableAlbumList.tsx:
--------------------------------------------------------------------------------
1 | import { Album } from "../db/get.ts"
2 | import SearchBox from "./SearchBox.tsx"
3 |
4 | export default function SearchableAlbumList(
5 | { albums, search }: { albums: Album[]; search: string },
6 | ) {
7 | const filteredAlbums = filterAlbums(albums, search ?? "")
8 | return (
9 | <>
10 |
11 |
12 | {filteredAlbums.map((album) => (
13 |
14 |

15 |
- {album.title}
16 |
17 | ))}
18 |
19 | >
20 | )
21 | }
22 |
23 | function filterAlbums(albums: Album[], search: string) {
24 | const keywords = search
25 | .toLowerCase()
26 | .split(" ")
27 | .filter((s) => s !== "")
28 | if (keywords.length === 0) {
29 | return albums
30 | }
31 | return albums.filter((album) => {
32 | const words = (album.artist + " " + album.title).toLowerCase().split(" ")
33 | return keywords.every((kw) => words.some((w) => w.startsWith(kw)))
34 | })
35 | }
36 |
--------------------------------------------------------------------------------
/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "tasks": {
3 | "start": "deno run -A server/dev.ts",
4 | "dev": "deno run -A --watch server/dev.ts"
5 | },
6 | "imports": {
7 | "react": "https://esm.sh/react@18.3.0-next-3706edb81-20230308&dev",
8 | "react/": "https://esm.sh/react@18.3.0-next-3706edb81-20230308&dev/",
9 | "react-dom": "https://esm.sh/react-dom@18.3.0-next-3706edb81-20230308&dev",
10 | "react-dom/": "https://esm.sh/react-dom@18.3.0-next-3706edb81-20230308&dev/",
11 | "react-server-dom-webpack": "https://esm.sh/react-server-dom-webpack@0.0.0-experimental-41b4714f1-20230328&dev",
12 | "react-server-dom-webpack/": "https://esm.sh/react-server-dom-webpack@0.0.0-experimental-41b4714f1-20230328&dev/"
13 | },
14 | "compilerOptions": {
15 | "jsx": "react-jsx",
16 | "jsxImportSource": "react",
17 | "lib": [
18 | "deno.window",
19 | "dom",
20 | "dom.iterable"
21 | ],
22 | "allowJs": true
23 | },
24 | "fmt": {
25 | "options": {
26 | "semiColons": false
27 | },
28 | "files": {
29 | "exclude": ["dist"]
30 | }
31 | },
32 | "lint": {
33 | "files": {
34 | "exclude": ["dist"]
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/server/utils.ts:
--------------------------------------------------------------------------------
1 | export const src = new URL("../app/", import.meta.url)
2 | export const dist = new URL("../dist/", import.meta.url)
3 |
4 | export function resolveSrc(path: string): URL {
5 | return new URL(path, src)
6 | }
7 |
8 | export function resolveDist(path: string): URL {
9 | return new URL(path, dist)
10 | }
11 |
12 | export function resolveClientDist(path: string | URL) {
13 | return new URL(path, resolveDist("client/"))
14 | }
15 |
16 | export function resolveServerDist(path: string | URL) {
17 | return new URL(path, resolveDist("server/"))
18 | }
19 |
20 | export const clientComponentMapUrl = resolveDist("clientComponentMap.json")
21 |
22 | export type ClientComponentMap = {
23 | [key: string]: {
24 | id: string
25 | chunks: unknown[]
26 | name: string
27 | async: boolean
28 | }
29 | }
30 |
31 | export async function writeClientComponentMap(bundleMap: ClientComponentMap) {
32 | await Deno.writeTextFile(clientComponentMapUrl, JSON.stringify(bundleMap))
33 | }
34 |
35 | export async function readClientComponentMap() {
36 | const bundleMap = await Deno.readTextFile(clientComponentMapUrl)
37 | return JSON.parse(bundleMap) as ClientComponentMap
38 | }
39 |
--------------------------------------------------------------------------------
/app/db/data/bjork-post.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "bjork-post",
3 | "artist": "Björk",
4 | "title": "Post",
5 | "cover": "https://i.discogs.com/nKIGMO-FhgjIZAxpTKIx__vLsrg6XTEUEbeMXtgnJMs/rs:fit/g:sm/q:90/h:581/w:588/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9SLTEwMjQ4/MS0xMjMyMTQ2Nzgy/LmpwZWc.jpeg",
6 | "songs": [
7 | {
8 | "title": "Army of Me",
9 | "duration": "4:00"
10 | },
11 | {
12 | "title": "Hyperballad",
13 | "duration": "5:21"
14 | },
15 | {
16 | "title": "The Modern Things",
17 | "duration": "4:00"
18 | },
19 | {
20 | "title": "It's Oh So Quiet",
21 | "duration": "4:00"
22 | },
23 | {
24 | "title": "Enjoy",
25 | "duration": "4:00"
26 | },
27 | {
28 | "title": "You've Been Flirting Again",
29 | "duration": "4:00"
30 | },
31 | {
32 | "title": "Isobel",
33 | "duration": "4:00"
34 | },
35 | {
36 | "title": "Possibly Maybe",
37 | "duration": "4:00"
38 | },
39 | {
40 | "title": "I Miss You",
41 | "duration": "4:00"
42 | },
43 | {
44 | "title": "Cover Me",
45 | "duration": "4:00"
46 | },
47 | {
48 | "title": "Headphones",
49 | "duration": "4:00"
50 | }
51 | ]
52 | }
53 |
--------------------------------------------------------------------------------
/app/db/data/glass-animals-how-to-be.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "glass-animals-how-to-be",
3 | "title": "How To Be A Human Being",
4 | "artist": "Glass Animals",
5 | "cover": "https://i.discogs.com/H-kC9IIS7dglokcAXmfrRHji0roeA3SMxrNFr9MtBzQ/rs:fit/g:sm/q:90/h:300/w:300/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9SLTg5NTM0/NzAtMTQ3MjE0NTUx/My03NjA2LmpwZWc.jpeg",
6 | "songs": [
7 | {
8 | "title": "Life Itself",
9 | "duration": "4:41"
10 | },
11 | {
12 | "title": "Youth",
13 | "duration": "3:50"
14 | },
15 | {
16 | "title": "Season 2 Episode 3",
17 | "duration": "3:52"
18 | },
19 | {
20 | "title": "Pork Soda",
21 | "duration": "4:13"
22 | },
23 | {
24 | "title": "Mama's Gun",
25 | "duration": "3:52"
26 | },
27 | {
28 | "title": "Cane Shuga",
29 | "duration": "3:52"
30 | },
31 | {
32 | "title": "[Premade Sandwiches]",
33 | "duration": "0:36"
34 | },
35 | {
36 | "title": "The Other Side Of Paradise",
37 | "duration": "3:52"
38 | },
39 | {
40 | "title": "Take A Slice",
41 | "duration": "3:52"
42 | },
43 | {
44 | "title": "Poplar St",
45 | "duration": "3:52"
46 | },
47 | {
48 | "title": "Agnes",
49 | "duration": "3:52"
50 | }
51 | ]
52 | }
53 |
--------------------------------------------------------------------------------
/app/document.tsx:
--------------------------------------------------------------------------------
1 | // TODO: generate this
2 | const importMap = {
3 | "imports": {
4 | "react": "https://esm.sh/react@18.3.0-next-3706edb81-20230308&dev",
5 | "react/": "https://esm.sh/react@18.3.0-next-3706edb81-20230308&dev/",
6 | "react-dom": "https://esm.sh/react-dom@18.3.0-next-3706edb81-20230308&dev",
7 | "react-dom/":
8 | "https://esm.sh/react-dom@18.3.0-next-3706edb81-20230308&dev/",
9 | "react-server-dom-webpack":
10 | "https://esm.sh/react-server-dom-webpack@0.0.0-experimental-41b4714f1-20230328&dev",
11 | "react-server-dom-webpack/":
12 | "https://esm.sh/react-server-dom-webpack@0.0.0-experimental-41b4714f1-20230328&dev/",
13 | "npm:nanoid": "https://esm.sh/nanoid",
14 | },
15 | }
16 |
17 | export function Document({ children }: { children: React.ReactNode }) {
18 | return (
19 |
20 |
21 |
22 |
26 | AbraMix
27 |
31 |
32 |
33 |
34 |
35 | {children}
36 |
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/app/utils/dev/live-reload.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The dev server opens a socket connection to tell
3 | * the browser when files have changed.
4 | * This listens to that socket to trigger a refresh.
5 | */
6 | let socket: WebSocket, reconnectionTimerId: number | undefined
7 |
8 | // Construct the WebSocket url from the current
9 | // page origin.
10 | const requestUrl = "ws://localhost:21717/"
11 | connect(() => {
12 | console.log("Live reload connected on", requestUrl)
13 | })
14 |
15 | function log(message: string) {
16 | console.info("[refresh] ", message)
17 | }
18 |
19 | function refresh() {
20 | window.location.reload()
21 | }
22 |
23 | /**
24 | * Create WebSocket, connect to the server and
25 | * listen for refresh events.
26 | */
27 | function connect(callback: (event: Event) => void) {
28 | if (socket) {
29 | socket.close()
30 | }
31 |
32 | socket = new WebSocket(requestUrl)
33 |
34 | // When the connection opens, execute the callback.
35 | socket.addEventListener("open", callback)
36 |
37 | socket.addEventListener("message", (event) => {
38 | if (event.data === "refresh") {
39 | log("refreshing...")
40 | refresh()
41 | }
42 | })
43 |
44 | // Handle when the WebSocket closes. We log
45 | // the loss of connection and set a timer to
46 | // start the connection again after a second.
47 | socket.addEventListener("close", () => {
48 | log("connection lost - reconnecting...")
49 |
50 | clearTimeout(reconnectionTimerId)
51 |
52 | reconnectionTimerId = setTimeout(() => {
53 | connect(refresh)
54 | }, 1000)
55 | })
56 | }
57 |
--------------------------------------------------------------------------------
/server/dev.ts:
--------------------------------------------------------------------------------
1 | import { toPathString } from "https://deno.land/std@0.184.0/fs/_util.ts"
2 | import { Application } from "https://deno.land/x/oak@v12.2.0/mod.ts"
3 | import {
4 | WebSocketClient,
5 | WebSocketServer,
6 | } from "https://deno.land/x/websocket@v0.1.4/mod.ts"
7 | import { router } from "./router.tsx"
8 |
9 | Deno.env.set("NODE_ENV", "development")
10 |
11 | function startHttpServer() {
12 | const app = new Application().use(router.routes())
13 |
14 | app.addEventListener("listen", () => {
15 | console.log(`⚛️ Future of React started on http://localhost:${port}`)
16 | })
17 |
18 | const port = 3000
19 | return app.listen({ port, hostname: "localhost" })
20 | }
21 |
22 | // File watcher to trigger browser refreshes
23 | // ------------
24 | const sockets = new Set()
25 | function startSocketServer() {
26 | const refreshPort = 21717
27 | const wsServer = new WebSocketServer(refreshPort)
28 |
29 | wsServer.on("connection", (ws) => {
30 | sockets.add(ws)
31 |
32 | ws.on("close", () => {
33 | sockets.delete(ws)
34 | })
35 |
36 | ws.send("connected")
37 | })
38 | }
39 |
40 | // /**
41 | // * Watch files in the `app/` directory
42 | // * and trigger a build + refresh on change.
43 | // */
44 | async function startFileWatcher() {
45 | const watcher = Deno.watchFs(
46 | toPathString(new URL("../app/", import.meta.url)),
47 | )
48 | for await (const _event of watcher) {
49 | for (const socket of sockets) {
50 | socket.send("refresh")
51 | }
52 | }
53 | }
54 |
55 | await Promise.all([startHttpServer(), startSocketServer(), startFileWatcher()])
56 |
--------------------------------------------------------------------------------
/app/db/data/lady-gaga-the-fame.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "lady-gaga-the-fame",
3 | "artist": "Lady Gaga",
4 | "title": "The Fame",
5 | "cover": "https://i.discogs.com/0MgbgSVEqOrRtLwARinYwVPQHVqIq-pR0csL19pdmTg/rs:fit/g:sm/q:90/h:396/w:400/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9SLTE0MzI5/OTEtMTIxOTI4Njg0/MS5qcGVn.jpeg",
6 | "songs": [
7 | {
8 | "title": "Just Dance",
9 | "duration": "4:01"
10 | },
11 | {
12 | "title": "LoveGame",
13 | "duration": "3:48"
14 | },
15 | {
16 | "title": "Paparazzi",
17 | "duration": "3:48"
18 | },
19 | {
20 | "title": "Poker Face",
21 | "duration": "3:58"
22 | },
23 | {
24 | "title": "Eh, Eh (Nothing Else I Can Say)",
25 | "duration": "3:47"
26 | },
27 | {
28 | "title": "Beautiful, Dirty, Rich",
29 | "duration": "3:47"
30 | },
31 | {
32 | "title": "The Fame",
33 | "duration": "3:47"
34 | },
35 | {
36 | "title": "Money Honey",
37 | "duration": "3:47"
38 | },
39 | {
40 | "title": "Starstruck",
41 | "duration": "3:47"
42 | },
43 | {
44 | "title": "Boys Boys Boys",
45 | "duration": "3:47"
46 | },
47 | {
48 | "title": "Paper Gangsta",
49 | "duration": "3:47"
50 | },
51 | {
52 | "title": "Brown Eyes",
53 | "duration": "3:47"
54 | },
55 | {
56 | "title": "I Like It Rough",
57 | "duration": "3:47"
58 | },
59 | {
60 | "title": "Summerboy",
61 | "duration": "3:47"
62 | },
63 | {
64 | "title": "Disco Heaven",
65 | "duration": "3:47"
66 | }
67 | ]
68 | }
69 |
--------------------------------------------------------------------------------
/app/_router.tsx:
--------------------------------------------------------------------------------
1 | // @ts-expect-error Module '"react"' has no exported member 'use'.
2 | import { startTransition, StrictMode, use, useEffect, useState } from "react"
3 | import { createRoot } from "react-dom/client"
4 | import { createFromFetch } from "react-server-dom-webpack/client"
5 |
6 | /** Dev-only dependencies */
7 | import { DevPanel } from "./utils/dev/DevPanel.tsx"
8 | import "./utils/dev/live-reload.ts"
9 |
10 | // HACK: map webpack resolution to native ESM
11 | // @ts-expect-error Property '__webpack_require__' does not exist on type 'Window & typeof globalThis'.
12 | window.__webpack_require__ = (id) => import(id)
13 |
14 | createRoot(document.body).render(
15 |
16 |
17 | ,
18 | )
19 |
20 | const callbacks: Array<() => void> = []
21 |
22 | declare global {
23 | interface Window {
24 | router: {
25 | navigate(url: string): void
26 | }
27 | }
28 | }
29 |
30 | window.router = {
31 | navigate(url) {
32 | window.history.replaceState({}, "", url)
33 | callbacks.forEach((cb) => cb())
34 | },
35 | }
36 |
37 | function Router() {
38 | const [url, setUrl] = useState("/rsc" + window.location.search)
39 |
40 | useEffect(() => {
41 | function handleNavigate() {
42 | startTransition(() => {
43 | setUrl("/rsc" + window.location.search)
44 | })
45 | }
46 | callbacks.push(handleNavigate)
47 | self.addEventListener("popstate", handleNavigate)
48 | return () => {
49 | callbacks.splice(callbacks.indexOf(handleNavigate), 1)
50 | self.removeEventListener("popstate", handleNavigate)
51 | }
52 | }, [])
53 |
54 | return (
55 | <>
56 |
57 |
58 | >
59 | )
60 | }
61 |
62 | const initialCache = new Map()
63 |
64 | function ServerOutput({ url }: { url: string }) {
65 | const [cache] = useState(initialCache)
66 | let lazyJsx = cache.get(url)
67 | if (!lazyJsx) {
68 | lazyJsx = createFromFetch(fetch(url))
69 | cache.set(url, lazyJsx)
70 | }
71 | return use(lazyJsx)
72 | }
73 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Simple RSC, Deno Edition 🦕
2 |
3 | > A simple React Server Components implementation that you can build yourself 🙌
4 | >
5 | > Converted from [Ben's original demo](https://github.com/bholmesdev/simple-rsc)
6 |
7 | [Watch the live demo with Dan Abramov here!](https://www.youtube.com/watch?v=Fctw7WjmxpU)
8 |
9 | ## ⭐️ Goals
10 |
11 | - ⚙️ Demo a build process to bundle server components and handle client
12 | components with the `"use client"` directive.
13 | - 🌊 Show how React server components are streamed to the browser with a simple
14 | Node server.
15 | - 📝 Reveal how a server component requests appear to the client with a robust
16 | developer panel.
17 | - 🦕 Make this work with Deno!
18 |
19 | ## Getting started
20 |
21 | First, ensure you have Deno installed:
22 | https://deno.land/manual/getting_started/installation
23 |
24 | Then, run the dev task:
25 |
26 | ```bash
27 | deno task dev
28 | ```
29 |
30 | This should trigger a build and start your server at http://localhost:3000 👀
31 |
32 | Hint: Try editing the `app/page.tsx` file to see changes appear in your browser.
33 |
34 | ## Project structure
35 |
36 | This project is broken up into the `app/` and `server/` directories. The most
37 | important entrypoints are listed below:
38 |
39 | ```sh
40 | app/ # 🥞 your full-stack application
41 | page.tsx # server index route.
42 | _router.tsx # client script that requests your `page.tsx`.
43 |
44 | server/ # 💿 your backend that builds and renders the `app/`
45 | router.ts # server router for streaming React server components
46 | build.ts # bundler to process server and client components
47 | ```
48 |
49 | ## 🙋♀️ What is _not_ included?
50 |
51 | - **File-based routing conventions.** This repo includes a _single_ index route,
52 | with support for processing query params. If you need multiple routes, you can
53 | try
54 | [NextJS' new `app/` directory.](https://beta.nextjs.org/docs/routing/defining-routes)
55 | - **Advance bundling for CSS-in-JS.**
56 | [A Tailwind script](https://tailwindcss.com/docs/installation/play-cdn) is
57 | included for playing with styles.
58 | - **Advice on production deploys.** This is a _learning tool_ to show how React
59 | Server Components are used, _not_ the bedrock for your next side project! See
60 | [React's updated "Start a New React Project" guide](https://react.dev/learn/start-a-new-react-project)
61 | for advice on building production-ready apps.
62 |
--------------------------------------------------------------------------------
/server/router.tsx:
--------------------------------------------------------------------------------
1 | import { typeByExtension } from "https://deno.land/std@0.183.0/media_types/type_by_extension.ts"
2 | import { toPathString } from "https://deno.land/std@0.184.0/fs/_util.ts"
3 | import * as esbuild from "https://deno.land/x/esbuild@v0.17.18/mod.js"
4 | import { Router } from "https://deno.land/x/oak@v12.2.0/mod.ts"
5 | import { renderToString } from "react-dom/server"
6 | import * as ReactServerDom from "react-server-dom-webpack/server.browser"
7 | import { Document } from "../app/document.tsx"
8 | import { loadPageComponent } from "./load-page-component.ts"
9 |
10 | export const router = new Router()
11 | const appFolderUrl = new URL("../app/", import.meta.url)
12 |
13 | // Serve HTML homepage that fetches and renders the server component.
14 | router.get("/", (ctx) => {
15 | const html = renderToString(
16 |
17 | Loading...
18 | ,
19 | )
20 | ctx.response.body = html
21 | ctx.response.headers.set("Content-Type", "text/html")
22 | })
23 |
24 | // Serve client-side components in the app folder
25 | router.get("/app/:file*", async (ctx) => {
26 | const entryUrl = new URL(ctx.params.file!, appFolderUrl)
27 |
28 | const result = await esbuild.build({
29 | entryPoints: [toPathString(entryUrl)],
30 | bundle: true,
31 | write: false,
32 | format: "esm",
33 | packages: "external",
34 | jsx: "automatic",
35 | sourcemap: "inline",
36 | })
37 |
38 | ctx.response.body = result.outputFiles[0].contents
39 | ctx.response.headers.set("Content-Type", getContentType(entryUrl.pathname))
40 | })
41 |
42 | router.get("/rsc", async (ctx) => {
43 | const { Component, componentMap } = await loadPageComponent(
44 | new URL("../app/page.tsx", import.meta.url),
45 | )
46 |
47 | console.log(componentMap)
48 |
49 | // 👀 This is where the magic happens!
50 | // Render the server component tree to a stream.
51 | // This renders your server components in real time and
52 | // sends each component to the browser as soon as its resolved.
53 | const stream: ReadableStream = ReactServerDom.renderToReadableStream(
54 | ,
55 | componentMap,
56 | )
57 | ctx.response.body = stream
58 | ctx.response.headers.set("Content-Type", "text/html")
59 | })
60 |
61 | function getContentType(path: string): string {
62 | if (path.endsWith(".ts")) return "text/javascript"
63 | if (path.endsWith(".tsx")) return "text/javascript"
64 |
65 | const contentType = typeByExtension(path)
66 | if (contentType) return contentType
67 |
68 | return "text/plain"
69 | }
70 |
--------------------------------------------------------------------------------
/server/load-page-component.ts:
--------------------------------------------------------------------------------
1 | import { toPathString } from "https://deno.land/std@0.184.0/fs/_util.ts"
2 | import * as path from "https://deno.land/std@0.184.0/path/mod.ts"
3 | import * as esbuild from "https://deno.land/x/esbuild@v0.17.18/mod.js"
4 | import { outdent } from "https://deno.land/x/outdent@v0.8.0/mod.ts"
5 | import * as swc from "https://deno.land/x/swc@0.2.1/mod.ts"
6 | import { ComponentMap } from "./component-map.ts"
7 |
8 | const reactComponentNameRegex = /^[A-Z]/
9 |
10 | export async function loadPageComponent(url: URL) {
11 | const resolver = clientComponentsResolver()
12 |
13 | const result = await esbuild.build({
14 | entryPoints: [toPathString(url)],
15 | bundle: true,
16 | packages: "external",
17 | format: "esm",
18 | write: false,
19 | jsx: "automatic",
20 | platform: "neutral",
21 | plugins: [resolver.plugin],
22 | sourcemap: "inline",
23 | })
24 |
25 | const module = await import(
26 | `data:text/javascript,${encodeURIComponent(result.outputFiles[0].text)}`
27 | )
28 | return { Component: module.default, componentMap: resolver.componentMap }
29 | }
30 |
31 | function clientComponentsResolver() {
32 | const componentMap: ComponentMap = {}
33 |
34 | const plugin: esbuild.Plugin = {
35 | name: "react-client-components",
36 | setup(build) {
37 | build.onLoad({ filter: /\.(jsx|tsx)$/ }, async (args) => {
38 | const source = await Deno.readTextFile(args.path)
39 |
40 | const ast = swc.parse(source, {
41 | syntax: "typescript",
42 | tsx: true,
43 | })
44 |
45 | const isClientComponentFile = ast.body.some((node) =>
46 | node.type === "ExpressionStatement" &&
47 | node.expression.type === "StringLiteral" &&
48 | node.expression.value === "use client"
49 | )
50 | if (!isClientComponentFile) {
51 | return
52 | }
53 |
54 | const fileUrl = path.toFileUrl(args.path)
55 | const projectRootUrl = new URL(import.meta.url)
56 |
57 | // get the path relative from the root
58 | const id = path.posix.join(
59 | "/",
60 | path.posix.relative(
61 | path.posix.dirname(projectRootUrl.pathname),
62 | fileUrl.pathname,
63 | ),
64 | )
65 |
66 | for (let i = ast.body.length - 1; i >= 0; i--) {
67 | const node = ast.body[i]
68 | if (
69 | node.type === "ExportDefaultDeclaration" &&
70 | node.decl.type === "FunctionExpression" &&
71 | node.decl.identifier.value.match(reactComponentNameRegex)
72 | ) {
73 | ast.body[i] = swc.parse(getClientComponentTemplate(id), {
74 | syntax: "typescript",
75 | tsx: true,
76 | }).body[0]
77 |
78 | componentMap[id] = {
79 | id,
80 | name: "default",
81 | chunks: [],
82 | async: true,
83 | }
84 | }
85 | }
86 |
87 | return {
88 | contents: swc.print(ast).code,
89 | loader: "tsx",
90 | }
91 | })
92 | },
93 | }
94 |
95 | return { plugin, componentMap }
96 | }
97 |
98 | function getClientComponentTemplate(id: string) {
99 | return outdent`
100 | export default Object.defineProperties(function() {
101 | throw new Error("Attempted to call the default export of ${id} from the server.")
102 | },{
103 | $$typeof: { value: Symbol.for("react.client.reference") },
104 | $$id: { value: ${JSON.stringify(id)} },
105 | })
106 | `
107 | }
108 |
--------------------------------------------------------------------------------
/app/utils/dev/DevPanel.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useRef, useState } from "react"
2 |
3 | export function DevPanel({ url }: { url: string }) {
4 | const initialContent: {
5 | type: "def" | "client" | "server"
6 | content: string
7 | key: string
8 | }[] = []
9 | const [content, setContent] = useState(initialContent)
10 |
11 | const { mouseMove, getResizeProps } = useWindowResize({
12 | direction: "vertical",
13 | })
14 |
15 | useEffect(() => {
16 | const abortController = new AbortController()
17 |
18 | fetch(url, {
19 | signal: abortController.signal,
20 | }).then(async (res) => {
21 | const reader = res.body?.getReader()
22 | if (!reader) return
23 |
24 | let allDone = false
25 |
26 | while (!allDone) {
27 | const { value, done } = await reader.read()
28 | if (done) {
29 | allDone = true
30 | } else {
31 | const decoded = new TextDecoder().decode(value)
32 | const segments = decoded.trim().split("\n")
33 | for (const segment of segments) {
34 | /** @type {'server' | 'client' | 'def'} */
35 | let type: "server" | "client" | "def" = "server"
36 | if (/^\d+:"\$/.test(segment)) {
37 | // Heuristic: messages starting with a "$"
38 | // are probably definitions.
39 | // Example: 2:"$Sreact.suspense"
40 | type = "def"
41 | } else if (/^\d+:I{"id"/.test(segment)) {
42 | // Heuristic: messages starting with I{"id"}
43 | // are probably client component imports.
44 | // Example: 4:I{"id":"/dist/client/SearchBox.js","chunks":[],"name":"default","async":true}
45 | type = "client"
46 | }
47 | setContent((state) => [
48 | ...state,
49 | { type, content: segment, key: crypto.randomUUID() },
50 | ])
51 | }
52 | }
53 | }
54 | })
55 |
56 | return () => abortController.abort()
57 | }, [url])
58 |
59 | return (
60 |
96 | )
97 | }
98 |
99 | type Direction = "vertical" | "horizontal"
100 |
101 | const toLocalStorageKey = (direction: Direction) =>
102 | `simple-rfc-devtool-resize-${direction}`
103 | const DEFAULT_HEIGHT = 260
104 |
105 | function useWindowResize({ direction }: { direction: Direction }) {
106 | const [mouseMove, setMouseMove] = useState(getInitialSize(direction))
107 | const [isMouseDown, setIsMouseDown] = useState(false)
108 | const ref = useRef(null)
109 |
110 | const handleMouseDown = useCallback(() => {
111 | setIsMouseDown(true)
112 | }, [])
113 |
114 | const handleMouseUp = useCallback(() => {
115 | setIsMouseDown(false)
116 | }, [])
117 |
118 | const handleMouseMove = useCallback(
119 | (event: MouseEvent) => {
120 | if (isMouseDown) {
121 | setMouseMove(direction === "vertical" ? event.pageY : event.pageX)
122 | }
123 | },
124 | [isMouseDown],
125 | )
126 |
127 | const getResizeProps = () => {
128 | return {
129 | onMouseDown: handleMouseDown,
130 | ref,
131 | }
132 | }
133 |
134 | useEffect(() => {
135 | if (isMouseDown) {
136 | self.addEventListener("mousemove", handleMouseMove)
137 | self.addEventListener("mouseup", handleMouseUp)
138 | }
139 |
140 | const timeout = setTimeout(
141 | () =>
142 | localStorage.setItem(
143 | toLocalStorageKey(direction),
144 | String(mouseMove === null ? "" : mouseMove),
145 | ),
146 | 200,
147 | )
148 |
149 | return () => {
150 | self.removeEventListener("mousemove", handleMouseMove)
151 | self.removeEventListener("mouseup", handleMouseUp)
152 | clearTimeout(timeout)
153 | }
154 | }, [isMouseDown])
155 |
156 | return {
157 | mouseMove,
158 | getResizeProps,
159 | }
160 | }
161 |
162 | function getDevtoolHeight(mouseMove: number) {
163 | return `${window.innerHeight - mouseMove}px`
164 | }
165 |
166 | function getInitialSize(direction: Direction) {
167 | const { localStorage } = window ?? {}
168 | return Number(
169 | localStorage?.getItem(toLocalStorageKey(direction)) ?? DEFAULT_HEIGHT,
170 | )
171 | }
172 |
--------------------------------------------------------------------------------
/deno.lock:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2",
3 | "remote": {
4 | "https://deno.land/std@0.183.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462",
5 | "https://deno.land/std@0.183.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3",
6 | "https://deno.land/std@0.183.0/async/deferred.ts": "42790112f36a75a57db4a96d33974a936deb7b04d25c6084a9fa8a49f135def8",
7 | "https://deno.land/std@0.183.0/bytes/bytes_list.ts": "31d664f4d42fa922066405d0e421c56da89d751886ee77bbe25a88bf0310c9d0",
8 | "https://deno.land/std@0.183.0/bytes/concat.ts": "d26d6f3d7922e6d663dacfcd357563b7bf4a380ce5b9c2bbe0c8586662f25ce2",
9 | "https://deno.land/std@0.183.0/bytes/copy.ts": "939d89e302a9761dcf1d9c937c7711174ed74c59eef40a1e4569a05c9de88219",
10 | "https://deno.land/std@0.183.0/bytes/ends_with.ts": "4228811ebc71615d27f065c54b5e815ec1972538772b0f413c0efe05245b472e",
11 | "https://deno.land/std@0.183.0/bytes/equals.ts": "b87494ce5442dc786db46f91378100028c402f83a14a2f7bbff6bda7810aefe3",
12 | "https://deno.land/std@0.183.0/bytes/includes_needle.ts": "76a8163126fb2f8bf86fd7f22192c3bb04bf6a20b987a095127c2ca08adf3ba6",
13 | "https://deno.land/std@0.183.0/bytes/index_of_needle.ts": "65c939607df609374c4415598fa4dad04a2f14c4d98cd15775216f0aaf597f24",
14 | "https://deno.land/std@0.183.0/bytes/last_index_of_needle.ts": "7181072883cb4908c6ce8f7a5bb1d96787eef2c2ab3aa94fe4268ab326a53cbf",
15 | "https://deno.land/std@0.183.0/bytes/mod.ts": "e869bba1e7a2e3a9cc6c2d55471888429a544e70a840c087672e656e7ba21815",
16 | "https://deno.land/std@0.183.0/bytes/repeat.ts": "6f5e490d8d72bcbf8d84a6bb04690b9b3eb5822c5a11687bca73a2318a842294",
17 | "https://deno.land/std@0.183.0/bytes/starts_with.ts": "3e607a70c9c09f5140b7a7f17a695221abcc7244d20af3eb47ccbb63f5885135",
18 | "https://deno.land/std@0.183.0/crypto/keystack.ts": "877ab0f19eb7d37ad6495190d3c3e39f58e9c52e0b6a966f82fd6df67ca55f90",
19 | "https://deno.land/std@0.183.0/crypto/timing_safe_equal.ts": "0fae34ee02264f309ae0b6e54e9746a7aba3996e5454903ed106967a7a9ef665",
20 | "https://deno.land/std@0.183.0/encoding/base64.ts": "144ae6234c1fbe5b68666c711dc15b1e9ee2aef6d42b3b4345bf9a6c91d70d0d",
21 | "https://deno.land/std@0.183.0/encoding/base64url.ts": "2ed4ba122b20fedf226c5d337cf22ee2024fa73a8f85d915d442af7e9ce1fae1",
22 | "https://deno.land/std@0.183.0/http/_negotiation/common.ts": "14d1a52427ab258a4b7161cd80e1d8a207b7cc64b46e911780f57ead5f4323c6",
23 | "https://deno.land/std@0.183.0/http/_negotiation/encoding.ts": "ff747d107277c88cb7a6a62a08eeb8d56dad91564cbcccb30694d5dc126dcc53",
24 | "https://deno.land/std@0.183.0/http/_negotiation/language.ts": "7bcddd8db3330bdb7ce4fc00a213c5547c1968139864201efd67ef2d0d51887d",
25 | "https://deno.land/std@0.183.0/http/_negotiation/media_type.ts": "58847517cd549384ad677c0fe89e0a4815be36fe7a303ea63cee5f6a1d7e1692",
26 | "https://deno.land/std@0.183.0/http/cookie_map.ts": "d148a5eaf35f19905dd5104126fa47ac71105306dd42f129732365e43108b28a",
27 | "https://deno.land/std@0.183.0/http/etag.ts": "4cb24b489e71c6bc333ab03b93edac303faddc199b3a6d0bfba284c8980a3dd2",
28 | "https://deno.land/std@0.183.0/http/http_errors.ts": "b9a18ef97d6c5966964de95e04d1f9f88a0f8bd8577c26fd402d9d632fb03a42",
29 | "https://deno.land/std@0.183.0/http/http_status.ts": "8a7bcfe3ac025199ad804075385e57f63d055b2aed539d943ccc277616d6f932",
30 | "https://deno.land/std@0.183.0/http/negotiation.ts": "46e74a6bad4b857333a58dc5b50fe8e5a4d5267e97292293ea65f980bd918086",
31 | "https://deno.land/std@0.183.0/http/server_sent_event.ts": "856764c8c058605bb618272833990b1f88b6de0dcc460f0f09749ba7e99dd656",
32 | "https://deno.land/std@0.183.0/io/buf_reader.ts": "abeb92b18426f11d72b112518293a96aef2e6e55f80b84235e8971ac910affb5",
33 | "https://deno.land/std@0.183.0/io/buf_writer.ts": "48c33c8f00b61dcbc7958706741cec8e59810bd307bc6a326cbd474fe8346dfd",
34 | "https://deno.land/std@0.183.0/io/buffer.ts": "17f4410eaaa60a8a85733e8891349a619eadfbbe42e2f319283ce2b8f29723ab",
35 | "https://deno.land/std@0.183.0/io/copy_n.ts": "0cc7ce07c75130f6fc18621ec1911c36e147eb9570664fee0ea12b1988167590",
36 | "https://deno.land/std@0.183.0/io/limited_reader.ts": "6c9a216f8eef39c1ee2a6b37a29372c8fc63455b2eeb91f06d9646f8f759fc8b",
37 | "https://deno.land/std@0.183.0/io/mod.ts": "2665bcccc1fd6e8627cca167c3e92aaecbd9897556b6f69e6d258070ef63fd9b",
38 | "https://deno.land/std@0.183.0/io/multi_reader.ts": "9c2a0a31686c44b277e16da1d97b4686a986edcee48409b84be25eedbc39b271",
39 | "https://deno.land/std@0.183.0/io/read_delim.ts": "c02b93cc546ae8caad8682ae270863e7ace6daec24c1eddd6faabc95a9d876a3",
40 | "https://deno.land/std@0.183.0/io/read_int.ts": "7cb8bcdfaf1107586c3bacc583d11c64c060196cb070bb13ae8c2061404f911f",
41 | "https://deno.land/std@0.183.0/io/read_lines.ts": "c526c12a20a9386dc910d500f9cdea43cba974e853397790bd146817a7eef8cc",
42 | "https://deno.land/std@0.183.0/io/read_long.ts": "f0aaa420e3da1261c5d33c5e729f09922f3d9fa49f046258d4ff7a00d800c71e",
43 | "https://deno.land/std@0.183.0/io/read_range.ts": "28152daf32e43dd9f7d41d8466852b0d18ad766cd5c4334c91fef6e1b3a74eb5",
44 | "https://deno.land/std@0.183.0/io/read_short.ts": "805cb329574b850b84bf14a92c052c59b5977a492cd780c41df8ad40826c1a20",
45 | "https://deno.land/std@0.183.0/io/read_string_delim.ts": "5dc9f53bdf78e7d4ee1e56b9b60352238ab236a71c3e3b2a713c3d78472a53ce",
46 | "https://deno.land/std@0.183.0/io/slice_long_to_bytes.ts": "48d9bace92684e880e46aa4a2520fc3867f9d7ce212055f76ecc11b22f9644b7",
47 | "https://deno.land/std@0.183.0/io/string_reader.ts": "da0f68251b3d5b5112485dfd4d1b1936135c9b4d921182a7edaf47f74c25cc8f",
48 | "https://deno.land/std@0.183.0/io/string_writer.ts": "8a03c5858c24965a54c6538bed15f32a7c72f5704a12bda56f83a40e28e5433e",
49 | "https://deno.land/std@0.183.0/media_types/_db.ts": "7606d83e31f23ce1a7968cbaee852810c2cf477903a095696cdc62eaab7ce570",
50 | "https://deno.land/std@0.183.0/media_types/_util.ts": "916efbd30b6148a716f110e67a4db29d6949bf4048997b754415dd7e42c52378",
51 | "https://deno.land/std@0.183.0/media_types/content_type.ts": "ad98a5aa2d95f5965b2796072284258710a25e520952376ed432b0937ce743bc",
52 | "https://deno.land/std@0.183.0/media_types/extension.ts": "a7cd28c9417143387cdfed27d4e8607ebcf5b1ec27eb8473d5b000144689fe65",
53 | "https://deno.land/std@0.183.0/media_types/extensions_by_type.ts": "43806d6a52a0d6d965ada9d20e60a982feb40bc7a82268178d94edb764694fed",
54 | "https://deno.land/std@0.183.0/media_types/format_media_type.ts": "f5e1073c05526a6f5a516ac5c5587a1abd043bf1039c71cde1166aa4328c8baf",
55 | "https://deno.land/std@0.183.0/media_types/get_charset.ts": "18b88274796fda5d353806bf409eb1d2ddb3f004eb4bd311662c4cdd8ac173db",
56 | "https://deno.land/std@0.183.0/media_types/mod.ts": "d3f0b99f85053bc0b98ecc24eaa3546dfa09b856dc0bbaf60d8956d2cdd710c8",
57 | "https://deno.land/std@0.183.0/media_types/parse_media_type.ts": "835c4112e1357e95b4f10d7cdea5ae1801967e444f48673ff8f1cb4d32af9920",
58 | "https://deno.land/std@0.183.0/media_types/type_by_extension.ts": "daa801eb0f11cdf199445d0f1b656cf116d47dcf9e5b85cc1e6b4469f5ee0432",
59 | "https://deno.land/std@0.183.0/media_types/vendor/mime-db.v1.52.0.ts": "6925bbcae81ca37241e3f55908d0505724358cda3384eaea707773b2c7e99586",
60 | "https://deno.land/std@0.183.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0",
61 | "https://deno.land/std@0.183.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b",
62 | "https://deno.land/std@0.183.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0",
63 | "https://deno.land/std@0.183.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000",
64 | "https://deno.land/std@0.183.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1",
65 | "https://deno.land/std@0.183.0/path/mod.ts": "bf718f19a4fdd545aee1b06409ca0805bd1b68ecf876605ce632e932fe54510c",
66 | "https://deno.land/std@0.183.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d",
67 | "https://deno.land/std@0.183.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1",
68 | "https://deno.land/std@0.183.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba",
69 | "https://deno.land/std@0.183.0/streams/_common.ts": "f45cba84f0d813de3326466095539602364a9ba521f804cc758f7a475cda692d",
70 | "https://deno.land/std@0.183.0/streams/buffer.ts": "d5b3d7d0299114e5b2ea895a8bf202a687fd915c5282f8096c7bae23b5a04407",
71 | "https://deno.land/std@0.183.0/streams/byte_slice_stream.ts": "225d57263a34325d7c96cb3dafeb478eec0e6fd05cd0458d678752eadd132bb4",
72 | "https://deno.land/std@0.183.0/streams/copy.ts": "75cbc795ff89291df22ddca5252de88b2e16d40c85d02840593386a8a1454f71",
73 | "https://deno.land/std@0.183.0/streams/delimiter_stream.ts": "f69e849b3d1f59f02424497273f411105a6f76a9f13da92aeeb9a2d554236814",
74 | "https://deno.land/std@0.183.0/streams/early_zip_readable_streams.ts": "4005fa74162b943f79899e5d7cb96adcbc0a6b867f9144974ed12d30e0a556e1",
75 | "https://deno.land/std@0.183.0/streams/iterate_reader.ts": "bbec1d45c2df2c0c5920bad0549351446fdc8e0886d99e95959b259dbcdb6072",
76 | "https://deno.land/std@0.183.0/streams/limited_bytes_transform_stream.ts": "05dc592ffaab83257494d22dd53917e56243c26e5e3129b3f13ddbbbc4785048",
77 | "https://deno.land/std@0.183.0/streams/limited_transform_stream.ts": "d69ab790232c1b86f53621ad41ef03c235f2abb4b7a1cd51960ad6e12ee55e38",
78 | "https://deno.land/std@0.183.0/streams/merge_readable_streams.ts": "5d6302888f4bb0616dafb5768771be0aec9bedc05fbae6b3d726d05ffbec5b15",
79 | "https://deno.land/std@0.183.0/streams/mod.ts": "c07ec010e700b9ea887dc36ca08729828bc7912f711e4054e24d33fd46282252",
80 | "https://deno.land/std@0.183.0/streams/read_all.ts": "ee319772fb0fd28302f97343cc48dfcf948f154fd0d755d8efe65814b70533be",
81 | "https://deno.land/std@0.183.0/streams/readable_stream_from_iterable.ts": "cd4bb9e9bf6dbe84c213beb1f5085c326624421671473e410cfaecad15f01865",
82 | "https://deno.land/std@0.183.0/streams/readable_stream_from_reader.ts": "bfc416c4576a30aac6b9af22c9dc292c20c6742141ee7c55b5e85460beb0c54e",
83 | "https://deno.land/std@0.183.0/streams/reader_from_iterable.ts": "55f68110dce3f8f2c87b834d95f153bc904257fc65175f9f2abe78455cb8047c",
84 | "https://deno.land/std@0.183.0/streams/reader_from_stream_reader.ts": "fa4971e5615a010e49492c5d1688ca1a4d17472a41e98b498ab89a64ebd7ac73",
85 | "https://deno.land/std@0.183.0/streams/text_delimiter_stream.ts": "20e680ab8b751390e359288ce764f9c47d164af11a263870746eeca4bc7d976b",
86 | "https://deno.land/std@0.183.0/streams/text_line_stream.ts": "0f2c4b33a5fdb2476f2e060974cba1347cefe99a4af33c28a57524b1a34750fa",
87 | "https://deno.land/std@0.183.0/streams/to_transform_stream.ts": "7f55fc0b14cf3ed0f8d10d8f41d05bdc40726e44a65c37f58705d10a615f0159",
88 | "https://deno.land/std@0.183.0/streams/writable_stream_from_writer.ts": "56fff5c82fb736fdd669b567cc0b2bbbe0351002cd13254eae26c366e2bed89a",
89 | "https://deno.land/std@0.183.0/streams/write_all.ts": "aec90152978581ea62d56bb53a5cbf487e6a89c902f87c5969681ffbdf32b998",
90 | "https://deno.land/std@0.183.0/streams/writer_from_stream_writer.ts": "07c7ee025151a190f37fc42cbb01ff93afc949119ebddc6e0d0df14df1bf6950",
91 | "https://deno.land/std@0.183.0/streams/zip_readable_streams.ts": "a9d81aa451240f79230add674809dbee038d93aabe286e2d9671e66591fc86ca",
92 | "https://deno.land/std@0.183.0/types.d.ts": "dbaeb2c4d7c526db9828fc8df89d8aecf53b9ced72e0c4568f97ddd8cda616a4",
93 | "https://deno.land/std@0.184.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462",
94 | "https://deno.land/std@0.184.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3",
95 | "https://deno.land/std@0.184.0/fs/_util.ts": "579038bebc3bd35c43a6a7766f7d91fbacdf44bc03468e9d3134297bb99ed4f9",
96 | "https://deno.land/std@0.184.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0",
97 | "https://deno.land/std@0.184.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b",
98 | "https://deno.land/std@0.184.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0",
99 | "https://deno.land/std@0.184.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000",
100 | "https://deno.land/std@0.184.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1",
101 | "https://deno.land/std@0.184.0/path/mod.ts": "bf718f19a4fdd545aee1b06409ca0805bd1b68ecf876605ce632e932fe54510c",
102 | "https://deno.land/std@0.184.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d",
103 | "https://deno.land/std@0.184.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1",
104 | "https://deno.land/std@0.184.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba",
105 | "https://deno.land/std@0.92.0/_util/assert.ts": "2f868145a042a11d5ad0a3c748dcf580add8a0dbc0e876eaa0026303a5488f58",
106 | "https://deno.land/std@0.92.0/_util/has_own_property.ts": "f5edd94ed3f3c20c517d812045deb97977e18501c9b7105b5f5c11a31893d7a2",
107 | "https://deno.land/std@0.92.0/async/deferred.ts": "624bef4b755b71394620508a0c234a93cb8020cbd1b04bfcdad41c174392cef6",
108 | "https://deno.land/std@0.92.0/async/delay.ts": "9de1d8d07d1927767ab7f82434b883f3d8294fb19cad819691a2ad81a728cf3d",
109 | "https://deno.land/std@0.92.0/async/mod.ts": "253b41c658d768613eacfb11caa0a9ca7148442f932018a45576f7f27554c853",
110 | "https://deno.land/std@0.92.0/async/mux_async_iterator.ts": "b9091909db04cdb0af6f7807677372f64c1488de6c4bd86004511b064bf230d6",
111 | "https://deno.land/std@0.92.0/async/pool.ts": "876f9e6815366cd017a3b4fbb9e9ae40310b1b6972f1bd541c94358bc11fb7e5",
112 | "https://deno.land/std@0.92.0/bytes/mod.ts": "1ae1ccfe98c4b979f12b015982c7444f81fcb921bea7aa215bf37d84f46e1e13",
113 | "https://deno.land/std@0.92.0/fmt/colors.ts": "db22b314a2ae9430ae7460ce005e0a7130e23ae1c999157e3bb77cf55800f7e4",
114 | "https://deno.land/std@0.92.0/hash/sha1.ts": "1cca324b4b253885a47f121adafcfac55b4cc96113e22b338e1db26f37a730b8",
115 | "https://deno.land/std@0.92.0/http/_io.ts": "bf1331dd3be8aace9120614c1fedc2bb2449edc4779e31b74c0181ea9173f702",
116 | "https://deno.land/std@0.92.0/http/http_status.ts": "ebaa9bebfb8adc3d7b20c49e11037e4eefd79629ad80d81383933f4cdc91b3eb",
117 | "https://deno.land/std@0.92.0/http/server.ts": "d4e17c2aa5a5c65a2d19b9f24483be5f6c2a3e03665996fdf973e53c43091b48",
118 | "https://deno.land/std@0.92.0/io/buffer.ts": "2a92f02c1d8daaebaf13e5678ea5969c89f4fab533f687b9e7e86f49f11c3118",
119 | "https://deno.land/std@0.92.0/io/bufio.ts": "4053ea5d978479be68ae4d73424045a59c6b7a6e8f66727e4bfde516baa07126",
120 | "https://deno.land/std@0.92.0/io/ioutil.ts": "275fa440494df9b4b3aa656301ced2eeac533feec128b3a39b2b40f4cd957e42",
121 | "https://deno.land/std@0.92.0/io/util.ts": "03ca10e063afce551c501505c607ec336a40b9cb72363f5508e2a9ac81096bbf",
122 | "https://deno.land/std@0.92.0/node/_utils.ts": "33b06f2877d3ee80f17190ee81fdc436755ce74b9c2a9a4492c7cdfe2a03e4c6",
123 | "https://deno.land/std@0.92.0/node/events.ts": "0feda0707e2229363f5df8b799fed41bb91de6ca7106f27c7a9f0a02ea11b9d4",
124 | "https://deno.land/std@0.92.0/testing/_diff.ts": "961eaf6d9f5b0a8556c9d835bbc6fa74f5addd7d3b02728ba7936ff93364f7a3",
125 | "https://deno.land/std@0.92.0/testing/asserts.ts": "83889f9a37bdab16cb68b023a15f9172ceb644f62c0e727c72d4870a666e53d6",
126 | "https://deno.land/std@0.92.0/textproto/mod.ts": "1c89b39a079dd158893ab2e9ff79391c66049433d6ca82da7d64b32280570d51",
127 | "https://deno.land/std@0.92.0/ws/mod.ts": "bc521b3066441eb115ac94e3507bcc73098542f81d8d3ce7aad8d837316ce990",
128 | "https://deno.land/x/denoflate@1.2.1/mod.ts": "f5628e44b80b3d80ed525afa2ba0f12408e3849db817d47a883b801f9ce69dd6",
129 | "https://deno.land/x/denoflate@1.2.1/pkg/denoflate.js": "b9f9ad9457d3f12f28b1fb35c555f57443427f74decb403113d67364e4f2caf4",
130 | "https://deno.land/x/denoflate@1.2.1/pkg/denoflate_bg.wasm.js": "d581956245407a2115a3d7e8d85a9641c032940a8e810acbd59ca86afd34d44d",
131 | "https://deno.land/x/esbuild@v0.17.18/mod.d.ts": "dc279a3a46f084484453e617c0cabcd5b8bd1920c0e562e4ea02dfc828c8f968",
132 | "https://deno.land/x/esbuild@v0.17.18/mod.js": "84b5044def8a2e94770b79d295a1dd74f5ee453514c5b4f33571e22e1c882898",
133 | "https://deno.land/x/esbuild@v0.17.18/wasm.d.ts": "dc279a3a46f084484453e617c0cabcd5b8bd1920c0e562e4ea02dfc828c8f968",
134 | "https://deno.land/x/esbuild@v0.17.18/wasm.js": "a9c4998c306807f39199e379904a84a62a64f8962e3341c70f276d618c7cf83d",
135 | "https://deno.land/x/lz4@v0.1.2/mod.ts": "4decfc1a3569d03fd1813bd39128b71c8f082850fe98ecfdde20025772916582",
136 | "https://deno.land/x/lz4@v0.1.2/wasm.js": "b9c65605327ba273f0c76a6dc596ec534d4cda0f0225d7a94ebc606782319e46",
137 | "https://deno.land/x/oak@v12.2.0/application.ts": "d7c712ce36ac5785179b534b055c229a667f4fd6090ec63a82338b94bd490181",
138 | "https://deno.land/x/oak@v12.2.0/body.ts": "c7392f1dae04a360838f43b9cdd2f83d29c1eff4e6071d5f0cf1f3999b1602bc",
139 | "https://deno.land/x/oak@v12.2.0/buf_reader.ts": "7cf96aa0ac670b75098113cf88a291a68332cc45efa8a9698f064ac5b8098a0f",
140 | "https://deno.land/x/oak@v12.2.0/content_disposition.ts": "8b8c3cb2fba7138cd5b7f82fc3b5ea39b33db924a824b28261659db7e164621e",
141 | "https://deno.land/x/oak@v12.2.0/context.ts": "8765e43b0438d4d56110152cadb25e066d378a07dc39db6daf3b9dfaee1e38ed",
142 | "https://deno.land/x/oak@v12.2.0/deps.ts": "14d49d20d95e18b884bbe18d93b3cfa98b2be2bbc65ea24ec8fefbb2e6480b00",
143 | "https://deno.land/x/oak@v12.2.0/etag.ts": "393cb69513b44058ea034571892ec6aa602ec50c45303a24c706d32ef57a3da4",
144 | "https://deno.land/x/oak@v12.2.0/headers.ts": "f50fb05614432bda971021633129aa2e8737e0844e0f01c27a937997b4d8dd4f",
145 | "https://deno.land/x/oak@v12.2.0/helpers.ts": "42212afa07a560b2958359cc19577417e89d9574d6579551a0af36ff7f00cc6e",
146 | "https://deno.land/x/oak@v12.2.0/http_server_native.ts": "0141e1339ed9a33bc26ce537ddab5adbb3542b35916d92de286aed4937e4a6d6",
147 | "https://deno.land/x/oak@v12.2.0/http_server_native_request.ts": "be315d476550e149c58d7ccd2812be30f373ceedc9c323c300eef03b7c071aa9",
148 | "https://deno.land/x/oak@v12.2.0/isMediaType.ts": "62d638abcf837ece3a8f07a4b7ca59794135cb0d4b73194c7d5837efd4161005",
149 | "https://deno.land/x/oak@v12.2.0/mediaTyper.ts": "042b853fc8e9c3f6c628dd389e03ef481552bf07242efc3f8a1af042102a6105",
150 | "https://deno.land/x/oak@v12.2.0/middleware.ts": "de14f045a2ddfe845d89b5d3140ff52cbcc6f3b3965391106ce04480f9786737",
151 | "https://deno.land/x/oak@v12.2.0/middleware/proxy.ts": "b927232f97ec18af4185d7912e45b1191e3ffe24a9c875262ad524211b1274c9",
152 | "https://deno.land/x/oak@v12.2.0/mod.ts": "c3bb5eec0fa11cf6dc9e0bf3543564a4ee1f3d152ceb675b383a5ee5bc37ce15",
153 | "https://deno.land/x/oak@v12.2.0/multipart.ts": "98fe9f226de8c26a16d067027b69fb1e34ad8c4055767dd157907d06cea36f9a",
154 | "https://deno.land/x/oak@v12.2.0/range.ts": "68a6df7ab3b868843e33f52deb94c3d4cab25cb9ef369691990c2ac15b04fafb",
155 | "https://deno.land/x/oak@v12.2.0/request.ts": "5852ad36389b48e0428a6f3c90854d01f10d1b15949b56001e1e75c2a00ef0f9",
156 | "https://deno.land/x/oak@v12.2.0/response.ts": "867d81f7eb0c65c7b8e0e0e9e145ededd5b6daa9ad922e6adc6a36a525f439a6",
157 | "https://deno.land/x/oak@v12.2.0/router.ts": "5b266091e55f634c9130e6de5dd331ddfc4c190ee7916a25e0a0f75502edbc32",
158 | "https://deno.land/x/oak@v12.2.0/send.ts": "5ec49f106294593f468317a0c885da4f3274bf6d0fe9e16a7304391730b4f4fb",
159 | "https://deno.land/x/oak@v12.2.0/structured_clone.ts": "9c2d21c62f616400305a60cbd29eb06764ee97edc423223424b6cf55df0e8be2",
160 | "https://deno.land/x/oak@v12.2.0/testing.ts": "a0be5c84981afde666de29630f34b09d944ca1a2fe6a5185644b60ad95e16d18",
161 | "https://deno.land/x/oak@v12.2.0/types.d.ts": "41951a18c3bfdb11e40707cab75da078ba8a4907cd7d4e11d8536bc2db0dde05",
162 | "https://deno.land/x/oak@v12.2.0/util.ts": "3af8c4ed04c6cc2bedbe66e562a77fc59c72df31c55a902a63885861ca1639d6",
163 | "https://deno.land/x/outdent@v0.8.0/mod.ts": "72630e680dcc36d5ae556fbff6900b12706c81a6fd592345fc98bcc0878fb3ca",
164 | "https://deno.land/x/outdent@v0.8.0/src/index.ts": "6dc3df4108d5d6fedcdb974844d321037ca81eaaa16be6073235ff3268841a22",
165 | "https://deno.land/x/path_to_regexp@v6.2.1/index.ts": "894060567837bae8fc9c5cbd4d0a05e9024672083d5883b525c031eea940e556",
166 | "https://deno.land/x/swc@0.2.1/lib/deno_swc.generated.js": "a112eb5a4871bcbd5b660d3fd9d7afdd5cf2bb9e135eb9b04aceaa24d16481a0",
167 | "https://deno.land/x/swc@0.2.1/mod.ts": "9e03f5daf4c2a25208e34e19ce3e997d2e23a5a11ee8c8ed58023b0e3626073f",
168 | "https://deno.land/x/websocket@v0.1.4/deps.ts": "1299887e27e09209d2eedc5c3b6552090842fac9e08f00dd235de39219efbd42",
169 | "https://deno.land/x/websocket@v0.1.4/lib/errors.ts": "efb2251dac1ac2036809b0bfb520d737aee70e362eba1e287c84d5299fb211e8",
170 | "https://deno.land/x/websocket@v0.1.4/lib/websocket.ts": "3363e0a8f59284de5439f5f3bd8831b65a2cfba047113fbc72e32934ebec3208",
171 | "https://deno.land/x/websocket@v0.1.4/mod.ts": "6b24e14dd1c5d64c1671dd678ca545cce9e2ebc91f67e8be864069808b948267",
172 | "https://esm.sh/react-dom@18.3.0-next-3706edb81-20230308&dev/server": "1e9eb7b3cf175b6b1b9037fd25f0eb5648033e477942bdf29c3aecb9e8435707",
173 | "https://esm.sh/react-server-dom-webpack@0.0.0-experimental-41b4714f1-20230328&dev/server.browser": "cd68aa8976c461804b726dc85bee5f7b967393866d6e90b7ef9987cc304715d4",
174 | "https://esm.sh/react@18.3.0-next-3706edb81-20230308&dev": "bcbe62fd564188140a63f168235a210bfaed3b0148539a091cf5b8ff0a22d6be",
175 | "https://esm.sh/react@18.3.0-next-3706edb81-20230308&dev/jsx-runtime": "b0bc11e3e58213f61b87857e8c406dbb0ee19f00ccd6b8118acb938f9033f376",
176 | "https://esm.sh/stable/react@0.0.0-experimental-41b4714f1-20230328/deno/react.development.mjs": "5908521fa018a12a70e58214083149801679282a0b37a5eb1cfbc1c2f513c35d",
177 | "https://esm.sh/stable/react@18.3.0-next-3706edb81-20230308/deno/jsx-runtime.development.js": "277162082c947ae0a3bfd9a2378b0b185c07c15d6d269e6c4c961d2acba67701",
178 | "https://esm.sh/stable/react@18.3.0-next-3706edb81-20230308/deno/react.development.mjs": "d255572e1f7f68d47f7d46792f7f5439466f9a8b577595792a76aecc3e5f7eeb",
179 | "https://esm.sh/v117/@swc/core@1.2.212/types.d.ts": "57ab440b40e26a05350f1e06d3977742c249ab20ea46b9103dad876d5d4e5dd5",
180 | "https://esm.sh/v117/@types/prop-types@15.7.5/index.d.ts": "6a386ff939f180ae8ef064699d8b7b6e62bc2731a62d7fbf5e02589383838dea",
181 | "https://esm.sh/v117/@types/react-dom@18.0.11/server~.d.ts": "c3ec76f1b085f2a1e7f722a768c8f3a082f8623b934c48f864ed67810f70b8e6",
182 | "https://esm.sh/v117/@types/react@18.0.37/global.d.ts": "49a253ec027e56c55c7450a0c331cfe96212b3d1cc215b1710ba94a083404cf3",
183 | "https://esm.sh/v117/@types/react@18.0.37/index.d.ts": "f901ba0415ae35c8f062758b2a849fe3aef2ce219323d2495114232c3afe9227",
184 | "https://esm.sh/v117/@types/react@18.0.37/jsx-runtime~.d.ts": "7efb28f322f647d652575b99e1d9cda1af17e93bdf6f649cc5d0a8527fc75324",
185 | "https://esm.sh/v117/@types/scheduler@0.16.3/tracing.d.ts": "f5a8b384f182b3851cec3596ccc96cb7464f8d3469f48c74bf2befb782a19de5",
186 | "https://esm.sh/v117/csstype@3.1.2/index.d.ts": "4c68749a564a6facdf675416d75789ee5a557afda8960e0803cf6711fa569288",
187 | "https://esm.sh/v117/react-dom@18.3.0-next-3706edb81-20230308/deno/react-dom.development.mjs": "ccaac87ffd09a35297f6a509f2f84553c5701e8e916e9c34eaee8f6af7a88821",
188 | "https://esm.sh/v117/react-dom@18.3.0-next-3706edb81-20230308/deno/server.development.js": "04d6e28065a9c6f57a1fd041866e7535a040cf7532673f0e0cd0b0884bb0469e",
189 | "https://esm.sh/v117/react-server-dom-webpack@0.0.0-experimental-41b4714f1-20230328/deno/server.browser.development.js": "4cd829fe16665c4626290fbb3ac11fa2cf9145cf3364a592cea771c4643c0c58",
190 | "https://esm.sh/v117/scheduler@0.24.0-next-3706edb81-20230308/deno/scheduler.development.mjs": "7afbda728264fb8520adc33435572dc38b8a741c47677fe6e483d58fce13d505"
191 | },
192 | "npm": {
193 | "specifiers": {
194 | "nanoid": "nanoid@4.0.2"
195 | },
196 | "packages": {
197 | "nanoid@4.0.2": {
198 | "integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==",
199 | "dependencies": {}
200 | }
201 | }
202 | }
203 | }
204 |
--------------------------------------------------------------------------------