From d9cab25aae62029557cff3346e7bd06e878999cd Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 12 Nov 2024 15:51:58 +0100 Subject: [PATCH] Fix server side rendering (#730) Fixes #706. --- .github/workflows/main.yml | 3 ++- src/nextjournal/clerk/builder.clj | 36 ++++++++++++++++++------- src/nextjournal/clerk/render.cljs | 3 ++- src/nextjournal/clerk/render/hooks.cljs | 4 ++- src/nextjournal/clerk/sci_env.cljs | 1 + ui_tests/playwright_tests.cljs | 12 ++++++--- 6 files changed, 43 insertions(+), 16 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e953a10e3..8e65c7d6e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -269,7 +269,8 @@ jobs: - name: Run Playwright tests against static assets run: | bb test:static-app :sha ${{ github.sha }} :skip-install true - bb test:static-app :skip-install true :url https://snapshots.nextjournal.com/clerk/book/${{ github.sha }}/book/index.html :index false :selector "span:has-text(\"Book of Clerk\")" + bb test:static-app :skip-install true :url https://snapshots.nextjournal.com/clerk/book/${{ github.sha }}/book/index.html :index false :selector "h1:has-text(\"Book of Clerk\")" + bb test:static-app :skip-install true :url https://snapshots.nextjournal.com/clerk-ssr/build/${{ github.sha }}/index.html :index false :selector "h1:has-text(\"Rule 30\")" deploy: needs: [build-and-upload-viewer-resources, test] diff --git a/src/nextjournal/clerk/builder.clj b/src/nextjournal/clerk/builder.clj index 5ecb15ec9..7b58de6a9 100644 --- a/src/nextjournal/clerk/builder.clj +++ b/src/nextjournal/clerk/builder.clj @@ -163,18 +163,36 @@ :path->doc path->doc :paths (vec (keys path->doc))))) +(defn- node-ssr! + [{:keys [viewer-js state] + :or {viewer-js + ;; for local REPL testing + "./public/js/viewer.js"}}] + (sh {:in (str "import '" viewer-js "';" + "globalThis.CLERK_SSR = true;" + "console.log(nextjournal.clerk.sci_env.ssr(" (pr-str (pr-str state)) "))")} + "node" + "--abort-on-uncaught-exception" + "--experimental-network-imports" + "--input-type=module" + "--trace-warnings")) + +(comment + (declare so) ;; captured in REPL in ssr! function + (node-ssr! {:state so}) + ) + (defn ssr! "Shells out to node to generate server-side-rendered html." [{:as static-app-opts :keys [report-fn resource->url]}] (report-fn {:stage :ssr}) - (let [{duration :time-ms :keys [result]} - (eval/time-ms (sh {:in (str "import '" (resource->url "/js/viewer.js") "';" - "console.log(nextjournal.clerk.sci_env.ssr(" (pr-str (pr-str static-app-opts)) "))")} - "node" - "--abort-on-uncaught-exception" - "--experimental-network-imports" - "--input-type=module" - "--trace-warnings")) + (let [doc (get (:path->doc static-app-opts) (:current-path static-app-opts)) + static-app-opts (-> (assoc static-app-opts :doc doc) + (dissoc :path->doc) + (assoc :render-router :serve)) + {duration :time-ms :keys [result]} + (eval/time-ms (node-ssr! {:viewer-js (resource->url "/js/viewer.js") + :state static-app-opts})) {:keys [out err exit]} result] (if (= 0 exit) (do @@ -203,9 +221,9 @@ (spit (fs/file out-path (str (or (not-empty path) "index") ".edn")) (viewer/->edn doc)) (spit out-html (view/->html (-> static-app-opts - (dissoc :path->doc) (assoc :current-path path) (cond-> ssr? ssr!) + (dissoc :path->doc) cleanup)))))) (when browse? (browse/browse-url (if-let [server-url (and (= out-path "public/build") (webserver/server-url))] diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index 831176c0d..59a60ae51 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -847,7 +847,8 @@ :status (.-status r) :headers (.-headers r)}))))) (then read-response+show-progress) - (then (fn [edn] (set-state! {:doc (read-string edn)}) {:ok true})) + (then (fn [edn] + (set-state! {:doc (read-string edn)}) {:ok true})) (catch (fn [e] (js/console.error "Fetch failed" e) (set-state! {:doc {:nextjournal/viewer {:render-fn (constantly [:<>])} ;; FIXME: make :error top level on state :nextjournal/value {:error (viewer/present e)}}}) diff --git a/src/nextjournal/clerk/render/hooks.cljs b/src/nextjournal/clerk/render/hooks.cljs index 0a7844c9a..0d372f1fa 100644 --- a/src/nextjournal/clerk/render/hooks.cljs +++ b/src/nextjournal/clerk/render/hooks.cljs @@ -75,7 +75,9 @@ (defn use-ref "React hook: useRef. Can also be used like an atom." ([] (use-ref nil)) - ([init] (specify-atom! (react/useRef init)))) + ([init] (if (unchecked-get js/globalThis "CLERK_SSR") + (atom init) + (specify-atom! (react/useRef init))))) (defn ^:private eval-fn "Invoke (f x) if f is a function, otherwise return f" diff --git a/src/nextjournal/clerk/sci_env.cljs b/src/nextjournal/clerk/sci_env.cljs index 4c8f4ac81..dbe7b3855 100644 --- a/src/nextjournal/clerk/sci_env.cljs +++ b/src/nextjournal/clerk/sci_env.cljs @@ -15,6 +15,7 @@ [applied-science.js-interop :as j] [cljs.math] [cljs.reader] + [cljs.repl] [clojure.string :as str] [edamame.core :as edamame] [goog.object] diff --git a/ui_tests/playwright_tests.cljs b/ui_tests/playwright_tests.cljs index f08434343..03883fa93 100644 --- a/ui_tests/playwright_tests.cljs +++ b/ui_tests/playwright_tests.cljs @@ -52,16 +52,20 @@ (p/do (goto page url) (.waitForLoadState page "networkidle") (p/let [selector (or (:selector @!opts) "div") - loc (.locator page selector) - loc (.first loc #js {:timeout 10000})] - (is (.isVisible loc #js {:timeout 10000}))))) + _ (prn :selector selector) + loc (.locator page selector #js {:timeout 10000}) + loc (.first loc #js {:timeout 10000}) + _ (.waitFor loc #js {:state "visible"}) + visible? (.isVisible loc)] + (is visible?)))) ([page url link] (p/let [txt (.innerText link)] (println "Visiting" (str url "#/" txt)) (p/do (.click link) (p/let [loc (.locator page "div") loc (.first loc #js {:timeout 10000}) - visible? (.isVisible loc #js {:timeout 10000})] + _ (.waitFor loc #js {:state "visible"}) + visible? (.isVisible loc)] (is visible?)))))) (deftest index-page-test