Skip to content

Commit e9ce789

Browse files
committed
rsc initial pass
1 parent 84cb944 commit e9ce789

File tree

22 files changed

+681
-18
lines changed

22 files changed

+681
-18
lines changed

core/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ resources/public/benchmark.js
88
target
99
.rebel_readline_history
1010
out
11+
rsc-out
1112
index.html
1213
node_modules
1314
server_render_test

core/deps.edn

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
com.adamrenklint/preo {:mvn/version "0.1.0"}}
44
:paths ["src" "resources"]
55
:aliases {:dev {:extra-deps {cljfmt/cljfmt {:mvn/version "0.8.0"}
6-
clj-kondo/clj-kondo {:mvn/version "2025.02.20"}}}
6+
clj-kondo/clj-kondo {:mvn/version "2025.02.20"}
7+
org.clojure/clojurescript {:mvn/version "1.11.60"}
8+
uix.dom/uix.dom {:local/root "../dom"}}}
79
:benchmark {:extra-paths ["benchmark"]
810
:extra-deps {reagent/reagent {:mvn/version "1.2.0"}
911
lilactown/helix {:mvn/version "0.1.10"}}}
1012

1113
:test {:extra-paths ["test" "dev"]
12-
:extra-deps {org.clojure/clojure {:mvn/version "1.11.1"}
14+
:extra-deps {org.clojure/clojure {:mvn/version "1.12.0"}
1315
org.clojure/clojurescript {:mvn/version "1.11.60"}
1416
thheller/shadow-cljs {:mvn/version "2.28.20"}
1517
uix.dom/uix.dom {:local/root "../dom"}
@@ -22,7 +24,11 @@
2224
:main-opts ["-m" "release"]}
2325

2426
:examples {:extra-paths ["dev"]
25-
:extra-deps {org.clojure/clojure {:mvn/version "1.11.1"}
27+
:extra-deps {org.clojure/clojure {:mvn/version "1.12.0"}
2628
org.clojure/clojurescript {:mvn/version "1.11.60"}
2729
thheller/shadow-cljs {:mvn/version "2.28.20"}
30+
metosin/reitit {:mvn/version "0.8.0"}
31+
org.apache.commons/commons-text {:mvn/version "1.13.1"}
32+
http-kit/http-kit {:mvn/version "2.8.0"}
33+
compojure/compojure {:mvn/version "1.7.1"}
2834
uix.dom/uix.dom {:local/root "../dom"}}}}}

core/dev/preo/core.cljs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
(ns preo.core)
2+
3+
(defn arg! [& args])
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
(ns uix.rsc-example.actions
2+
(:require [uix.rsc :refer [defaction]]))
3+
4+
(defaction vote [id score]
5+
;; todo: quick db,sqlite example
6+
(inc score))
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
(ns uix.rsc-example.client.root
2+
(:require [uix.dom.rsc :as dom.rsc]
3+
[uix.rsc-example.routes :refer [routes]]
4+
[uix.rsc-example.client.ui]))
5+
6+
(defn init []
7+
(dom.rsc/render-root
8+
{:container (js/document.getElementById "root")
9+
:routes routes
10+
:rsc-endpoint "/rsc"
11+
:server-actions-endpoint "/api"}))
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
(ns uix.rsc-example.client.ui
2+
(:require [uix.core :refer [defui $] :as uix]
3+
[uix.rsc-example.actions :as actions]))
4+
5+
;; ^:client turns client component into a client ref
6+
;; when the component is used in server components tree
7+
(defui ^:client vote-btn [{:keys [id score]}]
8+
(let [[score set-score] (uix/use-state score)
9+
vote #(-> (actions/vote id score)
10+
(.then set-score))]
11+
($ :button {:on-click vote
12+
:style {:text-decoration :underline
13+
:cursor :pointer}}
14+
(str "Vote " score))))
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
(ns uix.rsc-example.routes
2+
(:require [uix.rsc :refer [defroutes]]
3+
#?(:clj [uix.rsc-example.server.ui :as ui])))
4+
5+
(defroutes routes
6+
[["/" {:component ui/stories :title "new"}]
7+
["/askstories" {:component ui/stories :title "ask"}]
8+
["/showstories" {:component ui/stories :title "show"}]
9+
["/jobstories" {:component ui/stories :title "job"}]
10+
["/topstories" {:component ui/stories :title "top"}]
11+
["/beststories" {:component ui/stories :title "best"}]
12+
["/item/:id" {:component ui/item}]])
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
(ns uix.rsc-example.server.core
2+
(:require [clojure.edn :as edn]
3+
[clojure.java.io :as io]
4+
[uix.core :refer [defui $] :as uix]
5+
[uix.rsc :as rsc]
6+
[org.httpkit.server :as server]
7+
[compojure.core :refer [defroutes GET POST]]
8+
[compojure.route :as route]
9+
[ring.util.response :as resp]
10+
[reitit.core :as r]
11+
[uix.rsc-example.server.root :refer [root]]
12+
[uix.rsc-example.routes :refer [routes]])
13+
(:import (java.io PushbackReader))
14+
(:gen-class))
15+
16+
(defn read-end-stream [body]
17+
(with-open [reader (io/reader body)]
18+
(edn/read (PushbackReader. reader))))
19+
20+
(def router
21+
(r/router routes))
22+
23+
(defn handler [request]
24+
(let [{:keys [route]} (read-end-stream (:body request))
25+
route (r/match-by-path router (:path route))]
26+
;; request -> route -> react flight rows -> response stream
27+
(server/as-channel request
28+
{:on-open (fn [ch]
29+
(let [on-chunk (fn [chunk]
30+
(if (= chunk :done)
31+
(server/close ch)
32+
(server/send! ch chunk false)))]
33+
(rsc/render-to-flight-stream ($ root {:route route})
34+
{:on-chunk on-chunk})))})))
35+
36+
(defroutes server-routes*
37+
;; react flight payload endpoint
38+
(POST "/rsc" req
39+
(handler req))
40+
;; server actions endpoint
41+
(POST "/api" {body :body}
42+
(try
43+
(-> (rsc/handle-action (read-end-stream body))
44+
str
45+
(resp/response)
46+
(resp/header "Content-Type" "text/edn"))
47+
(catch Exception e
48+
(-> (resp/bad-request (ex-message e))
49+
(resp/header "Content-Type" "text/edn")))))
50+
;; static assets
51+
(route/files "/" {:root "./"})
52+
;; always serving index.html instead of 404
53+
(GET "/*" req
54+
;; todo: render flight payload into html on initial load
55+
(-> (resp/file-response "index.html" {:root "./"})
56+
(resp/header "Content-Type" "text/html"))))
57+
58+
(defn start-server []
59+
(server/run-server #'server-routes* {:port 8080})
60+
(println "Server is listening at http://localhost:8080"))
61+
62+
(defn -main [& args]
63+
(start-server))
64+
65+
(comment
66+
(def stop-server (start-server))
67+
(stop-server))
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
(ns uix.rsc-example.server.root
2+
(:require [uix.core :refer [defui $] :as uix]
3+
[uix.rsc :as rsc]
4+
[uix.rsc-example.routes :refer [routes]]))
5+
6+
(defui root [{:keys [route]}]
7+
(let [{:keys [path path-params data]} route
8+
{:keys [component]} data]
9+
($ :div.flex.flex-col.items-center
10+
($ :ul.flex.gap-2.text-sm.py-1.font-medium
11+
(for [[route-path {:keys [title]}] routes
12+
:when title]
13+
($ :li {:key route-path}
14+
($ rsc/link
15+
{:href route-path
16+
:class (if (= route-path path)
17+
"text-emerald-500"
18+
"text-stone-800")}
19+
title))))
20+
($ :div.max-w-128
21+
($ component {:path path :params path-params})))))
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
(ns uix.rsc-example.server.services
2+
(:require [org.httpkit.client :as http]
3+
[cheshire.core :as json]))
4+
5+
(defn fetch-json [path]
6+
(-> @(http/get path)
7+
:body
8+
(json/parse-string keyword)))
9+
10+
(defn fetch-story [id]
11+
(fetch-json (str "https://hacker-news.firebaseio.com/v0/item/" id ".json")))
12+
13+
(defn fetch-stories [pathname]
14+
(let [pathname (if (= pathname "/")
15+
"/newstories"
16+
pathname)]
17+
(->> (fetch-json (str "https://hacker-news.firebaseio.com/v0/" pathname ".json"))
18+
(take 10)
19+
(pmap fetch-story))))
20+
21+
(defn fetch-item [id]
22+
(-> (fetch-story id)
23+
(update :kids #(pmap fetch-item %))))

0 commit comments

Comments
 (0)