Skip to content

Commit b192bc7

Browse files
committed
Add React-shaped JSX runtime
1 parent 519de93 commit b192bc7

12 files changed

Lines changed: 1051 additions & 129 deletions

File tree

.changeset/fuzzy-icons-render.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@suckless/jsx": minor
3+
---
4+
5+
Add a synchronous server-only React-shaped rendering surface for pure render libraries, including lazy render handles, `createElement`, `forwardRef`, `memo`, context helpers, React-style DOM/SVG prop normalization, and lucide-react compatibility. Async component rendering is explicitly unsupported.

bun.lock

Lines changed: 21 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example/src/views.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { raw, type Children, type RawHtml } from "@suckless/jsx"
1+
import { raw, type RawHtml, type Renderable } from "@suckless/jsx"
22
import type { TranslateFn } from "@suckless/i18n"
33
import type { AppDict } from "./locales/en.ts"
44
import type { Locale, Post } from "./data.ts"
@@ -9,7 +9,7 @@ import CSS from "./styles.css" with { type: "text" }
99
export function Layout(props: {
1010
locale: Locale
1111
t: TranslateFn<AppDict>
12-
children?: Children
12+
children?: Renderable
1313
}): RawHtml {
1414
const { locale, t, children } = props
1515
const otherLocale: Locale = locale === "en" ? "es" : "en"

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"bunup": "^0.16.31",
2525
"fast-check": "^4.5.3",
2626
"jsqr": "^1.4.0",
27+
"lucide-react": "^1.16.0",
2728
"oxfmt": "^0.36.0",
2829
"oxlint": "^1.51.0",
2930
"oxlint-tsgolint": "^0.15.0",

packages/jsx/README.md

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# @suckless/jsx
22

3-
JSX-to-string runtime for server-side HTML rendering. No virtual DOM, no framework, no hydration — JSX expressions evaluate directly to strings with automatic XSS escaping.
3+
JSX-to-HTML runtime for server-side rendering. No client runtime, no hydration, no effects — JSX expressions produce render handles that stringify to escaped HTML.
44

55
## Install
66

@@ -42,12 +42,12 @@ const page = (
4242
</body>
4343
</html>
4444
)
45-
// page is a plain string: "<html><head><title>Hello</title></head>..."
45+
// String(page): "<html><head><title>Hello</title></head>..."
4646
```
4747

4848
### Components
4949

50-
Function components receive props and return strings:
50+
Function components receive props and return renderable HTML:
5151

5252
```tsx
5353
/** @jsxImportSource @suckless/jsx */
@@ -67,6 +67,8 @@ const html = (
6767
)
6868
```
6969

70+
Components are synchronous. Promise-returning components are rejected during rendering.
71+
7072
### Escaping
7173

7274
All string children and attribute values are escaped by default:
@@ -98,6 +100,32 @@ const items = (
98100
// → "<li>One</li><li>Two</li>"
99101
```
100102

103+
### React-shaped render libraries
104+
105+
The root export includes a small server-only React-shaped surface for pure render libraries:
106+
107+
- `createElement`
108+
- `forwardRef`
109+
- `memo`
110+
- `createContext` / `useContext`
111+
- `cloneElement`
112+
- `isValidElement`
113+
- `Children.toArray`
114+
115+
Alias React imports to this package when a dependency imports `react`:
116+
117+
```json
118+
{
119+
"alias": {
120+
"react": "@suckless/jsx",
121+
"react/jsx-runtime": "@suckless/jsx/jsx-runtime",
122+
"react/jsx-dev-runtime": "@suckless/jsx/jsx-dev-runtime"
123+
}
124+
}
125+
```
126+
127+
This supports pure server-rendered components such as icon libraries. It does not implement client state, effects, hydration, Suspense, or portals.
128+
101129
## API
102130

103131
### `escape(value: string): string`
@@ -108,17 +136,22 @@ Escapes `&`, `<`, `>`, `"`, and `'` to their HTML entity equivalents.
108136

109137
Wraps a string to bypass automatic escaping. Use only with trusted content.
110138

111-
### `Fragment(props: { children?: Children }): string`
139+
### `Fragment(props: { children?: Renderable }): RawHtml`
112140

113141
Renders children without a wrapper element. Used automatically by `<>...</>` syntax.
114142

143+
### `createElement(tag, props, ...children): RawHtml`
144+
145+
Creates a render handle using the classic React-style call shape.
146+
115147
## Type Safety
116148

117149
Every HTML and SVG element has strict per-element attribute types:
118150

119151
- Void elements (`<br>`, `<img>`, `<input>`, etc.) reject children at the type level
120152
- Element-specific attributes are enforced (`href` on `<a>`, not on `<div>`)
121153
- Standard HTML attributes (`class`, `for`) — not React conventions
154+
- Common React DOM/SVG aliases (`className`, `htmlFor`, `strokeWidth`, etc.) are normalized during rendering
122155
- No event handler types — this is a server-side renderer
123156
- `data-*` and `aria-*` attributes are supported on all elements
124157

0 commit comments

Comments
 (0)