Skip to content

JSX preserve mode generates invalid JSX for external component bindings #8047

@han-tyumi

Description

@han-tyumi

Thank you for filing! Check list:

  • Is it a bug? Usage questions should often be asked in the forum instead.
  • Concise, focused, friendly issue title & description.
  • A minimal, reproducible example.
  • OS and browser versions, if relevant.
  • Is it already fixed in master? Instructions

Summary

When using the generic JSX transform with "preserve": true, external component bindings generate invalid JSX syntax like <prim => Module.Component(prim)> instead of valid JSX.

ReScript Version

12.0.0

Minimal Reproduction

https://github.com/han-tyumi/rescript-jsx-preserve-bug

git clone https://github.com/han-tyumi/rescript-jsx-preserve-bug.git
cd rescript-jsx-preserve-bug
npm install
npx rescript build
cat Test.jsx
Files

rescript.json:

{
  "name": "preserve-bug",
  "sources": ["."],
  "package-specs": {
    "module": "esmodule",
    "in-source": true
  },
  "suffix": ".jsx",
  "jsx": {
    "module": "Preact",
    "preserve": true
  }
}

Preact.res (minimal bindings):

type element
type component<'props> = 'props => element

@module("preact/jsx-runtime")
external jsx: (component<'props>, 'props) => element = "jsx"

@module("preact/jsx-runtime")
external jsxs: (component<'props>, 'props) => element = "jsxs"

type fragmentProps = {children?: element}

@module("preact/jsx-runtime")
external jsxFragment: component<fragmentProps> = "Fragment"

type domProps = {children?: element}

module Elements = {
  external someElement: element => option<element> = "%identity"

  @module("preact/jsx-runtime")
  external jsx: (string, domProps) => element = "jsx"

  @module("preact/jsx-runtime")
  external jsxs: (string, domProps) => element = "jsxs"
}

Test.res:

// Component module pattern with external make
module Head = {
  type props = {children?: Preact.element}
  @module("some-lib")
  external make: props => Preact.element = "Head"
}

// Using the component
let test = <Head> <div /> </Head>

Expected Output

Valid JSX that can be processed by standard JSX transformers:

let test = <SomeLib.Head>
  <div />
</SomeLib.Head>;

Actual Output

Invalid JSX with arrow function syntax:

let test = <prim => SomeLib.Head(prim)>
  {Primitive_option.some(<div />)}
</prim => SomeLib.Head(prim)>;

Notes

  • Lowercase DOM elements work correctly in preserve mode (e.g., <div> stays as <div>)
  • Internal component modules (with ReScript-defined make functions) work correctly (e.g., <MyComponent.make>)
  • External component bindings produce the invalid arrow function syntax shown above

This prevents using preserve mode with frameworks like Preact/Fresh where you need to bind to external components.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    Status

    Backlog

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions