├── .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 | {album.title} 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 | 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 | --------------------------------------------------------------------------------