Skip to content

src/jsx-loader.js: several correctness/DX defects in JSX + inferred-observability compilation #282

Description

@jstockdi

What happened?

While exercising wc-compiler's JSX + inferred-observability compilation (via Greenwood's @greenwood/plugin-import-jsx), I hit a cluster of correctness/DX defects that all live in src/jsx-loader.js. Each is individually reproducible with a minimal .jsx component; several fail silently (green build, broken or missing output), and a few crash the build with an error that points at wc-compiler internals rather than the user's source.

Because they share one file and some are adjacent, I'm reporting them together; a PR follows with one commit (fix + regression test) per defect.

The defects:

  1. Unsupported JSX child expressions are silently dropped (~parseJsxElement, lines ~131–306). Calls, ternaries, binary ops, template literals, .map() lists, and computed members produce no output and no warning — e.g. {items.map(i => <li>{i}</li>)} renders an empty <ul>. Fix: emit a console.warn from the fall-through so the drop is visible (also handles the {' '} / {'text'} child-Literal idiom, which was itself dropped).
  2. class X extends HTMLElement {}; customElements.define(...); export default X; ships undefined.$$tmpl0(...) (lines ~417–427 / 770 / 819). componentName is only captured for the inline export default class Name form, so the effect-append pass emits undefined.$$tmpl0(...) → client TypeError on connect (build green, SSR correct).
  3. {this.count.get()} compiles to ${undefined.get()} (lines ~280–298), crashing SSR (Cannot read properties of undefined (reading 'get')) on a valid source line. Only the destructured {count.get()} form works today.
  4. A plain class Foo {} (no extends) in a .jsx file crashes compilation with Cannot read properties of null (reading 'name') (lines ~431 / 698; node.superClass is null), with no file named.
  5. export const inferredObservability = false still enables observability (line ~413): Boolean(node.declaration.declarations[0].init.raw) runs on the source text "false", and Boolean("false") === true.
  6. A component with its own observedAttributes / attributeChangedCallback gets duplicate members (line ~380 hasOwnObservedAttributes is dead/undefined, so the guards at ~505 / ~760 never fire); the user's definitions silently override the injected ones, disabling inferred attribute→signal sync.
  7. Arrow event handlers with double-quoted strings emit malformed HTML (line ~177): the serialized body is placed into a double-quoted onclick="…" unescaped, and /this./g rewrites this even inside string literals — e.g. onclick={() => alert("this rocks")} breaks the attribute and turns "this rocks" into self.rocks.
  8. A JSX root of <body>/<html> produces a nested full document (lines ~31–35, getParse): parse5's full-document parse + serialize inlines a complete <html><head></head><body>…</body></html> inside the page.

Steps to reproduce

Each defect has a minimal .jsx fixture; see the corresponding commit's regression test in the PR (each proves the failure on unpatched source and passes with the fix). Example (defect 3):

  1. A .jsx component with export const inferredObservability = true that reads {this.count.get()} directly in render().
  2. Compile it (build/SSR).
  3. Observe TypeError: Cannot read properties of undefined (reading 'get') pointing at the component's render, instead of rendering the value.

Environment

  • wc-compiler: 0.22.2 (current master, src/jsx-loader.js unchanged since the release)
  • Node: 22.20
  • OS: Linux
  • Reached via @greenwood/plugin-import-jsx (Greenwood v0.34.0), but all defects are in wc-compiler itself.

Additional Context

Each fix is minimal and confined; the diagnostic-only one (defect 1) is a non-fatal console.warn, not a hard error. Defect 3 covers the direct this.<signal>.get() read; the chained {this.todos.get().length} variant is a broader change left as a follow-up.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions