mirror of
https://github.com/kennethnym/jrx.git
synced 2026-03-20 03:41:18 +00:00
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "jrx",
|
"name": "jfx",
|
||||||
"build": {
|
"build": {
|
||||||
"context": ".",
|
"context": ".",
|
||||||
"dockerfile": "Dockerfile"
|
"dockerfile": "Dockerfile"
|
||||||
|
|||||||
28
README.md
28
README.md
@@ -1,22 +1,22 @@
|
|||||||
# jrx
|
# jfx
|
||||||
|
|
||||||
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 jrx @json-render/core
|
bun add jfx @json-render/core
|
||||||
```
|
```
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
Configure your `tsconfig.json` to use jrx as the JSX source:
|
Configure your `tsconfig.json` to use jfx as the JSX source:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"jsxImportSource": "jrx"
|
"jsxImportSource": "jfx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -24,7 +24,7 @@ Configure your `tsconfig.json` to use jrx as the JSX source:
|
|||||||
Or use a per-file pragma:
|
Or use a per-file pragma:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
/** @jsxImportSource jrx */
|
/** @jsxImportSource jfx */
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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 "jrx/jsx-runtime";
|
import { jsx } from "jfx/jsx-runtime";
|
||||||
import type { JrxNode } from "jrx";
|
import type { JfxNode } from "jfx";
|
||||||
|
|
||||||
export function Stack(props: Record<string, unknown>): JrxNode {
|
export function Stack(props: Record<string, unknown>): JfxNode {
|
||||||
return jsx("Stack", props);
|
return jsx("Stack", props);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Text(props: Record<string, unknown>): JrxNode {
|
export function Text(props: Record<string, unknown>): JfxNode {
|
||||||
return jsx("Text", props);
|
return jsx("Text", props);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Button(props: Record<string, unknown>): JrxNode {
|
export function Button(props: Record<string, unknown>): JfxNode {
|
||||||
return jsx("Button", props);
|
return jsx("Button", props);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -53,12 +53,12 @@ export function Button(props: Record<string, unknown>): JrxNode {
|
|||||||
### Render JSX to Spec JSON
|
### Render JSX to Spec JSON
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { render } from "jrx";
|
import { render } from "jfx";
|
||||||
import { Stack, Text, Button } from "./components";
|
import { Stack, Text, Button } from "./components";
|
||||||
|
|
||||||
const spec = render(
|
const spec = render(
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text content="Hello from jrx!" />
|
<Text content="Hello from jfx!" />
|
||||||
<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 jrx!" }
|
"props": { "content": "Hello from jfx!" }
|
||||||
},
|
},
|
||||||
"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 jrx 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 jfx 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
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
"configVersion": 1,
|
"configVersion": 1,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "jrx-example",
|
"name": "jfx-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",
|
||||||
"jrx": "file:..",
|
"jfx": "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",
|
||||||
@@ -171,7 +171,7 @@
|
|||||||
|
|
||||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@25.3.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q=="],
|
"@types/node": ["@types/node@20.19.35", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
||||||
|
|
||||||
@@ -241,9 +241,9 @@
|
|||||||
|
|
||||||
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
|
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
|
||||||
|
|
||||||
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
|
"jfx": ["jfx@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" } }],
|
||||||
|
|
||||||
"jrx": ["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" } }],
|
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
|
||||||
|
|
||||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
@@ -347,7 +347,7 @@
|
|||||||
|
|
||||||
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
|
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
|
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
|
||||||
|
|
||||||
@@ -369,10 +369,6 @@
|
|||||||
|
|
||||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||||
|
|
||||||
"happy-dom/@types/node": ["@types/node@20.19.35", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ=="],
|
"jfx/@types/react": ["@types/react@19.2.3", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-k5dJVszUiNr1DSe8Cs+knKR6IrqhqdhpUwzqhkS8ecQTSf3THNtbfIp/umqHMpX2bv+9dkx3fwDv/86LcSfvSg=="],
|
||||||
|
|
||||||
"jrx/@types/react": ["@types/react@19.2.3", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-k5dJVszUiNr1DSe8Cs+knKR6IrqhqdhpUwzqhkS8ecQTSf3THNtbfIp/umqHMpX2bv+9dkx3fwDv/86LcSfvSg=="],
|
|
||||||
|
|
||||||
"happy-dom/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import { jsx } from "jrx/jsx-runtime";
|
import { jsx } from "jfx/jsx-runtime";
|
||||||
import type { JrxNode } from "jrx";
|
import type { JfxNode } from "jfx";
|
||||||
|
|
||||||
|
|
||||||
export function Stack(props: Record<string, unknown>): JrxNode {
|
export function Stack(props: Record<string, unknown>): JfxNode {
|
||||||
return jsx("Stack", props);
|
return jsx("Stack", props);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Card(props: Record<string, unknown>): JrxNode {
|
export function Card(props: Record<string, unknown>): JfxNode {
|
||||||
return jsx("Card", props);
|
return jsx("Card", props);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Text(props: Record<string, unknown>): JrxNode {
|
export function Text(props: Record<string, unknown>): JfxNode {
|
||||||
return jsx("Text", props);
|
return jsx("Text", props);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Button(props: Record<string, unknown>): JrxNode {
|
export function Button(props: Record<string, unknown>): JfxNode {
|
||||||
return jsx("Button", props);
|
return jsx("Button", props);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Input(props: Record<string, unknown>): JrxNode {
|
export function Input(props: Record<string, unknown>): JfxNode {
|
||||||
return jsx("Input", props);
|
return jsx("Input", props);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "jrx-example",
|
"name": "jfx-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",
|
||||||
"jrx": "file:..",
|
"jfx": "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"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/** @jsxImportSource jrx */
|
/** @jsxImportSource jfx */
|
||||||
import { render } from "jrx";
|
import { render } from "jfx";
|
||||||
import { Stack, Card, Text, Button, Input } from "../components";
|
import { Stack, Card, Text, Button, Input } from "../components";
|
||||||
|
|
||||||
export const fullSpec = render(
|
export const fullSpec = render(
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/** @jsxImportSource jrx */
|
/** @jsxImportSource jfx */
|
||||||
import { render } from "jrx";
|
import { render } from "jfx";
|
||||||
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 jrx!" />
|
<Text content="Hello from jfx!" />
|
||||||
<Button label="Click me" />
|
<Button label="Click me" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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" }}>jrx examples</h1>
|
<h1 style={{ fontSize: "24px", marginBottom: "8px" }}>jfx examples</h1>
|
||||||
<p style={{ color: "var(--text-secondary)", fontSize: "14px", marginBottom: "24px" }}>
|
<p style={{ color: "var(--text-secondary)", fontSize: "14px", marginBottom: "24px" }}>
|
||||||
JSX → json-render Spec. Pick a spec to see the live UI and JSON
|
JSX → json-render Spec. Pick a spec to see the live UI and JSON
|
||||||
output.
|
output.
|
||||||
|
|||||||
@@ -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>jrx example</title>
|
<title>jfx example</title>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg: #f5f5f5;
|
--bg: #f5f5f5;
|
||||||
|
|||||||
@@ -46,4 +46,4 @@ const server = serve({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`jrx example server running at ${server.url}`);
|
console.log(`jfx example server running at ${server.url}`);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "jrx",
|
"name": "jfx",
|
||||||
"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.",
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export { render } from "./render";
|
export { render } from "./render";
|
||||||
export { isJrxNode, JRX_NODE, FRAGMENT } from "./types";
|
export { isJfxNode, JFX_NODE, FRAGMENT } from "./types";
|
||||||
export type { JrxNode, JrxComponent, RenderOptions } from "./types";
|
export type { JfxNode, JfxComponent, RenderOptions } from "./types";
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
/** @jsxImportSource react */
|
/** @jsxImportSource react */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integration tests: verify that Specs produced by jrx are consumable
|
* Integration tests: verify that Specs produced by jfx 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 jrx's jsx()/jsxs() via the component wrappers for building Specs.
|
* tree, and jfx'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 jrxRender } from "./render";
|
import { render as jfxRender } 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 jrx spec with @json-render/react
|
// Helper: render a jfx 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("jrx → @json-render/react round-trip", () => {
|
describe("jfx → @json-render/react round-trip", () => {
|
||||||
it("renders a single element", () => {
|
it("renders a single element", () => {
|
||||||
const spec = jrxRender(jsx(JText, { content: "Hello from jrx" }));
|
const spec = jfxRender(jsx(JText, { content: "Hello from jfx" }));
|
||||||
renderSpec(spec);
|
renderSpec(spec);
|
||||||
expect(screen.getByTestId("text").textContent).toBe("Hello from jrx");
|
expect(screen.getByTestId("text").textContent).toBe("Hello from jfx");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders nested elements with children", () => {
|
it("renders nested elements with children", () => {
|
||||||
const spec = jrxRender(
|
const spec = jfxRender(
|
||||||
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("jrx → @json-render/react round-trip", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("renders a tree with multiple children", () => {
|
it("renders a tree with multiple children", () => {
|
||||||
const spec = jrxRender(
|
const spec = jfxRender(
|
||||||
jsxs(JStack, {
|
jsxs(JStack, {
|
||||||
children: [
|
children: [
|
||||||
jsx(JText, { content: "First" }),
|
jsx(JText, { content: "First" }),
|
||||||
@@ -116,7 +116,7 @@ describe("jrx → @json-render/react round-trip", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("renders a deep tree", () => {
|
it("renders a deep tree", () => {
|
||||||
const spec = jrxRender(
|
const spec = jfxRender(
|
||||||
jsxs(JStack, {
|
jsxs(JStack, {
|
||||||
children: [
|
children: [
|
||||||
jsxs(JCard, {
|
jsxs(JCard, {
|
||||||
@@ -136,9 +136,9 @@ describe("jrx → @json-render/react round-trip", () => {
|
|||||||
// State + actions (adapted from chained-actions.test.tsx)
|
// State + actions (adapted from chained-actions.test.tsx)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
describe("jrx specs with state and actions", () => {
|
describe("jfx specs with state and actions", () => {
|
||||||
it("renders with initial state", () => {
|
it("renders with initial state", () => {
|
||||||
const spec = jrxRender(jsx(JText, { content: "Stateful" }), {
|
const spec = jfxRender(jsx(JText, { content: "Stateful" }), {
|
||||||
state: { count: 42 },
|
state: { count: 42 },
|
||||||
});
|
});
|
||||||
renderSpec(spec);
|
renderSpec(spec);
|
||||||
@@ -148,7 +148,7 @@ describe("jrx 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 = jrxRender(
|
const spec = jfxRender(
|
||||||
jsx(JButton, {
|
jsx(JButton, {
|
||||||
label: "Set",
|
label: "Set",
|
||||||
on: {
|
on: {
|
||||||
@@ -172,7 +172,7 @@ describe("jrx specs with state and actions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("chained pushState + setState resolves correctly", async () => {
|
it("chained pushState + setState resolves correctly", async () => {
|
||||||
const spec = jrxRender(
|
const spec = jfxRender(
|
||||||
jsx(JButton, {
|
jsx(JButton, {
|
||||||
label: "Chain",
|
label: "Chain",
|
||||||
on: {
|
on: {
|
||||||
@@ -206,7 +206,7 @@ describe("jrx specs with state and actions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("multiple pushState chain resolves correctly", async () => {
|
it("multiple pushState chain resolves correctly", async () => {
|
||||||
const spec = jrxRender(
|
const spec = jfxRender(
|
||||||
jsx(JButton, {
|
jsx(JButton, {
|
||||||
label: "Go",
|
label: "Go",
|
||||||
on: {
|
on: {
|
||||||
@@ -242,9 +242,9 @@ describe("jrx specs with state and actions", () => {
|
|||||||
// Spec structural validity
|
// Spec structural validity
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
describe("jrx spec structural validity", () => {
|
describe("jfx spec structural validity", () => {
|
||||||
it("all child references resolve to existing elements", () => {
|
it("all child references resolve to existing elements", () => {
|
||||||
const spec = jrxRender(
|
const spec = jfxRender(
|
||||||
jsxs(JStack, {
|
jsxs(JStack, {
|
||||||
children: [
|
children: [
|
||||||
jsxs(JCard, {
|
jsxs(JCard, {
|
||||||
@@ -272,12 +272,12 @@ describe("jrx spec structural validity", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("root element exists in elements map", () => {
|
it("root element exists in elements map", () => {
|
||||||
const spec = jrxRender(jsx(JCard, { title: "Root" }));
|
const spec = jfxRender(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 = jrxRender(
|
const spec = jfxRender(
|
||||||
jsxs(JStack, {
|
jsxs(JStack, {
|
||||||
children: [
|
children: [
|
||||||
jsx(JCard, { title: "A" }),
|
jsx(JCard, { title: "A" }),
|
||||||
@@ -294,9 +294,9 @@ describe("jrx 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("jrx specs with dynamic features", () => {
|
describe("jfx specs with dynamic features", () => {
|
||||||
it("$state prop expressions resolve at render time", () => {
|
it("$state prop expressions resolve at render time", () => {
|
||||||
const spec = jrxRender(
|
const spec = jfxRender(
|
||||||
jsx(JText, { content: { $state: "/message" } }),
|
jsx(JText, { content: { $state: "/message" } }),
|
||||||
{ state: { message: "Dynamic hello" } },
|
{ state: { message: "Dynamic hello" } },
|
||||||
);
|
);
|
||||||
@@ -306,7 +306,7 @@ describe("jrx specs with dynamic features", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("visibility condition hides element when false", () => {
|
it("visibility condition hides element when false", () => {
|
||||||
const spec = jrxRender(
|
const spec = jfxRender(
|
||||||
jsxs(JStack, {
|
jsxs(JStack, {
|
||||||
children: [
|
children: [
|
||||||
jsx(JText, {
|
jsx(JText, {
|
||||||
@@ -324,7 +324,7 @@ describe("jrx specs with dynamic features", () => {
|
|||||||
|
|
||||||
it("visibility condition shows element when true", () => {
|
it("visibility condition shows element when true", () => {
|
||||||
cleanup();
|
cleanup();
|
||||||
const spec = jrxRender(
|
const spec = jfxRender(
|
||||||
jsxs(JStack, {
|
jsxs(JStack, {
|
||||||
children: [
|
children: [
|
||||||
jsx(JText, {
|
jsx(JText, {
|
||||||
@@ -343,7 +343,7 @@ describe("jrx 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 = jrxRender(
|
const spec = jfxRender(
|
||||||
jsxs(JStack, {
|
jsxs(JStack, {
|
||||||
children: [
|
children: [
|
||||||
jsx(JButton, {
|
jsx(JButton, {
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import type {
|
|||||||
ActionBinding,
|
ActionBinding,
|
||||||
VisibilityCondition,
|
VisibilityCondition,
|
||||||
} from "@json-render/core";
|
} from "@json-render/core";
|
||||||
import { JRX_NODE, FRAGMENT, type JrxNode } from "./types";
|
import { JFX_NODE, FRAGMENT, type JfxNode } from "./types";
|
||||||
import type { JrxComponent } from "./types";
|
import type { JfxComponent } from "./types";
|
||||||
|
|
||||||
export { FRAGMENT as Fragment };
|
export { FRAGMENT as Fragment };
|
||||||
|
|
||||||
/** Props reserved by jrx — extracted from JSX props and placed on the UIElement level. */
|
/** Props reserved by jfx — 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 JrxNodes.
|
* Normalize a raw `children` value from JSX props into a flat array of JfxNodes.
|
||||||
* 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): JrxNode[] {
|
function normalizeChildren(raw: unknown): JfxNode[] {
|
||||||
if (raw == null || typeof raw === "boolean") return [];
|
if (raw == null || typeof raw === "boolean") return [];
|
||||||
|
|
||||||
if (Array.isArray(raw)) {
|
if (Array.isArray(raw)) {
|
||||||
const result: JrxNode[] = [];
|
const result: JfxNode[] = [];
|
||||||
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 JrxNode);
|
result.push(child as JfxNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [raw as JrxNode];
|
return [raw as JfxNode];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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 | JrxComponent;
|
type JsxType = string | typeof FRAGMENT | JfxComponent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 JrxNode directly.
|
* function components). The function returns a JfxNode directly.
|
||||||
*
|
*
|
||||||
* If `type` is a string or Fragment, a JrxNode is constructed inline.
|
* If `type` is a string or Fragment, a JfxNode is constructed inline.
|
||||||
*/
|
*/
|
||||||
function createNode(
|
function createNode(
|
||||||
type: JsxType,
|
type: JsxType,
|
||||||
rawProps: Record<string, unknown> | null,
|
rawProps: Record<string, unknown> | null,
|
||||||
): JrxNode {
|
): JfxNode {
|
||||||
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: JRX_NODE,
|
$$typeof: JFX_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,
|
||||||
): JrxNode {
|
): JfxNode {
|
||||||
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,
|
||||||
): JrxNode {
|
): JfxNode {
|
||||||
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 = JrxNode;
|
export type Element = JfxNode;
|
||||||
|
|
||||||
export interface ElementChildrenAttribute {
|
export interface ElementChildrenAttribute {
|
||||||
children: {};
|
children: {};
|
||||||
|
|||||||
@@ -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 { isJrxNode, FRAGMENT } from "./types";
|
import { isJfxNode, 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 JrxNode", () => {
|
it("jsx() with string type returns a JfxNode", () => {
|
||||||
const node = jsx("Card", { title: "Hello" });
|
const node = jsx("Card", { title: "Hello" });
|
||||||
expect(isJrxNode(node)).toBe(true);
|
expect(isJfxNode(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(isJrxNode(node)).toBe(true);
|
expect(isJfxNode(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 JrxNode with children", () => {
|
it("jsxs() returns a JfxNode 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(isJrxNode(node)).toBe(true);
|
expect(isJfxNode(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(isJrxNode(node)).toBe(true);
|
expect(isJfxNode(node)).toBe(true);
|
||||||
expect(node.props).toEqual({});
|
expect(node.props).toEqual({});
|
||||||
expect(node.children).toEqual([]);
|
expect(node.children).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 JrxNode } from "./types";
|
import { FRAGMENT, type JfxNode } 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-JrxNode input", () => {
|
it("throws for non-JfxNode input", () => {
|
||||||
expect(() => render({} as JrxNode)).toThrow(/expects a JrxNode/);
|
expect(() => render({} as JfxNode)).toThrow(/expects a JfxNode/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type { Spec, UIElement } from "@json-render/core";
|
import type { Spec, UIElement } from "@json-render/core";
|
||||||
import { FRAGMENT, type JrxNode, type RenderOptions, isJrxNode } from "./types";
|
import { FRAGMENT, type JfxNode, type RenderOptions, isJfxNode } from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flatten a JrxNode tree into a json-render `Spec`.
|
* Flatten a JfxNode 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 JrxNode (produced by JSX)
|
* @param node - Root JfxNode (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 JrxNode, type RenderOptions, isJrxNode } from "./types";
|
|||||||
* );
|
* );
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function render(node: JrxNode, options?: RenderOptions): Spec {
|
export function render(node: JfxNode, options?: RenderOptions): Spec {
|
||||||
if (!isJrxNode(node)) {
|
if (!isJfxNode(node)) {
|
||||||
throw new Error("render() expects a JrxNode produced by JSX.");
|
throw new Error("render() expects a JfxNode 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) JrxNodes.
|
* Returns an array of concrete (non-fragment) JfxNodes.
|
||||||
*/
|
*/
|
||||||
function expandChildren(children: JrxNode[]): JrxNode[] {
|
function expandChildren(children: JfxNode[]): JfxNode[] {
|
||||||
const result: JrxNode[] = [];
|
const result: JfxNode[] = [];
|
||||||
for (const child of children) {
|
for (const child of children) {
|
||||||
if (!isJrxNode(child)) continue;
|
if (!isJfxNode(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: JrxNode[]): JrxNode[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively flatten a JrxNode into the elements map.
|
* Recursively flatten a JfxNode into the elements map.
|
||||||
* Returns the key assigned to this node.
|
* Returns the key assigned to this node.
|
||||||
*/
|
*/
|
||||||
function flattenNode(
|
function flattenNode(
|
||||||
node: JrxNode,
|
node: JfxNode,
|
||||||
elements: Record<string, UIElement>,
|
elements: Record<string, UIElement>,
|
||||||
counters: Map<string, number>,
|
counters: Map<string, number>,
|
||||||
usedKeys: Set<string>,
|
usedKeys: Set<string>,
|
||||||
|
|||||||
@@ -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 jrx
|
* Runs @json-render/core's validateSpec against Specs produced by jfx
|
||||||
* to prove structural correctness.
|
* to prove structural correctness.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
} from "./test-components";
|
} from "./test-components";
|
||||||
|
|
||||||
describe("validateSpec on jrx-produced specs", () => {
|
describe("validateSpec on jfx-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 jrx-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 jrx output", () => {
|
it("no orphaned elements in jfx output", () => {
|
||||||
const spec = render(
|
const spec = render(
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text text="A" />
|
<Text text="A" />
|
||||||
@@ -101,7 +101,7 @@ describe("validateSpec on jrx-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 jrx output", () => {
|
it("no missing children in jfx output", () => {
|
||||||
const spec = render(
|
const spec = render(
|
||||||
<Stack>
|
<Stack>
|
||||||
<Card title="X">
|
<Card title="X">
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
import { jsx } from "./jsx-runtime";
|
import { jsx } from "./jsx-runtime";
|
||||||
import type { JrxNode } from "./types";
|
import type { JfxNode } from "./types";
|
||||||
|
|
||||||
export function Stack(props: Record<string, unknown>): JrxNode {
|
export function Stack(props: Record<string, unknown>): JfxNode {
|
||||||
return jsx("Stack", props);
|
return jsx("Stack", props);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Card(props: Record<string, unknown>): JrxNode {
|
export function Card(props: Record<string, unknown>): JfxNode {
|
||||||
return jsx("Card", props);
|
return jsx("Card", props);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Text(props: Record<string, unknown>): JrxNode {
|
export function Text(props: Record<string, unknown>): JfxNode {
|
||||||
return jsx("Text", props);
|
return jsx("Text", props);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Button(props: Record<string, unknown>): JrxNode {
|
export function Button(props: Record<string, unknown>): JfxNode {
|
||||||
return jsx("Button", props);
|
return jsx("Button", props);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Badge(props: Record<string, unknown>): JrxNode {
|
export function Badge(props: Record<string, unknown>): JfxNode {
|
||||||
return jsx("Badge", props);
|
return jsx("Badge", props);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function List(props: Record<string, unknown>): JrxNode {
|
export function List(props: Record<string, unknown>): JfxNode {
|
||||||
return jsx("List", props);
|
return jsx("List", props);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ListItem(props: Record<string, unknown>): JrxNode {
|
export function ListItem(props: Record<string, unknown>): JfxNode {
|
||||||
return jsx("ListItem", props);
|
return jsx("ListItem", props);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Select(props: Record<string, unknown>): JrxNode {
|
export function Select(props: Record<string, unknown>): JfxNode {
|
||||||
return jsx("Select", props);
|
return jsx("Select", props);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Input(props: Record<string, unknown>): JrxNode {
|
export function Input(props: Record<string, unknown>): JfxNode {
|
||||||
return jsx("Input", props);
|
return jsx("Input", props);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Divider(props: Record<string, unknown>): JrxNode {
|
export function Divider(props: Record<string, unknown>): JfxNode {
|
||||||
return jsx("Divider", props);
|
return jsx("Divider", props);
|
||||||
}
|
}
|
||||||
|
|||||||
44
src/types.ts
44
src/types.ts
@@ -4,19 +4,19 @@ import type {
|
|||||||
} from "@json-render/core";
|
} from "@json-render/core";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// JrxNode — intermediate representation produced by the JSX factory
|
// JfxNode — intermediate representation produced by the JSX factory
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sentinel symbol identifying a JrxNode (prevents plain objects from
|
* Sentinel symbol identifying a JfxNode (prevents plain objects from
|
||||||
* being mistaken for nodes).
|
* being mistaken for nodes).
|
||||||
*/
|
*/
|
||||||
export const JRX_NODE = Symbol.for("jrx.node");
|
export const JFX_NODE = Symbol.for("jfx.node");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sentinel symbol for Fragment grouping.
|
* Sentinel symbol for Fragment grouping.
|
||||||
*/
|
*/
|
||||||
export const FRAGMENT = Symbol.for("jrx.fragment");
|
export const FRAGMENT = Symbol.for("jfx.fragment");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A node in the intermediate JSX tree.
|
* A node in the intermediate JSX tree.
|
||||||
@@ -24,9 +24,9 @@ export const FRAGMENT = Symbol.for("jrx.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 JrxNode {
|
export interface JfxNode {
|
||||||
/** Brand symbol — always `JRX_NODE` */
|
/** Brand symbol — always `JFX_NODE` */
|
||||||
$$typeof: typeof JRX_NODE;
|
$$typeof: typeof JFX_NODE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component type name (e.g. `"Card"`, `"Button"`).
|
* Component type name (e.g. `"Card"`, `"Button"`).
|
||||||
@@ -38,7 +38,7 @@ export interface JrxNode {
|
|||||||
props: Record<string, unknown>;
|
props: Record<string, unknown>;
|
||||||
|
|
||||||
/** Child nodes */
|
/** Child nodes */
|
||||||
children: JrxNode[];
|
children: JfxNode[];
|
||||||
|
|
||||||
// -- Reserved / meta fields (extracted from JSX props) --
|
// -- Reserved / meta fields (extracted from JSX props) --
|
||||||
|
|
||||||
@@ -59,20 +59,20 @@ export interface JrxNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// JrxComponent — a function usable as a JSX tag that maps to a type string
|
// JfxComponent — a function usable as a JSX tag that maps to a type string
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A jrx component function. Works like a React function component:
|
* A jfx 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 JrxNode.
|
* with props and gets back a JfxNode.
|
||||||
*/
|
*/
|
||||||
export type JrxComponent = (props: Record<string, unknown>) => JrxNode;
|
export type JfxComponent = (props: Record<string, unknown>) => JfxNode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a jrx component for use as a JSX tag.
|
* Define a jfx component for use as a JSX tag.
|
||||||
*
|
*
|
||||||
* Creates a function that, when called with props, produces a JrxNode
|
* Creates a function that, when called with props, produces a JfxNode
|
||||||
* 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 JrxComponent = (props: Record<string, unknown>) => JrxNode;
|
|||||||
* 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): JrxComponent {
|
export function component(typeName: string): JfxComponent {
|
||||||
// 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: JRX_NODE,
|
$$typeof: JFX_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): JrxNode[] {
|
function normalizeChildrenRaw(raw: unknown): JfxNode[] {
|
||||||
if (raw == null || typeof raw === "boolean") return [];
|
if (raw == null || typeof raw === "boolean") return [];
|
||||||
if (Array.isArray(raw)) {
|
if (Array.isArray(raw)) {
|
||||||
const result: JrxNode[] = [];
|
const result: JfxNode[] = [];
|
||||||
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 JrxNode);
|
result.push(child as JfxNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
return [raw as JrxNode];
|
return [raw as JfxNode];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -140,10 +140,10 @@ export interface RenderOptions {
|
|||||||
// Type guard
|
// Type guard
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export function isJrxNode(value: unknown): value is JrxNode {
|
export function isJfxNode(value: unknown): value is JfxNode {
|
||||||
return (
|
return (
|
||||||
typeof value === "object" &&
|
typeof value === "object" &&
|
||||||
value !== null &&
|
value !== null &&
|
||||||
(value as JrxNode).$$typeof === JRX_NODE
|
(value as JfxNode).$$typeof === JFX_NODE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,11 @@
|
|||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"jsxImportSource": "jrx",
|
"jsxImportSource": "jfx",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"jrx/jsx-runtime": ["./src/jsx-runtime"],
|
"jfx/jsx-runtime": ["./src/jsx-runtime"],
|
||||||
"jrx/jsx-dev-runtime": ["./src/jsx-dev-runtime"]
|
"jfx/jsx-dev-runtime": ["./src/jsx-dev-runtime"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
|
|||||||
Reference in New Issue
Block a user