hive-cljs reads its config from either or both of two files, found by walking up from the directory you invoked it in. Nothing is required beyond one build id.
| File | Shape | Use when |
|---|---|---|
.hive-project.edn | :hive.cljs {…} submap, or flat :hive.cljs/* keys | the project is already a hive project — one descriptor, no extra file |
hive-cljs.edn | flat :hive.cljs/* keys | standalone project, or you want scenarios in their own file |
Both present ⇒ they merge one level deep, and hive-cljs.edn wins key by key.
So a descriptor can hold connectivity while the dedicated file holds scenarios:
;; .hive-project.edn
{:project-id "my-app"
:hive.cljs {:shadow {:port 9630 :nrepl-port 7889}
:builds {:app {:http-port 8280}}}}
;; hive-cljs.edn — overrides :port, keeps :nrepl-port, adds :e2e
{:hive.cljs/shadow {:port 9633}
:hive.cljs/e2e {:scenarios [{:id :smoke :steps [[:goto "/"]]}]}}
resolves to shadow {:port 9633 :nrepl-port 7889}, builds [:app], scenarios
[:smoke].
Inside .hive-project.edn both spellings work — nested short keys
(:hive.cljs {:builds …}) and flat namespaced keys (:hive.cljs/builds …).
Flat wins on collision. Descriptor keys that are not hive-cljs's (:project-id,
:carto, …) are ignored.
Two separate mechanisms, deliberately: one always on, one you ask for.
Discovery is unconditional. Resolution walks up from directory to the
nearest ancestor that authors hive-cljs config, exactly as git walks up to the
nearest .gitignore. That directory becomes :manifest/root. So cljs doctor
works from a subdirectory:
my-app/.hive-project.edn ← authors config
my-app/src/frontend/widgets/ ← invoke from here; root resolves to my-app/
Because :manifest/root moves, :artifacts-dir and the inferred :base-url
derive from the project, not from where you happened to stand.
Inheritance is opt-in. An ancestor that also authors config contributes nothing unless the nearest level asks for it:
;; workspace/.hive-project.edn — shared conventions
{:project-id "workspace"
:hive.cljs {:e2e {:browser :chromium :headless true :timeout-ms 15000}}}
;; workspace/my-app/hive-cljs.edn — opts in, overrides one key
{:hive.cljs/inherit true
:hive.cljs/e2e {:timeout-ms 30000}
:hive.cljs/builds {:app {:http-port 8280}}}
resolves to chromium, headless, :timeout-ms 30000. Drop :hive.cljs/inherit
and the workspace defaults are simply not consulted — a parent descriptor can
never silently change a child's :base-url or :port behind your back.
:manifest/sources lists every file that contributed, highest precedence
first: hive-cljs.edn before .hive-project.edn within a level, nearest level
before its ancestors. cljs doctor surfaces it, so the question "where did this
value come from, and what would I have to change to override it?" has one answer.
No level authors config ⇒ :manifest/not-found, carrying :searched (every path
examined on the way up) and :candidates — directories below you that do author
config, found to a bounded depth. Invoking from a workspace root with four apps
underneath lists all four rather than guessing one.
#hive/env — values from the environmentAny config value may be read from the environment, for harnesses that assign ports at run time:
{:hive.cljs/shadow {:port #hive/env SHADOW_PORT
:nrepl-port #hive/env [SHADOW_NREPL_PORT 7889]}}
Bare form: the variable's value, or nil when unset. Vector form: the second
element is the fallback for unset-or-blank. A value that reads as an integer is
coerced to a long, so :port type-checks without a wrapper.
:hive.cljs/shadow — connectivity{:host "localhost" ; default
:port 9630 ; default — the shadow HTTP/relay port
:nrepl-port 7889} ; NO default; omit and the runtime channel is :down
:port must match what shadow actually bound, which is not always 9630 — it
takes the next free port with only a warning. :nrepl-port is shadow-cljs.edn's
:nrepl {:port …}; without it, :eval, :expect-sub, :expect-db and
:dispatch report :cljs-eval/no-nrepl-port.
:hive.cljs/builds — build targets{:app {:shadow/id :app ; defaults to the map key
:http-port 8280 ; the dev-http port; used to infer :base-url
:entry "/"}} ; optional
A scenario that names no :build inherits the project's build when there is
exactly one. With two or more the choice is ambiguous, :plan/build is left
unset, and any runtime step returns a typed error telling you to set :build.
:hive.cljs/e2e — browser and scenarios{:base-url "http://localhost:8280" ; inferred from a build's :http-port
:browser :chromium ; :chromium | :firefox | :webkit
:headless true
:timeout-ms 15000
:artifacts-dir "<root>/.hive-cljs/artifacts"
:scenarios [{:id :login ; required
:build :app ; optional — inherited if unambiguous
:tags [:smoke] ; optional — selects for `e2e run` / watch
:doc "…" ; optional
:steps [[:goto "/"] …]}]}
Relative :goto URLs resolve against :base-url; absolute ones pass through.
Screenshots land in :artifacts-dir and are listed in the run report's
:run/artifacts.
Step vocabulary: steps.md.
:hive.cljs/watch — build → e2e coupling{:on-build-success [[:run-e2e {:tags [:smoke]}]]
:on-build-failure [] ; default: report the failure
:debounce-ms 500
:builds #{:app}} ; optional — omit to watch all builds
Actions are [kind opts] tuples; a bare keyword or 1-element vector is accepted
and normalized. Selectors for :run-e2e: {:scenarios [:a :b]}, {:tags [:smoke]},
or neither, which runs every scenario.
The watcher always produces a decision, including :ignore with a reason
("within debounce window", "build not watched by policy",
"no :on-build-success actions configured"), so cljs watch status explains why
nothing ran.
| Key | Default |
|---|---|
shadow :host / :port | "localhost" / 9630 |
shadow :nrepl-port | none — runtime channel disabled |
e2e :base-url | http://localhost:<first build's :http-port>, else http://localhost:8080 |
e2e :browser / :headless / :timeout-ms | :chromium / true / 15000 |
e2e :artifacts-dir | <root>/.hive-cljs/artifacts |
watch :debounce-ms | 500 |
watch :on-build-success / :on-build-failure | [] / [] |
watch :builds | all builds |
The normalized manifest is checked against a closed malli schema. A bad value is
returned as :manifest/invalid with a humanized explanation rather than throwing:
{:error :manifest/invalid
:explain {:manifest/shadow {:port ["should be an integer"]}}}
Unparseable EDN gives :manifest/unreadable with the cause.
cljs stalenessA session caches the resolved manifest. cljs staleness reports whether that
cached view still describes reality:
code {command: "cljs staleness", directory: "/path/to/my-app"}
{:staleness/manifest :stale
:staleness/sources [{:source/path "…/hive-cljs.edn" :source/exists? true
:source/modified 1753… :source/size 412} …]
:staleness/server :mismatch
:staleness/declared-builds [:app]
:staleness/reported-builds [:other-app]}
| Key | Reads |
|---|---|
:staleness/manifest | :stale when a contributing file changed on disk since the session opened |
:staleness/server | :ok when the connected server serves a build we declare, :mismatch when the sets are disjoint, :unknown when either side is empty |
:mismatch is the port-drift trap from setup.md step 3 made visible:
you are talking to another project's shadow server, and every verdict it gives
you is confident and wrong. cljs doctor carries the same verdict under
:server, plus a :warnings entry spelling it out.
Both axes need two witnesses. Nothing recorded on either side reports :unknown,
never a reassuring :ok — an unverified claim is not a passing one.
Reading the report is not required to get fresh config: any command reopens the
session automatically when its sources changed. staleness reports against the
view as it was before the call, so an edit is still visible in the report that
refreshes it.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |