Development Guide
Dev Workflow
Starting the dev server
npm run dev
# Equivalent to: npx shadow-cljs watch app
This starts shadow-cljs in watch mode, serving the app at http://localhost:8080 with hot module reloading. Code changes in src/main/ are automatically compiled and the re-render function is called to update the UI.
REPL-driven development
From a Clojure nREPL (e.g. via your editor), connect to the shadow-cljs REPL:
(require '[user :refer [cljs-repl]])
(cljs-repl) ;; connects to the :app build
(cljs-repl :test) ;; or connect to the :test build
This gives you a live ClojureScript REPL connected to the running browser session.
Running tests during development
npm run test:watch
This compiles and runs tests on every save. The :test build uses :node-test target, so tests run in Node.js without a browser.
Working with Specs
The app.specs namespace defines specs for all core data structures and key functions. To use specs in the REPL:
(require '[cljs.spec.alpha :as s])
(require '[app.specs])
;; Validate a tree node
(s/valid? :app.specs/tree-node
{:name "A" :branch-length 0.1 :children []})
;; => true
;; Explain why something doesn't conform
(s/explain :app.specs/tree-node
{:name "A"})
;; => val: {:name "A"} fails spec: :app.specs/tree-node
;; predicate: (contains? % :branch-length)
;; Check a parsed metadata structure
(s/valid? :app.specs/parsed-metadata
{:headers [{:key :Name :label "Name" :width 120}]
:data [{:Name "Alice"}]})
;; => true
Dev-Time Instrumentation
The app.dev-preload namespace is loaded as a shadow-cljs preload in the :app build. It provides:
- Expound — Sets
s/*explain-out*toexpound/printerfor readable error messages when specs fail. stest/instrument— Instruments allfdef’d functions at load time, checking argument specs on every call during development. This catches spec violations early without manual setup.
Instrumentation is automatically stripped from release builds.
Custom Generators
Custom generators for recursive and domain specs live in src/dev/app/spec_generators.cljs. These register generators via s/with-gen for specs like ::tree-node, ::positioned-node, ::metadata-header, and ::metadata-row.
Important: clojure.test.check is only on the :dev/:test classpath. Never require it from src/main/ namespaces.
Property-Based Testing
The app.generative-test namespace contains:
defspectests — Property-based tests for Newick round-trip, tree invariants, date parsing, etc.stest/checktests — Generative testing of fdef’d functions (newick->map,count-tips,parse-date,calculate-scale-unit, etc.)
Shared generators for tests live in src/test/app/generators.cljs.
Component Prop Validation (Dev Only)
Component prop specs are colocated with their components, but the validation logic lives in app.specs:
app.specs.cljdefinesdefui-with-spec, a macro that wrapsdefuiand callsvalidate-spec!in dev.app.specs.cljsdefines the specs and thevalidate-spec!helper.
Example usage:
(defui-with-spec TreeNode
[{:spec :app.specs/tree-node-props :props props
:opts {:check-unexpected-keys? true}}]
($ TreeNode* props))
Notes: - The macro keeps UIx prop handling intact, so props are still converted as usual. - Validation runs only under goog.DEBUG, so release builds are unaffected.
Adding a New Metadata Column Feature
- If you need new column behavior, add it to
csv/parse-metadatainapp.csv - Update the
::metadata-headerspec inapp.specsif the shape changes - The
MetadataColumncomponent inapp.components.metadatahandles rendering;MetadataTablemanages column layout
TSX Component Development
Pure rendering components are being extracted as TypeScript/TSX alongside the existing UIx implementations. This enables future portability to other React projects and Storybook-based component testing. The UIx components remain the canonical implementations for now.
Building TSX components
npm run tsx:build # One-shot compile
npm run tsx:watch # Continuous compile during dev
TSX sources live in src/tsx/components/ and compile to src/gen/components/ (gitignored). The compiled JS is on the shadow-cljs classpath and can be imported from ClojureScript.
Adding a new TSX component
- Create
src/tsx/components/MyComponent.tsx - Import shared types from
./typesand sibling components as needed - Define a props interface — all rendering parameters must come via props (no implicit layout/state dependencies)
- Export a named function component
- Run
npm run tsx:buildto compile - Keep the corresponding UIx component in its
app.components.*namespace in sync
Design guidelines
- TSX components should be pure functions of their props — the ClojureScript layer remains the single source of truth for layout constants and application state.
- Shared TypeScript interfaces (e.g.
PositionedNode) live insrc/tsx/components/types.tsand mirror theclojure.specdefinitions inapp.specs. - When passing complex data structures (e.g. tree nodes) from CLJS to TSX, the CLJS layer must convert from ClojureScript maps to plain JS objects (e.g. via
clj->js). - UIx’s
$macro automatically converts kebab-case keys to camelCase for non-UIx (JS) components, so:parent-xbecomesparentX.
Modifying Tree Layout
The layout algorithm in app.tree/prepare-tree is a multi-step pipeline:
assign-y-coords— depth-first traversal assigning sequential y values to leavesassign-x-coords— depth-first traversal accumulating branch lengths as x valuesassign-node-ids— depth-first traversal assigning unique:idintegers for stable React keysassign-leaf-names— bottom-up traversal precomputing a:leaf-namesset (descendant leaf names) on every node
To change spacing, modify the LAYOUT constant in app.layout. To change the algorithm itself, modify the assign-* functions in app.tree and update corresponding tests in app.tree-test.
Scale tick calculations (shared by the scale bar, sticky header, and gridlines) live in app.scale. Browser file I/O helpers (save-blob!, read-file!) live in app.io. Small shared utilities (client->svg, clamp) live in app.util.
Docker
A multi-stage Dockerfile builds the app with Clojure tooling and serves the static assets with nginx:
docker build -t phylo .
docker run -p 8080:80 phylo
A GitHub Actions workflow (.github/workflows/docker.yml) automatically builds and pushes the image to GitHub Container Registry on pushes to main.