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.
- package/README.md +88 -3
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# ngx-json-render
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/ngx-json-render) [](https://github.com/shteynu/ngx-json-render/actions/workflows/ci.yml) [](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
|
-
**
|
|
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 [](https://stackblitz.com/github/shteynu/ngx-json-render)
|
|
12
|
+
|
|
13
|
+

|
|
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
|
+
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:
|
|
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.
|
|
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": ">=
|
|
26
|
-
"@angular/core": ">=
|
|
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,
|