Closes #1769.
The problem
A single dropped closing tag (a missing </div>) is silently auto-closed by the HTML parser at EOF, re-parenting every following sibling inside the unclosed element. Those siblings inherit its x-show/display:none and render blank — with zero diagnostics at any layer: the server 200s, hydration succeeds, no console output. It's only visible by eye (or by walking getComputedStyle up the ancestor chain).
The fix
A coarse, void- and optional-close-aware open/close count per tag name, run at template-parse time and warning on any mismatch:
[stx] SettingsTab.stx: <div> 47 opened, 46 closed — an unclosed <div> (first opened at line 771) may swallow the content that follows it.template-tag-balance.ts— a small module: purefindUnbalancedTags()(fully unit-tested) +warnUnbalancedTags(). It's deliberately a count, not a parse — the goal is a cheap, high-signal heads-up.- Where it runs:
processDirectiveson the raw template of views/layouts, and the include pipeline on each@included partial's raw content (the issue's exact case — a tab component). Both check the RAW source before any processing auto-closes tags. - When it runs: on by default in development; opt-in via
debugelsewhere (options.debug || (!isProduction() && !isTest())). Off in the production build and under the test runner, so it never adds noise to CI or the shipped bundle. Deduped per file (warns once; re-arms on edit).
False-positive-free by design
It ignores, and there are tests for each: void elements (<br>, <img>, …), optional-close elements (<li>, <td>, <p>, …), self-closing components (<Icon />), and any </> inside <script>/<style>/comments/ interpolations/quoted attribute values. Component tags (<StxLink>…</StxLink>) are checked case-preserved, so an unbalanced component is caught too.
Verification
- 14 new tests (
test/template-tag-balance.test.ts): the pure counter across all the edge cases above,warnUnbalancedTagsmessage/dedupe, and aprocessDirectivesintegration test confirming the hook fires in dev and stays quiet otherwise. - Full suite green: 7810 pass, 0 fail.
- Manually confirmed: fires in
NODE_ENV=developmentwith nodebugflag; silent inNODE_ENV=production.
Notes for review
- The default-on-in-dev gate is the deliberate call (the request was to catch this during development, and
debugdefaults tofalse). Easy to flip todebug-only if you'd rather it be opt-in. - It reports every imbalanced tag name (unclosed and stray-close), keyed by the first opening line.