Summary
An unresolved inside a <script> body is emitted verbatim into the browser. In HTML that is correct — the client runtime may bind it later. Inside JavaScript nothing ever re-reads a mustache, so the only possible outcome is a SyntaxError that takes down the entire script, silently.
How it happens
A component interpolates a server value into its client script — the documented way to pass a prop into client code:
<script server>
export const stepNumber = $props.stepNumber // no default
</script>
<script client>
const stepNumber = {{ stepNumber }}
</script>Render <StepperStep /> with no stepNumber and the emitted script is:
const emit = defineEmits();
const stepNumber = {{ stepNumber }}; // ← SyntaxError: Unexpected token '{'
function onClick() { emit("click", stepNumber); }The whole scoped IIFE fails to parse, so onClick is never registered and the component is dead on the page. Nothing is logged.
Found in two @stacksjs/components components (fixed there in 8bf124c by defaulting the props), but the framework behaviour is what lets it happen to anyone.
Evidence
packages/stx/src/expressions.ts:791, in interpolateScriptExpressions:
const value = evaluateExpression(expr, context)
if (value === undefined) return match // ← leaves `` in JavaScriptThe same rule at :776 governs .
The rationale is stated one function down, in interpolateScriptAttributes: "Unresolvable expressions are left untouched, matching the 'preserve for the client' semantics used everywhere else." That is right for HTML and wrong for a script body — packages/stx/src/utils.ts:1425-1440 already explains that this exact construct "crashes with Unexpected token '{'", which is the failure this rule reintroduces whenever the value is undefined.
Two tests currently pin the behaviour (test/script-interpolation.test.ts:80, :70), so this is a deliberate decision to revisit rather than an oversight.
Ask
Decide between:
- Emit
undefinedwhen evaluation yieldsundefinedin a script body. Valid JavaScript, matches what$props.xmeans, and the script survives. Attribute interpolation keeps today's preserve semantics. - Fail the build with the file, line and expression named. Louder, and arguably right for something that can only ever be a syntax error.
Either beats shipping a mustache to the browser. If the compile-mode placeholder path needs the current behaviour, scope the change to non-compile builds.