ReviewOS

stacks/stx

fix(expressions): an unresolved {⁠{ }} in a script body ships a SyntaxError to the browser

#1989
Closed glennmichael123 opened this 9 days ago ·
9 days ago

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 JavaScript

The 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:

  1. Emit undefined when evaluation yields undefined in a script body. Valid JavaScript, matches what $props.x means, and the script survives. Attribute interpolation keeps today's preserve semantics.
  2. 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.

Sign in to comment on this issue.