ngx-json-render 0.3.0 → 0.4.0

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.
@@ -1,7 +1,7 @@
1
1
  import { NgComponentOutlet } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
3
  import { signal, Injectable, inject, computed, untracked, effect, isDevMode, DestroyRef, InjectionToken, input, output, viewChild, afterNextRender, ChangeDetectionStrategy, Component, Injector, forwardRef } from '@angular/core';
4
- import { createStateStore, getByPath, runValidation, resolveAction, nextActionDispatchId, notifyActionDispatch, notifyActionSettle, executeAction, isDevtoolsActive, subscribeDevtoolsActive, evaluateVisibility, resolveElementProps, resolveBindings, resolveActionParam, autoFixSpec, validateSpec, formatSpecIssues, createDirectiveRegistry, resolveRepeatStatePath, resolveRepeatItemStatePath, removeByPath, addByPath, setByPath, SPEC_DATA_PART_TYPE, nestedToFlat, createMixedStreamParser, defineSchema } from '@json-render/core';
4
+ import { createStateStore, getByPath, runValidation, resolveAction, nextActionDispatchId, notifyActionDispatch, notifyActionSettle, executeAction, isDevtoolsActive, subscribeDevtoolsActive, evaluateVisibility, resolveElementProps, resolveBindings, resolveActionParam, autoFixSpec, validateSpec, createDirectiveRegistry, resolveRepeatStatePath, resolveRepeatItemStatePath, removeByPath, addByPath, setByPath, SPEC_DATA_PART_TYPE, nestedToFlat, createMixedStreamParser, defineSchema } from '@json-render/core';
5
5
  export { autoFixSpec, createStateStore, formatSpecIssues, nestedToFlat, validateSpec } from '@json-render/core';
6
6
  import { flattenToPointers } from '@json-render/core/store-utils';
7
7
 
@@ -18,6 +18,11 @@ class JsonRenderRootContext {
18
18
  registry = EMPTY;
19
19
  loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
20
20
  fallback = EMPTY;
21
+ /**
22
+ * Caps on what the spec may cost the browser. Read as the tree renders, so
23
+ * they apply whatever `validate` is set to.
24
+ */
25
+ limits = EMPTY;
21
26
  /** External store (controlled mode). */
22
27
  store = EMPTY;
23
28
  /** Initial state (uncontrolled mode); falls back to `spec.state`. */
@@ -977,6 +982,16 @@ const ELEMENT_KEY = new InjectionToken('ngx-json-render ELEMENT_KEY');
977
982
  * `repeat` block.
978
983
  */
979
984
  const REPEAT_SCOPE = new InjectionToken('ngx-json-render REPEAT_SCOPE');
985
+ /**
986
+ * This element's position in the render tree. Provided by `<jr-element>` for
987
+ * everything it renders below itself, so each element can see its own depth
988
+ * and its own ancestry.
989
+ *
990
+ * @internal Not part of the public API: it exists so the renderer can refuse
991
+ * to draw an element that is its own ancestor, and to enforce
992
+ * `renderLimits.maxDepth`.
993
+ */
994
+ const RENDER_PATH = new InjectionToken('ngx-json-render RENDER_PATH');
980
995
  /**
981
996
  * Inject the render context inside a catalog component.
982
997
  *
@@ -1037,6 +1052,53 @@ function reportActionError(error) {
1037
1052
  return;
1038
1053
  console.error(error);
1039
1054
  }
1055
+ /**
1056
+ * Whether `inner` reads strictly deeper into state than `outer` — the state
1057
+ * path of one repeat item nested inside another's.
1058
+ *
1059
+ * `null` is the world outside any repeat, so stepping into one is always
1060
+ * progress. Below that it is a prefix test on the resolved path, and the
1061
+ * trailing separator is what keeps `/tree/10` from counting as a step inside
1062
+ * `/tree/1`.
1063
+ */
1064
+ function descendsInto(outer, inner) {
1065
+ if (inner === null)
1066
+ return false;
1067
+ if (outer === null)
1068
+ return true;
1069
+ return inner.startsWith(`${outer}/`);
1070
+ }
1071
+ /**
1072
+ * Whether this element renders itself again without getting anywhere — the
1073
+ * one arrangement that cannot terminate.
1074
+ *
1075
+ * An element reaching itself is not by itself a defect. It is how a tree gets
1076
+ * drawn — a comment thread, a file browser, a nested menu — where each pass
1077
+ * repeats over a path relative to the item it is already inside and so reads
1078
+ * one level further into the data. That ends when the data does.
1079
+ *
1080
+ * What separates the two is whether the repeat descended. Compare against the
1081
+ * nearest occurrence above: `/tree/0` then `/tree/0/children/1` is a tree
1082
+ * walking down, while `/items/0` then `/items/1` is a spec repeating over a
1083
+ * fixed array inside itself, and `/items/0` twice is one with no repeat
1084
+ * between the passes at all. Only the first can run out.
1085
+ */
1086
+ function repeatsItself(path) {
1087
+ for (let step = path.parent; step !== null; step = step.parent) {
1088
+ if (step.key !== path.key)
1089
+ continue;
1090
+ return !descendsInto(step.scopePath, path.scopePath);
1091
+ }
1092
+ return false;
1093
+ }
1094
+ /** The chain from the root down to `path`, as a line a human can follow. */
1095
+ function describePath(path) {
1096
+ const keys = [];
1097
+ for (let step = path; step !== null; step = step.parent) {
1098
+ keys.push(step.key);
1099
+ }
1100
+ return keys.reverse().join(' \u2192 ');
1101
+ }
1040
1102
  /**
1041
1103
  * Renders a single spec element: evaluates visibility, resolves prop
1042
1104
  * expressions against the state model, wires event/watch bindings, and
@@ -1050,6 +1112,7 @@ class JrElement {
1050
1112
  state = inject(JsonRenderStateService);
1051
1113
  actions = inject(JsonRenderActionsService);
1052
1114
  repeatScope = inject(REPEAT_SCOPE, { optional: true });
1115
+ parentPath = inject(RENDER_PATH, { optional: true });
1053
1116
  devtoolsActive = injectDevtoolsActive();
1054
1117
  /** The raw (unresolved) element from the spec. */
1055
1118
  rawElement = computed(() => this.root.spec()?.elements?.[this.elementKey()], ...(ngDevMode ? [{ debugName: "rawElement" }] : /* istanbul ignore next */ []));
@@ -1098,6 +1161,39 @@ class JrElement {
1098
1161
  }, ...(ngDevMode ? [{ debugName: "entry" }] : /* istanbul ignore next */ []));
1099
1162
  component = computed(() => this.entry()?.component ?? null, ...(ngDevMode ? [{ debugName: "component" }] : /* istanbul ignore next */ []));
1100
1163
  devtoolsKey = computed(() => this.devtoolsActive() ? this.elementKey() : null, ...(ngDevMode ? [{ debugName: "devtoolsKey" }] : /* istanbul ignore next */ []));
1164
+ /**
1165
+ * Where this element sits in the tree. Passed down to everything it renders
1166
+ * below itself, which is what lets each element see its own ancestry
1167
+ * without the renderer holding a registry of who is drawing what.
1168
+ */
1169
+ path = computed(() => {
1170
+ const parent = this.parentPath?.() ?? null;
1171
+ return {
1172
+ key: this.elementKey(),
1173
+ depth: (parent?.depth ?? 0) + 1,
1174
+ scopePath: this.repeatScope?.basePath() ?? null,
1175
+ parent,
1176
+ };
1177
+ }, ...(ngDevMode ? [{ debugName: "path" }] : /* istanbul ignore next */ []));
1178
+ /**
1179
+ * Why this element is not drawn, or null when it is.
1180
+ *
1181
+ * The cycle half is unconditional — not something `validate` turns on. An
1182
+ * element that renders itself without reading any deeper renders forever,
1183
+ * and a spec that never stops rendering takes the tab with it; refusing to
1184
+ * draw the repetition is the only outcome that leaves anything on screen.
1185
+ * It costs one walk up a chain of parents, bounded by `maxDepth` where one
1186
+ * is set and by the spec's own nesting where it is not.
1187
+ */
1188
+ refusal = computed(() => {
1189
+ const path = this.path();
1190
+ if (repeatsItself(path))
1191
+ return 'cycle';
1192
+ const maxDepth = this.root.limits()?.maxDepth;
1193
+ if (maxDepth !== undefined && path.depth > maxDepth)
1194
+ return 'depth';
1195
+ return null;
1196
+ }, ...(ngDevMode ? [{ debugName: "refusal" }] : /* istanbul ignore next */ []));
1101
1197
  renderCtx = {
1102
1198
  element: this.resolvedElement,
1103
1199
  props: computed(() => this.resolvedElement()?.props ?? {}),
@@ -1119,10 +1215,28 @@ class JrElement {
1119
1215
  providers: [
1120
1216
  { provide: RENDER_CONTEXT, useValue: this.renderCtx },
1121
1217
  { provide: ELEMENT_KEY, useValue: this.elementKey },
1218
+ { provide: RENDER_PATH, useValue: this.path },
1122
1219
  ],
1123
1220
  parent: inject(Injector),
1124
1221
  });
1125
1222
  constructor() {
1223
+ // Warn (once) when this element is refused. Both reasons are silent
1224
+ // failures on screen — something the spec asked for is missing — so
1225
+ // neither should be silent in the console.
1226
+ let warnedRefusal = false;
1227
+ effect(() => {
1228
+ const reason = this.refusal();
1229
+ if (!reason || warnedRefusal)
1230
+ return;
1231
+ warnedRefusal = true;
1232
+ const path = untracked(this.path);
1233
+ if (reason === 'cycle') {
1234
+ console.warn(`[ngx-json-render] Cycle in the spec: ${describePath(path)}. "${path.key}" renders itself again without reading any deeper into state${path.scopePath === null ? '' : ` (still at "${path.scopePath}")`}, so it never ends. It is not rendered again; everything above it still is. A tree that recurses needs a repeat with a relative statePath, like {"$item": "children"}.`);
1235
+ }
1236
+ else {
1237
+ console.warn(`[ngx-json-render] renderLimits.maxDepth (${untracked(this.root.limits)?.maxDepth}) reached at ${describePath(path)}. "${path.key}" and anything below it do not render.`);
1238
+ }
1239
+ });
1126
1240
  // Warn (once per type) about unknown component types.
1127
1241
  const warnedTypes = new Set();
1128
1242
  effect(() => {
@@ -1155,6 +1269,14 @@ class JrElement {
1155
1269
  });
1156
1270
  // Watch effect: fire actions when watched state paths change.
1157
1271
  effect((onCleanup) => {
1272
+ // A refused element does not act, not just draw nothing. `watch` is the
1273
+ // one thing an element does without being on screen, so leaving it wired
1274
+ // would let the element past `maxDepth` — or the one that closes a cycle
1275
+ // — keep dispatching actions from behind a cap that was supposed to have
1276
+ // stopped it. Reading the signal here also unwires it if the refusal
1277
+ // arrives later, when the limits change.
1278
+ if (this.refusal())
1279
+ return;
1158
1280
  const watchConfig = this.rawElement()?.watch;
1159
1281
  if (!watchConfig)
1160
1282
  return;
@@ -1241,7 +1363,7 @@ class JrElement {
1241
1363
  }
1242
1364
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JrElement, deps: [], target: i0.ɵɵFactoryTarget.Component });
1243
1365
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: JrElement, isStandalone: true, selector: "jr-element", inputs: { elementKey: { classPropertyName: "elementKey", publicName: "elementKey", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
1244
- @if (rawElement() && visible() && component()) {
1366
+ @if (!refusal() && rawElement() && visible() && component()) {
1245
1367
  @if (devtoolsKey(); as dk) {
1246
1368
  <span [attr.data-jr-key]="dk" style="display: contents">
1247
1369
  <ng-container *ngComponentOutlet="component(); injector: outletInjector" />
@@ -1255,7 +1377,7 @@ class JrElement {
1255
1377
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JrElement, decorators: [{
1256
1378
  type: Component,
1257
1379
  args: [{ selector: 'jr-element', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgComponentOutlet], template: `
1258
- @if (rawElement() && visible() && component()) {
1380
+ @if (!refusal() && rawElement() && visible() && component()) {
1259
1381
  @if (devtoolsKey(); as dk) {
1260
1382
  <span [attr.data-jr-key]="dk" style="display: contents">
1261
1383
  <ng-container *ngComponentOutlet="component(); injector: outletInjector" />
@@ -1267,53 +1389,325 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
1267
1389
  `, styles: [":host{display:contents}\n"] }]
1268
1390
  }], ctorParameters: () => [], propDecorators: { elementKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "elementKey", required: true }] }] } });
1269
1391
 
1392
+ /**
1393
+ * A hostile spec can fail the catalog's schema in as many places as it has
1394
+ * properties. Report enough to act on and stop; the spec is refused either
1395
+ * way, and a console full of them helps nobody.
1396
+ */
1397
+ const MAX_REPORTED_PROP_ISSUES = 20;
1398
+ /**
1399
+ * Whether this element's `repeat` descends into the item it is already inside,
1400
+ * rather than re-reading a fixed path in state.
1401
+ *
1402
+ * A relative statePath — `{"$item": "children"}` — resolves against the
1403
+ * enclosing item, so each pass reads one level further into the data and the
1404
+ * recursion ends where the data does. A plain string is absolute: every pass
1405
+ * reads the same array, and a spec that re-enters itself through one never
1406
+ * terminates.
1407
+ */
1408
+ function descendsIntoItems(element) {
1409
+ const repeat = element.repeat;
1410
+ return repeat !== undefined && typeof repeat.statePath !== 'string';
1411
+ }
1412
+ /** Every key an element renders below itself: `children` plus every slot. */
1413
+ function childKeysOf(element) {
1414
+ const slots = element.slots;
1415
+ if (!slots)
1416
+ return element.children ?? [];
1417
+ const fromSlots = Object.values(slots).flat();
1418
+ return element.children ? [...element.children, ...fromSlots] : fromSlots;
1419
+ }
1420
+ /**
1421
+ * Walk the element graph once for depth and cycles.
1422
+ *
1423
+ * Iterative on purpose. A recursive walk would blow the stack on exactly the
1424
+ * specs this exists to catch, which would make the guard a second way to
1425
+ * crash rather than the fix for the first.
1426
+ *
1427
+ * Depth is measured from `spec.root`, because that is what renders. Cycles
1428
+ * are swept for across every element, including the ones no parent reaches:
1429
+ * an unreachable cycle is still a defect, and one edit away from a reachable
1430
+ * one.
1431
+ *
1432
+ * Not every element that reaches itself is a defect, though. Drawing a tree —
1433
+ * a comment thread, a file browser, a nested menu — means an element that
1434
+ * repeats over a relative path and renders itself for each item, and that ends
1435
+ * when the data does. A back edge is only reported when nothing along it
1436
+ * descends into the data, which is the case that cannot end.
1437
+ *
1438
+ * `depthBelow` is memoised per element, so a shared subtree costs one visit
1439
+ * instead of one per path into it — without that, a spec whose elements each
1440
+ * name the next two is exponential to check. The memo makes the reported
1441
+ * depth a lower bound once a cycle has been cut, since which edge closes the
1442
+ * cycle depends on the order the walk reached it. The cycle is reported as an
1443
+ * error of its own, which is the finding worth acting on.
1444
+ */
1445
+ function analyseSpecGraph(spec) {
1446
+ const elements = spec.elements ?? {};
1447
+ const cycles = new Set();
1448
+ const depthBelow = new Map();
1449
+ const onPath = new Set();
1450
+ const walkFrom = (start) => {
1451
+ const first = elements[start];
1452
+ if (!first || depthBelow.has(start))
1453
+ return;
1454
+ const stack = [
1455
+ { key: start, kids: childKeysOf(first), next: 0, deepest: 0 },
1456
+ ];
1457
+ onPath.add(start);
1458
+ while (stack.length > 0) {
1459
+ const frame = stack[stack.length - 1];
1460
+ if (frame.next < frame.kids.length) {
1461
+ const childKey = frame.kids[frame.next++];
1462
+ const memo = depthBelow.get(childKey);
1463
+ if (memo !== undefined) {
1464
+ frame.deepest = Math.max(frame.deepest, memo);
1465
+ continue;
1466
+ }
1467
+ // A child that closes the cycle is not followed — that is what keeps
1468
+ // this walk finite — and not counted towards depth either.
1469
+ if (onPath.has(childKey)) {
1470
+ const from = stack.findIndex((frame) => frame.key === childKey);
1471
+ const descends = stack
1472
+ .slice(from)
1473
+ .some((frame) => descendsIntoItems(elements[frame.key]));
1474
+ if (!descends)
1475
+ cycles.add(childKey);
1476
+ continue;
1477
+ }
1478
+ const child = elements[childKey];
1479
+ // A child the spec never defined is validateSpec's finding, not ours.
1480
+ if (!child)
1481
+ continue;
1482
+ onPath.add(childKey);
1483
+ stack.push({
1484
+ key: childKey,
1485
+ kids: childKeysOf(child),
1486
+ next: 0,
1487
+ deepest: 0,
1488
+ });
1489
+ continue;
1490
+ }
1491
+ stack.pop();
1492
+ onPath.delete(frame.key);
1493
+ const depth = frame.deepest + 1;
1494
+ depthBelow.set(frame.key, depth);
1495
+ const parent = stack[stack.length - 1];
1496
+ if (parent)
1497
+ parent.deepest = Math.max(parent.deepest, depth);
1498
+ }
1499
+ };
1500
+ if (spec.root)
1501
+ walkFrom(spec.root);
1502
+ const depth = (spec.root ? depthBelow.get(spec.root) : undefined) ?? 0;
1503
+ for (const key of Object.keys(elements))
1504
+ walkFrom(key);
1505
+ return { depth, cycles: [...cycles] };
1506
+ }
1507
+ /** Report a cycle, naming the element that is its own descendant. */
1508
+ function cycleIssue(key) {
1509
+ return {
1510
+ severity: 'error',
1511
+ code: 'cycle',
1512
+ elementKey: key,
1513
+ message: `Element "${key}" renders itself against the same data, which never terminates. The cycle is broken where it closes — the repeated element does not render a second time — but the spec cannot be drawn as written. A tree that recurses needs a repeat with a relative statePath, like {"$item": "children"}.`,
1514
+ };
1515
+ }
1516
+ /** Report a breached element-count cap. */
1517
+ function elementCountIssue(count, limit) {
1518
+ return {
1519
+ severity: 'error',
1520
+ code: 'too_many_elements',
1521
+ message: `Spec has ${count} elements, over the limit of ${limit}. Nothing is rendered: renderLimits.maxElements is a cap on the whole spec.`,
1522
+ };
1523
+ }
1524
+ /** Report a breached depth cap. */
1525
+ function depthIssue(depth, limit) {
1526
+ return {
1527
+ severity: 'error',
1528
+ code: 'too_deep',
1529
+ message: `Spec nests ${depth} levels deep, over the limit of ${limit}. Elements below level ${limit} do not render.`,
1530
+ };
1531
+ }
1532
+ /**
1533
+ * Check a spec against a catalog: the component types it names, and then the
1534
+ * catalog's own schema.
1535
+ *
1536
+ * The schema pass is skipped while a type is unknown. A catalog schema keys
1537
+ * its element shapes off `type`, so one name it does not recognise turns into
1538
+ * a pile of prop errors describing a component that was never the point.
1539
+ */
1540
+ function catalogIssues(spec, catalog) {
1541
+ const issues = [];
1542
+ const known = new Set(catalog.componentNames);
1543
+ for (const [key, element] of Object.entries(spec.elements ?? {})) {
1544
+ if (known.has(element.type))
1545
+ continue;
1546
+ issues.push({
1547
+ severity: 'error',
1548
+ code: 'unknown_component',
1549
+ elementKey: key,
1550
+ message: `Element "${key}" has type "${element.type}", which this catalog does not define. Nothing renders for it.`,
1551
+ });
1552
+ }
1553
+ if (issues.length > 0)
1554
+ return issues;
1555
+ const result = catalog.validate(spec);
1556
+ if (result.success)
1557
+ return issues;
1558
+ return propIssues(result.error);
1559
+ }
1560
+ /**
1561
+ * Turn whatever the catalog's schema reported into issues.
1562
+ *
1563
+ * Read defensively rather than typed against the schema library: the shape
1564
+ * below (`issues`, each with `path` and `message`) is what every version of
1565
+ * it has produced, and a renderer that threw while explaining a bad spec
1566
+ * would be the worse failure.
1567
+ */
1568
+ function propIssues(error) {
1569
+ const raw = error?.issues;
1570
+ if (!Array.isArray(raw))
1571
+ return [];
1572
+ return raw.slice(0, MAX_REPORTED_PROP_ISSUES).map((entry) => {
1573
+ const item = entry;
1574
+ const path = Array.isArray(item.path) ? item.path : [];
1575
+ const message = typeof item.message === 'string' ? item.message : 'is not valid';
1576
+ // Paths are rooted at the spec, so `elements.<key>.props.<prop>` names the
1577
+ // element in its second segment.
1578
+ const elementKey = path[0] === 'elements' && typeof path[1] === 'string'
1579
+ ? path[1]
1580
+ : undefined;
1581
+ const where = path.length > 0 ? path.join('.') : 'spec';
1582
+ return {
1583
+ severity: 'error',
1584
+ code: 'invalid_props',
1585
+ ...(elementKey === undefined ? {} : { elementKey }),
1586
+ message: `${where}: ${message}`,
1587
+ };
1588
+ });
1589
+ }
1590
+
1270
1591
  const CLEAN = {
1271
1592
  spec: null,
1272
1593
  issues: [],
1273
1594
  fixes: [],
1274
1595
  hasErrors: false,
1596
+ blocked: false,
1275
1597
  };
1276
1598
  /**
1277
- * Check a spec the way `validate` asks for.
1599
+ * Check a spec the way `validate` and `renderLimits` ask for.
1278
1600
  *
1279
1601
  * Only lossless fixes are applied — `visible`, `on` and `repeat` that a model
1280
1602
  * put inside `props`, which the renderer would otherwise ignore, moved back
1281
1603
  * where they belong. The lossy ones prune content, and core's own guidance is
1282
1604
  * to re-prompt rather than accept that; a renderer that silently deleted
1283
1605
  * elements would be the worse failure.
1606
+ *
1607
+ * Note what is *not* here: cycles are broken as the tree renders, whatever
1608
+ * this returns, because a spec that never stops rendering is a crash rather
1609
+ * than an opinion about quality. This reports them so an app can see them.
1284
1610
  */
1285
- function checkSpec(spec, mode) {
1611
+ function checkSpec(spec, mode, options = {}) {
1286
1612
  if (!spec)
1287
1613
  return CLEAN;
1288
- if (mode === 'off') {
1289
- return { spec, issues: [], fixes: [], hasErrors: false };
1614
+ const limits = options.limits ?? null;
1615
+ const wantsStructure = mode !== 'off';
1616
+ // `maxRepeatItems` caps a live state array, which no amount of reading the
1617
+ // spec can predict — it is enforced as the repeat expands, not here.
1618
+ const maxElements = limits?.maxElements;
1619
+ const maxDepth = limits?.maxDepth;
1620
+ if (!wantsStructure && maxElements === undefined && maxDepth === undefined) {
1621
+ return { spec, issues: [], fixes: [], hasErrors: false, blocked: false };
1622
+ }
1623
+ // The order below is the point of this function, not an accident of it.
1624
+ //
1625
+ // Core's `validateSpec` walks the element tree by recursion, so the specs
1626
+ // that most need a limit are the ones that would overflow the stack proving
1627
+ // they exceed it. Each cheap, non-recursive cap therefore runs first and can
1628
+ // stop the expensive check from being attempted at all: count, then depth —
1629
+ // measured by a walk of our own that uses an explicit stack — and only then
1630
+ // the structural pass.
1631
+ //
1632
+ // `autoFixSpec` only relocates fields inside `props` and never touches
1633
+ // `children` or `slots`, so measuring the graph before fixing it measures
1634
+ // the same graph.
1635
+ if (maxElements !== undefined) {
1636
+ const count = Object.keys(spec.elements ?? {}).length;
1637
+ if (count > maxElements) {
1638
+ const issue = elementCountIssue(count, maxElements);
1639
+ return {
1640
+ spec,
1641
+ issues: [issue],
1642
+ fixes: [],
1643
+ hasErrors: true,
1644
+ blocked: true,
1645
+ };
1646
+ }
1647
+ }
1648
+ const issues = [];
1649
+ let tooDeep = false;
1650
+ if (wantsStructure || maxDepth !== undefined) {
1651
+ const graph = analyseSpecGraph(spec);
1652
+ if (maxDepth !== undefined && graph.depth > maxDepth) {
1653
+ tooDeep = true;
1654
+ issues.push(depthIssue(graph.depth, maxDepth));
1655
+ }
1656
+ if (wantsStructure) {
1657
+ for (const key of graph.cycles)
1658
+ issues.push(cycleIssue(key));
1659
+ }
1660
+ }
1661
+ // A spec past its depth cap is not handed to the recursive checks. The cap
1662
+ // is the finding worth having, and it renders truncated either way.
1663
+ const fixed = wantsStructure && !tooDeep
1664
+ ? autoFixSpec(spec, { lossy: false })
1665
+ : { spec, fixes: [] };
1666
+ if (wantsStructure && !tooDeep) {
1667
+ issues.push(...validateSpec(fixed.spec).issues);
1668
+ if (options.catalog) {
1669
+ issues.push(...catalogIssues(fixed.spec, options.catalog));
1670
+ }
1290
1671
  }
1291
- const fixed = autoFixSpec(spec, { lossy: false });
1292
- const { issues } = validateSpec(fixed.spec);
1293
1672
  return {
1294
1673
  spec: fixed.spec,
1295
1674
  issues,
1296
1675
  fixes: fixed.fixes,
1297
1676
  hasErrors: issues.some((issue) => issue.severity === 'error'),
1677
+ blocked: false,
1298
1678
  };
1299
1679
  }
1680
+ /**
1681
+ * Render issues as one indented block, errors and warnings alike.
1682
+ *
1683
+ * Core's `formatSpecIssues` drops warnings and only accepts core's own issue
1684
+ * codes, so it cannot describe what this package reports. Use this instead
1685
+ * when showing `issues()` to a developer.
1686
+ */
1687
+ function formatSpecCheckIssues(issues) {
1688
+ return issues
1689
+ .map((issue) => ` ${issue.severity === 'error' ? 'error ' : 'warning'} ${issue.elementKey ? `[${issue.elementKey}] ` : ''}${issue.message}`)
1690
+ .join('\n');
1691
+ }
1300
1692
  /**
1301
1693
  * Report a check to the console: fixes as information, issues at the severity
1302
- * the mode gives them. `strict` refuses to render, so its errors are reported
1303
- * as errors; under `warn` the spec still renders and the same finding is a
1694
+ * the outcome gives them. A spec that will not render — refused by a limit,
1695
+ * or by `strict` — is reported as an error, because something the developer
1696
+ * asked for is now missing from the screen; one that renders anyway is a
1304
1697
  * warning.
1305
1698
  */
1306
1699
  function reportSpecCheck(check, mode) {
1307
- if (mode === 'off')
1308
- return;
1309
1700
  if (check.fixes.length > 0) {
1310
1701
  console.info(`[ngx-json-render] Fixed ${check.fixes.length} spec issue(s) before rendering:\n` +
1311
1702
  check.fixes.map((fix) => ` - ${fix}`).join('\n'));
1312
1703
  }
1313
1704
  if (check.issues.length === 0)
1314
1705
  return;
1315
- const report = `[ngx-json-render] Spec validation:\n${formatSpecIssues([...check.issues])}`;
1316
- if (mode === 'strict' && check.hasErrors) {
1706
+ const report = `[ngx-json-render] Spec validation:\n${formatSpecCheckIssues(check.issues)}`;
1707
+ if (check.blocked) {
1708
+ console.error(`${report}\nNothing is rendered: the spec is over a renderLimits cap.`);
1709
+ }
1710
+ else if (mode === 'strict' && check.hasErrors) {
1317
1711
  console.error(`${report}\nNothing is rendered: validate="strict".`);
1318
1712
  }
1319
1713
  else {
@@ -1362,6 +1756,27 @@ class JsonRenderer {
1362
1756
  * arriving is expected to reference children that have not streamed in yet.
1363
1757
  */
1364
1758
  validate = input('off', ...(ngDevMode ? [{ debugName: "validate" }] : /* istanbul ignore next */ []));
1759
+ /**
1760
+ * Caps on what the spec may cost the browser: `maxElements`, `maxDepth` and
1761
+ * `maxRepeatItems`. Unset by default, and each field is independently
1762
+ * optional.
1763
+ *
1764
+ * Unlike `validate`, these are enforced in every mode — a number the app
1765
+ * chose is a control, not a report. A spec over `maxElements` renders
1766
+ * nothing; `maxDepth` and `maxRepeatItems` truncate where they are hit.
1767
+ * They apply while `loading` too: a partial spec is a subset of the
1768
+ * finished one, so a cap can only ever fire early, never falsely.
1769
+ */
1770
+ renderLimits = input(null, ...(ngDevMode ? [{ debugName: "renderLimits" }] : /* istanbul ignore next */ []));
1771
+ /**
1772
+ * The catalog the spec was generated for. Given one, `validate` also checks
1773
+ * what structure alone cannot: that every `type` is a component the catalog
1774
+ * defines, and that the props match its schema.
1775
+ *
1776
+ * Only read when `validate` is on, and — like the rest of the check —
1777
+ * skipped while `loading`.
1778
+ */
1779
+ catalog = input(null, ...(ngDevMode ? [{ debugName: "catalog" }] : /* istanbul ignore next */ []));
1365
1780
  /**
1366
1781
  * Initial state model (uncontrolled mode). Defaults to `spec.state`.
1367
1782
  * Ignored when `store` is provided.
@@ -1392,7 +1807,10 @@ class JsonRenderer {
1392
1807
  * is forced off: the spec is still arriving, and the missing children it
1393
1808
  * refers to are the normal state of a stream, not a defect.
1394
1809
  */
1395
- checked = computed(() => checkSpec(this.spec(), this.loading() ? 'off' : this.validate()), ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
1810
+ checked = computed(() => checkSpec(this.spec(), this.loading() ? 'off' : this.validate(), {
1811
+ limits: this.renderLimits(),
1812
+ catalog: this.catalog(),
1813
+ }), ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
1396
1814
  constructor() {
1397
1815
  const root = inject(JsonRenderRootContext);
1398
1816
  // The fixed spec, not the input: a `visible` the check moved out of
@@ -1401,6 +1819,7 @@ class JsonRenderer {
1401
1819
  root.registry = this.registry;
1402
1820
  root.loading = this.loading;
1403
1821
  root.fallback = this.fallback;
1822
+ root.limits = this.renderLimits;
1404
1823
  root.store = this.store;
1405
1824
  root.initialState = computed(() => this.state() ?? this.spec()?.state ?? {}, ...(ngDevMode ? [{ debugName: "initialState" }] : /* istanbul ignore next */ []));
1406
1825
  root.handlers = this.handlers;
@@ -1418,11 +1837,23 @@ class JsonRenderer {
1418
1837
  inject(JsonRenderValidationService);
1419
1838
  this.actions = inject(JsonRenderActionsService);
1420
1839
  // Reports once per distinct check rather than once per render: `checked`
1421
- // only recomputes when the spec, the mode or the loading flag change.
1422
- effect(() => reportSpecCheck(this.checked(), this.validate()));
1840
+ // only recomputes when the spec, the mode, the limits or the loading flag
1841
+ // change. Silent while loading — a limit can fire on a spec that is still
1842
+ // arriving, and reporting it on every patch would bury the one report
1843
+ // that describes the finished spec.
1844
+ effect(() => {
1845
+ if (this.loading())
1846
+ return;
1847
+ reportSpecCheck(this.checked(), this.validate());
1848
+ });
1423
1849
  }
1424
1850
  rootKey = computed(() => {
1425
1851
  const checked = this.checked();
1852
+ // A spec over an enforced cap does not render in any mode. Whether to
1853
+ // check was the app's decision; once it has made it, honouring the answer
1854
+ // is not also `validate`'s to decide.
1855
+ if (checked.blocked)
1856
+ return null;
1426
1857
  // A spec whose errors survived the fixes does not render under `strict`.
1427
1858
  // Reporting it and drawing half of it anyway is the behaviour the mode
1428
1859
  // exists to refuse.
@@ -1462,7 +1893,7 @@ class JsonRenderer {
1462
1893
  });
1463
1894
  }, ...(ngDevMode ? [{ debugName: "confirmInjector" }] : /* istanbul ignore next */ []));
1464
1895
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JsonRenderer, deps: [], target: i0.ɵɵFactoryTarget.Component });
1465
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: JsonRenderer, isStandalone: true, selector: "json-render", inputs: { spec: { classPropertyName: "spec", publicName: "spec", isSignal: true, isRequired: true, transformFunction: null }, registry: { classPropertyName: "registry", publicName: "registry", isSignal: true, isRequired: true, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, fallback: { classPropertyName: "fallback", publicName: "fallback", isSignal: true, isRequired: false, transformFunction: null }, validate: { classPropertyName: "validate", publicName: "validate", isSignal: true, isRequired: false, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null }, store: { classPropertyName: "store", publicName: "store", isSignal: true, isRequired: false, transformFunction: null }, handlers: { classPropertyName: "handlers", publicName: "handlers", isSignal: true, isRequired: false, transformFunction: null }, onAction: { classPropertyName: "onAction", publicName: "onAction", isSignal: true, isRequired: false, transformFunction: null }, navigate: { classPropertyName: "navigate", publicName: "navigate", isSignal: true, isRequired: false, transformFunction: null }, validationFunctions: { classPropertyName: "validationFunctions", publicName: "validationFunctions", isSignal: true, isRequired: false, transformFunction: null }, functions: { classPropertyName: "functions", publicName: "functions", isSignal: true, isRequired: false, transformFunction: null }, directives: { classPropertyName: "directives", publicName: "directives", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { stateChange: "stateChange" }, providers: [
1896
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: JsonRenderer, isStandalone: true, selector: "json-render", inputs: { spec: { classPropertyName: "spec", publicName: "spec", isSignal: true, isRequired: true, transformFunction: null }, registry: { classPropertyName: "registry", publicName: "registry", isSignal: true, isRequired: true, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, fallback: { classPropertyName: "fallback", publicName: "fallback", isSignal: true, isRequired: false, transformFunction: null }, validate: { classPropertyName: "validate", publicName: "validate", isSignal: true, isRequired: false, transformFunction: null }, renderLimits: { classPropertyName: "renderLimits", publicName: "renderLimits", isSignal: true, isRequired: false, transformFunction: null }, catalog: { classPropertyName: "catalog", publicName: "catalog", isSignal: true, isRequired: false, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null }, store: { classPropertyName: "store", publicName: "store", isSignal: true, isRequired: false, transformFunction: null }, handlers: { classPropertyName: "handlers", publicName: "handlers", isSignal: true, isRequired: false, transformFunction: null }, onAction: { classPropertyName: "onAction", publicName: "onAction", isSignal: true, isRequired: false, transformFunction: null }, navigate: { classPropertyName: "navigate", publicName: "navigate", isSignal: true, isRequired: false, transformFunction: null }, validationFunctions: { classPropertyName: "validationFunctions", publicName: "validationFunctions", isSignal: true, isRequired: false, transformFunction: null }, functions: { classPropertyName: "functions", publicName: "functions", isSignal: true, isRequired: false, transformFunction: null }, directives: { classPropertyName: "directives", publicName: "directives", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { stateChange: "stateChange" }, providers: [
1466
1897
  JsonRenderRootContext,
1467
1898
  JsonRenderStateService,
1468
1899
  JsonRenderValidationService,
@@ -1511,7 +1942,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
1511
1942
  }
1512
1943
  }
1513
1944
  `, styles: [":host{display:contents}\n"] }]
1514
- }], ctorParameters: () => [], propDecorators: { spec: [{ type: i0.Input, args: [{ isSignal: true, alias: "spec", required: true }] }], registry: [{ type: i0.Input, args: [{ isSignal: true, alias: "registry", required: true }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], fallback: [{ type: i0.Input, args: [{ isSignal: true, alias: "fallback", required: false }] }], validate: [{ type: i0.Input, args: [{ isSignal: true, alias: "validate", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], store: [{ type: i0.Input, args: [{ isSignal: true, alias: "store", required: false }] }], handlers: [{ type: i0.Input, args: [{ isSignal: true, alias: "handlers", required: false }] }], onAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "onAction", required: false }] }], navigate: [{ type: i0.Input, args: [{ isSignal: true, alias: "navigate", required: false }] }], validationFunctions: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationFunctions", required: false }] }], functions: [{ type: i0.Input, args: [{ isSignal: true, alias: "functions", required: false }] }], directives: [{ type: i0.Input, args: [{ isSignal: true, alias: "directives", required: false }] }], stateChange: [{ type: i0.Output, args: ["stateChange"] }] } });
1945
+ }], ctorParameters: () => [], propDecorators: { spec: [{ type: i0.Input, args: [{ isSignal: true, alias: "spec", required: true }] }], registry: [{ type: i0.Input, args: [{ isSignal: true, alias: "registry", required: true }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], fallback: [{ type: i0.Input, args: [{ isSignal: true, alias: "fallback", required: false }] }], validate: [{ type: i0.Input, args: [{ isSignal: true, alias: "validate", required: false }] }], renderLimits: [{ type: i0.Input, args: [{ isSignal: true, alias: "renderLimits", required: false }] }], catalog: [{ type: i0.Input, args: [{ isSignal: true, alias: "catalog", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], store: [{ type: i0.Input, args: [{ isSignal: true, alias: "store", required: false }] }], handlers: [{ type: i0.Input, args: [{ isSignal: true, alias: "handlers", required: false }] }], onAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "onAction", required: false }] }], navigate: [{ type: i0.Input, args: [{ isSignal: true, alias: "navigate", required: false }] }], validationFunctions: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationFunctions", required: false }] }], functions: [{ type: i0.Input, args: [{ isSignal: true, alias: "functions", required: false }] }], directives: [{ type: i0.Input, args: [{ isSignal: true, alias: "directives", required: false }] }], stateChange: [{ type: i0.Output, args: ["stateChange"] }] } });
1515
1946
 
1516
1947
  /**
1517
1948
  * Provides the repeat scope (item, index, absolute base path) to the elements
@@ -1586,13 +2017,43 @@ class JrChildren {
1586
2017
  }
1587
2018
  return resolved;
1588
2019
  }, ...(ngDevMode ? [{ debugName: "repeatBasePath" }] : /* istanbul ignore next */ []));
1589
- repeatItems = computed(() => {
2020
+ /** Every item the repeat's state array holds, before any cap. */
2021
+ allRepeatItems = computed(() => {
1590
2022
  const basePath = this.repeatBasePath();
1591
2023
  if (basePath === undefined)
1592
2024
  return [];
1593
2025
  return (getByPath(this.state.state(), basePath) ?? []);
2026
+ }, ...(ngDevMode ? [{ debugName: "allRepeatItems" }] : /* istanbul ignore next */ []));
2027
+ /**
2028
+ * The items this repeat actually renders.
2029
+ *
2030
+ * `repeat` iterates a state array the spec may itself have supplied, which
2031
+ * is the one place a spec sizes the render tree out of data rather than out
2032
+ * of its own structure — so the cap has to be applied here, as it expands,
2033
+ * rather than anywhere a spec can be inspected up front.
2034
+ */
2035
+ repeatItems = computed(() => {
2036
+ const items = this.allRepeatItems();
2037
+ const max = this.root.limits()?.maxRepeatItems;
2038
+ return max !== undefined && items.length > max
2039
+ ? items.slice(0, max)
2040
+ : items;
1594
2041
  }, ...(ngDevMode ? [{ debugName: "repeatItems" }] : /* istanbul ignore next */ []));
1595
2042
  constructor() {
2043
+ // Warn (once) when the cap holds items back. Reading the count only after
2044
+ // the two cheap guards keeps the effect from tracking the state array at
2045
+ // all once there is nothing left to say.
2046
+ let warnedRepeatCap = false;
2047
+ effect(() => {
2048
+ const max = this.root.limits()?.maxRepeatItems;
2049
+ if (max === undefined || warnedRepeatCap)
2050
+ return;
2051
+ const total = this.allRepeatItems().length;
2052
+ if (total <= max)
2053
+ return;
2054
+ warnedRepeatCap = true;
2055
+ console.warn(`[ngx-json-render] renderLimits.maxRepeatItems (${max}) reached: "${untracked(this.repeatBasePath)}" holds ${total} items, so ${total - max} of them do not render.`);
2056
+ });
1596
2057
  // Warn (once per key) about children referencing missing elements.
1597
2058
  const warned = new Set();
1598
2059
  effect(() => {
@@ -2238,6 +2699,10 @@ function injectUIStream(options) {
2238
2699
  const rawLines = signal([], ...(ngDevMode ? [{ debugName: "rawLines" }] : /* istanbul ignore next */ []));
2239
2700
  const issues = signal([], ...(ngDevMode ? [{ debugName: "issues" }] : /* istanbul ignore next */ []));
2240
2701
  const validate = options.validate ?? 'off';
2702
+ const checkOptions = {
2703
+ limits: options.renderLimits ?? null,
2704
+ catalog: options.catalog ?? null,
2705
+ };
2241
2706
  const session = createStreamSession();
2242
2707
  inject(DestroyRef).onDestroy(() => session.cancel());
2243
2708
  const stop = () => {
@@ -2292,7 +2757,7 @@ function injectUIStream(options) {
2292
2757
  signal: gate.signal,
2293
2758
  }, createLineConsumer(handleLine));
2294
2759
  gate.commit(() => {
2295
- const check = checkSpec(currentSpec, validate);
2760
+ const check = checkSpec(currentSpec, validate, checkOptions);
2296
2761
  reportSpecCheck(check, validate);
2297
2762
  if (check.spec) {
2298
2763
  currentSpec = check.spec;
@@ -2303,8 +2768,11 @@ function injectUIStream(options) {
2303
2768
  // app's onComplete is where a spec usually gets persisted, and handing
2304
2769
  // it one that cannot render is the outcome this mode is asked to
2305
2770
  // prevent. The spec stays put so the app can show or discard it.
2306
- if (validate === 'strict' && check.hasErrors) {
2307
- const invalid = new Error(`Generated spec failed validation:\n${formatSpecIssues([...check.issues])}`);
2771
+ // A limit the app set fails the generation whatever the mode: the
2772
+ // spec it would hand `onComplete` is one the app already said it did
2773
+ // not want rendered.
2774
+ if (check.blocked || (validate === 'strict' && check.hasErrors)) {
2775
+ const invalid = new Error(`Generated spec failed validation:\n${formatSpecCheckIssues(check.issues)}`);
2308
2776
  error.set(invalid);
2309
2777
  options.onError?.(invalid);
2310
2778
  return;
@@ -2361,6 +2829,10 @@ function injectChatUI(options) {
2361
2829
  const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
2362
2830
  const issues = signal([], ...(ngDevMode ? [{ debugName: "issues" }] : /* istanbul ignore next */ []));
2363
2831
  const validate = options.validate ?? 'off';
2832
+ const checkOptions = {
2833
+ limits: options.renderLimits ?? null,
2834
+ catalog: options.catalog ?? null,
2835
+ };
2364
2836
  const session = createStreamSession();
2365
2837
  inject(DestroyRef).onDestroy(() => session.cancel());
2366
2838
  const stop = () => {
@@ -2448,7 +2920,7 @@ function injectChatUI(options) {
2448
2920
  gate.commit(() => {
2449
2921
  // A reply that only talked has no spec to check, and reporting
2450
2922
  // "missing root" for a sentence would be nonsense.
2451
- const check = checkSpec(hasSpec ? currentSpec : null, validate);
2923
+ const check = checkSpec(hasSpec ? currentSpec : null, validate, checkOptions);
2452
2924
  if (hasSpec) {
2453
2925
  reportSpecCheck(check, validate);
2454
2926
  if (check.spec)
@@ -2464,8 +2936,11 @@ function injectChatUI(options) {
2464
2936
  // The reply keeps the fixed spec, so what renders in the bubble is
2465
2937
  // what was checked.
2466
2938
  messages.update((prev) => prev.map((m) => (m.id === assistantId ? finalMessage : m)));
2467
- if (validate === 'strict' && check.hasErrors) {
2468
- const invalid = new Error(`Generated spec failed validation:\n${formatSpecIssues([...check.issues])}`);
2939
+ // A limit the app set fails the generation whatever the mode: the
2940
+ // spec it would hand `onComplete` is one the app already said it did
2941
+ // not want rendered.
2942
+ if (check.blocked || (validate === 'strict' && check.hasErrors)) {
2943
+ const invalid = new Error(`Generated spec failed validation:\n${formatSpecCheckIssues(check.issues)}`);
2469
2944
  error.set(invalid);
2470
2945
  options.onError?.(invalid);
2471
2946
  return;
@@ -2611,5 +3086,5 @@ const schema = defineSchema((s) => ({
2611
3086
  * Generated bundle index. Do not edit.
2612
3087
  */
2613
3088
 
2614
- export { CONFIRM_CONTEXT, ELEMENT_KEY, JR_CONFIRM_DIALOG, JR_CONFIRM_LABELS, JrChildren, JrConfirmDialog, JrElement, JrRepeatScope, JsonRenderActionsService, JsonRenderRootContext, JsonRenderStateService, JsonRenderValidationService, JsonRenderer, RENDER_CONTEXT, REPEAT_SCOPE, applyPatch, buildSpecFromParts, checkSpec, createStoreSetState, defineRegistry, flatToTree, getTextFromParts, injectAction, injectActions, injectBoundProp, injectChatUI, injectConfirmContext, injectDevtoolsActive, injectElementKey, injectFieldValidation, injectRenderContext, injectRepeatScope, injectStateBinding, injectStateStore, injectStateValue, injectUIStream, injectValidation, isActionCancelled, jsonRenderMessage, schema };
3089
+ export { CONFIRM_CONTEXT, ELEMENT_KEY, JR_CONFIRM_DIALOG, JR_CONFIRM_LABELS, JrChildren, JrConfirmDialog, JrElement, JrRepeatScope, JsonRenderActionsService, JsonRenderRootContext, JsonRenderStateService, JsonRenderValidationService, JsonRenderer, RENDER_CONTEXT, REPEAT_SCOPE, applyPatch, buildSpecFromParts, checkSpec, createStoreSetState, defineRegistry, flatToTree, formatSpecCheckIssues, getTextFromParts, injectAction, injectActions, injectBoundProp, injectChatUI, injectConfirmContext, injectDevtoolsActive, injectElementKey, injectFieldValidation, injectRenderContext, injectRepeatScope, injectStateBinding, injectStateStore, injectStateValue, injectUIStream, injectValidation, isActionCancelled, jsonRenderMessage, schema };
2615
3090
  //# sourceMappingURL=ngx-json-render.mjs.map