ReviewOS

stacks/stx

DX: unbalanced tags in a component template fail silently — subtree swallowed with zero diagnostics

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

What happened

A component (ts-analytics' SettingsTab.stx) lost one closing </div> in an edit. The HTML parser auto-closed the element at EOF, silently re-parenting every subsequent sibling (three whole sub-tab sections) INSIDE an x-show div — so they inherited its display:none and rendered as a blank page whenever that tab wasn't selected.

Nothing surfaced the problem at any layer:

  • Server render: page 200s, no component error, markup 'valid' (auto-closed)
  • Client runtime: hydration succeeds, signals/gates all healthy, no console output
  • The failure is purely structural and only visible by eye (or by walking getComputedStyle up the ancestor chain in a WebView)

Repro sketch:

<div>
  <div x-show="tab() === 'a'">
    <p>tab A</p>
  <!-- missing </div> here -->
  <div x-show="tab() === 'b'"><p>tab B — invisible whenever tab A is hidden</p></div>
</div>

Ask

A dev-mode warning when a component template's open/close tag counts don't balance (per void-element-aware counting), e.g.:

[stx] SettingsTab.stx: 47 <div> opened, 46 closed — unclosed element starting at line 771 may swallow following content

Even a coarse per-tag-name count comparison at template-parse time would have caught this instantly instead of after production screenshots. A lint in stx check/pickier-stx would work too.

(Found while debugging stacksjs/ts-analytics — fixed there in 6961cbb; diagnosed by driving the page with WebView.evaluate() from the new screenshot machinery, which was excellent for this.)

10 days ago

FYI for whoever's implementing this (found while validating against real codebases, then backed my own overlay out in favor of the in-flight template-tag-balance.ts work):

  • False-positive scan: 207 production templates across ts-analytics + bench-review → 0 flagged with per-tag-name, void-element + self-closing-aware counting (script/style/comment/moustache bodies stripped first).
  • It catches the motivating bug: the pre-fix ts-analytics SettingsTab.stx reports <div> opened 86× closed 85× with a usable line hint.
  • Two real bugs in this repo's own examples the checker surfaced: examples/soccer-fantasy.stx (<div> 336/332) and examples/auto-dashboard/index.stx (extra </span> 20/21 + unclosed <productionstages>).
10 days ago

The shipped warning (1ed4e1b, in v0.2.82) fires false positives on templates that are provably balanced and render correctly. From ts-analytics' dev log:

[stx] SettingsTab.stx: <div> 74 opened, 72 closed — an unclosed <div> (first opened at line 3) ...
[stx] SettingsTab.stx: <span> 15 opened, 14 closed — an unclosed <span> (first opened at line 21) ...
[stx] [token].stx: <h1> 1 opened, 0 closed — an unclosed <h1> (first opened at line 63) ...

Ground truth: whole-file per-tag counts for these files balance exactly (ts-analytics has a CI guard doing void-element + self-closing aware counting with script/style/comment/moustache bodies stripped — test/template-hygiene.test.ts, green), and the rendered pages are verified working via WebView click-through.

The under-counted closes + a 1-opened/0-closed <h1> that visibly closes on the same source line suggest the checker is either (a) scanning a partially-transformed template (post-include/directive slice) rather than the source file, or (b) losing close tags in some contexts (attribute stripping / moustache regions?). Happy to compare against the counting approach in ts-analytics' guard if useful — persistent FPs on every page load will train people to ignore the warning.

Sign in to comment on this issue.