Skip to content

feat: builtin minimal JSX runtime#737

Open
sanman1k98 wants to merge 31 commits intovercel:mainfrom
sanman1k98:feat/jsx-runtime
Open

feat: builtin minimal JSX runtime#737
sanman1k98 wants to merge 31 commits intovercel:mainfrom
sanman1k98:feat/jsx-runtime

Conversation

@sanman1k98
Copy link

@sanman1k98 sanman1k98 commented Mar 12, 2026

JSX without installing React

Now we can write JSX for Satori without having to install React as a dependency. Just add a @jsxImportSource pragma directive at the top of your files.

/** @jsxRuntime automatic */
/** @jsxImportSource satori/jsx */

import satori from 'satori';

function MyComponent() {
  return <h1 style={{ fontSize: 16 }}>Hello from My Component</h1>
}

const svg = await satori(
  <div
    style={{
      backgroundColor: '#ff0',
      width: '100%',
      height: '100%',
      display: 'flex',
    }}
  >
    <MyComponent />
  </div>,
  {
    width: 100,
    height: 100,
    fonts,
  },
)

Types

The types are adapted from @types/react and include a JSX.IntrinsicElements interface for autocompleting HTML and SVG. Which means that it can be modified to only have the subset of elements and CSS properties supported by Satori for some beautiful autocomplete (but at that point I guess it wouldn't be a "minimal runtime" anymore).

Testing

pnpm run test --config='vitest.jsx-runtime.config.ts'

TODO

  • modify build script to add entrypoints for JSX submodules
  • add exports for satori/jsx, satori/jsx/jsx-runtime, and satori/jsx/jsx-dev-runtime
  • custom subset of JSX.IntrinsicElements (if ya wanna do that)
  • update the README and other documentation

I've been sitting on this for a while so I'll update when I remember more stuff...

Thank you for maintaining this awesome project and checking out my PR!

Run with `pnpm run test --config vitest.jsx-runtime.config.ts`
Can be used to set the "jsxImportSource" option using pragma directives
at the top of test files, for example:

```tsx
/** @jsxRuntime automatic */
/** @jsxImportSource #satori/jsx */
```
Satori will call component functions and pass its props as args when
calculating the layout of a sub-tree (see
https://github.com/vercel/satori/blob/50a794813c2ea3fc15a168e0cd144078c7f69815/src/layout.ts#L96).

This change also adds support for async custom components since Satori
awaits the calls to component functions, we just have to update the
types to reflect that.

all the tests pass now btw
React typings use explicity `any` types and empty interfaces (so that
users will be able to extend them).
Export all types and the `createElement()` function from this new
module. This module is intended to be public and users should be able to
import types and other helper functions from here.
Change the return type of the exported `jsx` function and extract the
'key' prop from props.
More featured `createElement()` function closer to the React
implementation.
Adapted from v19.1; only the types and interfaces that are used for
`JSX.IntrinsicElements` have been copied over.
Only an index signature has been specified.
From React typings package.
Adapted from React typings.
Using a relative import path since this old version of vitest doesn't
appear to support Node's subpath imports (i.e., module specifiers that
begin with "#"), but this creates problems for TypeScript.

Using `/** @jsxImportSource #satori/jsx */` at the top of the file will
make the TS errors go away, but will make vitest will fail to run the
file.

https://nodejs.org/docs/latest/api/packages.html#subpath-imports
@vercel
Copy link
Contributor

vercel bot commented Mar 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
satori-playground Ready Ready Preview, Comment, Open in v0 Mar 13, 2026 3:55pm

I noticed when manually inspecting the bundled declaration files, the
interfaces weren't being renamed according to how I was importing them
in the source file:

import type { IntrinsicElements as DefinedIntrinsicElements } from '...'

So the `IntrinsicElements` interface  was recursively referencing itself
in the JSX namespace because it was all bundled together in the same
file.

So in an abundance of caution, I just renamed it just to be safe.
@sanman1k98 sanman1k98 marked this pull request as ready for review March 13, 2026 15:45
@sanman1k98 sanman1k98 requested a review from shuding as a code owner March 13, 2026 15:45
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
@sanman1k98
Copy link
Author

I figured that creating a custom subset of HTML/SVG/CSS types sounds like a lot of work and that it should probably be in a separate PR haha

I’d love to hear what you think about this feature, and please let me know if there’s anything else I can help with!

@shuding
Copy link
Member

shuding commented Mar 15, 2026

Can you give some example use cases for this?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants