ngx-json-render 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +88 -3
  2. package/package.json +5 -4
package/README.md CHANGED
@@ -1,10 +1,16 @@
1
1
  # ngx-json-render
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/ngx-json-render)](https://www.npmjs.com/package/ngx-json-render) [![CI](https://github.com/shteynu/ngx-json-render/actions/workflows/ci.yml/badge.svg)](https://github.com/shteynu/ngx-json-render/actions/workflows/ci.yml) [![license](https://img.shields.io/npm/l/ngx-json-render)](https://github.com/shteynu/ngx-json-render/blob/main/LICENSE)
4
+
3
5
  Angular renderer for [json-render](https://github.com/vercel-labs/json-render) — give an LLM a catalog of your components, stream back a JSON spec, and render it as real Angular components. No `innerHTML`, no `eval`, no framework lock-in on the wire format.
4
6
 
5
7
  Built on `@json-render/core` (the same spec format, expressions, state store, actions, and streaming compiler used by the React, Vue, Solid, and Svelte renderers) and idiomatic modern Angular: standalone components, signals, zoneless-friendly, `OnPush` everywhere.
6
8
 
7
- **[Live demo](https://shteynu.github.io/ngx-json-render/)** — interactive spec (bindings, repeat, confirm, watch) and a replayable SpecStream showing progressive rendering ([source](https://github.com/shteynu/ngx-json-render/tree/main/projects/demo)).
9
+ This is an Angular **adapter over the official core**, not a second implementation of it — `@json-render/core` is a peer dependency, and the spec your model emits is the same one the React, Vue, Solid and Svelte renderers consume. A catalog and a spec written here move to another framework unchanged.
10
+
11
+ **[Live demo](https://shteynu.github.io/ngx-json-render/)** — interactive spec (bindings, repeat, confirm, watch) and a replayable SpecStream showing progressive rendering ([source](https://github.com/shteynu/ngx-json-render/tree/main/projects/demo)) — or [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/shteynu/ngx-json-render)
12
+
13
+ ![A SpecStream of RFC 6902 patches rendering progressively into an Angular dashboard](https://raw.githubusercontent.com/shteynu/ngx-json-render/main/docs/streaming.gif)
8
14
 
9
15
  ## Install
10
16
 
@@ -12,7 +18,7 @@ Built on `@json-render/core` (the same spec format, expressions, state store, ac
12
18
  npm install ngx-json-render @json-render/core zod
13
19
  ```
14
20
 
15
- Requires Angular ≥ 21.
21
+ Requires Angular ≥ 20.
16
22
 
17
23
  ## Quick start
18
24
 
@@ -138,9 +144,40 @@ export class GeneratePage {
138
144
  }
139
145
  ```
140
146
 
147
+ ### The server side
148
+
149
+ `injectUIStream` POSTs `{ prompt, context, currentSpec }` to your endpoint and expects the response body to be SpecStream JSONL — one RFC 6902 patch per line. Any server that can stream text works; with the [AI SDK](https://ai-sdk.dev) it's a few lines — `catalog.prompt()` teaches the model your component vocabulary and the patch protocol:
150
+
151
+ ```ts
152
+ // server.ts — Express shown; any Node server works the same way
153
+ import express from 'express';
154
+ import { streamText } from 'ai';
155
+ import { anthropic } from '@ai-sdk/anthropic';
156
+ import { catalog } from './catalog';
157
+
158
+ const app = express();
159
+ app.use(express.json());
160
+
161
+ app.post('/api/generate', async (req, res) => {
162
+ const { prompt } = req.body; // injectUIStream sends { prompt, context, currentSpec }
163
+
164
+ const result = streamText({
165
+ model: anthropic('claude-sonnet-5'),
166
+ system: catalog.prompt(),
167
+ prompt,
168
+ });
169
+
170
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
171
+ for await (const chunk of result.textStream) res.write(chunk);
172
+ res.end();
173
+ });
174
+ ```
175
+
176
+ The patches apply to the `spec` signal as each line arrives, so the UI assembles on screen while the model is still generating — exactly what the [demo's Streaming tab](https://shteynu.github.io/ngx-json-render/) replays. Prefer structured output? `catalog.jsonSchema()` exports a JSON Schema for `streamObject`/tool calls, and `catalog.validate(spec)` checks a finished spec against the catalog.
177
+
141
178
  Also available:
142
179
 
143
- - `injectChatUI({ api })` — chat + GenUI: assistant messages carrying both prose and specs (` ```spec ` fenced JSONL).
180
+ - `injectChatUI({ api })` — chat + GenUI: the endpoint takes `{ messages }` and streams prose mixed with ` ```spec ` fenced JSONL; each assistant message carries `text` and/or a `spec`.
144
181
  - `applyPatch(spec, patch)` — immutably apply one RFC 6902 patch to a spec.
145
182
  - `buildSpecFromParts` / `getTextFromParts` / `jsonRenderMessage` — derive specs from AI SDK `message.parts`.
146
183
  - `catalog.prompt()` / `buildUserPrompt` (from `@json-render/core`) — generate the system/user prompts for your catalog.
@@ -184,6 +221,54 @@ Share one store across renderers (or drive it from your own state management) by
184
221
 
185
222
  If a catalog component renders `<input [value]="ctx.props().value">`, remember that one-way bindings do not re-assert the DOM when the bound value returns to its previously applied value while the user typed in between (e.g. `pushState` + `clearStatePath`). Sync imperatively instead — see `InputComponent` in the demo app for the pattern.
186
223
 
224
+ ## Security
225
+
226
+ Specs are attacker-shaped input: whatever produced one — a model, a prompt, a
227
+ user's text inside that prompt — is not something you control. The renderer is
228
+ built so that a hostile spec cannot execute code, but it can still act within
229
+ the authority you hand it. What follows is what the renderer guarantees and
230
+ what stays your responsibility.
231
+
232
+ **A spec cannot execute code.** There is no `eval`, no `Function` constructor,
233
+ and no `innerHTML`/`bypassSecurityTrust` anywhere in this package or in
234
+ `@json-render/core`. Expressions (`$state`, `$item`, `$index`, `$bindState`)
235
+ are interpreted against the state model, not evaluated as JavaScript, and all
236
+ text reaches the DOM through Angular interpolation. Script injection through a
237
+ spec is not a thing you have to defend against.
238
+
239
+ **A spec can only name actions you registered.** Built-ins (`setState`,
240
+ `pushState`, `removeState`, `push`, `pop`, `validateForm`) are handled inside
241
+ the renderer; every other action name is looked up in the `handlers` you pass.
242
+ An unrecognised name logs a warning and does nothing.
243
+
244
+ The exception is `onAction`, which is a deliberate catch-all: when you pass it,
245
+ **every** action name in the spec reaches it, including ones you never put in
246
+ your catalog. If you use it, switch on the names you expect and ignore the
247
+ rest.
248
+
249
+ **A spec chooses its own state paths.** `setState`, `pushState`, `removeState`
250
+ and `onSuccess.set` all take a `statePath` straight from the spec, so a
251
+ generated UI can write anywhere in the state model it is rendered against —
252
+ and `push`/`pop` write `/currentScreen` and `/navStack`. In controlled mode
253
+ this is *your* store. Give the renderer a store scoped to the generated view
254
+ rather than the one holding session, entitlement or billing state.
255
+
256
+ **A spec chooses the navigation target.** `onSuccess: { navigate }` passes its
257
+ string to the `navigate` callback you provide, verbatim. Treat it as untrusted:
258
+ match it against known routes, and never hand it to `window.location` or
259
+ `router.navigateByUrl` unchecked.
260
+
261
+ **A spec sizes its own render tree.** `repeat` iterates a state array the spec
262
+ may itself have supplied, so specs are a denial-of-service surface against the
263
+ browser tab. Cap spec size and array lengths at the boundary where you accept
264
+ one.
265
+
266
+ **`confirm` is a UX affordance, not a security control.** It routes an action
267
+ through the confirmation dialog before the handler runs, but it is set on the
268
+ action binding *inside the spec* (`on.press.confirm`) — so the same party that
269
+ chose the action also chose whether to ask. Real authorization belongs in the
270
+ handler, on the server.
271
+
187
272
  ## API surface
188
273
 
189
274
  Components: `JsonRenderer` (`<json-render>`), `JrChildren`, `JrConfirmDialog`, `JrElement`, `JrRepeatScope`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngx-json-render",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Angular renderer for json-render — stream AI-generated JSON specs into real Angular components with signals.",
5
5
  "keywords": [
6
6
  "angular",
@@ -22,11 +22,12 @@
22
22
  "url": "https://github.com/shteynu/ngx-json-render/issues"
23
23
  },
24
24
  "peerDependencies": {
25
- "@angular/common": ">=21.0.0",
26
- "@angular/core": ">=21.0.0"
25
+ "@angular/common": ">=20.0.0",
26
+ "@angular/core": ">=20.0.0",
27
+ "@json-render/core": "^0.20.0",
28
+ "zod": "^4.0.0"
27
29
  },
28
30
  "dependencies": {
29
- "@json-render/core": "^0.20.0",
30
31
  "tslib": "^2.3.0"
31
32
  },
32
33
  "sideEffects": false,