ReviewOS

stacks/stx

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

#1769
Closed glennmichael123 opened this 9 days ago · {{ comments.length }} {{ comments.length === 1 ? 'comment' : 'comments' }}
9 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.)

9 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>).
9 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.