ReviewOS

also looking at this

stacks/stx

fix(router): stop executing data scripts on a fragment swap

#1802
Merged glennmichael123 wants to merge fix/1801-ld-json-fragment-swap into main
2 files +183 -0
8 days ago

Fixes #1801.

The line you couldn't pin

The issue says: "I could not conclusively prove which of these admits the node — the newContent.contains(s) check looks like it should exclude an in-container script, so my model is incomplete."

That instinct was right. Both quoted sites are innocent. prepareRoutedBodyScripts classifies by type, and the head/body collection requires a setup marker and !newContent.contains(s). Neither can admit an in-container ld+json.

The culprit is a third site the issue doesn't quote — and it's the one the stack trace actually names. doFragSwap extracts scripts with a regex over the raw HTML string, not the DOM:

// client.ts:505 — no type guard, unlike the DOM path
html.replace(/<script\b([^>]*)>([\s\S]*?)<\/script>/gi, function (m, attrs, code) {
  if (code && code.trim() && !isSignalsRuntimeScript(...)) {
    fragScripts.push({ text: code, ... })
    return '<script type="application/stx-pending" data-stx-route-script="' + slot + '">' + code + '</script>'
  }
  return ''
})

Full chain, matching the reported trace exactly:

steplinewhat happens
regex match505ld+json captured as if it were JS
placeholder emitted514rewritten to application/stx-pending, JSON kept as body
innerHTML = cleanFrag574placeholder lands in the container
fragScripts.forEach586doFragSwap → Array.forEach frame
brace wrap638'{' + code + '}'
replaceChild642SyntaxError: Unexpected token ':'

The fix

A type guard mirroring the one prepareRoutedBodyScripts already has, and returning the node untouched rather than stripping it — crawlers read structured data wherever it sits, and the browser won't execute a non-JS type anyway.

Covers importmap and speculationrules for free. That matters: an importmap executed as JS would break module resolution for the entire document.

Verification

5 regression tests, and I confirmed they fail without the guard (3 of 5 fail, the other two being the does-not-overreach cases). Full router suite: 116 pass, 0 fail. pickier clean.

The tests also pin the premise directly — expect(() => new Function('{' + LD_JSON + '}')).toThrow() — so if brace-wrapping ever stops throwing, the guard isn't silently testing nothing.

Note on the workaround

The @push('scripts') workaround in the issue is still fine, but shouldn't be needed after this. Worth keeping in mind that @structuredData renders at its call site, so inside a layout it lands in @section('content') — the guide's own ch. 4.9 breadcrumb example produces exactly the shape that triggered this.

🤖 Generated with Claude Code

2 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.