Initial implementation of jrx - JSX factory for json-render

JSX factory that compiles JSX trees into json-render Spec JSON.
Framework-agnostic custom jsx-runtime, no React dependency at runtime.

- jsx/jsxs/Fragment via jsxImportSource: "jrx"
- render() flattens JrxNode tree into { root, elements, state? } Spec
- Auto key generation (type-N) with explicit key override
- Full feature parity: visible, on, repeat, watch as reserved props
- Function components via component() or plain functions
- @json-render/core as peer dependency

Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
Kenneth Nym
2026-02-27 00:31:52 +00:00
commit f36256dda9
17 changed files with 2019 additions and 0 deletions

141
src/jsx-runtime.ts Normal file
View File

@@ -0,0 +1,141 @@
import type {
ActionBinding,
VisibilityCondition,
} from "@json-render/core";
import { JRX_NODE, FRAGMENT, type JrxNode } from "./types";
import type { JrxComponent } from "./types";
export { FRAGMENT as Fragment };
/** Props reserved by jrx — extracted from JSX props and placed on the UIElement level. */
const RESERVED_PROPS = new Set([
"key",
"children",
"visible",
"on",
"repeat",
"watch",
]);
/**
* Normalize a raw `children` value from JSX props into a flat array of JrxNodes.
* Handles: undefined, single node, nested arrays, and filters out nulls/booleans.
*/
function normalizeChildren(raw: unknown): JrxNode[] {
if (raw == null || typeof raw === "boolean") return [];
if (Array.isArray(raw)) {
const result: JrxNode[] = [];
for (const child of raw) {
if (child == null || typeof child === "boolean") continue;
if (Array.isArray(child)) {
result.push(...normalizeChildren(child));
} else {
result.push(child as JrxNode);
}
}
return result;
}
return [raw as JrxNode];
}
/**
* Extract component props, filtering out reserved prop names.
*/
function extractProps(
rawProps: Record<string, unknown>,
): Record<string, unknown> {
const props: Record<string, unknown> = {};
for (const k of Object.keys(rawProps)) {
if (!RESERVED_PROPS.has(k)) {
props[k] = rawProps[k];
}
}
return props;
}
/** Accepted tag types: string literal, Fragment symbol, or a function component. */
type JsxType = string | typeof FRAGMENT | JrxComponent;
/**
* Core factory — shared by `jsx` and `jsxs`.
*
* If `type` is a function, it is called with props (like React calls
* function components). The function returns a JrxNode directly.
*
* If `type` is a string or Fragment, a JrxNode is constructed inline.
*/
function createNode(
type: JsxType,
rawProps: Record<string, unknown> | null,
): JrxNode {
const p = rawProps ?? {};
// Function component — call it, just like React does.
if (typeof type === "function") {
return type(p);
}
return {
$$typeof: JRX_NODE,
type,
props: extractProps(p),
children: normalizeChildren(p.children),
key: p.key != null ? String(p.key) : undefined,
visible: p.visible as VisibilityCondition | undefined,
on: p.on as
| Record<string, ActionBinding | ActionBinding[]>
| undefined,
repeat: p.repeat as { statePath: string; key?: string } | undefined,
watch: p.watch as
| Record<string, ActionBinding | ActionBinding[]>
| undefined,
};
}
/**
* JSX factory for elements with a single child (or no children).
* Called by the automatic JSX transform (`react-jsx`).
*/
export function jsx(
type: JsxType,
props: Record<string, unknown> | null,
key?: string,
): JrxNode {
const node = createNode(type, props);
if (key != null) node.key = String(key);
return node;
}
/**
* JSX factory for elements with multiple static children.
* Called by the automatic JSX transform (`react-jsx`).
*/
export function jsxs(
type: JsxType,
props: Record<string, unknown> | null,
key?: string,
): JrxNode {
const node = createNode(type, props);
if (key != null) node.key = String(key);
return node;
}
// ---------------------------------------------------------------------------
// JSX namespace — tells TypeScript what JSX expressions are valid
// ---------------------------------------------------------------------------
export namespace JSX {
/** Any string tag is valid — component types come from the catalog at runtime. */
export interface IntrinsicElements {
[tag: string]: Record<string, unknown>;
}
/** The type returned by JSX expressions. */
export type Element = JrxNode;
export interface ElementChildrenAttribute {
children: {};
}
}