Rename package to @nym.sh/jrx

Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
2026-02-28 01:42:09 +00:00
parent 99fe5e1db9
commit e641c33f56
21 changed files with 142 additions and 142 deletions

View File

@@ -1,5 +1,5 @@
{ {
"name": "jsonsx", "name": "@nym.sh/jrx",
"build": { "build": {
"context": ".", "context": ".",
"dockerfile": "Dockerfile" "dockerfile": "Dockerfile"

View File

@@ -1,22 +1,22 @@
# jsonsx # @nym.sh/jrx
JSX factory for [json-render](https://github.com/vercel-labs/json-render). Write JSX, get Spec JSON. JSX factory for [json-render](https://github.com/vercel-labs/json-render). Write JSX, get Spec JSON.
## Install ## Install
```bash ```bash
bun add jsonsx @json-render/core bun add @nym.sh/jrx @json-render/core
``` ```
## Setup ## Setup
Configure your `tsconfig.json` to use jsonsx as the JSX source: Configure your `tsconfig.json` to use @nym.sh/jrx as the JSX source:
```json ```json
{ {
"compilerOptions": { "compilerOptions": {
"jsx": "react-jsx", "jsx": "react-jsx",
"jsxImportSource": "jsonsx" "jsxImportSource": "@nym.sh/jrx"
} }
} }
``` ```
@@ -24,7 +24,7 @@ Configure your `tsconfig.json` to use jsonsx as the JSX source:
Or use a per-file pragma: Or use a per-file pragma:
```tsx ```tsx
/** @jsxImportSource jsonsx */ /** @jsxImportSource @nym.sh/jrx */
``` ```
## Usage ## Usage
@@ -34,18 +34,18 @@ Or use a per-file pragma:
Create wrapper functions that map JSX tags to json-render component type names: Create wrapper functions that map JSX tags to json-render component type names:
```ts ```ts
import { jsx } from "jsonsx/jsx-runtime"; import { jsx } from "@nym.sh/jrx/jsx-runtime";
import type { JsonsxNode } from "jsonsx"; import type { JrxNode } from "@nym.sh/jrx";
export function Stack(props: Record<string, unknown>): JsonsxNode { export function Stack(props: Record<string, unknown>): JrxNode {
return jsx("Stack", props); return jsx("Stack", props);
} }
export function Text(props: Record<string, unknown>): JsonsxNode { export function Text(props: Record<string, unknown>): JrxNode {
return jsx("Text", props); return jsx("Text", props);
} }
export function Button(props: Record<string, unknown>): JsonsxNode { export function Button(props: Record<string, unknown>): JrxNode {
return jsx("Button", props); return jsx("Button", props);
} }
``` ```
@@ -53,12 +53,12 @@ export function Button(props: Record<string, unknown>): JsonsxNode {
### Render JSX to Spec JSON ### Render JSX to Spec JSON
```tsx ```tsx
import { render } from "jsonsx"; import { render } from "@nym.sh/jrx";
import { Stack, Text, Button } from "./components"; import { Stack, Text, Button } from "./components";
const spec = render( const spec = render(
<Stack> <Stack>
<Text content="Hello from jsonsx!" /> <Text content="Hello from jrx!" />
<Button label="Click me" /> <Button label="Click me" />
</Stack> </Stack>
); );
@@ -72,7 +72,7 @@ This produces:
"elements": { "elements": {
"text-1": { "text-1": {
"type": "Text", "type": "Text",
"props": { "content": "Hello from jsonsx!" } "props": { "content": "Hello from jrx!" }
}, },
"button-1": { "button-1": {
"type": "Button", "type": "Button",
@@ -135,7 +135,7 @@ These props are extracted from JSX and mapped to Spec fields rather than passed
## Example ## Example
The `example/` directory contains a Bun HTTP server that demonstrates jsonsx in action. It shows JSX source, live rendered UI (via `@json-render/react`), and JSON output side by side. The `example/` directory contains a Bun HTTP server that demonstrates jrx in action. It shows JSX source, live rendered UI (via `@json-render/react`), and JSON output side by side.
```bash ```bash
cd example cd example

View File

@@ -3,11 +3,11 @@
"configVersion": 1, "configVersion": 1,
"workspaces": { "workspaces": {
"": { "": {
"name": "jsonsx-example", "name": "jrx-example",
"dependencies": { "dependencies": {
"@json-render/core": "0.10.0", "@json-render/core": "0.10.0",
"@json-render/react": "0.10.0", "@json-render/react": "0.10.0",
"jsonsx": "file:..", "@nym.sh/jrx": "file:..",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4", "react-dom": "19.2.4",
"shiki": "^4.0.0", "shiki": "^4.0.0",
@@ -91,6 +91,8 @@
"@json-render/react": ["@json-render/react@0.10.0", "", { "dependencies": { "@json-render/core": "0.10.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-gyl3YiZ8CZZauAxvUtL1cYkO2mWnKkWmJEC9naqzxIjJp5oHg5v/4F+4zEx4xH5AeGAcY+g2/C7X9Kz8rWMRFg=="], "@json-render/react": ["@json-render/react@0.10.0", "", { "dependencies": { "@json-render/core": "0.10.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-gyl3YiZ8CZZauAxvUtL1cYkO2mWnKkWmJEC9naqzxIjJp5oHg5v/4F+4zEx4xH5AeGAcY+g2/C7X9Kz8rWMRFg=="],
"@nym.sh/jrx": ["@nym.sh/jrx@file:..", { "devDependencies": { "@json-render/core": "0.10.0", "@json-render/react": "0.10.0", "@testing-library/react": "16.3.2", "@types/bun": "^1.3.9", "@types/react": "19.2.3", "happy-dom": "18.0.1", "react": "19.2.4", "react-dom": "19.2.4", "tsup": "8.5.1", "typescript": "5.9.3" }, "peerDependencies": { "@json-render/core": ">=0.10.0" } }],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
@@ -245,8 +247,6 @@
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"jsonsx": ["jsonsx@file:..", { "devDependencies": { "@json-render/core": "0.10.0", "@json-render/react": "0.10.0", "@testing-library/react": "16.3.2", "@types/bun": "^1.3.9", "@types/react": "19.2.3", "happy-dom": "18.0.1", "react": "19.2.4", "react-dom": "19.2.4", "tsup": "8.5.1", "typescript": "5.9.3" }, "peerDependencies": { "@json-render/core": ">=0.10.0" } }],
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
@@ -369,6 +369,6 @@
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"jsonsx/@types/react": ["@types/react@19.2.3", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-k5dJVszUiNr1DSe8Cs+knKR6IrqhqdhpUwzqhkS8ecQTSf3THNtbfIp/umqHMpX2bv+9dkx3fwDv/86LcSfvSg=="], "@nym.sh/jrx/@types/react": ["@types/react@19.2.3", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-k5dJVszUiNr1DSe8Cs+knKR6IrqhqdhpUwzqhkS8ecQTSf3THNtbfIp/umqHMpX2bv+9dkx3fwDv/86LcSfvSg=="],
} }
} }

View File

@@ -1,23 +1,23 @@
import { jsx } from "jsonsx/jsx-runtime"; import { jsx } from "@nym.sh/jrx/jsx-runtime";
import type { JsonsxNode } from "jsonsx"; import type { JrxNode } from "@nym.sh/jrx";
export function Stack(props: Record<string, unknown>): JsonsxNode { export function Stack(props: Record<string, unknown>): JrxNode {
return jsx("Stack", props); return jsx("Stack", props);
} }
export function Card(props: Record<string, unknown>): JsonsxNode { export function Card(props: Record<string, unknown>): JrxNode {
return jsx("Card", props); return jsx("Card", props);
} }
export function Text(props: Record<string, unknown>): JsonsxNode { export function Text(props: Record<string, unknown>): JrxNode {
return jsx("Text", props); return jsx("Text", props);
} }
export function Button(props: Record<string, unknown>): JsonsxNode { export function Button(props: Record<string, unknown>): JrxNode {
return jsx("Button", props); return jsx("Button", props);
} }
export function Input(props: Record<string, unknown>): JsonsxNode { export function Input(props: Record<string, unknown>): JrxNode {
return jsx("Input", props); return jsx("Input", props);
} }

View File

@@ -1,5 +1,5 @@
{ {
"name": "jsonsx-example", "name": "jrx-example",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -10,7 +10,7 @@
"dependencies": { "dependencies": {
"@json-render/core": "0.10.0", "@json-render/core": "0.10.0",
"@json-render/react": "0.10.0", "@json-render/react": "0.10.0",
"jsonsx": "file:..", "@nym.sh/jrx": "file:..",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4", "react-dom": "19.2.4",
"shiki": "^4.0.0" "shiki": "^4.0.0"

View File

@@ -1,5 +1,5 @@
/** @jsxImportSource jsonsx */ /** @jsxImportSource @nym.sh/jrx */
import { render } from "jsonsx"; import { render } from "@nym.sh/jrx";
import { Stack, Card, Text, Button, Input } from "../components"; import { Stack, Card, Text, Button, Input } from "../components";
export const fullSpec = render( export const fullSpec = render(

View File

@@ -1,10 +1,10 @@
/** @jsxImportSource jsonsx */ /** @jsxImportSource @nym.sh/jrx */
import { render } from "jsonsx"; import { render } from "@nym.sh/jrx";
import { Stack, Text, Button } from "../components"; import { Stack, Text, Button } from "../components";
export const simpleSpec = render( export const simpleSpec = render(
<Stack> <Stack>
<Text content="Hello from jsonsx!" /> <Text content="Hello from jrx!" />
<Button label="Click me" /> <Button label="Click me" />
</Stack> </Stack>
); );

View File

@@ -75,7 +75,7 @@ export function App() {
if (!activeSpec) { if (!activeSpec) {
return ( return (
<div style={{ maxWidth: 600, margin: "60px auto", padding: "0 24px" }}> <div style={{ maxWidth: 600, margin: "60px auto", padding: "0 24px" }}>
<h1 style={{ fontSize: "24px", marginBottom: "8px" }}>jsonsx examples</h1> <h1 style={{ fontSize: "24px", marginBottom: "8px" }}>jrx examples</h1>
<p style={{ color: "var(--text-secondary)", fontSize: "14px", marginBottom: "24px" }}> <p style={{ color: "var(--text-secondary)", fontSize: "14px", marginBottom: "24px" }}>
JSX &rarr; json-render Spec. Pick a spec to see the live UI and JSON JSX &rarr; json-render Spec. Pick a spec to see the live UI and JSON
output. output.

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>jsonsx example</title> <title>jrx example</title>
<style> <style>
:root { :root {
--bg: #f5f5f5; --bg: #f5f5f5;

View File

@@ -46,4 +46,4 @@ const server = serve({
}, },
}); });
console.log(`jsonsx example server running at ${server.url}`); console.log(`jrx example server running at ${server.url}`);

View File

@@ -1,5 +1,5 @@
{ {
"name": "jsonsx", "name": "@nym.sh/jrx",
"version": "0.1.0", "version": "0.1.0",
"license": "MIT", "license": "MIT",
"description": "JSX factory for json-render. Write JSX, get Spec JSON.", "description": "JSX factory for json-render. Write JSX, get Spec JSON.",

View File

@@ -1,3 +1,3 @@
export { render } from "./render"; export { render } from "./render";
export { isJsonsxNode, JSONSX_NODE, FRAGMENT } from "./types"; export { isJrxNode, JRX_NODE, FRAGMENT } from "./types";
export type { JsonsxNode, JsonsxComponent, RenderOptions } from "./types"; export type { JrxNode, JrxComponent, RenderOptions } from "./types";

View File

@@ -1,11 +1,11 @@
/** @jsxImportSource react */ /** @jsxImportSource react */
/** /**
* Integration tests: verify that Specs produced by jsonsx are consumable * Integration tests: verify that Specs produced by jrx are consumable
* by @json-render/react's Renderer. * by @json-render/react's Renderer.
* *
* This file uses React JSX (via the pragma above) for the React component * This file uses React JSX (via the pragma above) for the React component
* tree, and jsonsx's jsx()/jsxs() via the component wrappers for building Specs. * tree, and jrx's jsx()/jsxs() via the component wrappers for building Specs.
*/ */
import { describe, it, expect, mock } from "bun:test"; import { describe, it, expect, mock } from "bun:test";
@@ -19,7 +19,7 @@ import {
} from "@json-render/react"; } from "@json-render/react";
import { useStateStore } from "@json-render/react"; import { useStateStore } from "@json-render/react";
import { jsx, jsxs } from "./jsx-runtime"; import { jsx, jsxs } from "./jsx-runtime";
import { render as jsonsxRender } from "./render"; import { render as jrxRender } from "./render";
import { import {
Stack as JStack, Stack as JStack,
Card as JCard, Card as JCard,
@@ -64,7 +64,7 @@ function StateProbe() {
const registry = { Button, Text, Stack, Card }; const registry = { Button, Text, Stack, Card };
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helper: render a jsonsx spec with @json-render/react // Helper: render a jrx spec with @json-render/react
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function renderSpec(spec: Spec, handlers?: Record<string, (...args: unknown[]) => void>) { function renderSpec(spec: Spec, handlers?: Record<string, (...args: unknown[]) => void>) {
@@ -80,15 +80,15 @@ function renderSpec(spec: Spec, handlers?: Record<string, (...args: unknown[]) =
// Basic rendering // Basic rendering
// ============================================================================= // =============================================================================
describe("jsonsx → @json-render/react round-trip", () => { describe("jrx → @json-render/react round-trip", () => {
it("renders a single element", () => { it("renders a single element", () => {
const spec = jsonsxRender(jsx(JText, { content: "Hello from jsonsx" })); const spec = jrxRender(jsx(JText, { content: "Hello from jrx" }));
renderSpec(spec); renderSpec(spec);
expect(screen.getByTestId("text").textContent).toBe("Hello from jsonsx"); expect(screen.getByTestId("text").textContent).toBe("Hello from jrx");
}); });
it("renders nested elements with children", () => { it("renders nested elements with children", () => {
const spec = jsonsxRender( const spec = jrxRender(
jsxs(JCard, { jsxs(JCard, {
title: "My Card", title: "My Card",
children: [jsx(JText, { content: "Inside card" })], children: [jsx(JText, { content: "Inside card" })],
@@ -101,7 +101,7 @@ describe("jsonsx → @json-render/react round-trip", () => {
}); });
it("renders a tree with multiple children", () => { it("renders a tree with multiple children", () => {
const spec = jsonsxRender( const spec = jrxRender(
jsxs(JStack, { jsxs(JStack, {
children: [ children: [
jsx(JText, { content: "First" }), jsx(JText, { content: "First" }),
@@ -116,7 +116,7 @@ describe("jsonsx → @json-render/react round-trip", () => {
}); });
it("renders a deep tree", () => { it("renders a deep tree", () => {
const spec = jsonsxRender( const spec = jrxRender(
jsxs(JStack, { jsxs(JStack, {
children: [ children: [
jsxs(JCard, { jsxs(JCard, {
@@ -136,9 +136,9 @@ describe("jsonsx → @json-render/react round-trip", () => {
// State + actions (adapted from chained-actions.test.tsx) // State + actions (adapted from chained-actions.test.tsx)
// ============================================================================= // =============================================================================
describe("jsonsx specs with state and actions", () => { describe("jrx specs with state and actions", () => {
it("renders with initial state", () => { it("renders with initial state", () => {
const spec = jsonsxRender(jsx(JText, { content: "Stateful" }), { const spec = jrxRender(jsx(JText, { content: "Stateful" }), {
state: { count: 42 }, state: { count: 42 },
}); });
renderSpec(spec); renderSpec(spec);
@@ -148,7 +148,7 @@ describe("jsonsx specs with state and actions", () => {
}); });
it("setState action updates state on button press", async () => { it("setState action updates state on button press", async () => {
const spec = jsonsxRender( const spec = jrxRender(
jsx(JButton, { jsx(JButton, {
label: "Set", label: "Set",
on: { on: {
@@ -172,7 +172,7 @@ describe("jsonsx specs with state and actions", () => {
}); });
it("chained pushState + setState resolves correctly", async () => { it("chained pushState + setState resolves correctly", async () => {
const spec = jsonsxRender( const spec = jrxRender(
jsx(JButton, { jsx(JButton, {
label: "Chain", label: "Chain",
on: { on: {
@@ -206,7 +206,7 @@ describe("jsonsx specs with state and actions", () => {
}); });
it("multiple pushState chain resolves correctly", async () => { it("multiple pushState chain resolves correctly", async () => {
const spec = jsonsxRender( const spec = jrxRender(
jsx(JButton, { jsx(JButton, {
label: "Go", label: "Go",
on: { on: {
@@ -242,9 +242,9 @@ describe("jsonsx specs with state and actions", () => {
// Spec structural validity // Spec structural validity
// ============================================================================= // =============================================================================
describe("jsonsx spec structural validity", () => { describe("jrx spec structural validity", () => {
it("all child references resolve to existing elements", () => { it("all child references resolve to existing elements", () => {
const spec = jsonsxRender( const spec = jrxRender(
jsxs(JStack, { jsxs(JStack, {
children: [ children: [
jsxs(JCard, { jsxs(JCard, {
@@ -272,12 +272,12 @@ describe("jsonsx spec structural validity", () => {
}); });
it("root element exists in elements map", () => { it("root element exists in elements map", () => {
const spec = jsonsxRender(jsx(JCard, { title: "Root" })); const spec = jrxRender(jsx(JCard, { title: "Root" }));
expect(spec.elements[spec.root]).toBeDefined(); expect(spec.elements[spec.root]).toBeDefined();
}); });
it("element count matches node count", () => { it("element count matches node count", () => {
const spec = jsonsxRender( const spec = jrxRender(
jsxs(JStack, { jsxs(JStack, {
children: [ children: [
jsx(JCard, { title: "A" }), jsx(JCard, { title: "A" }),
@@ -294,9 +294,9 @@ describe("jsonsx spec structural validity", () => {
// Dynamic features (ported from json-render's dynamic-forms.test.tsx) // Dynamic features (ported from json-render's dynamic-forms.test.tsx)
// ============================================================================= // =============================================================================
describe("jsonsx specs with dynamic features", () => { describe("jrx specs with dynamic features", () => {
it("$state prop expressions resolve at render time", () => { it("$state prop expressions resolve at render time", () => {
const spec = jsonsxRender( const spec = jrxRender(
jsx(JText, { content: { $state: "/message" } }), jsx(JText, { content: { $state: "/message" } }),
{ state: { message: "Dynamic hello" } }, { state: { message: "Dynamic hello" } },
); );
@@ -306,7 +306,7 @@ describe("jsonsx specs with dynamic features", () => {
}); });
it("visibility condition hides element when false", () => { it("visibility condition hides element when false", () => {
const spec = jsonsxRender( const spec = jrxRender(
jsxs(JStack, { jsxs(JStack, {
children: [ children: [
jsx(JText, { jsx(JText, {
@@ -324,7 +324,7 @@ describe("jsonsx specs with dynamic features", () => {
it("visibility condition shows element when true", () => { it("visibility condition shows element when true", () => {
cleanup(); cleanup();
const spec = jsonsxRender( const spec = jrxRender(
jsxs(JStack, { jsxs(JStack, {
children: [ children: [
jsx(JText, { jsx(JText, {
@@ -343,7 +343,7 @@ describe("jsonsx specs with dynamic features", () => {
it("watchers fire when watched state changes", async () => { it("watchers fire when watched state changes", async () => {
const loadCities = mock(); const loadCities = mock();
const spec = jsonsxRender( const spec = jrxRender(
jsxs(JStack, { jsxs(JStack, {
children: [ children: [
jsx(JButton, { jsx(JButton, {

View File

@@ -2,12 +2,12 @@ import type {
ActionBinding, ActionBinding,
VisibilityCondition, VisibilityCondition,
} from "@json-render/core"; } from "@json-render/core";
import { JSONSX_NODE, FRAGMENT, type JsonsxNode } from "./types"; import { JRX_NODE, FRAGMENT, type JrxNode } from "./types";
import type { JsonsxComponent } from "./types"; import type { JrxComponent } from "./types";
export { FRAGMENT as Fragment }; export { FRAGMENT as Fragment };
/** Props reserved by jsonsx — extracted from JSX props and placed on the UIElement level. */ /** Props reserved by jrx — extracted from JSX props and placed on the UIElement level. */
const RESERVED_PROPS = new Set([ const RESERVED_PROPS = new Set([
"key", "key",
"children", "children",
@@ -18,26 +18,26 @@ const RESERVED_PROPS = new Set([
]); ]);
/** /**
* Normalize a raw `children` value from JSX props into a flat array of JsonsxNodes. * 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. * Handles: undefined, single node, nested arrays, and filters out nulls/booleans.
*/ */
function normalizeChildren(raw: unknown): JsonsxNode[] { function normalizeChildren(raw: unknown): JrxNode[] {
if (raw == null || typeof raw === "boolean") return []; if (raw == null || typeof raw === "boolean") return [];
if (Array.isArray(raw)) { if (Array.isArray(raw)) {
const result: JsonsxNode[] = []; const result: JrxNode[] = [];
for (const child of raw) { for (const child of raw) {
if (child == null || typeof child === "boolean") continue; if (child == null || typeof child === "boolean") continue;
if (Array.isArray(child)) { if (Array.isArray(child)) {
result.push(...normalizeChildren(child)); result.push(...normalizeChildren(child));
} else { } else {
result.push(child as JsonsxNode); result.push(child as JrxNode);
} }
} }
return result; return result;
} }
return [raw as JsonsxNode]; return [raw as JrxNode];
} }
/** /**
@@ -56,20 +56,20 @@ function extractProps(
} }
/** Accepted tag types: string literal, Fragment symbol, or a function component. */ /** Accepted tag types: string literal, Fragment symbol, or a function component. */
type JsxType = string | typeof FRAGMENT | JsonsxComponent; type JsxType = string | typeof FRAGMENT | JrxComponent;
/** /**
* Core factory — shared by `jsx` and `jsxs`. * Core factory — shared by `jsx` and `jsxs`.
* *
* If `type` is a function, it is called with props (like React calls * If `type` is a function, it is called with props (like React calls
* function components). The function returns a JsonsxNode directly. * function components). The function returns a JrxNode directly.
* *
* If `type` is a string or Fragment, a JsonsxNode is constructed inline. * If `type` is a string or Fragment, a JrxNode is constructed inline.
*/ */
function createNode( function createNode(
type: JsxType, type: JsxType,
rawProps: Record<string, unknown> | null, rawProps: Record<string, unknown> | null,
): JsonsxNode { ): JrxNode {
const p = rawProps ?? {}; const p = rawProps ?? {};
// Function component — call it, just like React does. // Function component — call it, just like React does.
@@ -78,7 +78,7 @@ function createNode(
} }
return { return {
$$typeof: JSONSX_NODE, $$typeof: JRX_NODE,
type, type,
props: extractProps(p), props: extractProps(p),
children: normalizeChildren(p.children), children: normalizeChildren(p.children),
@@ -102,7 +102,7 @@ export function jsx(
type: JsxType, type: JsxType,
props: Record<string, unknown> | null, props: Record<string, unknown> | null,
key?: string, key?: string,
): JsonsxNode { ): JrxNode {
const node = createNode(type, props); const node = createNode(type, props);
if (key != null) node.key = String(key); if (key != null) node.key = String(key);
return node; return node;
@@ -116,7 +116,7 @@ export function jsxs(
type: JsxType, type: JsxType,
props: Record<string, unknown> | null, props: Record<string, unknown> | null,
key?: string, key?: string,
): JsonsxNode { ): JrxNode {
const node = createNode(type, props); const node = createNode(type, props);
if (key != null) node.key = String(key); if (key != null) node.key = String(key);
return node; return node;
@@ -133,7 +133,7 @@ export namespace JSX {
} }
/** The type returned by JSX expressions. */ /** The type returned by JSX expressions. */
export type Element = JsonsxNode; export type Element = JrxNode;
export interface ElementChildrenAttribute { export interface ElementChildrenAttribute {
children: {}; children: {};

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from "bun:test"; import { describe, it, expect } from "bun:test";
import { render } from "./render"; import { render } from "./render";
import { isJsonsxNode, FRAGMENT } from "./types"; import { isJrxNode, FRAGMENT } from "./types";
import { jsx, jsxs, Fragment } from "./jsx-runtime"; import { jsx, jsxs, Fragment } from "./jsx-runtime";
import { import {
Stack, Stack,
@@ -18,25 +18,25 @@ import {
// ============================================================================= // =============================================================================
describe("jsx factory", () => { describe("jsx factory", () => {
it("jsx() with string type returns a JsonsxNode", () => { it("jsx() with string type returns a JrxNode", () => {
const node = jsx("Card", { title: "Hello" }); const node = jsx("Card", { title: "Hello" });
expect(isJsonsxNode(node)).toBe(true); expect(isJrxNode(node)).toBe(true);
expect(node.type).toBe("Card"); expect(node.type).toBe("Card");
expect(node.props).toEqual({ title: "Hello" }); expect(node.props).toEqual({ title: "Hello" });
}); });
it("jsx() with component function resolves typeName", () => { it("jsx() with component function resolves typeName", () => {
const node = jsx(Card, { title: "Hello" }); const node = jsx(Card, { title: "Hello" });
expect(isJsonsxNode(node)).toBe(true); expect(isJrxNode(node)).toBe(true);
expect(node.type).toBe("Card"); expect(node.type).toBe("Card");
expect(node.props).toEqual({ title: "Hello" }); expect(node.props).toEqual({ title: "Hello" });
}); });
it("jsxs() returns a JsonsxNode with children", () => { it("jsxs() returns a JrxNode with children", () => {
const node = jsxs(Stack, { const node = jsxs(Stack, {
children: [jsx(Text, { content: "A" }), jsx(Text, { content: "B" })], children: [jsx(Text, { content: "A" }), jsx(Text, { content: "B" })],
}); });
expect(isJsonsxNode(node)).toBe(true); expect(isJrxNode(node)).toBe(true);
expect(node.children).toHaveLength(2); expect(node.children).toHaveLength(2);
}); });
@@ -73,7 +73,7 @@ describe("jsx factory", () => {
it("jsx() handles null props", () => { it("jsx() handles null props", () => {
const node = jsx("Divider", null); const node = jsx("Divider", null);
expect(isJsonsxNode(node)).toBe(true); expect(isJrxNode(node)).toBe(true);
expect(node.props).toEqual({}); expect(node.props).toEqual({});
expect(node.children).toEqual([]); expect(node.children).toEqual([]);
}); });

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from "bun:test"; import { describe, it, expect } from "bun:test";
import { render } from "./render"; import { render } from "./render";
import { FRAGMENT, type JsonsxNode } from "./types"; import { FRAGMENT, type JrxNode } from "./types";
import { jsx } from "./jsx-runtime"; import { jsx } from "./jsx-runtime";
import { import {
Stack, Stack,
@@ -300,7 +300,7 @@ describe("state passthrough", () => {
// ============================================================================= // =============================================================================
describe("error handling", () => { describe("error handling", () => {
it("throws for non-JsonsxNode input", () => { it("throws for non-JrxNode input", () => {
expect(() => render({} as JsonsxNode)).toThrow(/expects a JsonsxNode/); expect(() => render({} as JrxNode)).toThrow(/expects a JrxNode/);
}); });
}); });

View File

@@ -1,12 +1,12 @@
import type { Spec, UIElement } from "@json-render/core"; import type { Spec, UIElement } from "@json-render/core";
import { FRAGMENT, type JsonsxNode, type RenderOptions, isJsonsxNode } from "./types"; import { FRAGMENT, type JrxNode, type RenderOptions, isJrxNode } from "./types";
/** /**
* Flatten a JsonsxNode tree into a json-render `Spec`. * Flatten a JrxNode tree into a json-render `Spec`.
* *
* Analogous to `ReactDOM.render` but produces JSON instead of DOM mutations. * Analogous to `ReactDOM.render` but produces JSON instead of DOM mutations.
* *
* @param node - Root JsonsxNode (produced by JSX) * @param node - Root JrxNode (produced by JSX)
* @param options - Optional render configuration (e.g. initial state) * @param options - Optional render configuration (e.g. initial state)
* @returns A json-render `Spec` ready for any renderer * @returns A json-render `Spec` ready for any renderer
* *
@@ -20,9 +20,9 @@ import { FRAGMENT, type JsonsxNode, type RenderOptions, isJsonsxNode } from "./t
* ); * );
* ``` * ```
*/ */
export function render(node: JsonsxNode, options?: RenderOptions): Spec { export function render(node: JrxNode, options?: RenderOptions): Spec {
if (!isJsonsxNode(node)) { if (!isJrxNode(node)) {
throw new Error("render() expects a JsonsxNode produced by JSX."); throw new Error("render() expects a JrxNode produced by JSX.");
} }
if (node.type === FRAGMENT) { if (node.type === FRAGMENT) {
@@ -66,12 +66,12 @@ function generateKey(
/** /**
* Resolve the children of a node, expanding fragments inline. * Resolve the children of a node, expanding fragments inline.
* Returns an array of concrete (non-fragment) JsonsxNodes. * Returns an array of concrete (non-fragment) JrxNodes.
*/ */
function expandChildren(children: JsonsxNode[]): JsonsxNode[] { function expandChildren(children: JrxNode[]): JrxNode[] {
const result: JsonsxNode[] = []; const result: JrxNode[] = [];
for (const child of children) { for (const child of children) {
if (!isJsonsxNode(child)) continue; if (!isJrxNode(child)) continue;
if (child.type === FRAGMENT) { if (child.type === FRAGMENT) {
// Recursively expand nested fragments // Recursively expand nested fragments
result.push(...expandChildren(child.children)); result.push(...expandChildren(child.children));
@@ -83,11 +83,11 @@ function expandChildren(children: JsonsxNode[]): JsonsxNode[] {
} }
/** /**
* Recursively flatten a JsonsxNode into the elements map. * Recursively flatten a JrxNode into the elements map.
* Returns the key assigned to this node. * Returns the key assigned to this node.
*/ */
function flattenNode( function flattenNode(
node: JsonsxNode, node: JrxNode,
elements: Record<string, UIElement>, elements: Record<string, UIElement>,
counters: Map<string, number>, counters: Map<string, number>,
usedKeys: Set<string>, usedKeys: Set<string>,

View File

@@ -1,7 +1,7 @@
/** /**
* Ported from json-render's core/src/spec-validator.test.ts. * Ported from json-render's core/src/spec-validator.test.ts.
* *
* Runs @json-render/core's validateSpec against Specs produced by jsonsx * Runs @json-render/core's validateSpec against Specs produced by jrx
* to prove structural correctness. * to prove structural correctness.
*/ */
@@ -19,7 +19,7 @@ import {
Select, Select,
} from "./test-components"; } from "./test-components";
describe("validateSpec on jsonsx-produced specs", () => { describe("validateSpec on jrx-produced specs", () => {
it("validates a simple single-element spec", () => { it("validates a simple single-element spec", () => {
const spec = render(<Text text="hello" />); const spec = render(<Text text="hello" />);
const result = validateSpec(spec); const result = validateSpec(spec);
@@ -90,7 +90,7 @@ describe("validateSpec on jsonsx-produced specs", () => {
expect(result.issues.some((i) => i.code === "watch_in_props")).toBe(false); expect(result.issues.some((i) => i.code === "watch_in_props")).toBe(false);
}); });
it("no orphaned elements in jsonsx output", () => { it("no orphaned elements in jrx output", () => {
const spec = render( const spec = render(
<Stack> <Stack>
<Text text="A" /> <Text text="A" />
@@ -101,7 +101,7 @@ describe("validateSpec on jsonsx-produced specs", () => {
expect(result.issues.some((i) => i.code === "orphaned_element")).toBe(false); expect(result.issues.some((i) => i.code === "orphaned_element")).toBe(false);
}); });
it("no missing children in jsonsx output", () => { it("no missing children in jrx output", () => {
const spec = render( const spec = render(
<Stack> <Stack>
<Card title="X"> <Card title="X">

View File

@@ -1,42 +1,42 @@
import { jsx } from "./jsx-runtime"; import { jsx } from "./jsx-runtime";
import type { JsonsxNode } from "./types"; import type { JrxNode } from "./types";
export function Stack(props: Record<string, unknown>): JsonsxNode { export function Stack(props: Record<string, unknown>): JrxNode {
return jsx("Stack", props); return jsx("Stack", props);
} }
export function Card(props: Record<string, unknown>): JsonsxNode { export function Card(props: Record<string, unknown>): JrxNode {
return jsx("Card", props); return jsx("Card", props);
} }
export function Text(props: Record<string, unknown>): JsonsxNode { export function Text(props: Record<string, unknown>): JrxNode {
return jsx("Text", props); return jsx("Text", props);
} }
export function Button(props: Record<string, unknown>): JsonsxNode { export function Button(props: Record<string, unknown>): JrxNode {
return jsx("Button", props); return jsx("Button", props);
} }
export function Badge(props: Record<string, unknown>): JsonsxNode { export function Badge(props: Record<string, unknown>): JrxNode {
return jsx("Badge", props); return jsx("Badge", props);
} }
export function List(props: Record<string, unknown>): JsonsxNode { export function List(props: Record<string, unknown>): JrxNode {
return jsx("List", props); return jsx("List", props);
} }
export function ListItem(props: Record<string, unknown>): JsonsxNode { export function ListItem(props: Record<string, unknown>): JrxNode {
return jsx("ListItem", props); return jsx("ListItem", props);
} }
export function Select(props: Record<string, unknown>): JsonsxNode { export function Select(props: Record<string, unknown>): JrxNode {
return jsx("Select", props); return jsx("Select", props);
} }
export function Input(props: Record<string, unknown>): JsonsxNode { export function Input(props: Record<string, unknown>): JrxNode {
return jsx("Input", props); return jsx("Input", props);
} }
export function Divider(props: Record<string, unknown>): JsonsxNode { export function Divider(props: Record<string, unknown>): JrxNode {
return jsx("Divider", props); return jsx("Divider", props);
} }

View File

@@ -4,19 +4,19 @@ import type {
} from "@json-render/core"; } from "@json-render/core";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// JsonsxNode — intermediate representation produced by the JSX factory // JrxNode — intermediate representation produced by the JSX factory
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/** /**
* Sentinel symbol identifying a JsonsxNode (prevents plain objects from * Sentinel symbol identifying a JrxNode (prevents plain objects from
* being mistaken for nodes). * being mistaken for nodes).
*/ */
export const JSONSX_NODE = Symbol.for("jsonsx.node"); export const JRX_NODE = Symbol.for("jrx.node");
/** /**
* Sentinel symbol for Fragment grouping. * Sentinel symbol for Fragment grouping.
*/ */
export const FRAGMENT = Symbol.for("jsonsx.fragment"); export const FRAGMENT = Symbol.for("jrx.fragment");
/** /**
* A node in the intermediate JSX tree. * A node in the intermediate JSX tree.
@@ -24,9 +24,9 @@ export const FRAGMENT = Symbol.for("jsonsx.fragment");
* Created by the `jsx` / `jsxs` factory functions and consumed by `render()` * Created by the `jsx` / `jsxs` factory functions and consumed by `render()`
* which flattens the tree into a json-render `Spec`. * which flattens the tree into a json-render `Spec`.
*/ */
export interface JsonsxNode { export interface JrxNode {
/** Brand symbol — always `JSONSX_NODE` */ /** Brand symbol — always `JRX_NODE` */
$$typeof: typeof JSONSX_NODE; $$typeof: typeof JRX_NODE;
/** /**
* Component type name (e.g. `"Card"`, `"Button"`). * Component type name (e.g. `"Card"`, `"Button"`).
@@ -38,7 +38,7 @@ export interface JsonsxNode {
props: Record<string, unknown>; props: Record<string, unknown>;
/** Child nodes */ /** Child nodes */
children: JsonsxNode[]; children: JrxNode[];
// -- Reserved / meta fields (extracted from JSX props) -- // -- Reserved / meta fields (extracted from JSX props) --
@@ -59,20 +59,20 @@ export interface JsonsxNode {
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// JsonsxComponent — a function usable as a JSX tag that maps to a type string // JrxComponent — a function usable as a JSX tag that maps to a type string
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/** /**
* A jsonsx component function. Works like a React function component: * A jrx component function. Works like a React function component:
* when used as a JSX tag (`<Card />`), the factory calls the function * when used as a JSX tag (`<Card />`), the factory calls the function
* with props and gets back a JsonsxNode. * with props and gets back a JrxNode.
*/ */
export type JsonsxComponent = (props: Record<string, unknown>) => JsonsxNode; export type JrxComponent = (props: Record<string, unknown>) => JrxNode;
/** /**
* Define a jsonsx component for use as a JSX tag. * Define a jrx component for use as a JSX tag.
* *
* Creates a function that, when called with props, produces a JsonsxNode * Creates a function that, when called with props, produces a JrxNode
* with the given type name — just like a React component returns * with the given type name — just like a React component returns
* React elements. * React elements.
* *
@@ -82,12 +82,12 @@ export type JsonsxComponent = (props: Record<string, unknown>) => JsonsxNode;
* const spec = render(<Card title="Hello"><Text content="World" /></Card>); * const spec = render(<Card title="Hello"><Text content="World" /></Card>);
* ``` * ```
*/ */
export function component(typeName: string): JsonsxComponent { export function component(typeName: string): JrxComponent {
// Import createNodeFromString lazily to avoid circular dep // Import createNodeFromString lazily to avoid circular dep
// (jsx-runtime imports types). Instead, we build the node inline. // (jsx-runtime imports types). Instead, we build the node inline.
return (props: Record<string, unknown>) => { return (props: Record<string, unknown>) => {
return { return {
$$typeof: JSONSX_NODE, $$typeof: JRX_NODE,
type: typeName, type: typeName,
props: filterReserved(props), props: filterReserved(props),
children: normalizeChildrenRaw(props.children), children: normalizeChildrenRaw(props.children),
@@ -110,21 +110,21 @@ function filterReserved(props: Record<string, unknown>): Record<string, unknown>
return out; return out;
} }
function normalizeChildrenRaw(raw: unknown): JsonsxNode[] { function normalizeChildrenRaw(raw: unknown): JrxNode[] {
if (raw == null || typeof raw === "boolean") return []; if (raw == null || typeof raw === "boolean") return [];
if (Array.isArray(raw)) { if (Array.isArray(raw)) {
const result: JsonsxNode[] = []; const result: JrxNode[] = [];
for (const child of raw) { for (const child of raw) {
if (child == null || typeof child === "boolean") continue; if (child == null || typeof child === "boolean") continue;
if (Array.isArray(child)) { if (Array.isArray(child)) {
result.push(...normalizeChildrenRaw(child)); result.push(...normalizeChildrenRaw(child));
} else { } else {
result.push(child as JsonsxNode); result.push(child as JrxNode);
} }
} }
return result; return result;
} }
return [raw as JsonsxNode]; return [raw as JrxNode];
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -140,10 +140,10 @@ export interface RenderOptions {
// Type guard // Type guard
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export function isJsonsxNode(value: unknown): value is JsonsxNode { export function isJrxNode(value: unknown): value is JrxNode {
return ( return (
typeof value === "object" && typeof value === "object" &&
value !== null && value !== null &&
(value as JsonsxNode).$$typeof === JSONSX_NODE (value as JrxNode).$$typeof === JRX_NODE
); );
} }

View File

@@ -14,11 +14,11 @@
"outDir": "dist", "outDir": "dist",
"rootDir": "src", "rootDir": "src",
"jsx": "react-jsx", "jsx": "react-jsx",
"jsxImportSource": "jsonsx", "jsxImportSource": "@nym.sh/jrx",
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"jsonsx/jsx-runtime": ["./src/jsx-runtime"], "@nym.sh/jrx/jsx-runtime": ["./src/jsx-runtime"],
"jsonsx/jsx-dev-runtime": ["./src/jsx-dev-runtime"] "@nym.sh/jrx/jsx-dev-runtime": ["./src/jsx-dev-runtime"]
} }
}, },
"include": ["src"] "include": ["src"]