Liking cljdoc? Tell your friends :D

Piggieback Roadmap

A plan for evolving Piggieback's internals. This is a direction, not a contract: items can be reordered or dropped as we learn more. For how the current code works, see the architecture document.

Guiding principle

Piggieback's job is narrow and worth doing well: be the thin, correct adapter between nREPL and the official cljs.repl environments, and CIDER's built-in default cljs REPL. shadow-cljs and figwheel-main own the heavyweight build-tool space and bring their own nREPL integration, so Piggieback should not chase features they already cover (hot reload, asset serving, file watching). Almost every item below is "make the adapter simpler or more correct," not "add surface."

Sequencing rationale

The instinct to start small is right, and for a stronger reason than low risk: two of the small items are the seams that make the one scary refactor safe.

  • M1 (centralize nREPL-version compatibility) and M2 (isolate the ClojureScript-internals coupling into one thin eval core) carve out clean boundaries. Once they exist, B1 (own evaluation, drop the repl* driving) is a change behind a known interface instead of open-heart surgery.
  • C1 and C2 are standalone correctness wins with no dependency on anything else, so they ride along early and ship value immediately.

So the order is: seams and correctness first, structural simplification next, the core refactor once the seams are in place, and upstream work filed early because it has the longest lead time.

Status

  • Done: tooling modernization. CI matrix (CircleCI, JDK 8-25, Clojure 1.10-1.12, nREPL 1.0-1.7), deps.edn alongside Leiningen, clj-kondo added, cljfmt and Eastwood on current versions, dependencies refreshed.
  • Done: the Phase 2 correctness bugs from the 0.6.2 release (#62, #111, #120, #124).
  • Done: M1 - cross-nREPL-version compatibility centralized into the cider.piggieback.compat namespace.
  • Done: M2 - ClojureScript-internals coupling isolated into the cider.piggieback.cljs core namespace; the middleware no longer requires any cljs.* namespace directly.
  • Done: M5 - the interrupt limitation is documented in the README.
  • Done: M3 - the describe response surfaces per-session ClojureScript status via a :describe-fn.
  • Done: C1 - closing a session tears down an active ClojureScript repl-env, via the session's :close metadata hook.
  • Done: C2 - load-file evaluates the source sent in the message (unsaved buffers included) instead of re-reading from disk.
  • Done: S1 - the three-file load/in-ns structure is gone; Piggieback is now a public cider.piggieback namespace that lazily loads the cider.piggieback.cljs implementation, and the clj-kondo workarounds it forced are removed.
  • Done: B1 (pragmatic scope) - removed the worst setup warts: the per-class eval-codegen delegating env is now a single DelegatingReplEnv type, and the :print side-channel is gone (the session ns is set explicitly after setup). Full setup-ownership was deferred (see below).

Phase 1 - Seams and small modernizations

Low risk, high clarity, and partly preparatory for Phase 4.

M1 - Centralize cross-nREPL-version compatibility (done)

Today, support for the supported nREPL range is handled by resolve-based feature detection scattered through the implementation (nrepl-1-3+?, replying-PrintWriter lookup, the enqueue double-binding dance). Pull these into one small compatibility namespace with named, documented predicates and shims.

  • Why: one place to reason about version differences; less noise in the eval path.
  • Risk: low. Pure refactor, behaviour-preserving, fully covered by the nREPL matrix in CI.

M2 - Isolate the ClojureScript-internals coupling (done)

Gather the direct reaches into cljs.analyzer, cljs.env, cljs.closure, cljs.tagged-literals, and the non-public parts of cljs.repl into a single, thin, version-guarded "cljs eval core" namespace. The handler code should talk to that core, not to compiler internals directly.

  • Why: this is the seam B1 needs. It also localizes the fragility, so a breaking ClojureScript change touches one file.
  • Risk: low to moderate. Mechanical extraction, but it touches the hot path, so lean on the existing tests and add characterization tests where thin.
  • Prerequisite for: B1.

M3 - Surface ClojureScript state in describe (done)

Advertise Piggieback in the describe response and expose whether a session is currently in ClojureScript mode (and ideally which repl-env). Optionally echo cljs-ness on eval responses.

  • Why: tooling currently infers ClojureScript mode out of band. A protocol-level signal lets clients stop guessing.
  • Risk: low. Additive.

M4 - Robust print-mode detection

Replace the current string-name matching against specific print functions with a more robust signal for choosing plain vs pretty printing.

  • Why: the present check breaks the moment a different pretty-printer is used.
  • Risk: low.

M5 - Document the interrupt limitation (done)

A long-running JS eval cannot be cancelled cleanly. Document this clearly rather than leaving it implicit, and decide whether any best-effort behaviour is worth offering.

  • Why: sets correct expectations; cheap.
  • Risk: none (documentation).

Phase 2 - Standalone correctness wins

Independent of everything else; ship as soon as ready.

C1 - Tear down on session close (done)

Register cleanup on session close / expiry so the active repl-env's -tear-down runs even when the client never sends :cljs/quit (dropped connection, editor killed, client exit). Today the node subprocess or browser connection leaks.

  • Why: a real resource leak that bites anyone who closes an editor without quitting the cljs REPL first.
  • Risk: low to moderate. Needs care around nREPL session lifecycle hooks and not double-tearing-down on a normal quit. Add a regression test.

C2 - load-file evaluates the sent content (done)

Make load-file compile the source provided in the message against the right namespace and path, rather than re-reading the file from disk. This matches Clojure nREPL semantics ("load buffer" should load what is in the buffer).

  • Why: loading an unsaved buffer currently loads the last-saved version, silently.
  • Risk: moderate. Touches the load-file path and the path/namespace handling; needs tests for both saved and unsaved content.

Phase 3 - Structural simplification

S1 - Single namespace via requiring-resolve (done)

Collapse the three-file if-ns + (load ...) + (in-ns) structure into one namespace that resolves the ClojureScript machinery lazily at first use and returns a no-op middleware when ClojureScript is absent.

  • Why: removes the redefined-var / unresolved-symbol friction we currently paper over in clj-kondo config, and reads far more honestly. Clojure 1.10 (our floor) has requiring-resolve.
  • Risk: moderate. The no-ClojureScript path must stay a genuine no-op; test both with and without ClojureScript on the classpath.

Phase 4 - The core refactor

B1 - Tame the repl* setup warts (pragmatic scope, done)

The original plan was to stop driving cljs.repl/repl* for setup entirely: run -setup once, establish the analyzer/compiler bindings explicitly, run the initial requires as an ordinary evaluate-form, and use one uniform evaluation path for everything. Investigating it showed that owning setup means replicating repl*'s version-sensitive setup block (the analyzer bindings, the warnings merge, with-core-cljs/-setup, merge-opts) - trading coupling to repl*'s loop for coupling to its setup internals, with the matrix only exercising the Node env.

So the pragmatic subset was done instead, keeping repl* for the setup it does correctly while removing the two warts that don't earn their keep:

  • The per-class eval-codegen delegating env became a single DelegatingReplEnv type. The optional cljs.repl protocols are delegated to the wrapped env when it implements them and otherwise fall back to cljs.repl's own default, so one type mirrors each env per instance without generating a class per env class.
  • The :print side-channel is gone. The #62 fix already meant the compiler env is created up front and captured unconditionally; the post-setup namespace is always cljs.user, so the session ns is set explicitly after setup rather than smuggled out through :print.

B1-full - Own setup outright (deferred)

Still open, if the appetite and broader env coverage (browser, graal) arrive: fully replicate setup and collapse to a single evaluation path, deleting the delegating env entirely.

  • Why: a single eval path is conceptually cleaner and removes the last need for the delegating env.
  • Risk: high, and it replicates fragile repl* internals; the pragmatic scope already captured most of the maintainability win at low risk.

Phase 5 - Upstream work

File these early; they have the longest lead time and help the wider ecosystem (shadow-cljs and figwheel-main hit the same walls).

U1 - cljs.repl.node per-thread state (issues #105, #88)

cljs.repl.node keys its results/output state by thread name, which collapses nREPL's worker threads and prevents two node REPLs in one JVM. Propose keying by env identity instead. Until/unless it lands upstream, a vendored corrected node env is a fallback.

  • Why: lifts the "one node REPL per JVM" ceiling, which increasingly bites (two editor windows, or node plus browser at once).
  • Risk: the change itself is small; the lead time is in coordinating upstream.

U2 - node env *out* capture (root cause of #111)

Propose letting the node env take an explicit writer / output-fn instead of capturing *out* at setup. If accepted, Piggieback's forwarding-writer workaround can eventually be retired.

  • Why: removes load-bearing cleverness in favour of a clean upstream contract.
  • Risk: long lead time; the local workaround stays until it lands.

Dependency summary

flowchart LR
    M1["M1 nREPL compat seam"] --> B1["B1 own evaluation"]
    M2["M2 cljs eval core seam"] --> B1
    C1["C1 session-close teardown"]
    C2["C2 load-file content"]
    S1["S1 single-namespace rewrite"]
    M3["M3 describe surfaces cljs"]
    M4["M4 robust print mode"]
    M5["M5 document interrupt"]
    U1["U1 node per-thread state"]
    U2["U2 node *out* capture"]

    U2 -.->|"eventually retires"| FW["forwarding-writer workaround"]

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close