Verified on published stx-router@0.2.153 and on main (0dcb6fbd0e). Chromium via CDP.
Symptom
Clicking an StxLink on a page that has no <script client> does a full document load instead of a fragment swap, and logs:
[router] fetch error: TypeError: Cannot read properties of undefined (reading '_latestSetup')
at doFragSwap (/_stx/router.js:4:12710)
at completeFragSwap (/_stx/router.js:4:12934)Silent in practice: the page still arrives, so the only symptoms are a lost SPA navigation and a console error nobody is looking at.
Cause
The router loads on every page. The signals runtime does not — it is only injected on pages that have a <script client>. packages/router/src/client.ts:663 (and again at :996) then read window.stx._latestSetup with no guard:
log('[router] scripts done. _latestSetup:', !!window.stx._latestSetup);Measured directly in the browser, four pages of the same app:
| page | has <script client> | window.stx | window.stxRouter | _latestSetup |
|---|---|---|---|---|
/features | no | undefined | object | n/a |
/compare | no | undefined | object | n/a |
/pricing | yes | object | object | function |
/ | yes | object | object | function |
The middle column is the point: the router is live and intercepting clicks on /features, then reaches into an object that was never created. The throw is caught by the fetch path, which falls back to location.href.
Reproduction
Two marketing pages sharing a layout, one with a <script client> and one without. Click an internal StxLink from each.
| navigation | source has runtime | result |
|---|---|---|
/ → /pricing | yes | spa |
/pricing → /features | yes | spa |
/ → /compare/fathom | yes | spa |
/features → /features/goals | no | reload |
/compare → /compare/fathom | no | reload |
/use-cases → /use-cases/saas | no | reload |
It is source-dependent, not target-dependent: /compare/fathom is a clean SPA swap from / and a full reload from /compare. Deterministic — 3/3 runs on the failing pair.
Scope
Content pages have no client script by design, so the router is disabled on exactly the pages that most want prefetch and fast navigation. In the app I hit this on, 23 of 25 marketing pages are affected.
It also means converting anchors to StxLink buys nothing on those pages — the link is intercepted, then the swap aborts and reloads anyway.
Where I stopped
I did not determine whether "inject the runtime only when a client script exists" is deliberate. Two candidate fixes, and which is right depends on that:
- Guard the read —
window.stx && window.stx._latestSetup. Minimal, but leaves the router doing fragment swaps with no signals runtime present, which may have its own consequences I have not explored. - Inject the runtime whenever the router is enabled. Costs bytes on otherwise-static pages, but the router arguably cannot function without it.
Unrelated to #1801 — that commit never touched _latestSetup, and this reproduces identically on published 0.2.153 with no fork symlinks in place.