ReviewOS

stacks/stx

View-level `<script client>` is silently dropped — scripts only run inside components

#1698
Closed glennmichael123 opened this 9 days ago · {{ comments.length }} {{ comments.length === 1 ? 'comment' : 'comments' }}
9 days ago

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>
@endsection

Output HTML contains the <form @submit.prevent="submitSearch()"> markup, but:

  • view-level script ran never logs
  • search.initFromUrl() never runs (search query input never gets pre-populated from URL params)
  • submitSearch is undefined in the scope — the form's @submit.prevent fires but resolves to nothing
  • x-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 itself

Workaround: 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

  1. Silent. No compile warning, no console warning. The page renders. The directives are in the DOM. Nothing fires. Hours-of-debugging-time silent.
  2. 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.
  3. 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.64
  • bun-plugin-stx@0.2.64
  • View at resources/views/search.stx, layout at resources/layouts/default.stx
  • Components at resources/components/Bench/*.stx work fine; only view-level fails

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

9 days ago

Shipped in commit 1899048537 (fix: address seven reported issues across stx, bun-plugin, components).

When the view uses @extends + an explicit @section('content'), packages/stx/src/process.ts now does a salvage pass before invoking the layout: it strips top-level <script>/<style> blocks that landed outside any @section, appends them to sections.content, and lets the layout's @yield('content') slot them back in. Result: a view-level <script client> registers a scope and participates in directive binding exactly like a component-level one — option (A) from the spec.

Regression coverage: packages/stx/test/edge-cases/issue-fixes.test.ts describe block #1698 — view-level <script>/<style> salvaged when @extends is used (added in 8db98be). All 13 tests in that file pass on main.

Sign in to comment on this issue.