Summary
A component that receives a reactive binding from its parent is served as its own un-evaluated template by buddy serve, while bun run build renders the same component correctly at the same commit. The served page ships the component's internal expressions verbatim:
<div data-stx-parent-bindings="loading" :loading="loading()" data-stx-scope="stx_button_2_1tf9xn"
data-stx-props="{"type":"submit","fullWidth":"true","className":"btn-accent"}">
<button type="submit" class="{{ buttonClasses }}" :disabled="isDisabled()"
aria-busy="{{ loading ? 'true' : 'false' }}" :aria-busy="isLoading() ? 'true' : 'false'">class="{{ buttonClasses }}" reaches the browser as that literal string. It is not a hydration race — reading the DOM on the deployed page well after load gives:
{ "classAttr": "{{ buttonClasses }}", "background": "rgba(0, 0, 0, 0)",
"padding": "0px", "borderRadius": "0px", "width": 51.6 }The sign-in button of a production app was a bare run of unstyled text for as long as this was deployed.
The two paths disagree
Same commit, same page, same component:
<button> as emitted | |
|---|---|
bun run build → dist/login.html | class="inline-flex items-center justify-center font-semibold rounded-control transition-all … bg-accent-solid hover:bg-accent-solid-hover text-accent-ink shadow-md … w-full btn-accent" |
buddy serve (production) | class="{{ buttonClasses }}" |
The build output is correct, so nothing that inspects dist/ can detect this.
Scope: components with a parent reactive binding
Within the same app and the same deploy:
<Breadcrumb>— static props only — renders correctly on four live public pages:<nav class="flex items-center flex-wrap gap-2 text-sm shell subnav" aria-label="Breadcrumb">. Nodata-stx-scopewrapper, no raw expressions.<Button>— passed:loading="loading()"from the page — is wrapped indata-stx-scope/data-stx-props/data-stx-parent-bindings="loading"and its template is emitted raw.
The wrapper names the binding that distinguishes them (data-stx-parent-bindings="loading"), which suggests receiving a parent reactive binding is what switches the component into a client-hydrated mode whose hydration then never resolves the template under buddy serve. I have verified the two outcomes above directly; the mechanism is inference from the markup, not something I could isolate further from outside.
Affected in this app: 26 <Button> call sites across 9 files, every one of them passing :loading or :disabled.
Why nothing caught it
Worth stating, because the silence is the dangerous part. All of these were clean at the commit that shipped the broken page:
stx typecheck— 0 errors, 0 warnings across 62 script blocks and 615 template expressionstsc --noEmit— 0- lint — 0 errors, 0 warnings over 155 files
- test suite — 185 pass
bun run build— 33/33 pagesrelease:validate— passes, becausedist/is genuinely correct
The one existing check for this class of problem looks for {{ via document.body.innerText, and every leftover here is in an attribute, which innerText cannot contain.
A served attribute value never legitimately contains {{, so emitting one is always a bug. If the renderer cannot evaluate a component, failing loudly would be far better than serving its template — this is the same argument as #2004 and #2035, now with a third shape.
Environment
@stacksjs/components0.2.369,@stacksjs/stx0.2.369,bun-plugin-stx0.2.369stacks0.74.66, Bun 1.4.3-canary.1- Served by
bun node_modules/@stacksjs/buddy/dist/cli.js serve, which routes tobun-plugin-stx/serveagainstresources/views(notdist/)