Skip to content

feat(barebuild): write-side alpha — action + invalidate-on, demo port, independent CORS backend#262

Open
avanelsas wants to merge 14 commits into
mainfrom
feat/barebuild-write-side
Open

feat(barebuild): write-side alpha — action + invalidate-on, demo port, independent CORS backend#262
avanelsas wants to merge 14 commits into
mainfrom
feat/barebuild-write-side

Conversation

@avanelsas
Copy link
Copy Markdown
Owner

Summary

Ships the BareBuild write-side alpha (4.0.0-alpha.0) — two new orchestration
elements plus the shared plumbing, dogfooded through the demo app, and a second
backend that proves the elements are server-agnostic.

Alpha: shapes may change before stable; shipped on the alpha dist-tag only.

What's in it

New elements (src/baredom/components/)

  • <barebuild-action> — wraps a submit emitter by containment, POSTs/PUTs the values
    as JSON, publishes the HTTP result as .state + a barebuild-action-state event per
    phase. Optional valuesTransform hook for payload hygiene.
  • <barebuild-invalidate-on> — child of a source; on a when-phase/when-name match
    dispatches the document-level barebuild-invalidate {src} protocol that matching
    <barebuild-data> brokers self-match (origin + pathname + query) and refetch.
  • Shared barebuild/{protocol,lifecycle,listeners} so the read and write sides cannot
    drift; <barebuild-data> refactored onto the shared fetch lifecycle + invalidate.

Demo dogfood (barebuild/demo-app/)

  • Create / update / settings ported onto the declarative elements; deletes stay
    hand-wired but coordinate through the same barebuild-invalidate / barebuild-navigate
    protocols. One shared action-state-handler per flow.

Independent CORS backend — server-agnosticism (latest commit)

  • server/serve_api.clj — an API-only + CORS server (:3001) written from scratch
    (shares no code with serve.clj); server/API-CONTRACT.md documents the contract any
    stack can implement. A ?api=<origin> switch (one CLJS w/api chokepoint, normalized)
    drives the demo cross-origin. e2e/xorigin.spec.ts + a second Playwright webServer
    enforce it in CI.

Verification

  • clj-kondo (src + test) — 0 warnings; bb scripts/check-barebuild-boundary.bb clean.
  • shadow-cljs release lib (Closure Advanced) + shadow-cljs compile test — clean.
  • Full browser test suite green; demo e2e 16/16 (read + write single-origin, plus
    cross-origin against the independent backend).
  • serve.clj and the single-origin bb serve path untouched.

🤖 Generated with Claude Code

avanelsas and others added 14 commits June 2, 2026 16:39
ALPHA (4.0.0-alpha lane, `alpha` dist-tag) — explicitly unstable write-side
coordination, shipped separately from stable so adopters consent to churn.
barebuild-bind is deliberately NOT included (see write-side-design-notes.md).

New elements (three-layer pattern, golden-sample conformant):
- barebuild-action: wraps a submit emitter BY CONTAINMENT (listens on host for
  the configured `submit-event`, no selectors), reads values at `values-path`,
  JSON-fetches `action`/`method`, publishes a plain-JS `.state` + a
  barebuild-action-state {name, state} event per phase transition. AbortController
  teardown + 204-safe body parse mirror barebuild-data. submit(values) method.
- barebuild-invalidate-on: child of its source; listens on parentNode for the
  source event (default barebuild-action-state); when-phase/when-name AND-matchers
  (at least one required); dispatches document-level barebuild-invalidate {src}.
  invalidate() method.

Substrate (additive, non-breaking):
- barebuild-data now listens at document for barebuild-invalidate and refetches
  when detail.src matches its own src by exact URL.pathname. Listener added on
  connect, removed on disconnect (it lives on document, so it must not leak).

Build registration: shadow-cljs :lib modules, package.json exports, registry
barebuild-registers (kept OUT of the UI kitchen-sink all-registers).

Avoided a 40 KB trap: values-path parsing uses a tiny hand-parser, not
cljs.reader (which dragged the whole reader into the module — 44 KB → 4.5 KB,
now in line with barebuild-data at 3.1 KB). The EDN path DSL was speculative
surface the telemetry showed nobody needed anyway.

clj-kondo clean; shadow-cljs release lib (Closure Advanced) 0 warnings;
du-discipline guard clean. Tests + demo port + alpha docs are the next step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…red lifecycle, tests, docs)

Builds on the scaffold: extracts the shared spine, adds tests/demos/docs, and
folds in the idiom + Hickey review findings. ALPHA (4.0.0-alpha lane, `alpha`
dist-tag) — explicitly unstable; barebuild-bind still deferred.

Shared spine (src/baredom/components/barebuild/):
- protocol.cljs: single source of truth for the cross-component event-name
  handshake (data-state / action-state / invalidate) so the independently
  shipped ESM modules can't drift. Each model re-exports the names it uses.
- lifecycle.cljs: shared fetch + state-transition shell for both server-state
  brokers (abort stale-callback guard, 204/empty-body → success(nil) parse,
  AbortError suppression, shallow change guard). Read/write sides cannot drift.

Elements:
- barebuild-action: submit-emitter-by-containment, values-path, JSON fetch,
  barebuild-action-state {name,state}, submit() method.
- barebuild-invalidate-on: parentNode source listener, when-phase/when-name
  AND-matchers, document-level barebuild-invalidate {src}, invalidate() method.
- barebuild-data: extended additively to refetch on barebuild-invalidate
  (exact URL.pathname match; document listener added/removed with connect).

Review-driven refinements:
- payload-fn accessor (not a :payload-key string) in the lifecycle change guard:
  extern-safe, co-located with intent; removed the last gobj coupling. + a
  "two distinct payloads, same phase+status, re-fire" regression test.
- start-fetch! no longer mutates the caller's init literal (Object.assign).
- matches? is a pure predicate; the no-matcher diagnostic warns ONCE at connect
  (not per event). du/get-attr-trimmed centralizes "whitespace = absent" across
  the brokers (dropped mu from action + invalidate-on).

Tests for both new elements + the data invalidate path; demo HTML; per-element
docs reconciled to what shipped; README + components.md showcase entries.

clj-kondo clean; release lib (Closure Advanced) 0 warnings; full suite 5183 SUCCESS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t bugs the port found

The "would I use it?" graduation test: port the demo's hand-wired write side onto
the alpha declarative elements. Started with CREATE (the flagship, modal-based).

Two REAL barebuild-action bugs the port surfaced (both fixed):
- Missing <slot>: ensure-shadow! passed slot? = false (copied from the childless
  barebuild-data), so the wrapped form stayed in the light DOM and FUNCTIONED but
  never RENDERED (0×0). A content-wrapper needs a slot. Fixed: slot? = true.
- :host{display:none} → display:block: a wrapper must not hide its content
  (display:contents collapses it as a flex-item; block is layout-neutral).
Root cause of both: a containment-wrapper can't be cloned from a leaf broker.

CREATE port (works; full Playwright e2e 11/11, incl. a new write-path test):
- index.html: <barebuild-action> wraps the MODAL (not the form, which would displace
  it as x-modal's slotted child and break ::slotted layout); <barebuild-invalidate-on>
  refetches /api/tasks on success. core.cljs registers the two elements.
- write_side: on-create-submit! (fetch+refresh) → on-create-state!, which only reacts
  to barebuild-action-state for the side-effects the elements DON'T cover (close modal,
  reset, toast). without-blanks dropped — exposing the payload-cleanliness gap (the
  action encodes values as-is, so status comes through "").

Evaluation recorded in write-side-design-notes.md: declarative fetch+invalidate is
real and less code for the canonical flow, but every flow still needs imperative
side-effects, and update/settings (dynamic URLs, number coercion) + the two deletes
(no-values / dynamic-row triggers) are deferred/poor-fit. UPDATE/SETTINGS/DELETE
remain hand-wired pending a decision on the full port.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tive, deletes via protocol

Completes the demo port onto the alpha elements (Playwright e2e 14/14).

UPDATE + SETTINGS — now declarative:
- index.html: <barebuild-action> wraps the edit form (PUT) and the settings form (PUT),
  each with a child <barebuild-invalidate-on>. The edit action's dynamic /api/tasks/:id
  `action`/`src` is set in detail/on-route-change (the one imperative line a param route
  needs). write_side replaces on-edit-submit!/on-settings-save! with a shared
  toasting-state-handler (the only uncovered side-effect is the toast).

DELETE — hand-wired trigger, PROTOCOL coordination (the recommended shape):
- The action can't drive deletes (a confirm dialogue and N per-id row buttons carry no
  form values; the row case is a dynamic list → event delegation). So the trigger stays
  hand-wired, but coordination goes through the SAME document protocols the declarative
  flows use: detail-delete dispatches barebuild-navigate; row-delete dispatches
  barebuild-invalidate {src} (new dispatch-invalidate! helper) — the identical event
  <barebuild-invalidate-on> emits. Board refetches the same way regardless of trigger.

The reframe this produced: barebuild-invalidate / barebuild-navigate are PUBLIC PROTOCOLS,
the elements are sugar over them. So "DELETE doesn't fit barebuild-action" dissolves —
its trigger is correctly hand-wired, its coordination uniformly declarative.

Docs: write-side-design-notes.md evaluation rewritten for the full port (the protocol
reframe + residual gaps: payload cleanliness — status "" / page-size string, slotted-wrapper
placement). docs/barebuild-action.md gains the rendering/placement corrections (slot,
display:block, wrap-the-host) and the protocol framing. Removed now-dead write_side code
(with-number, refresh-data!, api-settings, on-edit-submit!, on-settings-save!).

clj-kondo clean; demo compiles 0 warnings; full e2e 14/14.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Folds in the post-review refactor and the final-audit fixes; keeps the branch
internally clean and CI-green (still an unmerged alpha — see below).

Post-review (shared abstractions + the payload seam):
- Extract barebuild/listeners.cljs: the bind/detach/rebind listener mechanics
  shared by barebuild-action (host) and barebuild-invalidate-on (parent), so the
  two can't drift. lifecycle.cljs stays the shared fetch/state-transition shell.
- barebuild-action gains a `valuesTransform` public property — the seam for payload
  hygiene the action can't itself know. Closes the port's payload-cleanliness gap:
  the demo installs without-blanks (create) + with-number (settings) through it, so
  create no longer POSTs status:"" and settings persists page-size as a number.
  (update gets none — PUT/merge treats a blank as a deliberate clear.)
- action `name` is trimmed (get-attr-trimmed) to match invalidate-on's when-name.

Audit fixes:
- Boundary-check CI was failing on the literal word "querySelector" in a comment
  (the code has none); reworded so `check-barebuild-boundary.bb` passes.
- do-submit! now guards a nil body (a valuesTransform returning nil, or .submit(nil))
  — no more silent "null"-body POST; the imperative entry gets the same guard as the
  event path.
- Dedupe the "httpStatus" state-field literal into protocol/field-http-status (used by
  both barebuild-data and barebuild-action model constructors).
- Drop the now-stale "page-size persists as a string" comment in the demo index.html.

NOT YET MERGEABLE TO MAIN (deliberate): main has no alpha dist-tag lane, so these
elements would ship on the stable `latest` tag on the next release. Keep as the
long-lived alpha branch until the alpha-publish machinery exists.

clj-kondo 0/0; du-discipline + barebuild-boundary pass; release lib (Closure) 0
warnings; karma 5188; demo e2e 14/14.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The create / update / settings flows all publish the same
barebuild-action-state, but the phase→toast skeleton was duplicated
across on-create-state! and toasting-state-handler. Collapse them into a
single action-state-handler factory (toast by phase + optional
success-only effect), and isolate create's modal-only side-effect as
close-and-reset-new-task! passed in as a value. Edit/settings are inline
forms their own invalidate-on refetch re-fills, so they need only a toast.

One source of truth for the happy/error path; the per-flow difference now
reads off the call site as data rather than separate handler bodies.

Verified: clj-kondo clean, shadow-cljs release app (advanced) clean,
write-path e2e 4/4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
package.json version → 4.0.0-alpha.0 + CHANGELOG entry, on feat/barebuild-write-side.
Tagging v4.0.0-alpha.0 will publish on the npm `alpha` dist-tag (per the lane added in
#261); `latest` and the stable 3.x line are unaffected. build.clj / deps.edn / README
stay on 3.x deliberately — the Clojars deploy is skipped for pre-releases and the README
documents the stable install.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cism

Add a second, INDEPENDENT backend for the demo so the BareBuild elements are
shown to work against any server honouring the contract — not just the original
babashka one. The elements are vanilla fetch + CustomEvents holding no client
state (DOM = f(server value)), so a backend is valid iff it speaks the contract
and is the source of truth.

- server/serve_api.clj: API-only + CORS server (port 3001), written from scratch
  (shares no code with serve.clj). Mirrors the contract exactly: POST 201 +
  status:"todo" default, PUT/PATCH/settings merge, DELETE 204, 404/405, monotonic
  ids. `bb serve-api` task added.
- server/API-CONTRACT.md: the contract as a standalone spec, so a third (cross-
  stack) server can be dropped in with no guesswork.
- Cross-origin switch (CLJS-only): wiring/api-base reads ?api once, normalizes
  (strips trailing slash, warns on a missing scheme), and every API URL flows
  through one w/api chokepoint. write_side re-points the declarative create/
  settings actions at the base only in cross-origin mode; the default single-
  origin path leaves the relative HTML literals untouched.
- e2e: playwright.config boots a second webServer (bb serve-api); xorigin.spec.ts
  proves a page on :3000 drives the :3001 backend over CORS (incl. trailing-slash
  base + the settings preflight), enforcing the agnosticism claim in CI.

serve.clj, the library, and the single-origin bb serve / e2e path are untouched.
Verified: barebuild-boundary check, clj-kondo, shadow-cljs release app (advanced),
bb load of serve_api.clj, e2e 16/16 (incl. cross-origin).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dom alpha

The `^3.4.0` pin was a placeholder from before the write-side elements shipped, so
a plain `npm install` (and thus `bb serve`) failed with ETARGET. The alpha is now on
npm (`4.0.0-alpha.0`, `alpha` dist-tag), so bump the pin to `^4.0.0-alpha.0` — the
demo resolves from the registry and the one-time local-pack seed is no longer needed.

CI's demo-e2e still packs+installs the freshly built lib OVER this pin, so it keeps
testing the local build, not the published one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
x-select's `.value` returned the value ATTRIBUTE, which a user selection never updates
(only the inner <select> + the dispatched select-change event changed). So consumers that
read `el.value` — x-form's collect-values, the demo's status filter — saw a stale value:
the filter looked dead, and forms silently submitted the pre-selection value.

Every other BareDOM form control makes `.value`/`.checked` reflect the current value (the
contract x-form relies on); x-select was the lone exception. Override its `value` getter to
read the live inner <select>, with one disambiguation: a non-empty value set BEFORE its
<option> exists (async-loaded options) is surfaced as pending via the attribute until the
option arrives. This preserves both existing tests (value-attr-not-auto-set-on-change,
string-property-value) and adds two regression tests (selection reflection; set-before-options).

clj-kondo clean; full karma 5190.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Carries the x-select .value-reflection fix (#263) into the write-side
alpha channel. Element surface unchanged from 4.0.0-alpha.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…stic demo)

Replace the `?api=<origin>` URL switch with a dogfooded navbar <x-select> backend
picker, so a demo user swaps which server the app talks to with a click — the
interactive proof that BareBuild works against any server honouring the contract.

- wiring/backends is the whole story as DATA: each row {:value :label :base}; adding
  one (e.g. a future Node server on :3002) is a single row + that server implementing
  API-CONTRACT.md with CORS. `active-backend` resolves once at load (?backend deep-link
  → sticky sessionStorage → default same-origin), api-base is normalised (trailing
  slash stripped), every API URL flows through w/api.
- Resolve (pure) is de-complected from persist: capture-backend! (validated deep-link
  capture) + select-backend! both go through one store-backend!; the load-time env
  reads are documented as the deliberate epochal choice.
- core/init-backend-select! populates the picker from backends, reflects the active
  choice, and on a real change persists + reloads (brokers re-fetch cleanly). The
  choice is sticky for the tab, so in-app navigation/reload no longer revert it.
- Pin bumped to the published @vanelsas/baredom@^4.0.0-alpha.1.

a11y: the x-select carries aria-label. e2e: drives via ?backend=bb-cors; adds picker-
switch, navigation-persistence, and upgrade-safety (no spurious reload) tests; the
playwright config boots both servers so the cross-origin path is enforced in CI.

Verified: clj-kondo, barebuild-boundary, shadow-cljs release app (advanced), bb load
of serve_api.clj, e2e 19/19. serve.clj and the single-origin path are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant