Summary
A top-level function foo() {} declared in <script data-stx-scoped> is reachable as a global in buddy dev, and is not reachable in the built/packaged app. Inline onclick="foo()" handlers in the same template therefore work in development and throw ReferenceError: Can't find variable: foo in production.
Because a packaged desktop app has no console open, this presents as "the button does nothing" with no diagnostic at all.
Reproduction
A view with a scoped block and a template that calls into it from markup:
<script data-stx-scoped>
async function doCleanSelected() { /* ... */ }
</script>
<button onclick='doCleanSelected()'>Clean Selected</button>Serve it with buddy dev → the button works; typeof window.doCleanSelected === 'function'.
Build it (buddy build:views, packaged via build:desktop / build:dmg) and open the app → clicking throws:
error: ReferenceError: Can't find variable: doCleanSelected @http://127.0.0.1:50438/app:1Same source file, same block, opposite behaviour.
What differs between the two outputs
Fetching the same route from each:
| dev | built | |
|---|---|---|
function doCleanSelected inline in the HTML | yes | yes |
_stx/chunk.*.js referenced by the page | 0 | 6 |
So it is not simply "dev inlines, build extracts" — the declaration is present inline in both. The difference appears to be in how the built page's runtime/router executes data-stx-scoped blocks: in the built app the block's top-level declarations do not land on window, whereas in dev they do. The error's location (/app:1, the SPA shell rather than the route's own URL) suggests the router re-executing the block in a scope on navigation.
I have not tried to pin down the exact mechanism inside the runtime — the observable contract is what matters here, and it differs between the two modes.
Why this is worth fixing rather than documenting
The failure is invisible in every environment a developer actually tests in. It only appears in the packaged artifact, where there is no console, so the symptom is a dead button rather than an error. In our case (stacksjs/system-cleaner) four handlers — doCleanSelected, doEmptyTrash, doCleanDir, scanCleanupSizes — were dead in every shipped build while working perfectly in dev.
Either behaviour alone would be fine; it is the divergence that is the bug. If scoping is intended, dev should scope too, so the breakage shows up immediately.
Workaround
Assign explicitly at the end of the block:
window.doCleanSelected = doCleanSelectedWorth noting this is what the framework's own shipped public/disk-panel.js does (window.startDiskScan = function () {...}), which is why that panel's buttons kept working while the scoped-block ones did not.
Possible directions
- Hoist top-level function declarations in a
data-stx-scopedblock towindow, matching dev. - Or scope consistently in dev, and emit a build-time diagnostic when a template's inline
onclick/onchangenames an identifier that the scoped block declares but does not export — that would have turned this into a build error instead of a silent production failure.
Versions
@stacksjs/stx0.2.281- Stacks 0.74.32, Bun 1.4.3
- macOS 26.6.2, arm64