ReviewOS

stacks/stx

JSX-style prop={expr} on a component is silently dropped (multi-line) or passed as a literal string (single-line)

#1956
Closed glennmichael123 opened this 10 days ago ·
10 days ago

A JSX-style prop={expr} on a component tag is not parsed as an expression. stx documents :prop="expr" for this, but the failure is silent and takes two forms, one of which loses the prop entirely. packages/components/examples/form-examples.stx uses the syntax today, so the shipped example is affected.

Found while fixing the silent <script server> failures in ffeaec7058.

What happens

From packages/components/examples/form-examples.stx:123:

<Form
  id="profileForm"
  initialValues={{
    name: 'John Doe',
    email: 'john@example.com',
    bio: 'Web developer and designer',
    notifications: true
  }}
  onSubmit={handleEditProfile}
  className="space-y-4"
>

Measured by logging context.props when Form's server script runs:

{"id":"profileForm","className":"space-y-4"}
  • A multi-line = prop is dropped entirely. initialValues never reaches the component, so the form renders with no initial values.
  • A single-line ={expr} prop arrives as a literal string, braces included. The other form on the same page passes onSubmit={handleMultiStepSubmit} and the component receives "onSubmit":"{handleMultiStepSubmit}" — a string where a handler is expected.

Nothing warns in either case.

It also corrupts the render context

The object literal is split across whitespace into separate context keys:

initialValues, name:, 'John, Doe',, email:, 'john@example.com',,
bio:, 'Web, developer, and, designer',, notifications:, true, }}, onSubmit, ...

Those fragments become candidate parameter names for the generated <script server> function. One of them, true, is identifier-shaped but reserved, and it threw before the script body ran — which silently dropped the server script of six shipped components (Button, TextInput, EmailInput, Textarea, Form, Checkbox) into static extraction on every render.

That part is fixed in ffeaec7058: a malformed attribute can no longer take down a whole server script. The prop data loss and the context pollution above are not fixed, and are what this issue is about.

Two decisions

  1. Should prop={expr} be supported? It is what someone coming from JSX writes. If yes, it needs parsing as an expression, including across lines. If no, it should be a build/dev-time error rather than a dropped prop and a shattered context.
  2. The example should be corrected either way, since it currently demonstrates syntax that does not work: :initialValues="{ ... }" and @submit="handleEditProfile".

Note that packages/components/examples/form-examples.stx is used as a perf fixture in #1945 measurements, so changing it shifts those numbers.

Repro

bun -e '
const { renderView } = await import("./packages/stx/src/build-views")
await renderView("packages/components/examples/form-examples.stx", {}, { componentsDir: "packages/components/src/ui" })
'

Log context.props in extractVariables for form/form.stx, or add a <script server> to any component and pass it a prop=.

🤖 Generated with Claude Code

10 days ago

Half of this is fixed in 75aed7784a; the decision in the issue is deliberately left open.

Fixed: the tag no longer shatters. All three attribute parsers read an unquoted value "until whitespace" — parseMultilineAttributes and parseAttributes in component-processing.ts, and parseAllAttributes in component-renderer.ts, the last being the one the component render path actually uses. A braced value is now read whole, handling nesting and ignoring braces inside quotes; an unterminated value degrades to one bad prop instead of a shattered tag.

On form-examples.stx: junk context keys per render went from many to 0, and initialValues now reaches the component instead of vanishing.

Still true: the value arrives as a string, braces included — "{handleEditProfile}", "". Each spelling now warns once, naming the documented form:

[stx] onSubmit={...} is not stx syntax, so onSubmit is passed as the literal string "{handleEditProfile}".
Use :onSubmit="handleEditProfile" for a server-evaluated value, or onSubmit="..." for a plain string.

Left to decide (question 1 in this issue): whether prop={expr} should be evaluated, making it equivalent to :prop="expr". That adds a fourth attribute syntax alongside :, x- and @, which is a language call rather than a bug fix, so I did not make it. If the answer is yes, the parse work is already done and it is a small change at the point where props are categorised; if the answer is no, the warning above is the whole story.

Question 2, the example page, is also untouched. packages/components/examples/form-examples.stx still demonstrates syntax that does not work; it wants :initialValues="{ ... }" and @submit="handleEditProfile". I left it alone because it doubles as the perf fixture in #1945 and editing it shifts those numbers — worth doing deliberately rather than as a side effect of this fix.

🤖 Generated with Claude Code

10 days ago

Both questions in this issue are now answered; superseding my earlier comment, which was written midway and said the second was still open.

1. prop={expr} is supported (59e6dd57cb). It is rewritten to :prop="expr" before props are categorised, so there is one evaluation path — client-reactive detection, loop scoping, the :if/:for/:show carve-outs and boolean shorthand all apply unchanged. The JSX double brace unwraps by one layer, so config= evaluates the object literal { a: 1 }. Documented in CLAUDE.md and docs/guide/prefix-convention.md.

2. The parse bug is fixed (75aed7784a). All three attribute parsers read an unquoted value "until whitespace"; a braced value is now read whole, handling nesting and braces inside quotes. On form-examples.stx, junk context keys per render went from many to 0, and initialValues now arrives as a real object:

{ name: 'John Doe', email: 'john@example.com', bio: '...', notifications: true }

Worth knowing before writing prop={…}: it is a prop, not an event. onSubmit={handleEditProfile} now evaluates server-side, where a <script client> handler does not exist, so it is undefined — previously it was the literal string "{handleEditProfile}". Both are wrong; a handler wants @submit="handleEditProfile".

That means packages/components/examples/form-examples.stx is still subtly wrong: its initialValues works now, but onSubmit={handleEditProfile} and onSubmit={handleMultiStepSubmit} silently evaluate to undefined. I left the file alone because it doubles as the perf fixture in #1945 and editing it shifts those numbers — worth doing deliberately rather than as a side effect. Happy to fix it on request.

One near-miss, recorded because the shape is easy to repeat: the first version did the rewrite after parsing, where any fully-braced value looks like an expression — including prop="", ordinary interpolation. <StxLink to=""> then evaluated the object literal { forgotPasswordHref } and rendered href="[object Object]" across the auth components. Only the parser knows the braces were unquoted, so the decision belongs there; that case now has its own test.

🤖 Generated with Claude Code

Sign in to comment on this issue.