diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index 7eb679f6f..42ed11e51 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -7,7 +7,8 @@ taoensso.encore/defonce clojure.core/defonce taoensso.encore/defalias clojure.core/def promesa.core/let clojure.core/let - shadow.cljs.modern/defclass clojure.core/defprotocol} + shadow.cljs.modern/defclass clojure.core/defprotocol + nextjournal.clerk.utils/if-bb clojure.core/if} :linters {:clojure-lsp/unused-public-var {:level :off} :consistent-alias {:aliases {datomic.api datomic clojure.string str diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 35901968d..663b8b1be 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -71,7 +71,7 @@ jobs: - { os: windows-latest, shell: powershell } clojure: - 1.10.3 - - 1.12.0 + - 1.12.1 defaults: run: shell: ${{matrix.sys.shell}} @@ -105,6 +105,53 @@ jobs: bb test:clj :kaocha/reporter '[kaocha.report/documentation]' \ :clojure '"${{ matrix.clojure }}"' + test-bb: + runs-on: ${{matrix.sys.os}} + + strategy: + matrix: + sys: + - { os: macos-latest, shell: bash } + - { os: ubuntu-latest, shell: bash } + - { os: windows-latest, shell: powershell } + defaults: + run: + shell: ${{matrix.sys.shell}} + + steps: + - uses: actions/checkout@v2 + + - name: 🔧 Install java + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: 🔧 Install clojure + uses: DeLaGuardo/setup-clojure@master + with: + bb: latest + + - name: 🗝 maven cache + uses: actions/cache@v3 + with: + path: | + ~/.m2 + ~/.gitlibs + ~/.deps.clj + key: ${{ runner.os }}-maven-test-${{ hashFiles('deps.edn') }}-${{ hashFiles('bb.edn') }} + - name: 🔧 Install bb from dev build (until next release) + shell: bash + run: | + curl -sLO https://raw.githubusercontent.com/babashka/babashka/master/install + chmod +x ./install + ./install --dev-build --dir /tmp + cp /tmp/bb "$(which bb)" + - name: 🧪 Run tests + shell: bash + run: | + bb test:bb + static-build: runs-on: ubuntu-latest needs: [build-and-upload-viewer-resources] diff --git a/CHANGELOG.md b/CHANGELOG.md index af91dae17..4ce953444 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ Changes can be: ## Unreleased +* 🌟 Add compatibility with Babashka + + Currently requires a dev build of Babashka ([6b89b78](https://github.com/babashka/babashka/commit/6b89b78c38e72d296f211e024d4079bf1504f3e4) + or newer). + * 🕵🏻 New dependency analyzer implementation Based on the analyzer from [hyperfiddle/rfc](https://github.com/hyperfiddle/rcf). This replaces `tools.analyzer` and drops the dependency. diff --git a/bb.edn b/bb.edn index 2fcb7d934..7e3140451 100644 --- a/bb.edn +++ b/bb.edn @@ -1,9 +1,7 @@ -{:min-bb-version "0.9.159" +{:min-bb-version "1.12.202" :paths ["bb"] - :deps {io.github.nextjournal/dejavu {:git/sha "4980e0cc18c9b09fb220874ace94ba6b57a749ca"} - io.github.nextjournal/cas-client {:git/sha "d9f838937ebc8b645fe5764949e72a6df8e344de"} - mvxcvi/multiformats {:git/url "https://github.com/greglook/clj-multiformats" - :git/sha "1189f1fb26db180cd8dcfd50518cdf553c0ff9e1"}} + :deps {io.github.nextjournal/dejavu {:git/sha "7276cd9cec1bad001d595b52cee9e83a60d43bf0"} + io.github.nextjournal/cas-client {:git/sha "114d3d88d38a2068ec844b0e6c808eaaa6aa64ef"}} :tasks {:requires ([tasks :as t] @@ -56,6 +54,22 @@ clojure (str ":" clojure-version)) *command-line-args*))} + test:bb {:doc "Run babashka tests" + :extra-paths ["test"] + :extra-deps {io.github.nextjournal/clerk {:local/root "."} + nubank/matcher-combinators {:mvn/version "3.5.1"} + org.clojure/test.check {:mvn/version "1.1.1"} + io.github.cognitect-labs/test-runner {:git/tag "v0.5.1" :git/sha "dfb30dd"}} + :task cognitect.test-runner/-main} + + dev:bb {:extra-deps {io.github.nextjournal/clerk {:local/root "."}} + :extra-paths ["notebooks" "resources" "test"] + :task (exec 'tasks/bb-nrepl)} + + build:bb {:extra-deps {io.github.nextjournal/clerk {:local/root "."}} + :extra-paths ["notebooks" "resources" "test"] + :task (exec 'nextjournal.clerk/build!)} + playwright:version {:doc "Print used playwright version from ui_tests/yarn.json" :task (print (->> (babashka.process/shell {:out :string} "grep -E 'playwright-core \"(.*)\"' ui_tests/yarn.lock") :out diff --git a/bb/tasks.clj b/bb/tasks.clj index 8243cc4d8..ab66998d0 100644 --- a/bb/tasks.clj +++ b/bb/tasks.clj @@ -70,3 +70,14 @@ (defn version [] (shared/version)) + +(defn bb-nrepl [args] + (System/setProperty "clerk.resource_manifest" (str {"/js/viewer.js" "/js/viewer.js"})) + ((requiring-resolve 'babashka.nrepl.server/start-server!) {:host "localhost" :port 1339}) + (spit ".nrepl-port" "1339") + ((requiring-resolve 'nextjournal.clerk/serve!) args) + (-> (Runtime/getRuntime) + (.addShutdownHook (Thread. (fn [] + ((requiring-resolve 'nextjournal.clerk/halt!)) + (fs/delete ".nrepl-port"))))) + (deref (promise))) diff --git a/deps.edn b/deps.edn index f44fea67d..ed57e3cdb 100644 --- a/deps.edn +++ b/deps.edn @@ -1,19 +1,20 @@ {:paths ["src" "resources" "bb"] - :deps {org.clojure/clojure {:mvn/version "1.10.3"} + :deps {org.clojure/clojure {:mvn/version "1.11.1"} org.clojure/java.classpath {:mvn/version "1.0.0"} babashka/fs {:mvn/version "0.5.22"} borkdude/edamame {:mvn/version "1.4.28"} weavejester/dependency {:mvn/version "0.2.1"} com.nextjournal/beholder {:mvn/version "1.0.2"} org.flatland/ordered {:mvn/version "1.15.12"} - io.github.nextjournal/markdown {:mvn/version "0.7.184"} + io.github.nextjournal/markdown {:mvn/version "0.7.186"} babashka/process {:mvn/version "0.4.16"} - io.github.nextjournal/dejavu {:git/sha "4980e0cc18c9b09fb220874ace94ba6b57a749ca"} + io.github.nextjournal/dejavu {:git/sha "7276cd9cec1bad001d595b52cee9e83a60d43bf0"} io.github.babashka/sci.nrepl {:mvn/version "0.0.2"} com.taoensso/nippy {:mvn/version "3.4.2"} - mvxcvi/multiformats {:mvn/version "0.3.107"} + mvxcvi/multiformats {:mvn/version "1.0.125"} + mvxcvi/alphabase {:mvn/version "3.0.185"} com.pngencoder/pngencoder {:mvn/version "0.13.1"} http-kit/http-kit {:mvn/version "2.8.0"} @@ -41,7 +42,7 @@ cider/cider-nrepl {:mvn/version "0.55.7"} nrepl/nrepl {:mvn/version "1.3.1"} com.clojure-goes-fast/clj-async-profiler {:mvn/version "1.3.0"} - io.github.nextjournal/cas-client {:git/sha "d9f838937ebc8b645fe5764949e72a6df8e344de"} + io.github.nextjournal/cas-client {:git/sha "114d3d88d38a2068ec844b0e6c808eaaa6aa64ef"} org.slf4j/slf4j-nop {:mvn/version "2.0.7"} org.babashka/cli {:mvn/version "0.5.40"} org.clojure/data.int-map {:mvn/version "1.3.0"}} @@ -93,7 +94,7 @@ :nextjournal.garden/aliases [:demo]} :build {:deps {io.github.nextjournal/clerk {:local/root "."} - io.github.nextjournal/cas-client {:git/sha "d9f838937ebc8b645fe5764949e72a6df8e344de"} + io.github.nextjournal/cas-client {:git/sha "114d3d88d38a2068ec844b0e6c808eaaa6aa64ef"} io.github.clojure/tools.build {:git/tag "v0.10.3" :git/sha "15ead66"} io.github.slipset/deps-deploy {:git/sha "b4359c5d67ca002d9ed0c4b41b710d7e5a82e3bf"}} :extra-paths ["bb" "src" "resources"] ;; for loading lookup-url in build diff --git a/notebooks/babashka.clj b/notebooks/babashka.clj new file mode 100644 index 000000000..c00464709 --- /dev/null +++ b/notebooks/babashka.clj @@ -0,0 +1,31 @@ +(ns babashka + ; {:nextjournal.clerk/no-cache true} + (:require [babashka.fs :as fs] + [nextjournal.clerk :as clerk])) + +(System/getProperty "babashka.version") + +*file* + +(fs/exists? *file*) + +;;;; + +;;;; dude + +(+ 1 2 3) + +(prn :dude) + +java.io.File + +(def x 2) + +(prn x) + +(clerk/with-viewer + { + :render-fn '(fn [_] + [:div {:style {:color "green"}} + "Hello"])} + nil) diff --git a/src/nextjournal/beholder.bb b/src/nextjournal/beholder.bb new file mode 100644 index 000000000..e6d96e0c9 --- /dev/null +++ b/src/nextjournal/beholder.bb @@ -0,0 +1,5 @@ +(ns nextjournal.beholder + "Babashka runtime no-op stubs") + +(defn watch [cb & args] nil) +(defn stop [w] nil) diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index 57efebd7c..0a63cfd85 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -662,3 +662,7 @@ (clear-cache!) (halt!) ) + +(comment + (with-cache 1) + ) diff --git a/src/nextjournal/clerk/always_array_map.clj b/src/nextjournal/clerk/always_array_map.clj index 908aed0e0..3583ddcd1 100644 --- a/src/nextjournal/clerk/always_array_map.clj +++ b/src/nextjournal/clerk/always_array_map.clj @@ -1,6 +1,7 @@ (ns nextjournal.clerk.always-array-map "A persistent data structure that is based on array-map, but doesn't turn into a hash-map by using assoc etc. - Prints like a normal Clojure map in the order of insertion.") + Prints like a normal Clojure map in the order of insertion." + (:require [nextjournal.clerk.utils :as utils])) (set! *warn-on-reflection* true) @@ -9,52 +10,79 @@ (declare ->AlwaysArrayMap) -(deftype AlwaysArrayMap [^clojure.lang.PersistentArrayMap the-map] - clojure.lang.ILookup - (valAt [_ k] - (get the-map k)) - - clojure.lang.Seqable - (seq [_] - (seq the-map)) - - clojure.lang.IPersistentMap - (assoc [_ k v] - (if (< (count the-map) 8) - (->AlwaysArrayMap (assoc the-map k v)) - (->AlwaysArrayMap (assoc-after the-map k v)))) - - (assocEx [_ _k _v] - (throw (ex-info "Not implemented" {}))) - - (without [_ k] - (->AlwaysArrayMap (dissoc the-map k))) - - clojure.lang.Associative - (containsKey [_ k] - (contains? the-map k)) - - clojure.lang.IPersistentCollection - (equiv [_ other] - (= the-map other)) - (count [_] - (count the-map)) - - java.lang.Iterable - (iterator [_] - (.iterator the-map)) - - clojure.lang.IMeta - (meta [_] - (meta the-map)) - - clojure.lang.IObj - (withMeta [_ meta] - (->AlwaysArrayMap (with-meta the-map meta))) - - Object - (toString [_] - "")) +(utils/if-bb + (defn ->AlwaysArrayMap [m] + (proxy [clojure.lang.APersistentMap clojure.lang.IMeta clojure.lang.IObj] + [] + (valAt + ([k] + (get m k)) + ([k default-value] + (get m k default-value))) + (iterator [] + (.iterator ^java.lang.Iterable m)) + + (containsKey [k] (contains? m k)) + (entryAt [k] (when (contains? m k) + (get this k))) + (equiv [other] (= m other)) + (empty [] (empty m)) + (count [] (count m)) + (assoc [k v] (if (< (count m) 8) + (->AlwaysArrayMap (assoc m k v)) + (->AlwaysArrayMap (assoc-after m k v)))) + (without [k] (->AlwaysArrayMap (dissoc m k))) + (seq [] (seq m)) + ; a lot of map users expect meta to work + (meta [] (meta m)) + (withMeta [meta] (->AlwaysArrayMap (with-meta m meta))))) + + (deftype AlwaysArrayMap [^clojure.lang.PersistentArrayMap the-map] + clojure.lang.ILookup + (valAt [_ k] + (get the-map k)) + + clojure.lang.Seqable + (seq [_] + (seq the-map)) + + clojure.lang.IPersistentMap + (assoc [_ k v] + (if (< (count the-map) 8) + (->AlwaysArrayMap (assoc the-map k v)) + (->AlwaysArrayMap (assoc-after the-map k v)))) + + (assocEx [_ _k _v] + (throw (ex-info "Not implemented" {}))) + + (without [_ k] + (->AlwaysArrayMap (dissoc the-map k))) + + clojure.lang.Associative + (containsKey [_ k] + (contains? the-map k)) + + clojure.lang.IPersistentCollection + (equiv [_ other] + (= the-map other)) + (count [_] + (count the-map)) + + java.lang.Iterable + (iterator [_] + (.iterator the-map)) + + clojure.lang.IMeta + (meta [_] + (meta the-map)) + + clojure.lang.IObj + (withMeta [_ meta] + (->AlwaysArrayMap (with-meta the-map meta))) + + Object + (toString [_] + ""))) (defn assoc-before [aam k v] (->AlwaysArrayMap (apply array-map (list* k v (interleave (keys aam) (vals aam)))))) @@ -62,19 +90,20 @@ (defn always-array-map [& kvs] (->AlwaysArrayMap (apply array-map kvs))) -(defmethod print-method AlwaysArrayMap - [v ^java.io.Writer writer] - (.write writer "{") - (let [write-kv! (fn [k v] - (.write writer (pr-str k)) - (.write writer " ") - (.write writer (pr-str v)))] - (doseq [[k v] (butlast v)] - (write-kv! k v) - (.write writer ", ")) - (let [[k v] (last v)] - (write-kv! k v))) - (.write writer "}")) +(utils/when-not-bb + (defmethod print-method AlwaysArrayMap + [v ^java.io.Writer writer] + (.write writer "{") + (let [write-kv! (fn [k v] + (.write writer (pr-str k)) + (.write writer " ") + (.write writer (pr-str v)))] + (doseq [[k v] (butlast v)] + (write-kv! k v) + (.write writer ", ")) + (let [[k v] (last v)] + (write-kv! k v))) + (.write writer "}"))) (comment (pr-str (always-array-map 1 2)) diff --git a/src/nextjournal/clerk/analyzer.clj b/src/nextjournal/clerk/analyzer.clj index 1e6ff5b88..c8193034c 100644 --- a/src/nextjournal/clerk/analyzer.clj +++ b/src/nextjournal/clerk/analyzer.clj @@ -6,16 +6,18 @@ [clojure.java.io :as io] [clojure.set :as set] [clojure.string :as str] - [multiformats.base.b58 :as b58] [multiformats.hash :as hash] [nextjournal.clerk.analyzer.impl :as ana :refer [analyze*]] [nextjournal.clerk.classpath :as cp] [nextjournal.clerk.config :as config] [nextjournal.clerk.parser :as parser] + [nextjournal.clerk.utils :as utils] [nextjournal.clerk.walk :as walk] - [taoensso.nippy :as nippy] [weavejester.dependency :as dep])) +(when-not utils/bb? + (require '[taoensso.nippy :as nippy])) + (set! *warn-on-reflection* true) (defn deref? [form] @@ -42,10 +44,10 @@ #_(no-cache? '^{:nextjournal.clerk/no-cache false} (def ^:nextjournal.clerk/no-cache my-rand (rand-int 10))) (defn sha1-base58 [s] - (->> s hash/sha1 hash/encode b58/format-btc)) + (->> s hash/sha1 hash/encode (utils/->base58))) (defn sha2-base58 [s] - (->> s hash/sha2-512 hash/encode b58/format-btc)) + (->> s hash/sha2-512 hash/encode (utils/->base58))) #_(sha1-base58 "hello") @@ -80,12 +82,15 @@ e))))))) (defn analyze-form [form] - (with-bindings {clojure.lang.Compiler/LOADER (clojure.lang.RT/makeClassLoader)} + (with-bindings (utils/if-bb + {} + {clojure.lang.Compiler/LOADER (clojure.lang.RT/makeClassLoader)}) (binding [ana/*deps* (or ana/*deps* (atom #{}))] (analyze-form* (rewrite-defcached form))))) (defn ^:private var->protocol [v] - (or (:protocol (meta v)) + (or (let [p (:protocol (meta v))] + (when (var? p) p)) v)) (defn get-vars+forward-declarations [nodes] @@ -382,13 +387,14 @@ ([separator ns] (str/replace (namespace-munge ns) "." separator))) (defn ns->file [ns] - (some (fn [dir] - (some (fn [ext] - (let [path (str dir fs/file-separator (ns->path ns) ext)] - (when (fs/exists? path) - path))) - [".clj" ".cljc"])) - (cp/classpath-directories))) + (let [ns-path (ns->path ns)] + (some (fn [dir] + (some (fn [ext] + (let [path (str dir fs/file-separator ns-path ext)] + (when (fs/exists? path) + path))) + [".clj" ".cljc"])) + (cp/classpath-directories)))) (defn var->file [var] (when-let [file-from-var (-> var meta :file)] @@ -416,18 +422,19 @@ (defn guard [x f] (when (f x) x)) -(defn symbol->jar [sym] - (some-> (if (qualified-symbol? sym) - (-> sym namespace symbol) - sym) - ^Class resolve - .getProtectionDomain - .getCodeSource - .getLocation - ^java.net.URL (guard #(= "file" (.getProtocol ^java.net.URL %))) - .getFile - (guard #(str/ends-with? % ".jar")) - normalize-filename)) +(utils/when-not-bb + (defn symbol->jar [sym] + (some-> (if (qualified-symbol? sym) + (-> sym namespace symbol) + sym) + ^Class resolve + .getProtectionDomain + .getCodeSource + .getLocation + ^java.net.URL (guard #(= "file" (.getProtocol ^java.net.URL %))) + .getFile + (guard #(str/ends-with? % ".jar")) + normalize-filename))) #_(symbol->jar 'io.methvin.watcher.PathUtils) #_(symbol->jar 'io.methvin.watcher.PathUtils/cast) @@ -453,7 +460,7 @@ (if-let [ns (and (qualified-symbol? sym) (-> sym namespace symbol find-ns))] (or (ns->file ns) (ns->jar ns)) - (symbol->jar sym))))) + (utils/when-not-bb (symbol->jar sym)))))) #_(find-location `inc) #_(find-location `*print-dup*) @@ -646,10 +653,12 @@ (let [digest-fn (case hash-type :sha1 sha1-base58 :sha512 sha2-base58)] - (binding [nippy/*incl-metadata?* false] - (-> value - nippy/fast-freeze - digest-fn))))) + (utils/if-bb (-> value pr-str digest-fn) + #_{:clj-kondo/ignore [:unresolved-namespace]} + (binding [nippy/*incl-metadata?* false] + (-> value + nippy/fast-freeze + digest-fn)))))) #_(valuehash (range 100)) #_(valuehash :sha1 (range 100)) @@ -674,7 +683,6 @@ (assoc-in state [:->hash deref-dep] (->hash-str (eval deref-dep)))) analyzed-doc (sort topo-comp deref-deps-to-eval))] - #_(prn :hash-deref-deps/form form :deref-deps deref-deps-to-eval) (hash doc-with-deref-dep-hashes (sort topo-comp (dep/transitive-dependents-set graph deref-deps-to-eval)))) analyzed-doc)) diff --git a/src/nextjournal/clerk/cljs_libs.clj b/src/nextjournal/clerk/cljs_libs.clj index 3cc898ef1..dd650a9a8 100644 --- a/src/nextjournal/clerk/cljs_libs.clj +++ b/src/nextjournal/clerk/cljs_libs.clj @@ -3,14 +3,14 @@ (:require [clojure.java.io :as io] [clojure.string :as str] - [weavejester.dependency :as tnsd] [edamame.core :as e] - [nextjournal.clerk.viewer :as v] [nextjournal.clerk.always-array-map :as aam] [nextjournal.clerk.analyzer :refer [valuehash]] + [nextjournal.clerk.viewer :as v] [nextjournal.clerk.walk :as w] [rewrite-clj.node :as rnode] - [rewrite-clj.parser :as rparse])) + [rewrite-clj.parser :as rparse] + [weavejester.dependency :as tnsd])) (def already-loaded-sci-namespaces (atom '#{applied-science.js-interop diff --git a/src/nextjournal/clerk/eval.clj b/src/nextjournal/clerk/eval.clj index 4989bd098..915bf5ed0 100644 --- a/src/nextjournal/clerk/eval.clj +++ b/src/nextjournal/clerk/eval.clj @@ -1,41 +1,51 @@ (ns nextjournal.clerk.eval "Clerk's incremental evaluation with in-memory and disk-persisted caching layers." + {:clj-kondo/config '{:linters {:unresolved-namespace {:exclude [nippy]}}}} (:require [babashka.fs :as fs] + [clojure.edn :as edn] [clojure.java.io :as io] [clojure.main :as main] [clojure.string :as str] - [multiformats.base.b58 :as b58] [nextjournal.clerk.analyzer :as analyzer] [nextjournal.clerk.config :as config] [nextjournal.clerk.parser :as parser] - [nextjournal.clerk.viewer :as v] - [taoensso.nippy :as nippy]) - (:import (java.awt.image BufferedImage) - (javax.imageio ImageIO))) + [nextjournal.clerk.utils :as utils] + [nextjournal.clerk.viewer :as v])) -(comment - (alter-var-root #'nippy/*freeze-serializable-allowlist* (fn [_] "allow-and-record")) - (alter-var-root #'nippy/*thaw-serializable-allowlist* (fn [_] "allow-and-record")) - (nippy/get-recorded-serializable-classes)) +(utils/when-not-bb + (do (require '[taoensso.nippy :as nippy]) + (import '(java.awt.image BufferedImage) + '(javax.imageio ImageIO)))) + +#_(comment + (alter-var-root #'nippy/*freeze-serializable-allowlist* (fn [_] "allow-and-record")) + (alter-var-root #'nippy/*thaw-serializable-allowlist* (fn [_] "allow-and-record")) + (nippy/get-recorded-serializable-classes)) ;; nippy tweaks -(alter-var-root #'nippy/*thaw-serializable-allowlist* (fn [_] (conj nippy/default-thaw-serializable-allowlist "java.io.File" "clojure.lang.Var" "clojure.lang.Namespace"))) -(nippy/extend-freeze BufferedImage :java.awt.image.BufferedImage [x out] (ImageIO/write x "png" (ImageIO/createImageOutputStream out))) -(nippy/extend-thaw :java.awt.image.BufferedImage [in] (ImageIO/read in)) +(utils/when-not-bb + #_:clj-kondo/ignore + (do + (alter-var-root #'nippy/*thaw-serializable-allowlist* (fn [_] (conj nippy/default-thaw-serializable-allowlist "java.io.File" "clojure.lang.Var" "clojure.lang.Namespace"))) + (nippy/extend-freeze BufferedImage :java.awt.image.BufferedImage [x out] (ImageIO/write x "png" (ImageIO/createImageOutputStream out))) + (nippy/extend-thaw :java.awt.image.BufferedImage [in] (ImageIO/read in)))) #_(-> [(clojure.java.io/file "notebooks") (find-ns 'user)] nippy/freeze nippy/thaw) (defn ->cache-file [hash] - (str config/cache-dir fs/file-separator hash)) + (utils/if-bb + (str (fs/file config/cache-dir (str "bb_" hash))) + (str (fs/file config/cache-dir hash)))) (defn wrapped-with-metadata [value hash] (cond-> {:nextjournal/value value} - hash (assoc :nextjournal/blob-id (cond-> hash (not (string? hash)) b58/format-btc)))) + hash (assoc :nextjournal/blob-id (cond-> hash (not (string? hash)) (utils/->base58))))) #_(wrap-with-blob-id :test "foo") (defn hash+store-in-cas! [x] - (let [^bytes ba (nippy/freeze x) + (let [^bytes ba (utils/if-bb (.getBytes (pr-str x)) + (nippy/freeze x)) multihash (analyzer/sha2-base58 ba) file (->cache-file multihash)] (when-not (fs/exists? file) @@ -45,7 +55,10 @@ (defn thaw-from-cas [hash] ;; TODO: validate hash and retry or re-compute in case of a mismatch - (nippy/thaw-from-file (->cache-file hash))) + (let [f (->cache-file hash)] + (utils/if-bb + (-> f fs/read-all-bytes (String. "utf-8") edn/read-string) + (nippy/thaw-from-file f)) )) #_(thaw-from-cas (hash+store-in-cas! (range 42))) #_(thaw-from-cas "8Vv6q6La171HEs28ZuTdsn9Ukg6YcZwF5WRFZA1tGk2BP5utzRXNKYq9Jf9HsjFa6Y4L1qAAHzMjpZ28TCj1RTyAdx") @@ -91,7 +104,7 @@ (and (some? value) (try (and (not (analyzer/exceeds-bounded-count-limit? value)) - (some? (nippy/freezable? value))) + (utils/if-bb (= value (edn/read-string (pr-str value))) (some? (nippy/freezable? value)))) ;; can error on e.g. lazy-cat fib ;; TODO: propagate error information here (catch Exception _ @@ -152,9 +165,11 @@ (seq @!interned-vars) (assoc :nextjournal/interned @!interned-vars)))) (catch Throwable t - (let [triaged (main/ex-triage (Throwable->map t))] - (throw (ex-info (main/ex-str triaged) - (merge triaged (analyzer/form->ex-data form)))))))) + (utils/if-bb + (throw t) + (let [triaged (main/ex-triage (Throwable->map t))] + (throw (ex-info (main/ex-str triaged) + (merge triaged (analyzer/form->ex-data form))))))))) (defn maybe-eval-viewers [{:as opts :nextjournal/keys [viewer viewers]}] (cond-> opts @@ -261,6 +276,9 @@ (boolean (and (string? (:file doc)) (str/ends-with? (:file doc) ".cljs")))) +;; TODO: used in builder to drop analyzer dependency, cfr. below +(defn analyze-doc [doc] (-> doc analyzer/build-graph analyzer/hash)) + (defn +eval-results "Evaluates the given `parsed-doc` using the `in-memory-cache` and augments it with the results." [in-memory-cache {:as parsed-doc :keys [set-status-fn no-cache]}] @@ -280,7 +298,7 @@ (when set-status-fn (set-status-fn {:progress 0.10 :status "Analyzing…"})) (-> parsed-doc - (assoc :blob->result in-memory-cache) + (assoc :blob->result in-memory-cache) analyzer/build-graph analyzer/hash)))] (when (and (not-empty (:var->block-id analyzed-doc)) @@ -313,5 +331,4 @@ (eval-doc in-memory-cache (parser/parse-clojure-string code-string)))) #_(eval-string "(+ 39 3)") - #_(nextjournal.clerk/show! "notebooks/hello.md") diff --git a/src/nextjournal/clerk/parser.cljc b/src/nextjournal/clerk/parser.cljc index f5f736e35..c6b2746bf 100644 --- a/src/nextjournal/clerk/parser.cljc +++ b/src/nextjournal/clerk/parser.cljc @@ -1,9 +1,10 @@ (ns nextjournal.clerk.parser "Clerk's Parser turns Clojure & Markdown files and strings into Clerk documents." (:refer-clojure :exclude [read-string]) - (:require #?@(:clj [[clojure.tools.reader :as tools.reader] + (:require #?@(:bb [[clojure.tools.reader :as tools.reader] + [multiformats.hash :as hash]] + :clj [[clojure.tools.reader :as tools.reader] [taoensso.nippy :as nippy] - [multiformats.base.b58 :as b58] [multiformats.hash :as hash]] :cljs [[goog.crypt] [goog.crypt.Sha1]]) @@ -11,6 +12,7 @@ [clojure.string :as str] [clojure.zip] [edamame.core :as edamame] + #?(:clj [nextjournal.clerk.utils :refer [->base58]]) [nextjournal.markdown :as markdown] [rewrite-clj.node :as n] [rewrite-clj.parser :as p] @@ -20,10 +22,10 @@ #?(:clj (defn auto-resolves [ns] - (as-> (ns-aliases ns) $ - (assoc $ :current (ns-name *ns*)) - (zipmap (keys $) - (map ns-name (vals $)))))) + (let [aliases (ns-aliases ns) + aliases (assoc aliases :current (ns-name *ns*))] + (zipmap (keys aliases) + (map ns-name (vals aliases)))))) #_(auto-resolves (find-ns 'nextjournal.clerk.parser)) #_(auto-resolves (find-ns 'cards)) @@ -33,7 +35,8 @@ (edamame/parse-string s {:all true :read-cond :allow :regex #(list `re-pattern %) - :features #{:clj} + :features #?(:bb #{:bb :clj} + :default #{:clj}) :end-location false :row-key :line :col-key :column @@ -332,7 +335,7 @@ #?(:clj (defn sha1-base58 [s] - (->> s hash/sha1 hash/encode b58/format-btc))) + (->> s hash/sha1 hash/encode ->base58))) #?(:cljs (defn hash-sha1 [x] @@ -365,7 +368,8 @@ (guess-var form))] var (let [hash-fn (fn [x] - #?(:clj (-> x nippy/fast-freeze sha1-base58) + #?(:bb (sha1-base58 (pr-str x)) + :clj (-> x nippy/fast-freeze sha1-base58) :cljs (hash-sha1 x)))] (symbol (str *ns*) (case type @@ -399,11 +403,11 @@ (cond-> form (supports-meta? form) (vary-meta merge (cond-> loc - (:file opts) (assoc :clojure.core/eval-file - (str #?(:clj (cond-> (:file opts) - (instance? java.net.URL (:file opts)) + file (assoc :clojure.core/eval-file + (str #?(:clj (cond-> file + (instance? java.net.URL file) extract-file) - :cljs (:file opts)))))))) + :cljs file))))))) (defn add-doc-settings [{:as doc :keys [blocks]}] (if-let [first-form (some :form blocks)] @@ -445,7 +449,8 @@ (if-let [node (first nodes)] (recur (cond (code-tags (n/tag node)) - (let [form (try (read-string (n/string node)) + (let [nstring (n/string node) + form (try (read-string nstring) (catch Exception e (throw (ex-info (str "Clerk failed reading block: " (ex-message e) @@ -465,7 +470,7 @@ (parse-global-block-settings form)) code-block {:type :code :settings (merge-settings next-block-settings (parse-local-block-settings form)) - :text (n/string node) + :text nstring :form (add-loc opts loc form) :loc loc}] (when (ns? form) @@ -477,7 +482,6 @@ (update :blocks conj (add-block-id code-block))) (not (contains? state :ns)) (assoc :ns *ns*))) - (and add-comment-on-line? (whitespace-on-line-tags (n/tag node))) (-> state (assoc :add-comment-on-line? (not (n/comment? node))) diff --git a/src/nextjournal/clerk/utils.clj b/src/nextjournal/clerk/utils.clj new file mode 100644 index 000000000..0b4911488 --- /dev/null +++ b/src/nextjournal/clerk/utils.clj @@ -0,0 +1,25 @@ +(ns nextjournal.clerk.utils + (:require [alphabase.base58 :as base58])) + +(def bb? (System/getProperty "babashka.version")) + +(defmacro if-bb [then else] + (if bb? then else)) + +(defmacro if-not-bb-and [conds then else] + (if bb? + else + `(if ~conds + ~then + ~else))) + +(defmacro when-bb [& body] + (when bb? + `(do ~@body))) + +(defmacro when-not-bb [& body] + (when (not bb?) + `(do ~@body))) + +(defn ->base58 [x] + (base58/encode x)) diff --git a/src/nextjournal/clerk/viewer.cljc b/src/nextjournal/clerk/viewer.cljc index 193eb45f1..0b4d91a89 100644 --- a/src/nextjournal/clerk/viewer.cljc +++ b/src/nextjournal/clerk/viewer.cljc @@ -8,7 +8,6 @@ #?@(:clj [[babashka.fs :as fs] [clojure.repl :refer [demunge]] [clojure.tools.reader :as tools.reader] - [editscript.edit] [nextjournal.clerk.config :as config] [nextjournal.clerk.analyzer :as analyzer]] :cljs [[goog.crypt] @@ -18,11 +17,18 @@ [sci.impl.vars] [sci.lang] [applied-science.js-interop :as j]]) + #?@(:bb [] + :clj [[editscript.edit]]) [nextjournal.clerk.parser :as parser] [nextjournal.clerk.walk :as w] [nextjournal.markdown :as md] [nextjournal.markdown.utils :as md.utils]) - #?(:clj (:import (com.pngencoder PngEncoder) + #?(:bb (:import (clojure.lang IDeref IAtom) + (java.nio.file Files StandardOpenOption) + (java.net URI URL) + (java.util Base64) + (java.lang Throwable)) + :clj (:import (com.pngencoder PngEncoder) (clojure.lang IDeref IAtom) (java.lang Throwable) (java.awt.image BufferedImage) @@ -38,7 +44,8 @@ (-invoke [_ x y] (@f x y))])) ;; Make sure `RenderFn` is changed atomically -#?(:clj +#?(:bb nil + :clj (extend-protocol editscript.edit/IType RenderFn (get-type [_] :val))) @@ -88,6 +95,11 @@ (.write w (if-let [opts (not-empty (dissoc (into {} v) :f :form))] (str "#clerk/render-fn+opts " [opts (:form v)]) (str "#clerk/render-fn " (:form v)))))) + +#?(:bb + (defn ordered-map-reader-bb [coll] + (omap/ordered-map coll))) + #?(:cljs (defn ordered-map-reader-cljs [coll] (omap/ordered-map (vec coll)))) @@ -96,7 +108,8 @@ {'clerk/render-fn ->render-fn 'clerk/render-fn+opts ->render-fn+opts 'clerk/unreadable-edn eval - 'ordered/map #?(:clj omap/ordered-map-reader-clj + 'ordered/map #?(:bb ordered-map-reader-bb + :clj omap/ordered-map-reader-clj :cljs ordered-map-reader-cljs)}) #_(binding [*data-readers* {'render-fn ->render-fn}] @@ -598,7 +611,8 @@ (= :single-file package) (data-uri-base64-encode (fs/read-all-bytes src) (Files/probeContentType (fs/path src))) :else (str "/_fs/" src)))) -#?(:clj +#?(:bb nil + :clj (defn read-image [image-or-url] (ImageIO/read (if (string? image-or-url) @@ -613,8 +627,9 @@ (defn md-image->viewer [doc block-id idx {:keys [attrs]}] (with-viewer `html-viewer #?(:clj {:nextjournal/render-opts {:id (processed-block-id block-id [idx])} - :nextjournal/width (try (image-width (read-image (:src attrs))) - (catch Throwable _ :prose))}) + :nextjournal/width #?(:bb :prose + :clj (try (image-width (read-image (:src attrs))) + (catch Throwable _ :prose)))}) [:div.flex.flex-col.items-center.not-prose.mb-4 [:img (update attrs :src process-image-source doc)]])) @@ -943,7 +958,24 @@ (partial present-ex-data wrapped-value) datafy/datafy))))}) -#?(:clj +(def buffered-image-viewer #?(:bb {} + :cljs nil + :clj {:pred #(instance? BufferedImage %) + :transform-fn (fn [{image :nextjournal/value}] + (let [w (.getWidth image) + h (.getHeight image) + r (float (/ w h))] + (-> {:nextjournal/value (.. (PngEncoder.) + (withBufferedImage image) + (withCompressionLevel 1) + (toBytes)) + :nextjournal/content-type "image/png" + :nextjournal/width (if (and (< 2 r) (< 900 w)) :full :wide)} + mark-presented))) + :render-fn '(fn [blob] (v/html [:figure.flex.flex-col.items-center.not-prose [:img {:src (v/url-for blob)}]]))})) + +#?(:bb nil + :clj (defn buffered-image->bytes [^BufferedImage image] (.. (PngEncoder.) (withBufferedImage image) @@ -951,7 +983,8 @@ (toBytes)))) (def image-viewer - {#?@(:clj [:pred #(instance? BufferedImage %) + {#?@(:bb [] + :clj [:pred #(instance? BufferedImage %) :transform-fn (fn [{image :nextjournal/value}] (-> {:nextjournal/value (buffered-image->bytes image) :nextjournal/content-type "image/png" @@ -1878,7 +1911,9 @@ ([image-or-url] (image {} image-or-url)) ([viewer-opts image-or-url] (with-viewer (:name image-viewer) viewer-opts - #?(:cljs image-or-url :clj (read-image image-or-url))))) + #?(:cljs image-or-url + :bb image-or-url + :clj (read-image image-or-url))))) (defn caption [text content] (col diff --git a/src/nextjournal/clerk/webserver.clj b/src/nextjournal/clerk/webserver.clj index 610116077..b7c53a90e 100644 --- a/src/nextjournal/clerk/webserver.clj +++ b/src/nextjournal/clerk/webserver.clj @@ -1,4 +1,5 @@ (ns nextjournal.clerk.webserver + {:clj-kondo/config '{:linters {:unresolved-namespace {:exclude [sci.core]}}}} (:require [babashka.fs :as fs] [clojure.edn :as edn] [clojure.java.browse :as browse] @@ -6,16 +7,20 @@ [clojure.pprint :as pprint] [clojure.set :as set] [clojure.string :as str] - [editscript.core :as editscript] [nextjournal.clerk.config :as config] [nextjournal.clerk.git :as git] [nextjournal.clerk.paths :as paths] + [nextjournal.clerk.utils :as u] [nextjournal.clerk.view :as view] [nextjournal.clerk.viewer :as v] [org.httpkit.server :as httpkit] [sci.nrepl.browser-server :as sci.nrepl]) (:import (java.nio.file Files))) +(u/if-bb + (require '[editscript.core :as-alias editscript]) + (require '[editscript.core :as editscript])) + (defonce !clients (atom #{})) (defonce !doc (atom nil)) (defonce !last-sender-ch (atom nil)) @@ -153,16 +158,14 @@ presented)) (defn update-doc! [{:as doc :keys [nav-path fragment skip-history?]}] - (broadcast! (if (and (:ns @!doc) (= (:ns @!doc) (:ns doc))) - {:type :patch-state! :patch (editscript/get-edits (editscript/diff (meta @!doc) (present+reset! doc) {:algo :quick}))} - (cond-> {:type :set-state! - :doc (present+reset! doc)} - (and nav-path (not skip-history?)) + (broadcast! (u/if-not-bb-and (and (:ns @!doc) (= (:ns @!doc) (:ns doc))) + {:type :patch-state! :patch (editscript/get-edits (editscript/diff (meta @!doc) (present+reset! doc) {:algo :quick}))} + (cond-> {:type :set-state! + :doc (present+reset! doc)} + (and nav-path (not skip-history?)) (assoc :effects [(v/->render-eval (list 'nextjournal.clerk.render/history-push-state (cond-> {:path nav-path} fragment (assoc :fragment fragment))))]))))) -#_(update-doc! (help-doc)) - (defn update-error! [ex] (update-doc! (assoc @!doc :error ex))) @@ -196,7 +199,9 @@ :sync! (if-let [var (resolve (:var-name msg))] (try (binding [*sender-ch* sender-ch] - (swap! @var editscript/patch (editscript/edits->script (:patch msg)))) + (u/if-bb + (throw (ex-info "Not implemented" {})) + (swap! @var editscript/patch (editscript/edits->script (:patch msg))))) (catch Exception ex (throw (doto (ex-info (str "Clerk cannot update synced var `" (:var-name msg) "`.") msg ex) update-error!)))) @@ -316,7 +321,12 @@ (try (show! (merge {:skip-history? true} (select-keys opts [:expanded-paths :index :git/sha :git/url :git/prefix])) file-or-ns) - (catch Exception _)) + (catch ^:sci/error Exception e + (u/if-bb + (binding [*out* *err*] + (println + (str/join "\n" (sci.core/format-stacktrace (sci.core/stacktrace e))))) + nil))) {:status 200 :headers {"Content-Type" "text/html" "Cache-Control" "no-store"} :body (view/->html {:doc (view/doc->viewer @!doc) @@ -339,6 +349,11 @@ "_ws" {:status 200 :body "upgrading..."} "favicon.ico" {:status 404} (serve-notebook req)) + (catch ^:sci/error Exception e + {:status 500 + :body (u/if-bb + (pr-str (sci.core/format-stacktrace (sci.core/stacktrace e))) + (with-out-str (pprint/pprint (Throwable->map e))))}) (catch Throwable e {:status 500 :body (with-out-str (pprint/pprint (Throwable->map e)))})))) diff --git a/test/nextjournal/clerk/analyzer_test.clj b/test/nextjournal/clerk/analyzer_test.clj index 0e1c2e0b6..f7a7b218a 100644 --- a/test/nextjournal/clerk/analyzer_test.clj +++ b/test/nextjournal/clerk/analyzer_test.clj @@ -13,6 +13,8 @@ [nextjournal.clerk.fixtures.dep-b] [nextjournal.clerk.fixtures.issue-660-repro] [nextjournal.clerk.parser :as parser] + [nextjournal.clerk.test-utils] + [nextjournal.clerk.utils :as utils] [weavejester.dependency :as dep]) (:import (clojure.lang ExceptionInfo))) @@ -27,10 +29,12 @@ (deftest ns->file (testing "ns arg" - (is (= (str (fs/file "src" "nextjournal" "clerk" "analyzer.clj")) (ana/ns->file (find-ns 'nextjournal.clerk.analyzer))))) + (is (str/ends-with? (ana/ns->file (find-ns 'nextjournal.clerk.analyzer)) + (str (fs/file "src" "nextjournal" "clerk" "analyzer.clj")) ))) (testing "symbol cljc" - (is (= (str (fs/file "src" "nextjournal" "clerk" "viewer.cljc")) (ana/ns->file 'nextjournal.clerk.viewer))))) + (is (str/ends-with? (ana/ns->file 'nextjournal.clerk.viewer) + (str (fs/file "src" "nextjournal" "clerk" "viewer.cljc")) )))) (deftest no-cache? (with-ns-binding 'nextjournal.clerk.analyzer-test @@ -91,7 +95,7 @@ (is (not (contains? (:deps (ana/analyze '(let [my-local (fn [])] (my-local)))) 'clojure.set/union))))) -(deftest analyze +(deftest analyze-test (testing "quoted forms aren't confused with variable dependencies" (is (match? {:deps #{`inc}} (ana/analyze '(do inc)))) @@ -100,13 +104,15 @@ (testing "locals that shadow existing vars shouldn't show up in the deps" (is (= #{'clojure.core/let} (:deps (ana/analyze '(let [+ 2] +)))))) - (testing "symbol referring to a java class" - (is (match? {:deps #{'io.methvin.watcher.PathUtils}} - (ana/analyze 'io.methvin.watcher.PathUtils)))) + (utils/when-not-bb + (testing "symbol referring to a java class" + (is (match? {:deps #{'io.methvin.watcher.PathUtils}} + (ana/analyze 'io.methvin.watcher.PathUtils))))) - (testing "namespaced symbol referring to a java thing" - (is (match? {:deps #{'io.methvin.watcher.hashing.FileHasher}} - (ana/analyze 'io.methvin.watcher.hashing.FileHasher/DEFAULT_FILE_HASHER)))) + (utils/when-not-bb + (testing "namespaced symbol referring to a java thing" + (is (match? {:deps #{'io.methvin.watcher.hashing.FileHasher}} + (ana/analyze 'io.methvin.watcher.hashing.FileHasher/DEFAULT_FILE_HASHER))))) (is (match? {:ns-effect? false :vars '#{nextjournal.clerk.analyzer/foo} @@ -152,10 +158,10 @@ (is (match? {:ns-effect? false :vars '#{nextjournal.clerk.analyzer-test/!state} - :deps #{'clojure.core/atom - 'clojure.core/let - 'clojure.core/when-not - 'clojure.core/defonce}} + :deps (m/embeds #{'clojure.core/atom + 'clojure.core/let + 'clojure.core/when-not + 'clojure.core/defonce})} (with-ns-binding 'nextjournal.clerk.analyzer-test (ana/analyze '(defonce !state (atom {})))))) @@ -193,15 +199,17 @@ (testing "macro-expansion defining var occurs in deps" (is (= 2 (count (:deps (ana/analyze '(nextjournal.clerk.fixtures.macros/emit-nonsense)))))))) -(deftest symbol->jar - (is (ana/symbol->jar 'io.methvin.watcher.PathUtils)) - (is (ana/symbol->jar 'io.methvin.watcher.PathUtils/cast)) - (testing "does not resolve jdk builtins" - (is (not (ana/symbol->jar 'java.net.http.HttpClient/newHttpClient))))) +(utils/when-not-bb + (deftest symbol->jar + (is (ana/symbol->jar 'io.methvin.watcher.PathUtils)) + (is (ana/symbol->jar 'io.methvin.watcher.PathUtils/cast)) + (testing "does not resolve jdk builtins" + (is (not (ana/symbol->jar 'java.net.http.HttpClient/newHttpClient)))))) (deftest find-location - (testing "clojure.core/inc" - (is (re-find #"clojure-1\..*\.jar" (ana/find-location 'clojure.core/inc)))) + (utils/when-not-bb + (testing "clojure.core/inc" + (is (re-find #"clojure-1\..*\.jar" (ana/find-location 'clojure.core/inc))))) (testing "weavejester.dependency/graph" (is (re-find #"dependency-.*\.jar" (ana/find-location 'weavejester.dependency/graph))))) @@ -303,13 +311,14 @@ (inc a#)))))))) (deftest analyze-doc - (is (match? #{{:form '(ns example-notebook), - :deps set?} - {:form '#{1 3 2}} - {:jar string? :hash string?}} - (-> "^:nextjournal.clerk/no-cache (ns example-notebook) + (utils/when-not-bb + (is (match? #{{:form '(ns example-notebook), + :deps set?} + {:form '#{1 3 2}} + {:jar string? :hash string?}} + (-> "^:nextjournal.clerk/no-cache (ns example-notebook) #{3 1 2}" - analyze-string :->analysis-info vals set))) + analyze-string :->analysis-info vals set)))) (testing "preserves *ns*" (with-ns-binding 'nextjournal.clerk.analyzer-test (is (= (find-ns 'nextjournal.clerk.analyzer-test) diff --git a/test/nextjournal/clerk/builder_test.clj b/test/nextjournal/clerk/builder_test.clj index 85e5daad3..1c00b7655 100644 --- a/test/nextjournal/clerk/builder_test.clj +++ b/test/nextjournal/clerk/builder_test.clj @@ -1,13 +1,17 @@ (ns nextjournal.clerk.builder-test - (:require [babashka.fs :as fs] - [clojure.java.browse :as browse] - [clojure.string :as str] - [clojure.test :refer [deftest is testing]] - [matcher-combinators.test] - [nextjournal.clerk.builder :as builder] - [nextjournal.clerk.viewer :as viewer]) - (:import (clojure.lang ExceptionInfo) - (java.io File))) + (:require + [babashka.fs :as fs] + [clojure.java.browse :as browse] + [clojure.string :as str] + [clojure.test :refer [deftest is testing]] + [matcher-combinators.test] + [nextjournal.clerk.builder :as builder] + [nextjournal.clerk.test-utils] + [nextjournal.clerk.utils :as utils] + [nextjournal.clerk.viewer :as viewer]) + (:import + (clojure.lang ExceptionInfo) + (java.io File))) (deftest url-canonicalize (testing "canonicalization of file components into url components" @@ -61,12 +65,13 @@ (is (= "notebooks/hello.clj" (get backlink 2))) (is (= [:<> "@" [:span.tabular-nums "SHASHAS"]] (get backlink 3)))))) - (testing "image is saved to _data dir" - (is (fs/with-temp-dir [temp-dir {}] - (builder/build-static-app! {:index "notebooks/viewers/single_image.clj" - :out-path temp-dir - :report-fn identity}) - (first (map fs/file-name (fs/list-dir (fs/file temp-dir "_data") "**.png")))))) + (utils/when-not-bb + (testing "image is saved to _data dir" + (is (fs/with-temp-dir [temp-dir {}] + (builder/build-static-app! {:index "notebooks/viewers/single_image.clj" + :out-path temp-dir + :report-fn identity}) + (first (map fs/file-name (fs/list-dir (fs/file temp-dir "_data") "**.png"))))))) (testing "SCI .cljs sources are deduplicated" (fs/with-temp-dir [temp-dir {}] (builder/build-static-app! {:paths ["test/nextjournal/clerk/fixtures/require_cljs_bundle_dedupe_1.clj" diff --git a/test/nextjournal/clerk/eval_test.clj b/test/nextjournal/clerk/eval_test.clj index 12b5de2c8..a40f8e444 100644 --- a/test/nextjournal/clerk/eval_test.clj +++ b/test/nextjournal/clerk/eval_test.clj @@ -7,6 +7,7 @@ [nextjournal.clerk :as clerk] [nextjournal.clerk.eval :as eval] [nextjournal.clerk.parser :as parser] + [nextjournal.clerk.utils :as u] [nextjournal.clerk.view :as view] [nextjournal.clerk.viewer :as viewer] [nextjournal.clerk.webserver :as webserver])) @@ -64,7 +65,6 @@ (let [{:as result :keys [blob->result]} (eval/eval-string "(ns my-random-test-ns-2) {inc (java.util.UUID/randomUUID)}")] (is (= result (eval/eval-string blob->result "(ns my-random-test-ns-2) {inc (java.util.UUID/randomUUID)}"))))) - (testing "side-effecting expression returning nil gets cached in memory" (let [code "(ns my-random-test-ns-2) (do (clojure.lang.Var/intern (the-ns 'my-random-test-ns-2) 'ruuid (java.util.UUID/randomUUID)) nil)" {:keys [blob->result]} (eval/eval-string code)] @@ -255,8 +255,9 @@ (testing "class is not cachable" (is (not (eval/cachable? java.lang.String))) (is (not (eval/cachable? {:foo java.lang.String})))) - (testing "image is cachable" - (is (eval/cachable? (javax.imageio.ImageIO/read (io/file "trees.png")))))) + (u/when-not-bb + (testing "image is cachable" + (is (eval/cachable? (javax.imageio.ImageIO/read (io/file "trees.png"))))))) (deftest show!-test (testing "in-memory cache is preserved when exception is thrown (#549)" diff --git a/test/nextjournal/clerk/parser_test.clj b/test/nextjournal/clerk/parser_test.clj index 97cb848ca..3a8639f4b 100644 --- a/test/nextjournal/clerk/parser_test.clj +++ b/test/nextjournal/clerk/parser_test.clj @@ -2,6 +2,7 @@ (:require [clojure.test :refer [deftest is testing]] [matcher-combinators.test :refer [match?]] [nextjournal.clerk.parser :as parser] + [nextjournal.clerk.utils :as utils] [nextjournal.clerk.view :as view])) (defmacro with-ns-binding [ns-sym & body] @@ -190,13 +191,22 @@ par two")))) (deftest add-block-ids (testing "assigns block ids" - (is (= '[foo/anon-expr-5drCkCGrPisMxHpJVeyoWwviSU3pfm - foo/bar - foo/bar#2 - foo/anon-expr-5dsbEK7B7yDZqzyteqsY2ndKVE9p3G - foo/anon-expr-5dsbEK7B7yDZqzyteqsY2ndKVE9p3G#2] - (->> "(ns foo {:nextjournal.clerk/visibility {:code :fold}}) (def bar :baz) (def bar :baz) (rand-int 42) (rand-int 42)" - parser/parse-clojure-string :blocks (mapv :id)))))) + (let [ids (->> "(ns foo {:nextjournal.clerk/visibility {:code :fold}}) (def bar :baz) (def bar :baz) (rand-int 42) (rand-int 42)" + parser/parse-clojure-string :blocks (mapv :id))] + (is (every? symbol ids)) + (is (match? + (utils/if-bb + '[foo/anon-expr-5duGkCsyuG2a1BWUegjnh4f6pNNqgk + foo/bar + foo/bar#2 + foo/anon-expr-5dssY1D9kQSNgSWwDLCN2B3YEwrqWQ + foo/anon-expr-5dssY1D9kQSNgSWwDLCN2B3YEwrqWQ#2] + '[foo/anon-expr-5drCkCGrPisMxHpJVeyoWwviSU3pfm + foo/bar + foo/bar#2 + foo/anon-expr-5dsbEK7B7yDZqzyteqsY2ndKVE9p3G + foo/anon-expr-5dsbEK7B7yDZqzyteqsY2ndKVE9p3G#2]) + ids))))) (deftest parse-file-test (testing "parsing a Clojure file" diff --git a/test/nextjournal/clerk/test_utils.clj b/test/nextjournal/clerk/test_utils.clj new file mode 100644 index 000000000..c56b7a640 --- /dev/null +++ b/test/nextjournal/clerk/test_utils.clj @@ -0,0 +1,21 @@ +(ns nextjournal.clerk.test-utils + (:require [clojure.test :as t])) + + +(def timer (atom nil)) + +(defmethod t/report :begin-test-var [m] + (binding [*out* *err*] + (println "===" (-> m :var meta :name)) + (reset! timer (. System (nanoTime))))) + +(defmethod t/report :end-test-var [_m] + (println (str "Elapsed time: " (/ (double (- (. System (nanoTime)) @timer)) 1000000.0) " msecs")) + (println) + (when-let [rc t/*report-counters*] + (when-let [{:keys [:fail :error]} @rc] + (when (and (= "true" (System/getenv "FAIL_FAST")) + (or (pos? fail) (pos? error))) + (binding [*out* *err*] + (println "=== Failing fast")) + (System/exit 1))))) diff --git a/test/nextjournal/clerk/viewer_test.clj b/test/nextjournal/clerk/viewer_test.clj index dfae8c371..becf323b6 100644 --- a/test/nextjournal/clerk/viewer_test.clj +++ b/test/nextjournal/clerk/viewer_test.clj @@ -8,6 +8,7 @@ [nextjournal.clerk.config :as config] [nextjournal.clerk.eval :as eval] [nextjournal.clerk.eval-test :as eval-test] + [nextjournal.clerk.utils :as utils] [nextjournal.clerk.view :as view] [nextjournal.clerk.viewer :as v])) @@ -83,31 +84,32 @@ (testing "elision inside html" (let [value (v/html [:div [:ul [:li {:nextjournal/value (range 30)}]]])] (is (= (v/->value value) (v/->value (v/desc->values (resolve-elision (v/present value)))))))) - - (testing "resolving elided blobs" - (let [{:nextjournal/keys [presented blob-id]} - (-> (eval/eval-string "(ns nextjournal.clerk.viewer-test.elision-and-images + (utils/when-not-bb + (testing "resolving elided blobs" + (let [{:nextjournal/keys [presented blob-id]} + (-> (eval/eval-string "(ns nextjournal.clerk.viewer-test.elision-and-images (:require [nextjournal.clerk :as clerk])) (clerk/image \"trees.png\")") - view/doc->viewer - :nextjournal/value - :blocks second :nextjournal/value second - :nextjournal/value)] + view/doc->viewer + :nextjournal/value + :blocks second :nextjournal/value second + :nextjournal/value)] - ;; n.c.viewer/process-blobs replaces bytes with an elision containing path and blob-id - (is (= {:blob-id blob-id :path [1]} (:nextjournal/value presented))) - (is (= "image/png" (:nextjournal/content-type presented))) + ;; n.c.viewer/process-blobs replaces bytes with an elision containing path and blob-id + (is (= {:blob-id blob-id :path [1]} (:nextjournal/value presented))) + (is (= "image/png" (:nextjournal/content-type presented))) - ;; this is the mechanism that let image contents be resolved via n.c.webserver/serve-blob - ;; see also n.c.render/url-for - (is (bytes? (:nextjournal/value - ((:present-elision-fn (meta presented)) - (select-keys (:nextjournal/value presented) [:path])))))))) + ;; this is the mechanism that let image contents be resolved via n.c.webserver/serve-blob + ;; see also n.c.render/url-for + (is (bytes? (:nextjournal/value + ((:present-elision-fn (meta presented)) + (select-keys (:nextjournal/value presented) [:path]))))))))) (deftest default-viewers (testing "viewers have names matching vars" (doseq [[viewer-name viewer] (into {} - (map (juxt :name (comp deref resolve :name))) + (map (juxt :name (fn [v] + (some-> v :name resolve deref)))) v/default-viewers)] (is (= viewer-name (:name viewer)))))) @@ -368,19 +370,21 @@ (let [test-doc (eval/eval-string ";; Some inline image ![alt](trees.png) here.")] (is (not-empty (tree-re-find (view/doc->viewer {:package :single-file} test-doc) #"data:image/png;base64"))))) - (testing "Local images are content addressed for default static builds" - (let [test-doc (eval/eval-string ";; Some inline image ![alt](trees.png) here.")] - (is (not-empty (tree-re-find (view/doc->viewer {:package :directory :out-path (str (fs/temp-dir))} test-doc) #"_data/.+\.png"))))) + (utils/when-not-bb + (testing "Local images are content addressed for default static builds" + (let [test-doc (eval/eval-string ";; Some inline image ![alt](trees.png) here.")] + (is (not-empty (tree-re-find (view/doc->viewer {:package :directory :out-path (str (fs/temp-dir))} test-doc) #"_data/.+\.png")))))) - (testing "Doc options are propagated to blob processing" - (let [test-doc (eval/eval-string "(java.awt.image.BufferedImage. 20 20 1)")] - (is (not-empty (tree-re-find (view/doc->viewer {:package :single-file - :out-path builder/default-out-path} test-doc) - #"data:image/png;base64"))) + (utils/when-not-bb + (testing "Doc options are propagated to blob processing" + (let [test-doc (eval/eval-string "(java.awt.image.BufferedImage. 20 20 1)")] + (is (not-empty (tree-re-find (view/doc->viewer {:package :single-file + :out-path builder/default-out-path} test-doc) + #"data:image/png;base64"))) - (is (not-empty (tree-re-find (view/doc->viewer {:package :directory - :out-path builder/default-out-path} test-doc) - #"_data/.+\.png"))))) + (is (not-empty (tree-re-find (view/doc->viewer {:package :directory + :out-path builder/default-out-path} test-doc) + #"_data/.+\.png")))))) (testing "presentations are pure, result hashes are stable" (let [test-doc (eval/eval-string "(range 100)")] @@ -513,18 +517,20 @@ (is (= "#clerk/unreadable-edn (symbol \"~\")" (pr-str (symbol "~"))))) - (testing "symbols and keywords with two slashes readable by `read-string` but not `tools.reader/read-string` print as #clerk/unreadable-edn" - (is (= "#clerk/unreadable-edn (symbol \"foo\" \"bar/baz\")" - (pr-str (read-string "foo/bar/baz")))) - (is (= "#clerk/unreadable-edn (keyword \"foo\" \"bar/baz\")" - (pr-str (read-string ":foo/bar/baz"))))) + (utils/when-not-bb + ;; TODO? + (testing "symbols and keywords with two slashes readable by `read-string` but not `tools.reader/read-string` print as #clerk/unreadable-edn" + (is (= "#clerk/unreadable-edn (symbol \"foo\" \"bar/baz\")" + (pr-str (read-string "foo/bar/baz")))) + (is (= "#clerk/unreadable-edn (keyword \"foo\" \"bar/baz\")" + (pr-str (read-string ":foo/bar/baz"))))) - (testing "splicing reader conditional prints normally (issue #338)" - (is (= "?@" (pr-str (symbol "?@"))))) + (testing "splicing reader conditional prints normally (issue #338)" + (is (= "?@" (pr-str (symbol "?@"))))) - (testing "custom print-method for symbol preserves metadata" - (is (-> (binding [*print-meta* true] - (pr-str '[^:foo bar])) read-string first meta :foo)))) + (testing "custom print-method for symbol preserves metadata" + (is (-> (binding [*print-meta* true] + (pr-str '[^:foo bar])) read-string first meta :foo))))) (deftest removed-metadata (is (= "(do 'this)" diff --git a/test/nextjournal/clerk/walk_test.clj b/test/nextjournal/clerk/walk_test.clj index 3c89b6697..5d0d53f22 100644 --- a/test/nextjournal/clerk/walk_test.clj +++ b/test/nextjournal/clerk/walk_test.clj @@ -1,5 +1,6 @@ (ns nextjournal.clerk.walk-test (:require [clojure.test :refer [deftest is are]] + [nextjournal.clerk.utils :as utils] [nextjournal.clerk.walk :as walk])) (deftest test-postwalk @@ -79,9 +80,10 @@ (reduce + (map (comp inc val) c)))) (is (= (walk/walk inc #(reduce + %) c) (reduce + (map inc c))))) - (when (instance? clojure.lang.Sorted c) - (is (= (.comparator ^clojure.lang.Sorted c) - (.comparator ^clojure.lang.Sorted walked)))))))) + (utils/when-not-bb + (when (instance? clojure.lang.Sorted c) + (is (= (.comparator ^clojure.lang.Sorted c) + (.comparator ^clojure.lang.Sorted walked))))))))) ; Checks that walk preserves the MapEntry type. See CLJ-2031. (deftest walk-mapentry diff --git a/test/nextjournal/clerk/webserver_test.clj b/test/nextjournal/clerk/webserver_test.clj index 2cab25260..ef37c60e4 100644 --- a/test/nextjournal/clerk/webserver_test.clj +++ b/test/nextjournal/clerk/webserver_test.clj @@ -2,8 +2,10 @@ (:require [clojure.java.io :as io] [clojure.test :refer [deftest is testing]] [nextjournal.clerk.eval :as eval] - [nextjournal.clerk.viewer :as viewer] + [nextjournal.clerk.test-utils] + [nextjournal.clerk.utils :as utils] [nextjournal.clerk.view :as view] + [nextjournal.clerk.viewer :as viewer] [nextjournal.clerk.webserver :as webserver])) (deftest ->file-or-ns @@ -11,56 +13,57 @@ (is (= "notebooks/rule_30.clj" (webserver/->file-or-ns "notebooks/rule_30.clj")))) (deftest serve-blob - (testing "lazy loading of simple range" - (let [doc (let [doc (eval/eval-string "(range 100)")] - (with-meta doc (view/doc->viewer doc))) - {:nextjournal/keys [presented fetch-opts]} (-> doc meta :nextjournal/value :blocks first :nextjournal/value second :nextjournal/value) - {:nextjournal/keys [value]} presented - {elision-viewer :nextjournal/viewer elision-fetch-opts :nextjournal/value} (peek value) - {:keys [body]} (webserver/serve-blob doc (merge fetch-opts {:fetch-opts elision-fetch-opts}))] - (is (= `nextjournal.clerk.viewer/elision-viewer (:name elision-viewer))) - (is body) - (is (= (-> body webserver/read-msg :nextjournal/value first :nextjournal/value) 20)))) + (utils/when-not-bb + (testing "lazy loading of simple range" + (let [doc (let [doc (eval/eval-string "(range 100)")] + (with-meta doc (view/doc->viewer doc))) + {:nextjournal/keys [presented fetch-opts]} (-> doc meta :nextjournal/value :blocks first :nextjournal/value second :nextjournal/value) + {:nextjournal/keys [value]} presented + {elision-viewer :nextjournal/viewer elision-fetch-opts :nextjournal/value} (peek value) + {:keys [body]} (webserver/serve-blob doc (merge fetch-opts {:fetch-opts elision-fetch-opts}))] + (is (= `nextjournal.clerk.viewer/elision-viewer (:name elision-viewer))) + (is body) + (is (= (-> body webserver/read-msg :nextjournal/value first :nextjournal/value) 20)))) - (testing "lazy loading of images" - (let [doc (let [doc (eval/eval-string "(ns nextjournal.clerk.webserver-test.lazy-load-image + (testing "lazy loading of images" + (let [doc (let [doc (eval/eval-string "(ns nextjournal.clerk.webserver-test.lazy-load-image {:nextjournal.clerk/no-cache true} (:require [nextjournal.clerk :as clerk])) (clerk/image \"trees.png\") ")] - (with-meta doc (view/doc->viewer doc))) - {:nextjournal/keys [presented fetch-opts]} (-> doc meta :nextjournal/value :blocks second :nextjournal/value second :nextjournal/value) - {:nextjournal/keys [value]} presented] - (is (= "image/png" (:nextjournal/content-type presented))) - (is (= (:blob-id fetch-opts) (:blob-id (:nextjournal/value presented)))) - ;; the presented image has been processed to only contain their blob-id in the value - ;; serve blob resolve their original contents via the elision mechanism - (let [response-body (:body (webserver/serve-blob doc (merge fetch-opts {:fetch-opts value})))] - (is (bytes? response-body)) - (is (= (seq (viewer/buffered-image->bytes (viewer/read-image "trees.png"))) - (seq response-body))))) + (with-meta doc (view/doc->viewer doc))) + {:nextjournal/keys [presented fetch-opts]} (-> doc meta :nextjournal/value :blocks second :nextjournal/value second :nextjournal/value) + {:nextjournal/keys [value]} presented] + (is (= "image/png" (:nextjournal/content-type presented))) + (is (= (:blob-id fetch-opts) (:blob-id (:nextjournal/value presented)))) + ;; the presented image has been processed to only contain their blob-id in the value + ;; serve blob resolve their original contents via the elision mechanism + (let [response-body (:body (webserver/serve-blob doc (merge fetch-opts {:fetch-opts value})))] + (is (bytes? response-body)) + (is (= (seq (viewer/buffered-image->bytes (viewer/read-image "trees.png"))) + (seq response-body))))) - (testing "lazy loading of elided images" - (let [doc (let [doc (eval/eval-string "(ns nextjournal.clerk.webserver-test.lazy-load-image + (testing "lazy loading of elided images" + (let [doc (let [doc (eval/eval-string "(ns nextjournal.clerk.webserver-test.lazy-load-image {:nextjournal.clerk/no-cache true} (:require [nextjournal.clerk :as clerk])) (concat (range 20) (list (clerk/image \"trees.png\")))")] - (with-meta doc (view/doc->viewer doc))) - {:nextjournal/keys [presented fetch-opts]} (-> doc meta :nextjournal/value :blocks second :nextjournal/value second :nextjournal/value) - {:nextjournal/keys [value]} presented - {elision-fetch-opts :nextjournal/value v :nextjournal/viewer} (peek value)] - (is (= (:name viewer/elision-viewer) (:name v))) - (let [{:keys [body]} (webserver/serve-blob doc (merge fetch-opts {:fetch-opts elision-fetch-opts})) - {expanded-value :nextjournal/value} - (binding [*data-readers* (assoc viewer/data-readers 'object (fn [_v] [:__object__]))] - (read-string body))] - (is (= "image/png" (-> expanded-value first :nextjournal/content-type))) - ;; blobs contained inside fetched elisions are being processed - (is (= {:blob-id (:blob-id fetch-opts) :path [1 20]} - (-> expanded-value first :nextjournal/value)))))))) + (with-meta doc (view/doc->viewer doc))) + {:nextjournal/keys [presented fetch-opts]} (-> doc meta :nextjournal/value :blocks second :nextjournal/value second :nextjournal/value) + {:nextjournal/keys [value]} presented + {elision-fetch-opts :nextjournal/value v :nextjournal/viewer} (peek value)] + (is (= (:name viewer/elision-viewer) (:name v))) + (let [{:keys [body]} (webserver/serve-blob doc (merge fetch-opts {:fetch-opts elision-fetch-opts})) + {expanded-value :nextjournal/value} + (binding [*data-readers* (assoc viewer/data-readers 'object (fn [_v] [:__object__]))] + (read-string body))] + (is (= "image/png" (-> expanded-value first :nextjournal/content-type))) + ;; blobs contained inside fetched elisions are being processed + (is (= {:blob-id (:blob-id fetch-opts) :path [1 20]} + (-> expanded-value first :nextjournal/value))))))))) (deftest serve-file-test (testing "serving a file resource"