feat(barebuild): write-side alpha — action + invalidate-on, demo port, independent CORS backend#262
Open
avanelsas wants to merge 14 commits into
Open
feat(barebuild): write-side alpha — action + invalidate-on, demo port, independent CORS backend#262avanelsas wants to merge 14 commits into
avanelsas wants to merge 14 commits into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
What's in it
New elements (
src/baredom/components/)<barebuild-action>— wraps a submit emitter by containment, POSTs/PUTs the valuesas JSON, publishes the HTTP result as
.state+ abarebuild-action-stateevent perphase. Optional
valuesTransformhook for payload hygiene.<barebuild-invalidate-on>— child of a source; on awhen-phase/when-namematchdispatches the document-level
barebuild-invalidate {src}protocol that matching<barebuild-data>brokers self-match (origin + pathname + query) and refetch.barebuild/{protocol,lifecycle,listeners}so the read and write sides cannotdrift;
<barebuild-data>refactored onto the shared fetch lifecycle + invalidate.Demo dogfood (
barebuild/demo-app/)hand-wired but coordinate through the same
barebuild-invalidate/barebuild-navigateprotocols. One shared
action-state-handlerper 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.mddocuments the contract anystack can implement. A
?api=<origin>switch (one CLJSw/apichokepoint, normalized)drives the demo cross-origin.
e2e/xorigin.spec.ts+ a second PlaywrightwebServerenforce it in CI.
Verification
bb scripts/check-barebuild-boundary.bbclean.shadow-cljs release lib(Closure Advanced) +shadow-cljs compile test— clean.cross-origin against the independent backend).
serve.cljand the single-originbb servepath untouched.🤖 Generated with Claude Code