Summary
A <script client> block placed at the top of a view file (resources/views/**/*.stx) is silently dropped at compile time. No scope is registered, no state/derived/onMount/event handlers fire. The HTML renders fine, the directives (:if, :for, x-model, @click, @submit) appear in the output, but the runtime has nothing to wire them to.
The same <script client> placed inside a component (resources/components/**/*.stx, included via @include) works correctly.
There is no compile warning. No runtime warning. The page just doesn't react.
Reproduction
{{-- resources/views/search.stx — DOES NOT WORK --}}
@extends('layouts/default')
<script client>
const search = useStore('search')
function submitSearch(): void {
console.log('submit fired')
search.updateUrlQuery()
}
// Top-level call: should run on hydration.
console.log('view-level script ran')
search.initFromUrl()
</script>
@section('content')
<form @submit.prevent="submitSearch()">
<input x-model="search.query" />
<button type="submit">Search</button>
</form>
@endsectionOutput HTML contains the <form @submit.prevent="submitSearch()"> markup, but:
view-level script rannever logssearch.initFromUrl()never runs (search query input never gets pre-populated from URL params)submitSearchis undefined in the scope — the form's@submit.preventfires but resolves to nothingx-model="search.query"doesn't bind two-way
$ curl -s http://localhost:4000/search | grep -oE 'stx_scope_[A-Za-z]+_[0-9]+' | sort -u
stx_scope_BenchHeader_26 # only the layout's header gets a scope
# no scope for the view itselfWorkaround: move every <script client> block into a component, include it from the view as a thin shell:
{{-- resources/views/search.stx --}}
@extends('layouts/default')
@section('content')
@include('Bench/SearchView')
@endsection{{-- resources/components/Bench/SearchView.stx --}}
<script client>
const search = useStore('search')
function submitSearch(): void { ... }
search.initFromUrl()
</script>
<form @submit.prevent="submitSearch()"> ... </form>With the move, the scope registers and everything works.
Why it's bad
- Silent. No compile warning, no console warning. The page renders. The directives are in the DOM. Nothing fires. Hours-of-debugging-time silent.
- Looks correct. View files have
<script client>blocks throughout the framework's own example templates and documentation. There's no obvious reason a view-level block shouldn't behave like a component-level block. - Cascades. Once view-level scripts are silently dropped, every downstream symptom looks like a different bug: "search doesn't fire", "x-model doesn't sync", "onMount doesn't fire", "the form @submit isn't bound" — each of those gets debugged in isolation before you trace back to the view boundary.
Expected behaviour
One of:
(A) Treat view-level <script client> exactly like component-level. The view file gets its own scope. Bindings work.
(B) Emit a clear compile-time warning when a <script client> is encountered at view level. Document the constraint.
(A) is much better DX. The thin-shell workaround means every non-trivial view has to be split into views/x.stx + components/Bench/X.stx, doubling the file count for no semantic reason. (B) is a strict improvement over the current silent drop, even if (A) is on the roadmap.
Environment
@stacksjs/stx@0.2.64bun-plugin-stx@0.2.64- View at
resources/views/search.stx, layout atresources/layouts/default.stx - Components at
resources/components/Bench/*.stxwork fine; only view-level fails
Related
This is the root cause behind a class of "x doesn't work" reports in real projects. Once you internalise the rule "views are thin shells, all logic must live in a component", the rest of the framework is consistent — but discovering the rule is currently a debugging-archaeology exercise.
🤖 Filed via Claude Code