Initial implementation of jrx - JSX factory for json-render

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

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

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

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules/
dist/
*.log
.DS_Store
coverage/

281
bun.lock Normal file
View File

@@ -0,0 +1,281 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "jrx",
"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",
},
},
},
"packages": {
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@json-render/core": ["@json-render/core@0.10.0", "", { "dependencies": { "zod": "^4.0.0" } }, "sha512-aqV9vwpBMIH7J/3d0szxfAwB6Wa2t1U3g9DkiFaFAYlvAotnpGiR+XeOYVK6CxeIJsq0RK7BaUKOa0arWEGNWg=="],
"@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=="],
"@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-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
"@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
"@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="],
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/node": ["@types/node@20.19.35", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ=="],
"@types/react": ["@types/react@19.2.3", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-k5dJVszUiNr1DSe8Cs+knKR6IrqhqdhpUwzqhkS8ecQTSf3THNtbfIp/umqHMpX2bv+9dkx3fwDv/86LcSfvSg=="],
"@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
"aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
"bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="],
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
"confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
"esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"happy-dom": ["happy-dom@18.0.1", "", { "dependencies": { "@types/node": "^20.0.0", "@types/whatwg-mimetype": "^3.0.2", "whatwg-mimetype": "^3.0.0" } }, "sha512-qn+rKOW7KWpVTtgIUi6RVmTBZJSe2k0Db0vh1f7CWrWclkkc7/Q+FrOfkZIb2eiErLyqu5AXEzE7XthO9JVxRA=="],
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"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=="],
"load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="],
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
"postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="],
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
"react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="],
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
"tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
}
}

2
bunfig.toml Normal file
View File

@@ -0,0 +1,2 @@
[test]
preload = ["./src/test-preload.ts"]

51
package.json Normal file
View File

@@ -0,0 +1,51 @@
{
"name": "jrx",
"version": "0.1.0",
"license": "MIT",
"description": "JSX factory for json-render. Write JSX, get Spec JSON.",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
},
"./jsx-runtime": {
"types": "./dist/jsx-runtime.d.ts",
"import": "./dist/jsx-runtime.mjs",
"require": "./dist/jsx-runtime.js"
},
"./jsx-dev-runtime": {
"types": "./dist/jsx-dev-runtime.d.ts",
"import": "./dist/jsx-dev-runtime.mjs",
"require": "./dist/jsx-dev-runtime.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"test": "bun test",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"@json-render/core": ">=0.10.0"
},
"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"
}
}

3
src/index.ts Normal file
View File

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

385
src/integration.test.tsx Normal file
View File

@@ -0,0 +1,385 @@
/** @jsxImportSource react */
/**
* Integration tests: verify that Specs produced by jrx are consumable
* by @json-render/react's Renderer.
*
* 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.
*/
import { describe, it, expect, mock } from "bun:test";
import React from "react";
import { render as reactRender, act, fireEvent, screen, cleanup } from "@testing-library/react";
import type { Spec } from "@json-render/core";
import {
JSONUIProvider,
Renderer,
type ComponentRenderProps,
} from "@json-render/react";
import { useStateStore } from "@json-render/react";
import { jsx, jsxs } from "./jsx-runtime";
import { render as jrxRender } from "./render";
import {
Stack as JStack,
Card as JCard,
Text as JText,
Button as JButton,
} from "./test-components";
// ---------------------------------------------------------------------------
// React stub components (rendered by @json-render/react's Renderer)
// ---------------------------------------------------------------------------
function Button({ element, emit }: ComponentRenderProps<{ label: string }>) {
return (
<button data-testid="btn" onClick={() => emit("press")}>
{element.props.label}
</button>
);
}
function Text({ element }: ComponentRenderProps<{ content: string }>) {
return <span data-testid="text">{element.props.content}</span>;
}
function Stack({ children }: ComponentRenderProps) {
return <div data-testid="stack">{children}</div>;
}
function Card({ element, children }: ComponentRenderProps<{ title: string }>) {
return (
<div data-testid="card">
<h3>{element.props.title}</h3>
{children}
</div>
);
}
function StateProbe() {
const { state } = useStateStore();
return <pre data-testid="state-probe">{JSON.stringify(state)}</pre>;
}
const registry = { Button, Text, Stack, Card };
// ---------------------------------------------------------------------------
// Helper: render a jrx spec with @json-render/react
// ---------------------------------------------------------------------------
function renderSpec(spec: Spec, handlers?: Record<string, (...args: unknown[]) => void>) {
return reactRender(
<JSONUIProvider registry={registry} initialState={spec.state} handlers={handlers}>
<Renderer spec={spec} registry={registry} />
<StateProbe />
</JSONUIProvider>,
);
}
// =============================================================================
// Basic rendering
// =============================================================================
describe("jrx → @json-render/react round-trip", () => {
it("renders a single element", () => {
const spec = jrxRender(jsx(JText, { content: "Hello from jrx" }));
renderSpec(spec);
expect(screen.getByTestId("text").textContent).toBe("Hello from jrx");
});
it("renders nested elements with children", () => {
const spec = jrxRender(
jsxs(JCard, {
title: "My Card",
children: [jsx(JText, { content: "Inside card" })],
}),
);
renderSpec(spec);
expect(screen.getByTestId("card")).toBeDefined();
expect(screen.getByText("My Card")).toBeDefined();
expect(screen.getByTestId("text").textContent).toBe("Inside card");
});
it("renders a tree with multiple children", () => {
const spec = jrxRender(
jsxs(JStack, {
children: [
jsx(JText, { content: "First" }),
jsx(JText, { content: "Second" }),
jsx(JButton, { label: "Click" }),
],
}),
);
renderSpec(spec);
expect(screen.getByTestId("stack")).toBeDefined();
expect(screen.getByTestId("btn").textContent).toBe("Click");
});
it("renders a deep tree", () => {
const spec = jrxRender(
jsxs(JStack, {
children: [
jsxs(JCard, {
title: "Outer",
children: [jsx(JText, { content: "Deep" })],
}),
],
}),
);
renderSpec(spec);
expect(screen.getByText("Outer")).toBeDefined();
expect(screen.getByTestId("text").textContent).toBe("Deep");
});
});
// =============================================================================
// State + actions (adapted from chained-actions.test.tsx)
// =============================================================================
describe("jrx specs with state and actions", () => {
it("renders with initial state", () => {
const spec = jrxRender(jsx(JText, { content: "Stateful" }), {
state: { count: 42 },
});
renderSpec(spec);
const probe = screen.getByTestId("state-probe");
const state = JSON.parse(probe.textContent!);
expect(state.count).toBe(42);
});
it("setState action updates state on button press", async () => {
const spec = jrxRender(
jsx(JButton, {
label: "Set",
on: {
press: {
action: "setState",
params: { statePath: "/clicked", value: true },
},
},
}),
{ state: { clicked: false } },
);
renderSpec(spec);
await act(async () => {
fireEvent.click(screen.getByTestId("btn"));
});
const state = JSON.parse(screen.getByTestId("state-probe").textContent!);
expect(state.clicked).toBe(true);
});
it("chained pushState + setState resolves correctly", async () => {
const spec = jrxRender(
jsx(JButton, {
label: "Chain",
on: {
press: [
{
action: "pushState",
params: { statePath: "/items", value: "new-item" },
},
{
action: "setState",
params: {
statePath: "/observed",
value: { $state: "/items" },
},
},
],
},
}),
{ state: { items: ["initial"], observed: "not yet set" } },
);
renderSpec(spec);
await act(async () => {
fireEvent.click(screen.getByTestId("btn"));
});
const state = JSON.parse(screen.getByTestId("state-probe").textContent!);
expect(state.items).toEqual(["initial", "new-item"]);
expect(state.observed).toEqual(["initial", "new-item"]);
});
it("multiple pushState chain resolves correctly", async () => {
const spec = jrxRender(
jsx(JButton, {
label: "Go",
on: {
press: [
{ action: "pushState", params: { statePath: "/items", value: "a" } },
{ action: "pushState", params: { statePath: "/items", value: "b" } },
{
action: "setState",
params: {
statePath: "/snapshot",
value: { $state: "/items" },
},
},
],
},
}),
{ state: { items: [], snapshot: null } },
);
renderSpec(spec);
await act(async () => {
fireEvent.click(screen.getByTestId("btn"));
});
const state = JSON.parse(screen.getByTestId("state-probe").textContent!);
expect(state.items).toEqual(["a", "b"]);
expect(state.snapshot).toEqual(["a", "b"]);
});
});
// =============================================================================
// Spec structural validity
// =============================================================================
describe("jrx spec structural validity", () => {
it("all child references resolve to existing elements", () => {
const spec = jrxRender(
jsxs(JStack, {
children: [
jsxs(JCard, {
title: "A",
children: [
jsx(JText, { content: "1" }),
jsx(JText, { content: "2" }),
],
}),
jsx(JButton, { label: "Go" }),
],
}),
);
for (const el of Object.values(spec.elements)) {
if (el.children) {
for (const childKey of el.children) {
expect(
spec.elements[childKey],
`Missing element "${childKey}"`,
).toBeDefined();
}
}
}
});
it("root element exists in elements map", () => {
const spec = jrxRender(jsx(JCard, { title: "Root" }));
expect(spec.elements[spec.root]).toBeDefined();
});
it("element count matches node count", () => {
const spec = jrxRender(
jsxs(JStack, {
children: [
jsx(JCard, { title: "A" }),
jsx(JCard, { title: "B" }),
jsx(JText, { content: "C" }),
],
}),
);
expect(Object.keys(spec.elements)).toHaveLength(4);
});
});
// =============================================================================
// Dynamic features (ported from json-render's dynamic-forms.test.tsx)
// =============================================================================
describe("jrx specs with dynamic features", () => {
it("$state prop expressions resolve at render time", () => {
const spec = jrxRender(
jsx(JText, { content: { $state: "/message" } }),
{ state: { message: "Dynamic hello" } },
);
renderSpec(spec);
expect(screen.getByTestId("text").textContent).toBe("Dynamic hello");
});
it("visibility condition hides element when false", () => {
const spec = jrxRender(
jsxs(JStack, {
children: [
jsx(JText, {
content: "Visible",
visible: { $state: "/show", eq: true },
}),
],
}),
{ state: { show: false } },
);
renderSpec(spec);
expect(screen.queryByTestId("text")).toBeNull();
});
it("visibility condition shows element when true", () => {
cleanup();
const spec = jrxRender(
jsxs(JStack, {
children: [
jsx(JText, {
content: "Visible",
visible: { $state: "/show", eq: true },
}),
],
}),
{ state: { show: true } },
);
renderSpec(spec);
expect(screen.getByTestId("text").textContent).toBe("Visible");
});
it("watchers fire when watched state changes", async () => {
const loadCities = mock();
const spec = jrxRender(
jsxs(JStack, {
children: [
jsx(JButton, {
label: "Set Country",
on: {
press: {
action: "setState",
params: { statePath: "/country", value: "US" },
},
},
}),
jsx(JText, {
content: "watcher",
watch: {
"/country": {
action: "loadCities",
params: { country: { $state: "/country" } },
},
},
}),
],
}),
{ state: { country: "" } },
);
renderSpec(spec, { loadCities });
expect(loadCities).not.toHaveBeenCalled();
await act(async () => {
fireEvent.click(screen.getByTestId("btn"));
});
expect(loadCities).toHaveBeenCalledTimes(1);
expect(loadCities).toHaveBeenCalledWith(
expect.objectContaining({ country: "US" }),
);
});
});

7
src/jsx-dev-runtime.ts Normal file
View File

@@ -0,0 +1,7 @@
// Dev runtime re-exports the production runtime.
// The automatic JSX transform looks for jsx-dev-runtime in development mode.
export { jsx, jsxs, Fragment } from "./jsx-runtime";
export type { JSX } from "./jsx-runtime";
// jsxDEV is the dev-mode factory — same signature as jsx
export { jsx as jsxDEV } from "./jsx-runtime";

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

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

283
src/jsx.test.tsx Normal file
View File

@@ -0,0 +1,283 @@
import { describe, it, expect } from "bun:test";
import { render } from "./render";
import { isJrxNode, FRAGMENT } from "./types";
import { jsx, jsxs, Fragment } from "./jsx-runtime";
import {
Stack,
Card,
Text,
Button,
Badge,
List,
ListItem,
Select,
} from "./test-components";
// =============================================================================
// JSX factory basics (direct function calls — tests the factory itself)
// =============================================================================
describe("jsx factory", () => {
it("jsx() with string type returns a JrxNode", () => {
const node = jsx("Card", { title: "Hello" });
expect(isJrxNode(node)).toBe(true);
expect(node.type).toBe("Card");
expect(node.props).toEqual({ title: "Hello" });
});
it("jsx() with component function resolves typeName", () => {
const node = jsx(Card, { title: "Hello" });
expect(isJrxNode(node)).toBe(true);
expect(node.type).toBe("Card");
expect(node.props).toEqual({ title: "Hello" });
});
it("jsxs() returns a JrxNode with children", () => {
const node = jsxs(Stack, {
children: [jsx(Text, { content: "A" }), jsx(Text, { content: "B" })],
});
expect(isJrxNode(node)).toBe(true);
expect(node.children).toHaveLength(2);
});
it("Fragment is the FRAGMENT symbol", () => {
expect(Fragment).toBe(FRAGMENT);
});
it("jsx() extracts key from third argument", () => {
const node = jsx(Card, { title: "Hi" }, "my-key");
expect(node.key).toBe("my-key");
expect(node.props).toEqual({ title: "Hi" });
});
it("jsx() extracts reserved props", () => {
const vis = { $state: "/show" };
const on = { press: { action: "submit" } };
const repeat = { statePath: "/items" };
const watch = { "/x": { action: "reload" } };
const node = jsx(Button, {
label: "Go",
visible: vis,
on,
repeat,
watch,
});
expect(node.props).toEqual({ label: "Go" });
expect(node.visible).toEqual(vis);
expect(node.on).toEqual(on);
expect(node.repeat).toEqual(repeat);
expect(node.watch).toEqual(watch);
});
it("jsx() handles null props", () => {
const node = jsx("Divider", null);
expect(isJrxNode(node)).toBe(true);
expect(node.props).toEqual({});
expect(node.children).toEqual([]);
});
it("jsx() filters null/boolean children", () => {
const node = jsxs(Stack, {
children: [null, jsx(Text, { content: "A" }), false, undefined, true],
});
expect(node.children).toHaveLength(1);
expect(node.children[0].type).toBe("Text");
});
});
// =============================================================================
// JSX syntax → render() integration
// =============================================================================
describe("JSX syntax → render() integration", () => {
it("renders a single element", () => {
const spec = render(<Card title="Hello" />);
expect(spec.root).toBe("card-1");
expect(spec.elements["card-1"].type).toBe("Card");
expect(spec.elements["card-1"].props).toEqual({ title: "Hello" });
});
it("renders nested elements", () => {
const spec = render(
<Card title="Root">
<Text content="Child" />
</Card>,
);
expect(Object.keys(spec.elements)).toHaveLength(2);
expect(spec.elements["card-1"].children).toEqual(["text-1"]);
expect(spec.elements["text-1"].props).toEqual({ content: "Child" });
});
it("renders multiple children", () => {
const spec = render(
<Stack>
<Card title="A" />
<Card title="B" />
<Button label="Click" />
</Stack>,
);
expect(spec.elements["stack-1"].children).toEqual([
"card-1",
"card-2",
"button-1",
]);
});
it("renders deeply nested tree", () => {
const spec = render(
<Stack>
<Card title="Outer">
<Stack>
<Text content="Deep" />
</Stack>
</Card>
</Stack>,
);
expect(Object.keys(spec.elements)).toHaveLength(4);
expect(spec.elements["stack-1"].children).toEqual(["card-1"]);
expect(spec.elements["card-1"].children).toEqual(["stack-2"]);
expect(spec.elements["stack-2"].children).toEqual(["text-1"]);
});
it("handles explicit key prop", () => {
const spec = render(<Card key="main" title="Hello" />);
expect(spec.root).toBe("main");
expect(spec.elements["main"].props).toEqual({ title: "Hello" });
});
it("handles fragments as children", () => {
const spec = render(
<Stack>
<>
<Text content="A" />
<Text content="B" />
</>
<Button label="C" />
</Stack>,
);
expect(spec.elements["stack-1"].children).toEqual([
"text-1",
"text-2",
"button-1",
]);
});
it("handles visible prop", () => {
const spec = render(
<Text content="Conditional" visible={{ $state: "/show" }} />,
);
const el = spec.elements[spec.root];
expect(el.visible).toEqual({ $state: "/show" });
expect((el.props as Record<string, unknown>).visible).toBeUndefined();
});
it("handles on prop", () => {
const spec = render(
<Button label="Submit" on={{ press: { action: "submitForm" } }} />,
);
const el = spec.elements[spec.root];
expect(el.on).toEqual({ press: { action: "submitForm" } });
});
it("handles repeat prop", () => {
const spec = render(
<List repeat={{ statePath: "/items", key: "id" }}>
<ListItem />
</List>,
);
const el = spec.elements[spec.root];
expect(el.repeat).toEqual({ statePath: "/items", key: "id" });
});
it("handles watch prop", () => {
const spec = render(
<Select watch={{ "/country": { action: "loadCities" } }} />,
);
const el = spec.elements[spec.root];
expect(el.watch).toEqual({ "/country": { action: "loadCities" } });
});
it("passes state through render options", () => {
const spec = render(<Card title="Hello" />, { state: { count: 0 } });
expect(spec.state).toEqual({ count: 0 });
});
it("throws on root fragment", () => {
expect(() =>
render(
<>
<Card />
<Text />
</>,
),
).toThrow(/single root element/);
});
it("handles chained actions", () => {
const spec = render(
<Button
label="Multi"
on={{
press: [
{ action: "setState", params: { statePath: "/a", value: 1 } },
{ action: "submitForm" },
],
}}
/>,
);
const el = spec.elements[spec.root];
expect(Array.isArray(el.on!.press)).toBe(true);
expect((el.on!.press as unknown[]).length).toBe(2);
});
it("handles complex visibility conditions", () => {
const spec = render(
<Text
content="Complex"
visible={{
$or: [
{ $state: "/isAdmin" },
{ $state: "/count", gt: 5 },
],
}}
/>,
);
const el = spec.elements[spec.root];
expect(el.visible).toEqual({
$or: [
{ $state: "/isAdmin" },
{ $state: "/count", gt: 5 },
],
});
});
it("produces a valid Spec structure (all children exist)", () => {
const spec = render(
<Stack>
<Card title="A">
<Text content="1" />
<Badge text="tag" />
</Card>
<Card title="B">
<Button label="Click" />
</Card>
</Stack>,
);
for (const [, el] of Object.entries(spec.elements)) {
if (el.children) {
for (const childKey of el.children) {
expect(
spec.elements[childKey],
`Missing element "${childKey}"`,
).toBeDefined();
}
}
}
expect(spec.elements[spec.root]).toBeDefined();
expect(Object.keys(spec.elements)).toHaveLength(6);
});
});

306
src/render.test.tsx Normal file
View File

@@ -0,0 +1,306 @@
import { describe, it, expect } from "bun:test";
import { render } from "./render";
import { FRAGMENT, type JrxNode } from "./types";
import { jsx } from "./jsx-runtime";
import {
Stack,
Card,
Text,
Button,
Badge,
List,
ListItem,
Select,
} from "./test-components";
// =============================================================================
// render() — basic output shape
// =============================================================================
describe("render() output shape", () => {
it("produces a Spec with root and elements", () => {
const spec = render(<Card title="Hello" />);
expect(spec.root).toBeDefined();
expect(spec.elements).toBeDefined();
expect(typeof spec.root).toBe("string");
expect(typeof spec.elements).toBe("object");
});
it("root key points to an existing element", () => {
const spec = render(<Card />);
expect(spec.elements[spec.root]).toBeDefined();
});
it("single element has correct type and props", () => {
const spec = render(<Button label="Click" />);
const el = spec.elements[spec.root];
expect(el.type).toBe("Button");
expect(el.props).toEqual({ label: "Click" });
});
it("single element without children omits children field", () => {
const spec = render(<Text content="hi" />);
const el = spec.elements[spec.root];
expect(el.children).toBeUndefined();
});
});
// =============================================================================
// Key auto-generation
// =============================================================================
describe("key auto-generation", () => {
it("generates keys from lowercase type name", () => {
const spec = render(<Card />);
expect(spec.root).toBe("card-1");
});
it("increments counter for same type", () => {
const spec = render(
<Stack>
<Card title="A" />
<Card title="B" />
</Stack>,
);
const childKeys = spec.elements[spec.root].children!;
expect(childKeys).toEqual(["card-1", "card-2"]);
});
it("uses separate counters per type", () => {
const spec = render(
<Stack>
<Card />
<Text />
<Card />
</Stack>,
);
const childKeys = spec.elements[spec.root].children!;
expect(childKeys).toEqual(["card-1", "text-1", "card-2"]);
});
it("counter resets between render() calls", () => {
const spec1 = render(<Card />);
const spec2 = render(<Card />);
expect(spec1.root).toBe("card-1");
expect(spec2.root).toBe("card-1");
});
});
// =============================================================================
// Explicit key override
// =============================================================================
describe("explicit key override", () => {
it("uses explicit key when provided", () => {
const spec = render(<Card key="main-card" />);
expect(spec.root).toBe("main-card");
});
it("explicit key does not appear in props", () => {
const spec = render(<Card title="Hi" key="my-card" />);
const el = spec.elements["my-card"];
expect(el.props).toEqual({ title: "Hi" });
expect((el.props as Record<string, unknown>).key).toBeUndefined();
});
it("throws on duplicate explicit keys", () => {
expect(() =>
render(
<Stack>
<Card key="same" />
<Text key="same" />
</Stack>,
),
).toThrow(/Duplicate element key "same"/);
});
it("throws when explicit key collides with auto-generated key", () => {
expect(() =>
render(
<Stack>
<Card />
<Button key="card-1" />
</Stack>,
),
).toThrow(/Duplicate element key "card-1"/);
});
});
// =============================================================================
// Nested children
// =============================================================================
describe("nested children", () => {
it("flattens a two-level tree", () => {
const spec = render(
<Card title="Root">
<Text content="Child" />
</Card>,
);
expect(Object.keys(spec.elements)).toHaveLength(2);
expect(spec.elements[spec.root].children).toEqual(["text-1"]);
expect(spec.elements["text-1"].type).toBe("Text");
expect(spec.elements["text-1"].props).toEqual({ content: "Child" });
});
it("flattens a deep tree", () => {
const spec = render(
<Stack>
<Card title="A">
<Text content="Nested" />
</Card>
<Button label="Click" />
</Stack>,
);
expect(Object.keys(spec.elements)).toHaveLength(4);
expect(spec.elements["stack-1"].children).toEqual(["card-1", "button-1"]);
expect(spec.elements["card-1"].children).toEqual(["text-1"]);
expect(spec.elements["text-1"].children).toBeUndefined();
expect(spec.elements["button-1"].children).toBeUndefined();
});
it("all child keys reference existing elements", () => {
const spec = render(
<Stack>
<Card>
<Text />
<Badge />
</Card>
<Button />
</Stack>,
);
for (const el of Object.values(spec.elements)) {
if (el.children) {
for (const childKey of el.children) {
expect(spec.elements[childKey]).toBeDefined();
}
}
}
});
});
// =============================================================================
// Fragment support
// =============================================================================
describe("fragments", () => {
it("expands fragment children inline", () => {
const spec = render(
<Stack>
<>
<Text content="A" />
<Text content="B" />
</>
</Stack>,
);
expect(spec.elements["stack-1"].children).toEqual(["text-1", "text-2"]);
expect(Object.keys(spec.elements)).toHaveLength(3);
});
it("expands nested fragments", () => {
const spec = render(
<Stack>
<>
<>
<Text content="Deep" />
</>
</>
</Stack>,
);
expect(spec.elements["stack-1"].children).toEqual(["text-1"]);
});
it("throws when fragment is at root", () => {
expect(() =>
render(
<>
<Card />
<Text />
</>,
),
).toThrow(/single root element/);
});
});
// =============================================================================
// Reserved prop extraction
// =============================================================================
describe("reserved prop extraction", () => {
it("places visible on UIElement, not in props", () => {
const condition = { $state: "/show" };
const spec = render(<Text content="hi" visible={condition} />);
const el = spec.elements[spec.root];
expect(el.visible).toEqual(condition);
expect((el.props as Record<string, unknown>).visible).toBeUndefined();
});
it("places on bindings on UIElement, not in props", () => {
const onBindings = { press: { action: "submit" } };
const spec = render(<Button label="Go" on={onBindings} />);
const el = spec.elements[spec.root];
expect(el.on).toEqual(onBindings);
expect((el.props as Record<string, unknown>).on).toBeUndefined();
});
it("places repeat on UIElement, not in props", () => {
const repeatConfig = { statePath: "/items", key: "id" };
const spec = render(
<List repeat={repeatConfig}>
<ListItem />
</List>,
);
const el = spec.elements[spec.root];
expect(el.repeat).toEqual(repeatConfig);
expect((el.props as Record<string, unknown>).repeat).toBeUndefined();
});
it("places watch on UIElement, not in props", () => {
const watchConfig = { "/country": { action: "loadCities" } };
const spec = render(<Select watch={watchConfig} />);
const el = spec.elements[spec.root];
expect(el.watch).toEqual(watchConfig);
expect((el.props as Record<string, unknown>).watch).toBeUndefined();
});
it("omits undefined meta fields from UIElement", () => {
const spec = render(<Text content="plain" />);
const el = spec.elements[spec.root];
expect("visible" in el).toBe(false);
expect("on" in el).toBe(false);
expect("repeat" in el).toBe(false);
expect("watch" in el).toBe(false);
});
});
// =============================================================================
// State passthrough
// =============================================================================
describe("state passthrough", () => {
it("includes state in Spec when provided", () => {
const state = { count: 0, items: ["a", "b"] };
const spec = render(<Card />, { state });
expect(spec.state).toEqual(state);
});
it("omits state from Spec when not provided", () => {
const spec = render(<Card />);
expect(spec.state).toBeUndefined();
});
});
// =============================================================================
// Error handling
// =============================================================================
describe("error handling", () => {
it("throws for non-JrxNode input", () => {
expect(() => render({} as JrxNode)).toThrow(/expects a JrxNode/);
});
});

143
src/render.ts Normal file
View File

@@ -0,0 +1,143 @@
import type { Spec, UIElement } from "@json-render/core";
import { FRAGMENT, type JrxNode, type RenderOptions, isJrxNode } from "./types";
/**
* Flatten a JrxNode tree into a json-render `Spec`.
*
* Analogous to `ReactDOM.render` but produces JSON instead of DOM mutations.
*
* @param node - Root JrxNode (produced by JSX)
* @param options - Optional render configuration (e.g. initial state)
* @returns A json-render `Spec` ready for any renderer
*
* @example
* ```tsx
* const spec = render(
* <Card title="Hello">
* <Text content="World" />
* </Card>,
* { state: { count: 0 } }
* );
* ```
*/
export function render(node: JrxNode, options?: RenderOptions): Spec {
if (!isJrxNode(node)) {
throw new Error("render() expects a JrxNode produced by JSX.");
}
if (node.type === FRAGMENT) {
throw new Error(
"render() requires a single root element. Fragments cannot be used at the root level.",
);
}
const counters = new Map<string, number>();
const elements: Record<string, UIElement> = {};
const usedKeys = new Set<string>();
const rootKey = flattenNode(node, elements, counters, usedKeys);
const spec: Spec = { root: rootKey, elements };
if (options?.state) {
spec.state = options.state;
}
return spec;
}
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
/**
* Generate a unique key for a node based on its type.
* Pattern: `{lowercase-type}-{counter}` (e.g. `card-1`, `card-2`).
*/
function generateKey(
type: string,
counters: Map<string, number>,
): string {
const base = type.toLowerCase();
const count = (counters.get(base) ?? 0) + 1;
counters.set(base, count);
return `${base}-${count}`;
}
/**
* Resolve the children of a node, expanding fragments inline.
* Returns an array of concrete (non-fragment) JrxNodes.
*/
function expandChildren(children: JrxNode[]): JrxNode[] {
const result: JrxNode[] = [];
for (const child of children) {
if (!isJrxNode(child)) continue;
if (child.type === FRAGMENT) {
// Recursively expand nested fragments
result.push(...expandChildren(child.children));
} else {
result.push(child);
}
}
return result;
}
/**
* Recursively flatten a JrxNode into the elements map.
* Returns the key assigned to this node.
*/
function flattenNode(
node: JrxNode,
elements: Record<string, UIElement>,
counters: Map<string, number>,
usedKeys: Set<string>,
): string {
// Determine key
const key = node.key ?? generateKey(node.type as string, counters);
if (usedKeys.has(key)) {
throw new Error(
`Duplicate element key "${key}". Keys must be unique within a single render() call.`,
);
}
usedKeys.add(key);
// Expand fragment children and recursively flatten
const concreteChildren = expandChildren(node.children);
const childKeys: string[] = [];
for (const child of concreteChildren) {
const childKey = flattenNode(child, elements, counters, usedKeys);
childKeys.push(childKey);
}
// Build the UIElement
const element: UIElement = {
type: node.type as string,
props: node.props,
};
if (childKeys.length > 0) {
element.children = childKeys;
}
if (node.visible !== undefined) {
element.visible = node.visible;
}
if (node.on !== undefined) {
element.on = node.on;
}
if (node.repeat !== undefined) {
element.repeat = node.repeat;
}
if (node.watch !== undefined) {
element.watch = node.watch;
}
elements[key] = element;
return key;
}

166
src/spec-validator.test.tsx Normal file
View File

@@ -0,0 +1,166 @@
/**
* Ported from json-render's core/src/spec-validator.test.ts.
*
* Runs @json-render/core's validateSpec against Specs produced by jrx
* to prove structural correctness.
*/
import { describe, it, expect } from "bun:test";
import { validateSpec } from "@json-render/core";
import { render } from "./render";
import {
Stack,
Card,
Text,
Button,
Badge,
List,
ListItem,
Select,
} from "./test-components";
describe("validateSpec on jrx-produced specs", () => {
it("validates a simple single-element spec", () => {
const spec = render(<Text text="hello" />);
const result = validateSpec(spec);
expect(result.valid).toBe(true);
expect(result.issues).toHaveLength(0);
});
it("validates a parent-child spec", () => {
const spec = render(
<Stack>
<Text text="hello" />
</Stack>,
);
const result = validateSpec(spec);
expect(result.valid).toBe(true);
expect(result.issues).toHaveLength(0);
});
it("validates a deep tree", () => {
const spec = render(
<Stack>
<Card title="A">
<Text text="1" />
<Text text="2" />
</Card>
<Button label="Go" />
</Stack>,
);
const result = validateSpec(spec);
expect(result.valid).toBe(true);
expect(result.issues).toHaveLength(0);
});
it("validates spec with visible at element level (not in props)", () => {
const spec = render(
<Text text="conditional" visible={{ $state: "/show" }} />,
);
const result = validateSpec(spec);
expect(result.issues.some((i) => i.code === "visible_in_props")).toBe(false);
});
it("validates spec with on at element level (not in props)", () => {
const spec = render(
<Button label="Click" on={{ press: { action: "submit" } }} />,
);
const result = validateSpec(spec);
expect(result.issues.some((i) => i.code === "on_in_props")).toBe(false);
});
it("validates spec with repeat at element level (not in props)", () => {
const spec = render(
<Stack repeat={{ statePath: "/items" }}>
<Text text="item" />
</Stack>,
);
const result = validateSpec(spec);
expect(result.issues.some((i) => i.code === "repeat_in_props")).toBe(false);
});
it("validates spec with watch at element level (not in props)", () => {
const spec = render(
<Select
label="Country"
watch={{ "/form/country": { action: "loadCities" } }}
/>,
);
const result = validateSpec(spec);
expect(result.issues.some((i) => i.code === "watch_in_props")).toBe(false);
});
it("no orphaned elements in jrx output", () => {
const spec = render(
<Stack>
<Text text="A" />
<Text text="B" />
</Stack>,
);
const result = validateSpec(spec, { checkOrphans: true });
expect(result.issues.some((i) => i.code === "orphaned_element")).toBe(false);
});
it("no missing children in jrx output", () => {
const spec = render(
<Stack>
<Card title="X">
<Text text="nested" />
<Badge text="tag" />
</Card>
<Button label="action" />
</Stack>,
);
const result = validateSpec(spec);
expect(result.issues.some((i) => i.code === "missing_child")).toBe(false);
expect(result.valid).toBe(true);
});
it("validates spec with state", () => {
const spec = render(<Text text="stateful" />, {
state: { count: 0, items: ["a", "b"] },
});
const result = validateSpec(spec);
expect(result.valid).toBe(true);
});
it("validates spec with all features combined", () => {
const spec = render(
<Stack>
<Text text="header" visible={{ $state: "/showHeader" }} />
<List repeat={{ statePath: "/items", key: "id" }}>
<ListItem
title={{ $item: "name" }}
on={{ press: { action: "selectItem" } }}
/>
</List>
<Select
label="Country"
watch={{ "/country": { action: "loadCities" } }}
/>
<Button
label="Submit"
on={{
press: [
{ action: "validateForm" },
{ action: "submitForm" },
],
}}
/>
</Stack>,
{ state: { showHeader: true, items: [], country: "" } },
);
const result = validateSpec(spec);
expect(result.valid).toBe(true);
expect(result.issues).toHaveLength(0);
for (const el of Object.values(spec.elements)) {
const props = el.props as Record<string, unknown>;
expect(props.visible).toBeUndefined();
expect(props.on).toBeUndefined();
expect(props.repeat).toBeUndefined();
expect(props.watch).toBeUndefined();
}
});
});

42
src/test-components.ts Normal file
View File

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

18
src/test-preload.ts Normal file
View File

@@ -0,0 +1,18 @@
import { Window } from "happy-dom";
const window = new Window({ url: "http://localhost" });
// Register DOM globals for @testing-library/react
for (const key of Object.getOwnPropertyNames(window)) {
if (!(key in globalThis)) {
Object.defineProperty(globalThis, key, {
value: (window as unknown as Record<string, unknown>)[key],
writable: true,
configurable: true,
});
}
}
Object.defineProperty(globalThis, "window", { value: window, writable: true, configurable: true });
Object.defineProperty(globalThis, "document", { value: window.document, writable: true, configurable: true });
Object.defineProperty(globalThis, "navigator", { value: window.navigator, writable: true, configurable: true });

149
src/types.ts Normal file
View File

@@ -0,0 +1,149 @@
import type {
ActionBinding,
VisibilityCondition,
} from "@json-render/core";
// ---------------------------------------------------------------------------
// JrxNode — intermediate representation produced by the JSX factory
// ---------------------------------------------------------------------------
/**
* Sentinel symbol identifying a JrxNode (prevents plain objects from
* being mistaken for nodes).
*/
export const JRX_NODE = Symbol.for("jrx.node");
/**
* Sentinel symbol for Fragment grouping.
*/
export const FRAGMENT = Symbol.for("jrx.fragment");
/**
* A node in the intermediate JSX tree.
*
* Created by the `jsx` / `jsxs` factory functions and consumed by `render()`
* which flattens the tree into a json-render `Spec`.
*/
export interface JrxNode {
/** Brand symbol — always `JRX_NODE` */
$$typeof: typeof JRX_NODE;
/**
* Component type name (e.g. `"Card"`, `"Button"`).
* For fragments this is the `FRAGMENT` symbol.
*/
type: string | typeof FRAGMENT;
/** Component props (reserved props already extracted) */
props: Record<string, unknown>;
/** Child nodes */
children: JrxNode[];
// -- Reserved / meta fields (extracted from JSX props) --
/** Explicit element key (overrides auto-generation) */
key: string | undefined;
/** Visibility condition */
visible: VisibilityCondition | undefined;
/** Event bindings */
on: Record<string, ActionBinding | ActionBinding[]> | undefined;
/** Repeat configuration */
repeat: { statePath: string; key?: string } | undefined;
/** State watchers */
watch: Record<string, ActionBinding | ActionBinding[]> | undefined;
}
// ---------------------------------------------------------------------------
// JrxComponent — a function usable as a JSX tag that maps to a type string
// ---------------------------------------------------------------------------
/**
* A jrx component function. Works like a React function component:
* when used as a JSX tag (`<Card />`), the factory calls the function
* with props and gets back a JrxNode.
*/
export type JrxComponent = (props: Record<string, unknown>) => JrxNode;
/**
* Define a jrx component for use as a JSX tag.
*
* Creates a function that, when called with props, produces a JrxNode
* with the given type name — just like a React component returns
* React elements.
*
* @example
* ```tsx
* const Card = component("Card");
* const spec = render(<Card title="Hello"><Text content="World" /></Card>);
* ```
*/
export function component(typeName: string): JrxComponent {
// Import createNodeFromString lazily to avoid circular dep
// (jsx-runtime imports types). Instead, we build the node inline.
return (props: Record<string, unknown>) => {
return {
$$typeof: JRX_NODE,
type: typeName,
props: filterReserved(props),
children: normalizeChildrenRaw(props.children),
key: props.key != null ? String(props.key) : undefined,
visible: props.visible as VisibilityCondition | undefined,
on: props.on as Record<string, ActionBinding | ActionBinding[]> | undefined,
repeat: props.repeat as { statePath: string; key?: string } | undefined,
watch: props.watch as Record<string, ActionBinding | ActionBinding[]> | undefined,
};
};
}
const RESERVED = new Set(["key", "children", "visible", "on", "repeat", "watch"]);
function filterReserved(props: Record<string, unknown>): Record<string, unknown> {
const out: Record<string, unknown> = {};
for (const k of Object.keys(props)) {
if (!RESERVED.has(k)) out[k] = props[k];
}
return out;
}
function normalizeChildrenRaw(raw: unknown): JrxNode[] {
if (raw == null || typeof raw === "boolean") return [];
if (Array.isArray(raw)) {
const result: JrxNode[] = [];
for (const child of raw) {
if (child == null || typeof child === "boolean") continue;
if (Array.isArray(child)) {
result.push(...normalizeChildrenRaw(child));
} else {
result.push(child as JrxNode);
}
}
return result;
}
return [raw as JrxNode];
}
// ---------------------------------------------------------------------------
// render() options
// ---------------------------------------------------------------------------
export interface RenderOptions {
/** Initial state to include in the Spec output */
state?: Record<string, unknown>;
}
// ---------------------------------------------------------------------------
// Type guard
// ---------------------------------------------------------------------------
export function isJrxNode(value: unknown): value is JrxNode {
return (
typeof value === "object" &&
value !== null &&
(value as JrxNode).$$typeof === JRX_NODE
);
}

25
tsconfig.json Normal file
View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2020", "DOM"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"jsx": "react-jsx",
"jsxImportSource": "jrx",
"baseUrl": ".",
"paths": {
"jrx/jsx-runtime": ["./src/jsx-runtime"],
"jrx/jsx-dev-runtime": ["./src/jsx-dev-runtime"]
}
},
"include": ["src"]
}

12
tsup.config.ts Normal file
View File

@@ -0,0 +1,12 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts", "src/jsx-runtime.ts", "src/jsx-dev-runtime.ts"],
format: ["cjs", "esm"],
dts: true,
sourcemap: true,
clean: true,
jsx: "transform",
jsxFactory: "jsx",
jsxFragment: "Fragment",
});