ngx-json-render 0.6.0 → 0.7.1
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 +23 -7
- package/fesm2022/ngx-json-render.mjs +189 -29
- package/fesm2022/ngx-json-render.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ngx-json-render.d.ts +32 -9
package/README.md
CHANGED
|
@@ -215,7 +215,7 @@ export class GeneratePage {
|
|
|
215
215
|
`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:
|
|
216
216
|
|
|
217
217
|
```ts
|
|
218
|
-
// server.ts — Express shown
|
|
218
|
+
// server.ts — Express shown (plain Node needs one extra import, see below)
|
|
219
219
|
import express from 'express';
|
|
220
220
|
import { streamText } from 'ai';
|
|
221
221
|
import { anthropic } from '@ai-sdk/anthropic';
|
|
@@ -239,7 +239,9 @@ app.post('/api/generate', async (req, res) => {
|
|
|
239
239
|
});
|
|
240
240
|
```
|
|
241
241
|
|
|
242
|
-
|
|
242
|
+
`catalog.ts` imports `schema` from `ngx-json-render`, and that entry point also loads Angular and the renderer's components, which are published partially compiled. In an Angular SSR app (`ng new --ssr`) the Angular CLI finishes compiling them when it builds the server, so the route above goes into its `server.ts` unchanged and works both under `ng serve` and from the built server. On Angular 20 and 21, add `"prebundle": { "exclude": ["zod"] }` to the `serve` options in `angular.json` first: their Vite 7 dev server can't transform zod for the server (`Cannot split a chunk that has already been edited`) and answers every page and API route with a 500. A Node server the Angular CLI doesn't build, run with `node` or `tsx`, fails on that import with `'@angular/compiler' is not available` — make `import '@angular/compiler';` its first import.
|
|
243
|
+
|
|
244
|
+
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 `checkSpec(spec, 'strict', { catalog })` checks a finished spec against the catalog, props included — see [Checking what the model produced](#checking-what-the-model-produced) for why that and not `catalog.validate(spec)` alone.
|
|
243
245
|
|
|
244
246
|
### Chat + GenUI
|
|
245
247
|
|
|
@@ -372,12 +374,23 @@ is a patch that has not arrived yet, not a defect, so nothing is reported and
|
|
|
372
374
|
`strict` keeps rendering; the hooks check once, when the generation completes.
|
|
373
375
|
|
|
374
376
|
Pass the catalog too, and the same check covers what structure alone cannot
|
|
375
|
-
see — every `type` is one the catalog defines, and
|
|
377
|
+
see — every `type` is one the catalog defines, and each element's props match
|
|
378
|
+
its component's schema:
|
|
376
379
|
|
|
377
380
|
```html
|
|
378
381
|
<json-render [spec]="spec()" [registry]="registry" [catalog]="catalog" validate="strict" />
|
|
379
382
|
```
|
|
380
383
|
|
|
384
|
+
A prop written as an expression — `{"$state": …}`, `{"$bindState": …}`,
|
|
385
|
+
`{"$template": …}`, a directive — is not checked: it has no value until render
|
|
386
|
+
time. The catalog's spec schema also holds every element to the grammar, which
|
|
387
|
+
wants a `children` array even on a leaf.
|
|
388
|
+
|
|
389
|
+
The props check is this package's own. Core's `catalog.validate(spec)` gives an
|
|
390
|
+
element's props its component's schema only when the catalog has exactly one
|
|
391
|
+
component; with more, it checks element shapes and `type` names but not a
|
|
392
|
+
single prop.
|
|
393
|
+
|
|
381
394
|
### Capping what a spec may cost
|
|
382
395
|
|
|
383
396
|
`validate` describes a spec. `renderLimits` constrains it:
|
|
@@ -754,10 +767,13 @@ not generate should set all three. See
|
|
|
754
767
|
[Capping what a spec may cost](#capping-what-a-spec-may-cost).
|
|
755
768
|
|
|
756
769
|
**A spec can name components you never built.** `validate` with a `catalog`
|
|
757
|
-
reports every `type` the catalog does not define and every prop its
|
|
758
|
-
rejects; `strict` refuses such a spec outright.
|
|
759
|
-
|
|
760
|
-
|
|
770
|
+
reports every `type` the catalog does not define and every prop its component's
|
|
771
|
+
schema rejects; `strict` refuses such a spec outright. A prop bound to an
|
|
772
|
+
expression (`$state`, `$bindState`, `$template`, …) is not among them — its value
|
|
773
|
+
only exists at render time — so a component should not count on such a prop
|
|
774
|
+
having the type its schema declares. Without a catalog the renderer only warns
|
|
775
|
+
and draws nothing in that element's place, which degrades well but tells you
|
|
776
|
+
nothing until you read the console.
|
|
761
777
|
|
|
762
778
|
**`confirm` is a UX affordance, not a security control.** It routes an action
|
|
763
779
|
through the confirmation dialog before the handler runs, but it is set on the
|
|
@@ -1898,63 +1898,219 @@ function depthIssue(depth, limit) {
|
|
|
1898
1898
|
};
|
|
1899
1899
|
}
|
|
1900
1900
|
/**
|
|
1901
|
-
* Check a spec against a catalog: the component types it names,
|
|
1902
|
-
* catalog's
|
|
1901
|
+
* Check a spec against a catalog: the component types it names, then the
|
|
1902
|
+
* catalog's spec schema, then each element's props against its own
|
|
1903
|
+
* component's schema.
|
|
1903
1904
|
*
|
|
1904
|
-
* The
|
|
1905
|
-
*
|
|
1906
|
-
*
|
|
1905
|
+
* The props need that pass of their own. Core builds one schema for the whole
|
|
1906
|
+
* spec, and it gives an element's props a component's schema only when the
|
|
1907
|
+
* catalog has exactly one component; with more, every element's props are an
|
|
1908
|
+
* open record, whatever its `type`. So `catalog.validate` holds an element to
|
|
1909
|
+
* its shape — `children`, a `type` from the catalog — and not to its props.
|
|
1910
|
+
*
|
|
1911
|
+
* Whatever that schema did say about the props of an element checked here is
|
|
1912
|
+
* dropped: with a one-component catalog it would be reported twice, and it
|
|
1913
|
+
* judges expressions this pass knows to leave alone.
|
|
1914
|
+
*
|
|
1915
|
+
* The schema passes are skipped while a type is unknown. That is an error on
|
|
1916
|
+
* its own, and the spec schema would only restate it as a `type` outside the
|
|
1917
|
+
* ones it accepts.
|
|
1907
1918
|
*/
|
|
1908
1919
|
function catalogIssues(spec, catalog) {
|
|
1909
|
-
const
|
|
1920
|
+
const elements = Object.entries(spec.elements ?? {});
|
|
1910
1921
|
const known = new Set(catalog.componentNames);
|
|
1911
|
-
|
|
1922
|
+
const unknown = [];
|
|
1923
|
+
for (const [key, element] of elements) {
|
|
1912
1924
|
if (known.has(element.type))
|
|
1913
1925
|
continue;
|
|
1914
|
-
|
|
1926
|
+
unknown.push({
|
|
1915
1927
|
severity: 'error',
|
|
1916
1928
|
code: 'unknown_component',
|
|
1917
1929
|
elementKey: key,
|
|
1918
1930
|
message: `Element "${key}" has type "${element.type}", which this catalog does not define. Nothing renders for it.`,
|
|
1919
1931
|
});
|
|
1920
1932
|
}
|
|
1921
|
-
if (
|
|
1922
|
-
return
|
|
1923
|
-
const
|
|
1933
|
+
if (unknown.length > 0)
|
|
1934
|
+
return unknown;
|
|
1935
|
+
const schemas = new Map();
|
|
1936
|
+
for (const [key, element] of elements) {
|
|
1937
|
+
const schema = propsSchemaOf(catalog.data, element.type);
|
|
1938
|
+
if (schema)
|
|
1939
|
+
schemas.set(key, schema);
|
|
1940
|
+
}
|
|
1941
|
+
const found = specSchemaIssues(spec, catalog).filter(({ path }) => !(path[0] === 'elements' &&
|
|
1942
|
+
typeof path[1] === 'string' &&
|
|
1943
|
+
schemas.has(path[1]) &&
|
|
1944
|
+
path[2] === 'props'));
|
|
1945
|
+
for (const [key, element] of elements) {
|
|
1946
|
+
const room = MAX_REPORTED_PROP_ISSUES - found.length;
|
|
1947
|
+
if (room <= 0)
|
|
1948
|
+
break;
|
|
1949
|
+
const schema = schemas.get(key);
|
|
1950
|
+
if (schema)
|
|
1951
|
+
found.push(...propsIssues(key, element, schema, room));
|
|
1952
|
+
}
|
|
1953
|
+
return found.slice(0, MAX_REPORTED_PROP_ISSUES).map(toSpecCheckIssue);
|
|
1954
|
+
}
|
|
1955
|
+
/**
|
|
1956
|
+
* The props schema a catalog declares for a component type, if it declares
|
|
1957
|
+
* one that can be called.
|
|
1958
|
+
*/
|
|
1959
|
+
function propsSchemaOf(data, type) {
|
|
1960
|
+
const components = data
|
|
1961
|
+
?.components;
|
|
1962
|
+
if (typeof components !== 'object' ||
|
|
1963
|
+
components === null ||
|
|
1964
|
+
!Object.hasOwn(components, type)) {
|
|
1965
|
+
return undefined;
|
|
1966
|
+
}
|
|
1967
|
+
const props = components[type]?.props;
|
|
1968
|
+
return typeof props
|
|
1969
|
+
?.safeParse === 'function'
|
|
1970
|
+
? props
|
|
1971
|
+
: undefined;
|
|
1972
|
+
}
|
|
1973
|
+
/** What the catalog's spec schema reported, with paths rooted at the spec. */
|
|
1974
|
+
function specSchemaIssues(spec, catalog) {
|
|
1975
|
+
try {
|
|
1976
|
+
const result = catalog.validate(spec);
|
|
1977
|
+
return result.success ? [] : readIssues(result.error);
|
|
1978
|
+
}
|
|
1979
|
+
catch (error) {
|
|
1980
|
+
return [{ path: [], message: threwMessage('the catalog schema', error) }];
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* What an element's component schema says about its props, leaving out what
|
|
1985
|
+
* it says about expressions, with paths rooted at the spec.
|
|
1986
|
+
*
|
|
1987
|
+
* A schema that throws counts as a rejection, not a crash. Real ones do:
|
|
1988
|
+
* `ValidationConfigSchema`, which every Material input's `validation` prop
|
|
1989
|
+
* uses, recurses through `enabled` and overflows the stack a thousand
|
|
1990
|
+
* `$and`s deep.
|
|
1991
|
+
*/
|
|
1992
|
+
function propsIssues(key, element, schema, room) {
|
|
1993
|
+
const at = ['elements', key, 'props'];
|
|
1994
|
+
let result;
|
|
1995
|
+
try {
|
|
1996
|
+
result = schema.safeParse(element.props);
|
|
1997
|
+
}
|
|
1998
|
+
catch (error) {
|
|
1999
|
+
return [
|
|
2000
|
+
{
|
|
2001
|
+
path: at,
|
|
2002
|
+
message: threwMessage(`the "${element.type}" schema`, error),
|
|
2003
|
+
},
|
|
2004
|
+
];
|
|
2005
|
+
}
|
|
1924
2006
|
if (result.success)
|
|
1925
|
-
return
|
|
1926
|
-
|
|
2007
|
+
return [];
|
|
2008
|
+
const issues = [];
|
|
2009
|
+
for (const { path, message } of readIssues(result.error)) {
|
|
2010
|
+
if (issues.length >= room)
|
|
2011
|
+
break;
|
|
2012
|
+
if (reachesExpression(element.props, path))
|
|
2013
|
+
continue;
|
|
2014
|
+
issues.push({ path: [...at, ...path], message });
|
|
2015
|
+
}
|
|
2016
|
+
return issues;
|
|
2017
|
+
}
|
|
2018
|
+
/**
|
|
2019
|
+
* Whether a props issue is about an expression: it sits on one, inside one,
|
|
2020
|
+
* or on a value holding one.
|
|
2021
|
+
*
|
|
2022
|
+
* `{"$state": "/title"}` where the schema wants a string is not a defect.
|
|
2023
|
+
* Core resolves every such value before the component sees it, so the schema
|
|
2024
|
+
* would be judging something that never renders. Any `$`-prefixed key counts
|
|
2025
|
+
* — directive names must start with `$`, and so would an expression added to
|
|
2026
|
+
* core later. A value merely holding one is left alone too, since a union or
|
|
2027
|
+
* a refinement over it ran on the unresolved value.
|
|
2028
|
+
*
|
|
2029
|
+
* The props object itself is not an expression: core resolves the values
|
|
2030
|
+
* inside it, never the object.
|
|
2031
|
+
*/
|
|
2032
|
+
function reachesExpression(props, path) {
|
|
2033
|
+
let value = props;
|
|
2034
|
+
for (const segment of path) {
|
|
2035
|
+
if (typeof value !== 'object' || value === null)
|
|
2036
|
+
return false;
|
|
2037
|
+
value = value[segment];
|
|
2038
|
+
if (isExpression(value))
|
|
2039
|
+
return true;
|
|
2040
|
+
}
|
|
2041
|
+
return holdsExpression(value);
|
|
2042
|
+
}
|
|
2043
|
+
/** A value core resolves at render time — or will, once it knows the key. */
|
|
2044
|
+
function isExpression(value) {
|
|
2045
|
+
return (typeof value === 'object' &&
|
|
2046
|
+
value !== null &&
|
|
2047
|
+
!Array.isArray(value) &&
|
|
2048
|
+
Object.keys(value).some((key) => key.startsWith('$')));
|
|
1927
2049
|
}
|
|
1928
2050
|
/**
|
|
1929
|
-
*
|
|
2051
|
+
* Whether anything nested inside `value` is an expression.
|
|
2052
|
+
*
|
|
2053
|
+
* Iterative, and it remembers what it has seen, for the same reason as
|
|
2054
|
+
* {@link analyseSpecGraph}: props are as attacker-shaped as the rest of the
|
|
2055
|
+
* spec, and a check that overflowed or looped on them would be a second way
|
|
2056
|
+
* to crash.
|
|
2057
|
+
*/
|
|
2058
|
+
function holdsExpression(value) {
|
|
2059
|
+
const pending = [value];
|
|
2060
|
+
const seen = new Set();
|
|
2061
|
+
while (pending.length > 0) {
|
|
2062
|
+
const current = pending.pop();
|
|
2063
|
+
if (typeof current !== 'object' || current === null)
|
|
2064
|
+
continue;
|
|
2065
|
+
if (seen.has(current))
|
|
2066
|
+
continue;
|
|
2067
|
+
seen.add(current);
|
|
2068
|
+
for (const child of Object.values(current)) {
|
|
2069
|
+
if (isExpression(child))
|
|
2070
|
+
return true;
|
|
2071
|
+
pending.push(child);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
return false;
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Read what a schema reported.
|
|
1930
2078
|
*
|
|
1931
2079
|
* Read defensively rather than typed against the schema library: the shape
|
|
1932
2080
|
* below (`issues`, each with `path` and `message`) is what every version of
|
|
1933
2081
|
* it has produced, and a renderer that threw while explaining a bad spec
|
|
1934
2082
|
* would be the worse failure.
|
|
1935
2083
|
*/
|
|
1936
|
-
function
|
|
2084
|
+
function readIssues(error) {
|
|
1937
2085
|
const raw = error?.issues;
|
|
1938
2086
|
if (!Array.isArray(raw))
|
|
1939
2087
|
return [];
|
|
1940
|
-
return raw.
|
|
2088
|
+
return raw.map((entry) => {
|
|
1941
2089
|
const item = entry;
|
|
1942
|
-
const path =
|
|
1943
|
-
const message =
|
|
1944
|
-
// Paths are rooted at the spec, so `elements.<key>.props.<prop>` names the
|
|
1945
|
-
// element in its second segment.
|
|
1946
|
-
const elementKey = path[0] === 'elements' && typeof path[1] === 'string'
|
|
1947
|
-
? path[1]
|
|
1948
|
-
: undefined;
|
|
1949
|
-
const where = path.length > 0 ? path.join('.') : 'spec';
|
|
2090
|
+
const path = item?.path;
|
|
2091
|
+
const message = item?.message;
|
|
1950
2092
|
return {
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
...(elementKey === undefined ? {} : { elementKey }),
|
|
1954
|
-
message: `${where}: ${message}`,
|
|
2093
|
+
path: Array.isArray(path) ? path : [],
|
|
2094
|
+
message: typeof message === 'string' ? message : 'is not valid',
|
|
1955
2095
|
};
|
|
1956
2096
|
});
|
|
1957
2097
|
}
|
|
2098
|
+
function threwMessage(schema, error) {
|
|
2099
|
+
const reason = error instanceof Error ? ` (${error.name}: ${error.message})` : '';
|
|
2100
|
+
return `${schema} threw instead of answering${reason}, so this counts as rejected`;
|
|
2101
|
+
}
|
|
2102
|
+
function toSpecCheckIssue({ path, message }) {
|
|
2103
|
+
// Paths are rooted at the spec, so `elements.<key>.props.<prop>` names the
|
|
2104
|
+
// element in its second segment.
|
|
2105
|
+
const elementKey = path[0] === 'elements' && typeof path[1] === 'string' ? path[1] : undefined;
|
|
2106
|
+
const where = path.length > 0 ? path.map(String).join('.') : 'spec';
|
|
2107
|
+
return {
|
|
2108
|
+
severity: 'error',
|
|
2109
|
+
code: 'invalid_props',
|
|
2110
|
+
...(elementKey === undefined ? {} : { elementKey }),
|
|
2111
|
+
message: `${where}: ${message}`,
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
1958
2114
|
|
|
1959
2115
|
const CLEAN = {
|
|
1960
2116
|
spec: null,
|
|
@@ -2139,7 +2295,11 @@ class JsonRenderer {
|
|
|
2139
2295
|
/**
|
|
2140
2296
|
* The catalog the spec was generated for. Given one, `validate` also checks
|
|
2141
2297
|
* what structure alone cannot: that every `type` is a component the catalog
|
|
2142
|
-
* defines,
|
|
2298
|
+
* defines, that every element has the shape the catalog's spec schema asks
|
|
2299
|
+
* for — a `children` array, even on a leaf — and that its props match its
|
|
2300
|
+
* component's schema. A prop written as an expression — `$state`,
|
|
2301
|
+
* `$bindState`, `$template`, a directive — has no value until render time,
|
|
2302
|
+
* so it is not checked.
|
|
2143
2303
|
*
|
|
2144
2304
|
* Only read when `validate` is on, and — like the rest of the check —
|
|
2145
2305
|
* skipped while `loading`.
|