ngx-json-render 0.8.2 → 0.9.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.
@@ -23,6 +23,11 @@ class JsonRenderRootContext {
23
23
  * they apply whatever `validate` is set to.
24
24
  */
25
25
  limits = EMPTY;
26
+ /**
27
+ * The catalog the spec was generated for. While loading, an element whose
28
+ * props do not pass its component's schema waits for them.
29
+ */
30
+ catalog = EMPTY;
26
31
  /** External store (controlled mode). */
27
32
  store = EMPTY;
28
33
  /** Initial state (uncontrolled mode); falls back to `spec.state`. */
@@ -1106,98 +1111,486 @@ function injectDevtoolsActive() {
1106
1111
  }
1107
1112
 
1108
1113
  /**
1109
- * The state paths an element's expressions can read, or `null` when that
1110
- * can't be known from the spec.
1114
+ * A hostile spec can fail the catalog's schema in as many places as it has
1115
+ * properties. Report enough to act on and stop; the spec is refused either
1116
+ * way, and a console full of them helps nobody.
1117
+ */
1118
+ const MAX_REPORTED_PROP_ISSUES = 20;
1119
+ /**
1120
+ * Whether this element's `repeat` descends into the item it is already inside,
1121
+ * rather than re-reading a fixed path in state.
1111
1122
  *
1112
- * Worked out from the spec alone, before anything resolves, so that an element
1113
- * can depend on those paths rather than on the whole state. The answer may
1114
- * name more paths than a given resolution reads — both branches of a `$cond`,
1115
- * say — but never fewer: a path left out would be a write the element misses,
1116
- * and it would keep showing the old value with nothing in the console.
1123
+ * A relative statePath — `{"$item": "children"}` — resolves against the
1124
+ * enclosing item, so each pass reads one level further into the data and the
1125
+ * recursion ends where the data does. A plain string is absolute: every pass
1126
+ * reads the same array, and a spec that re-enters itself through one never
1127
+ * terminates.
1128
+ */
1129
+ function descendsIntoItems(element) {
1130
+ const repeat = element.repeat;
1131
+ return repeat !== undefined && typeof repeat.statePath !== 'string';
1132
+ }
1133
+ /** Every key an element renders below itself: `children` plus every slot. */
1134
+ function childKeysOf(element) {
1135
+ const slots = element.slots;
1136
+ if (!slots)
1137
+ return element.children ?? [];
1138
+ const fromSlots = Object.values(slots).flat();
1139
+ return element.children ? [...element.children, ...fromSlots] : fromSlots;
1140
+ }
1141
+ /**
1142
+ * Walk the element graph once for depth and cycles.
1117
1143
  *
1118
- * `null` is the safe answer, and it is given whenever the spec holds something
1119
- * this reader does not model:
1144
+ * Iterative on purpose. A recursive walk would blow the stack on exactly the
1145
+ * specs this exists to catch, which would make the guard a second way to
1146
+ * crash rather than the fix for the first.
1120
1147
  *
1121
- * - a registered directive, whose `resolve` receives the whole context and may
1122
- * read any path at all;
1123
- * - a `$`-prefixed key it doesn't recognise, which is what an expression added
1124
- * to core after this was written would look like;
1125
- * - a read of the whole state (`''` or `'/'`), and a condition it can't parse.
1148
+ * Depth is measured from `spec.root`, because that is what renders. Cycles
1149
+ * are swept for across every element, including the ones no parent reaches:
1150
+ * an unreachable cycle is still a defect, and one edit away from a reachable
1151
+ * one.
1126
1152
  *
1127
- * The walk mirrors core's `resolvePropValue` and `evaluateVisibility`, in the
1128
- * same order, because the order decides which expression an object is.
1153
+ * Not every element that reaches itself is a defect, though. Drawing a tree —
1154
+ * a comment thread, a file browser, a nested menu — means an element that
1155
+ * repeats over a relative path and renders itself for each item, and that ends
1156
+ * when the data does. A back edge is only reported when nothing along it
1157
+ * descends into the data, which is the case that cannot end.
1158
+ *
1159
+ * `depthBelow` is memoised per element, so a shared subtree costs one visit
1160
+ * instead of one per path into it — without that, a spec whose elements each
1161
+ * name the next two is exponential to check. The memo makes the reported
1162
+ * depth a lower bound once a cycle has been cut, since which edge closes the
1163
+ * cycle depends on the order the walk reached it. The cycle is reported as an
1164
+ * error of its own, which is the finding worth acting on.
1129
1165
  */
1130
- function collectStateReads(element, directives, repeatBasePath) {
1131
- const reader = new StateReader(directives, repeatBasePath);
1132
- const understood = reader.condition(element.visible) &&
1133
- Object.values(element.props ?? {}).every((value) => reader.value(value));
1134
- return understood ? reader.paths : null;
1166
+ function analyseSpecGraph(spec) {
1167
+ const elements = spec.elements ?? {};
1168
+ const cycles = new Set();
1169
+ const depthBelow = new Map();
1170
+ const onPath = new Set();
1171
+ const walkFrom = (start) => {
1172
+ const first = elements[start];
1173
+ if (!first || depthBelow.has(start))
1174
+ return;
1175
+ const stack = [
1176
+ { key: start, kids: childKeysOf(first), next: 0, deepest: 0 },
1177
+ ];
1178
+ onPath.add(start);
1179
+ while (stack.length > 0) {
1180
+ const frame = stack[stack.length - 1];
1181
+ if (frame.next < frame.kids.length) {
1182
+ const childKey = frame.kids[frame.next++];
1183
+ const memo = depthBelow.get(childKey);
1184
+ if (memo !== undefined) {
1185
+ frame.deepest = Math.max(frame.deepest, memo);
1186
+ continue;
1187
+ }
1188
+ // A child that closes the cycle is not followed — that is what keeps
1189
+ // this walk finite — and not counted towards depth either.
1190
+ if (onPath.has(childKey)) {
1191
+ const from = stack.findIndex((frame) => frame.key === childKey);
1192
+ const descends = stack
1193
+ .slice(from)
1194
+ .some((frame) => descendsIntoItems(elements[frame.key]));
1195
+ if (!descends)
1196
+ cycles.add(childKey);
1197
+ continue;
1198
+ }
1199
+ const child = elements[childKey];
1200
+ // A child the spec never defined is validateSpec's finding, not ours.
1201
+ if (!child)
1202
+ continue;
1203
+ onPath.add(childKey);
1204
+ stack.push({
1205
+ key: childKey,
1206
+ kids: childKeysOf(child),
1207
+ next: 0,
1208
+ deepest: 0,
1209
+ });
1210
+ continue;
1211
+ }
1212
+ stack.pop();
1213
+ onPath.delete(frame.key);
1214
+ const depth = frame.deepest + 1;
1215
+ depthBelow.set(frame.key, depth);
1216
+ const parent = stack[stack.length - 1];
1217
+ if (parent)
1218
+ parent.deepest = Math.max(parent.deepest, depth);
1219
+ }
1220
+ };
1221
+ if (spec.root)
1222
+ walkFrom(spec.root);
1223
+ const depth = (spec.root ? depthBelow.get(spec.root) : undefined) ?? 0;
1224
+ for (const key of Object.keys(elements))
1225
+ walkFrom(key);
1226
+ return { depth, cycles: [...cycles] };
1135
1227
  }
1136
- function isObject(value) {
1137
- return typeof value === 'object' && value !== null;
1228
+ /** Report a cycle, naming the element that is its own descendant. */
1229
+ function cycleIssue(key) {
1230
+ return {
1231
+ severity: 'error',
1232
+ code: 'cycle',
1233
+ elementKey: key,
1234
+ 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"}.`,
1235
+ };
1138
1236
  }
1139
- /** Comparison operators a single condition may carry, as core reads them. */
1140
- const COMPARISONS = ['eq', 'neq', 'gt', 'gte', 'lt', 'lte'];
1141
- class StateReader {
1142
- directives;
1143
- repeatBasePath;
1144
- paths = new Set();
1145
- constructor(directives, repeatBasePath) {
1146
- this.directives = directives;
1147
- this.repeatBasePath = repeatBasePath;
1237
+ /** Report a breached element-count cap. */
1238
+ function elementCountIssue(count, limit) {
1239
+ return {
1240
+ severity: 'error',
1241
+ code: 'too_many_elements',
1242
+ message: `Spec has ${count} elements, over the limit of ${limit}. Nothing is rendered: renderLimits.maxElements is a cap on the whole spec.`,
1243
+ };
1244
+ }
1245
+ /** Report a breached depth cap. */
1246
+ function depthIssue(depth, limit) {
1247
+ return {
1248
+ severity: 'error',
1249
+ code: 'too_deep',
1250
+ message: `Spec nests ${depth} levels deep, over the limit of ${limit}. Elements below level ${limit} do not render.`,
1251
+ };
1252
+ }
1253
+ /**
1254
+ * Check a spec against a catalog: the component types it names, then the
1255
+ * catalog's spec schema, then each element's props against its own
1256
+ * component's schema.
1257
+ *
1258
+ * The props need that pass of their own. Core builds one schema for the whole
1259
+ * spec, and it gives an element's props a component's schema only when the
1260
+ * catalog has exactly one component; with more, every element's props are an
1261
+ * open record, whatever its `type`. So `catalog.validate` holds an element to
1262
+ * its shape — `children`, a `type` from the catalog — and not to its props.
1263
+ *
1264
+ * Whatever that schema did say about the props of an element checked here is
1265
+ * dropped: with a one-component catalog it would be reported twice, and it
1266
+ * judges expressions this pass knows to leave alone.
1267
+ *
1268
+ * The schema passes are skipped while a type is unknown. That is an error on
1269
+ * its own, and the spec schema would only restate it as a `type` outside the
1270
+ * ones it accepts.
1271
+ */
1272
+ function catalogIssues(spec, catalog) {
1273
+ const elements = Object.entries(spec.elements ?? {});
1274
+ const known = new Set(catalog.componentNames);
1275
+ const unknown = [];
1276
+ for (const [key, element] of elements) {
1277
+ if (known.has(element.type))
1278
+ continue;
1279
+ unknown.push({
1280
+ severity: 'error',
1281
+ code: 'unknown_component',
1282
+ elementKey: key,
1283
+ message: `Element "${key}" has type "${element.type}", which this catalog does not define. Nothing renders for it.`,
1284
+ });
1148
1285
  }
1149
- /** A prop value. Returns false when it can read state this can't name. */
1150
- value(value) {
1151
- if (!isObject(value))
1152
- return true;
1153
- if (typeof value['$state'] === 'string')
1154
- return this.state(value['$state']);
1155
- if (typeof value['$item'] === 'string')
1156
- return this.item(value['$item']);
1157
- if ('$index' in value && value['$index'] === true)
1158
- return true;
1159
- // A two-way binding reads the path it writes back to.
1160
- if (typeof value['$bindState'] === 'string') {
1161
- return this.state(value['$bindState']);
1162
- }
1163
- if (typeof value['$bindItem'] === 'string') {
1164
- return this.boundItem(value['$bindItem']);
1165
- }
1166
- if ('$cond' in value && '$then' in value && '$else' in value) {
1167
- return (this.condition(value['$cond']) &&
1168
- this.value(value['$then']) &&
1169
- this.value(value['$else']));
1170
- }
1171
- if (typeof value['$computed'] === 'string') {
1172
- // The function is handed its resolved arguments and nothing else.
1173
- const args = value['args'];
1174
- return (!args || Object.values(Object(args)).every((arg) => this.value(arg)));
1175
- }
1176
- if (typeof value['$template'] === 'string') {
1177
- return this.template(value['$template']);
1178
- }
1179
- if (Array.isArray(value))
1180
- return value.every((item) => this.value(item));
1181
- if (this.directives) {
1182
- for (const name of this.directives.keys()) {
1183
- if (name in value)
1184
- return false;
1185
- }
1186
- }
1187
- for (const [key, entry] of Object.entries(value)) {
1188
- if (key.startsWith('$') || !this.value(entry))
1189
- return false;
1190
- }
1191
- return true;
1286
+ if (unknown.length > 0)
1287
+ return unknown;
1288
+ const schemas = new Map();
1289
+ for (const [key, element] of elements) {
1290
+ const schema = propsSchemaOf(catalog.data, element.type);
1291
+ if (schema)
1292
+ schemas.set(key, schema);
1192
1293
  }
1193
- /** A `visible` condition, or the condition of a `$cond`. */
1194
- condition(condition) {
1195
- if (condition === undefined || typeof condition === 'boolean')
1196
- return true;
1197
- if (Array.isArray(condition)) {
1198
- return condition.every((single) => this.singleCondition(single));
1199
- }
1200
- if (!isObject(condition))
1294
+ const found = specSchemaIssues(spec, catalog).filter(({ path }) => !(path[0] === 'elements' &&
1295
+ typeof path[1] === 'string' &&
1296
+ schemas.has(path[1]) &&
1297
+ path[2] === 'props'));
1298
+ for (const [key, element] of elements) {
1299
+ const room = MAX_REPORTED_PROP_ISSUES - found.length;
1300
+ if (room <= 0)
1301
+ break;
1302
+ const schema = schemas.get(key);
1303
+ if (schema)
1304
+ found.push(...propsIssues(key, element, schema, room));
1305
+ }
1306
+ return found.slice(0, MAX_REPORTED_PROP_ISSUES).map(toSpecCheckIssue);
1307
+ }
1308
+ /**
1309
+ * Whether an element's props have arrived: they pass its component's schema,
1310
+ * leaving out what the schema says about expressions, the same way
1311
+ * {@link catalogIssues} judges them. The renderer holds an element back on
1312
+ * this while the spec streams.
1313
+ *
1314
+ * The props are taken as written in the spec, not as resolved. What a stream
1315
+ * leaves out is spec, and an expression that is there has arrived, whatever
1316
+ * it resolves to; checking the spec also keeps state writes from re-running
1317
+ * the schema.
1318
+ *
1319
+ * True when the catalog declares no schema that can be called for the type:
1320
+ * there is nothing to wait for. False when the schema throws, or fails
1321
+ * without saying where — the element mounts once loading ends either way.
1322
+ */
1323
+ function propsArrived(element, catalog) {
1324
+ const schema = propsSchemaOf(catalog.data, element.type);
1325
+ if (!schema)
1326
+ return true;
1327
+ const props = element.props ?? {};
1328
+ let result;
1329
+ try {
1330
+ result = schema.safeParse(props);
1331
+ }
1332
+ catch {
1333
+ return false;
1334
+ }
1335
+ if (result.success)
1336
+ return true;
1337
+ const issues = readIssues(result.error);
1338
+ return (issues.length > 0 &&
1339
+ issues.every(({ path }) => reachesExpression(props, path)));
1340
+ }
1341
+ /**
1342
+ * The props schema a catalog declares for a component type, if it declares
1343
+ * one that can be called.
1344
+ */
1345
+ function propsSchemaOf(data, type) {
1346
+ const components = data
1347
+ ?.components;
1348
+ if (typeof components !== 'object' ||
1349
+ components === null ||
1350
+ !Object.hasOwn(components, type)) {
1351
+ return undefined;
1352
+ }
1353
+ const props = components[type]?.props;
1354
+ return typeof props
1355
+ ?.safeParse === 'function'
1356
+ ? props
1357
+ : undefined;
1358
+ }
1359
+ /** What the catalog's spec schema reported, with paths rooted at the spec. */
1360
+ function specSchemaIssues(spec, catalog) {
1361
+ try {
1362
+ const result = catalog.validate(spec);
1363
+ return result.success ? [] : readIssues(result.error);
1364
+ }
1365
+ catch (error) {
1366
+ return [{ path: [], message: threwMessage('the catalog schema', error) }];
1367
+ }
1368
+ }
1369
+ /**
1370
+ * What an element's component schema says about its props, leaving out what
1371
+ * it says about expressions, with paths rooted at the spec.
1372
+ *
1373
+ * A schema that throws counts as a rejection, not a crash. Real ones do:
1374
+ * `ValidationConfigSchema`, which every Material input's `validation` prop
1375
+ * uses, recurses through `enabled` and overflows the stack a thousand
1376
+ * `$and`s deep.
1377
+ */
1378
+ function propsIssues(key, element, schema, room) {
1379
+ const at = ['elements', key, 'props'];
1380
+ let result;
1381
+ try {
1382
+ result = schema.safeParse(element.props);
1383
+ }
1384
+ catch (error) {
1385
+ return [
1386
+ {
1387
+ path: at,
1388
+ message: threwMessage(`the "${element.type}" schema`, error),
1389
+ },
1390
+ ];
1391
+ }
1392
+ if (result.success)
1393
+ return [];
1394
+ const issues = [];
1395
+ for (const { path, message } of readIssues(result.error)) {
1396
+ if (issues.length >= room)
1397
+ break;
1398
+ if (reachesExpression(element.props, path))
1399
+ continue;
1400
+ issues.push({ path: [...at, ...path], message });
1401
+ }
1402
+ return issues;
1403
+ }
1404
+ /**
1405
+ * Whether a props issue is about an expression: it sits on one, inside one,
1406
+ * or on a value holding one.
1407
+ *
1408
+ * `{"$state": "/title"}` where the schema wants a string is not a defect.
1409
+ * Core resolves every such value before the component sees it, so the schema
1410
+ * would be judging something that never renders. Any `$`-prefixed key counts
1411
+ * — directive names must start with `$`, and so would an expression added to
1412
+ * core later. A value merely holding one is left alone too, since a union or
1413
+ * a refinement over it ran on the unresolved value.
1414
+ *
1415
+ * The props object itself is not an expression: core resolves the values
1416
+ * inside it, never the object.
1417
+ */
1418
+ function reachesExpression(props, path) {
1419
+ let value = props;
1420
+ for (const segment of path) {
1421
+ if (typeof value !== 'object' || value === null)
1422
+ return false;
1423
+ value = value[segment];
1424
+ if (isExpression(value))
1425
+ return true;
1426
+ }
1427
+ return holdsExpression(value);
1428
+ }
1429
+ /** A value core resolves at render time — or will, once it knows the key. */
1430
+ function isExpression(value) {
1431
+ return (typeof value === 'object' &&
1432
+ value !== null &&
1433
+ !Array.isArray(value) &&
1434
+ Object.keys(value).some((key) => key.startsWith('$')));
1435
+ }
1436
+ /**
1437
+ * Whether anything nested inside `value` is an expression.
1438
+ *
1439
+ * Iterative, and it remembers what it has seen, for the same reason as
1440
+ * {@link analyseSpecGraph}: props are as attacker-shaped as the rest of the
1441
+ * spec, and a check that overflowed or looped on them would be a second way
1442
+ * to crash.
1443
+ */
1444
+ function holdsExpression(value) {
1445
+ const pending = [value];
1446
+ const seen = new Set();
1447
+ while (pending.length > 0) {
1448
+ const current = pending.pop();
1449
+ if (typeof current !== 'object' || current === null)
1450
+ continue;
1451
+ if (seen.has(current))
1452
+ continue;
1453
+ seen.add(current);
1454
+ for (const child of Object.values(current)) {
1455
+ if (isExpression(child))
1456
+ return true;
1457
+ pending.push(child);
1458
+ }
1459
+ }
1460
+ return false;
1461
+ }
1462
+ /**
1463
+ * Read what a schema reported.
1464
+ *
1465
+ * Read defensively rather than typed against the schema library: the shape
1466
+ * below (`issues`, each with `path` and `message`) is what every version of
1467
+ * it has produced, and a renderer that threw while explaining a bad spec
1468
+ * would be the worse failure.
1469
+ */
1470
+ function readIssues(error) {
1471
+ const raw = error?.issues;
1472
+ if (!Array.isArray(raw))
1473
+ return [];
1474
+ return raw.map((entry) => {
1475
+ const item = entry;
1476
+ const path = item?.path;
1477
+ const message = item?.message;
1478
+ return {
1479
+ path: Array.isArray(path) ? path : [],
1480
+ message: typeof message === 'string' ? message : 'is not valid',
1481
+ };
1482
+ });
1483
+ }
1484
+ function threwMessage(schema, error) {
1485
+ const reason = error instanceof Error ? ` (${error.name}: ${error.message})` : '';
1486
+ return `${schema} threw instead of answering${reason}, so this counts as rejected`;
1487
+ }
1488
+ function toSpecCheckIssue({ path, message }) {
1489
+ // Paths are rooted at the spec, so `elements.<key>.props.<prop>` names the
1490
+ // element in its second segment.
1491
+ const elementKey = path[0] === 'elements' && typeof path[1] === 'string' ? path[1] : undefined;
1492
+ const where = path.length > 0 ? path.map(String).join('.') : 'spec';
1493
+ return {
1494
+ severity: 'error',
1495
+ code: 'invalid_props',
1496
+ ...(elementKey === undefined ? {} : { elementKey }),
1497
+ message: `${where}: ${message}`,
1498
+ };
1499
+ }
1500
+
1501
+ /**
1502
+ * The state paths an element's expressions can read, or `null` when that
1503
+ * can't be known from the spec.
1504
+ *
1505
+ * Worked out from the spec alone, before anything resolves, so that an element
1506
+ * can depend on those paths rather than on the whole state. The answer may
1507
+ * name more paths than a given resolution reads — both branches of a `$cond`,
1508
+ * say — but never fewer: a path left out would be a write the element misses,
1509
+ * and it would keep showing the old value with nothing in the console.
1510
+ *
1511
+ * `null` is the safe answer, and it is given whenever the spec holds something
1512
+ * this reader does not model:
1513
+ *
1514
+ * - a registered directive, whose `resolve` receives the whole context and may
1515
+ * read any path at all;
1516
+ * - a `$`-prefixed key it doesn't recognise, which is what an expression added
1517
+ * to core after this was written would look like;
1518
+ * - a read of the whole state (`''` or `'/'`), and a condition it can't parse.
1519
+ *
1520
+ * The walk mirrors core's `resolvePropValue` and `evaluateVisibility`, in the
1521
+ * same order, because the order decides which expression an object is.
1522
+ */
1523
+ function collectStateReads(element, directives, repeatBasePath) {
1524
+ const reader = new StateReader(directives, repeatBasePath);
1525
+ const understood = reader.condition(element.visible) &&
1526
+ Object.values(element.props ?? {}).every((value) => reader.value(value));
1527
+ return understood ? reader.paths : null;
1528
+ }
1529
+ function isObject(value) {
1530
+ return typeof value === 'object' && value !== null;
1531
+ }
1532
+ /** Comparison operators a single condition may carry, as core reads them. */
1533
+ const COMPARISONS = ['eq', 'neq', 'gt', 'gte', 'lt', 'lte'];
1534
+ class StateReader {
1535
+ directives;
1536
+ repeatBasePath;
1537
+ paths = new Set();
1538
+ constructor(directives, repeatBasePath) {
1539
+ this.directives = directives;
1540
+ this.repeatBasePath = repeatBasePath;
1541
+ }
1542
+ /** A prop value. Returns false when it can read state this can't name. */
1543
+ value(value) {
1544
+ if (!isObject(value))
1545
+ return true;
1546
+ if (typeof value['$state'] === 'string')
1547
+ return this.state(value['$state']);
1548
+ if (typeof value['$item'] === 'string')
1549
+ return this.item(value['$item']);
1550
+ if ('$index' in value && value['$index'] === true)
1551
+ return true;
1552
+ // A two-way binding reads the path it writes back to.
1553
+ if (typeof value['$bindState'] === 'string') {
1554
+ return this.state(value['$bindState']);
1555
+ }
1556
+ if (typeof value['$bindItem'] === 'string') {
1557
+ return this.boundItem(value['$bindItem']);
1558
+ }
1559
+ if ('$cond' in value && '$then' in value && '$else' in value) {
1560
+ return (this.condition(value['$cond']) &&
1561
+ this.value(value['$then']) &&
1562
+ this.value(value['$else']));
1563
+ }
1564
+ if (typeof value['$computed'] === 'string') {
1565
+ // The function is handed its resolved arguments and nothing else.
1566
+ const args = value['args'];
1567
+ return (!args || Object.values(Object(args)).every((arg) => this.value(arg)));
1568
+ }
1569
+ if (typeof value['$template'] === 'string') {
1570
+ return this.template(value['$template']);
1571
+ }
1572
+ if (Array.isArray(value))
1573
+ return value.every((item) => this.value(item));
1574
+ if (this.directives) {
1575
+ for (const name of this.directives.keys()) {
1576
+ if (name in value)
1577
+ return false;
1578
+ }
1579
+ }
1580
+ for (const [key, entry] of Object.entries(value)) {
1581
+ if (key.startsWith('$') || !this.value(entry))
1582
+ return false;
1583
+ }
1584
+ return true;
1585
+ }
1586
+ /** A `visible` condition, or the condition of a `$cond`. */
1587
+ condition(condition) {
1588
+ if (condition === undefined || typeof condition === 'boolean')
1589
+ return true;
1590
+ if (Array.isArray(condition)) {
1591
+ return condition.every((single) => this.singleCondition(single));
1592
+ }
1593
+ if (!isObject(condition))
1201
1594
  return false;
1202
1595
  for (const group of ['$and', '$or']) {
1203
1596
  if (group in condition) {
@@ -1668,7 +2061,34 @@ class JrElement {
1668
2061
  const el = this.rawElement();
1669
2062
  return el ? this.root.resolveEntry(el.type) : undefined;
1670
2063
  }, ...(ngDevMode ? [{ debugName: "entry" }] : /* istanbul ignore next */ []));
1671
- component = computed(() => this.entry()?.component ?? null, ...(ngDevMode ? [{ debugName: "component" }] : /* istanbul ignore next */ []));
2064
+ /** Set once the real component has been shown; see {@link held}. */
2065
+ mounted = false;
2066
+ /**
2067
+ * Whether the component waits for its props: the spec is still loading, a
2068
+ * catalog was given, and the props written so far do not pass the
2069
+ * component's schema (see {@link propsArrived}).
2070
+ *
2071
+ * One-way. Once the component has been shown it is never swapped back for
2072
+ * the fallback, even if a later patch takes a prop away again: a component
2073
+ * flickering out mid-stream is worse than one showing a gap. Once latched
2074
+ * this reads no signals, so a mounted element pays nothing for the gate on
2075
+ * later patches — and neither does any element when nothing is loading.
2076
+ */
2077
+ held = computed(() => {
2078
+ if (this.mounted)
2079
+ return false;
2080
+ const el = this.rawElement();
2081
+ const catalog = this.root.catalog();
2082
+ const held = !!el && !!catalog && this.root.loading() && !propsArrived(el, catalog);
2083
+ if (!held && this.entry() && this.visible() && !this.refusal()) {
2084
+ this.mounted = true;
2085
+ }
2086
+ return held;
2087
+ }, ...(ngDevMode ? [{ debugName: "held" }] : /* istanbul ignore next */ []));
2088
+ /** What the outlet shows: the component, or its fallback while held. */
2089
+ component = computed(() => this.held()
2090
+ ? (this.entry()?.fallback ?? null)
2091
+ : (this.entry()?.component ?? null), ...(ngDevMode ? [{ debugName: "component" }] : /* istanbul ignore next */ []));
1672
2092
  devtoolsKey = computed(() => this.devtoolsActive() ? this.elementKey() : null, ...(ngDevMode ? [{ debugName: "devtoolsKey" }] : /* istanbul ignore next */ []));
1673
2093
  /**
1674
2094
  * Where this element sits in the tree. Passed down to everything it renders
@@ -1884,432 +2304,163 @@ class JrElement {
1884
2304
  stateModel: this.state.getSnapshot(),
1885
2305
  };
1886
2306
  }
1887
- /**
1888
- * Fire-and-forget entry point for event emission: dispatching is async, but
1889
- * a template listener has nowhere to await it, so the failure must be
1890
- * handled here rather than escaping as an unhandled rejection.
1891
- */
1892
- fireEvent(eventName) {
1893
- this.emitEvent(eventName).catch(reportActionError);
1894
- }
1895
- async emitEvent(eventName) {
1896
- const binding = untracked(this.rawElement)?.on?.[eventName];
1897
- if (binding)
1898
- await this.dispatch(binding);
1899
- }
1900
- /**
1901
- * Run an `on` or `watch` binding's actions in order. Params are resolved
1902
- * per action, just before it runs, so a `$state` read in a later action of
1903
- * the chain sees what an earlier one wrote.
1904
- */
1905
- async dispatch(binding) {
1906
- for (const b of Array.isArray(binding) ? binding : [binding]) {
1907
- if (!b.params) {
1908
- await this.actions.execute(b);
1909
- continue;
1910
- }
1911
- const liveCtx = this.liveResolutionCtx();
1912
- const resolved = {};
1913
- for (const [key, val] of Object.entries(b.params)) {
1914
- resolved[key] = resolveActionParam(val, liveCtx);
1915
- }
1916
- await this.actions.execute({ ...b, params: resolved });
1917
- }
1918
- }
1919
- eventHandle(eventName) {
1920
- const el = this.rawElement();
1921
- const binding = el?.on?.[eventName];
1922
- if (!binding) {
1923
- return { emit: () => { }, shouldPreventDefault: false, bound: false };
1924
- }
1925
- const actionBindings = Array.isArray(binding) ? binding : [binding];
1926
- return {
1927
- emit: () => this.fireEvent(eventName),
1928
- shouldPreventDefault: actionBindings.some((b) => b.preventDefault),
1929
- bound: true,
1930
- };
1931
- }
1932
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JrElement, deps: [], target: i0.ɵɵFactoryTarget.Component });
1933
- 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: `
1934
- @if (!refusal() && rawElement() && visible() && component()) {
1935
- @if (devtoolsKey(); as dk) {
1936
- <span [attr.data-jr-key]="dk" style="display: contents">
1937
- <ng-container *ngComponentOutlet="component(); injector: outletInjector" />
1938
- </span>
1939
- } @else {
1940
- <ng-container *ngComponentOutlet="component(); injector: outletInjector" />
1941
- }
1942
- }
1943
- `, isInline: true, styles: [":host{display:contents}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1944
- }
1945
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JrElement, decorators: [{
1946
- type: Component,
1947
- args: [{ selector: 'jr-element', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgComponentOutlet], template: `
1948
- @if (!refusal() && rawElement() && visible() && component()) {
1949
- @if (devtoolsKey(); as dk) {
1950
- <span [attr.data-jr-key]="dk" style="display: contents">
1951
- <ng-container *ngComponentOutlet="component(); injector: outletInjector" />
1952
- </span>
1953
- } @else {
1954
- <ng-container *ngComponentOutlet="component(); injector: outletInjector" />
1955
- }
1956
- }
1957
- `, styles: [":host{display:contents}\n"] }]
1958
- }], ctorParameters: () => [], propDecorators: { elementKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "elementKey", required: true }] }] } });
1959
-
1960
- /**
1961
- * A hostile spec can fail the catalog's schema in as many places as it has
1962
- * properties. Report enough to act on and stop; the spec is refused either
1963
- * way, and a console full of them helps nobody.
1964
- */
1965
- const MAX_REPORTED_PROP_ISSUES = 20;
1966
- /**
1967
- * Whether this element's `repeat` descends into the item it is already inside,
1968
- * rather than re-reading a fixed path in state.
1969
- *
1970
- * A relative statePath — `{"$item": "children"}` — resolves against the
1971
- * enclosing item, so each pass reads one level further into the data and the
1972
- * recursion ends where the data does. A plain string is absolute: every pass
1973
- * reads the same array, and a spec that re-enters itself through one never
1974
- * terminates.
1975
- */
1976
- function descendsIntoItems(element) {
1977
- const repeat = element.repeat;
1978
- return repeat !== undefined && typeof repeat.statePath !== 'string';
1979
- }
1980
- /** Every key an element renders below itself: `children` plus every slot. */
1981
- function childKeysOf(element) {
1982
- const slots = element.slots;
1983
- if (!slots)
1984
- return element.children ?? [];
1985
- const fromSlots = Object.values(slots).flat();
1986
- return element.children ? [...element.children, ...fromSlots] : fromSlots;
1987
- }
1988
- /**
1989
- * Walk the element graph once for depth and cycles.
1990
- *
1991
- * Iterative on purpose. A recursive walk would blow the stack on exactly the
1992
- * specs this exists to catch, which would make the guard a second way to
1993
- * crash rather than the fix for the first.
1994
- *
1995
- * Depth is measured from `spec.root`, because that is what renders. Cycles
1996
- * are swept for across every element, including the ones no parent reaches:
1997
- * an unreachable cycle is still a defect, and one edit away from a reachable
1998
- * one.
1999
- *
2000
- * Not every element that reaches itself is a defect, though. Drawing a tree —
2001
- * a comment thread, a file browser, a nested menu — means an element that
2002
- * repeats over a relative path and renders itself for each item, and that ends
2003
- * when the data does. A back edge is only reported when nothing along it
2004
- * descends into the data, which is the case that cannot end.
2005
- *
2006
- * `depthBelow` is memoised per element, so a shared subtree costs one visit
2007
- * instead of one per path into it — without that, a spec whose elements each
2008
- * name the next two is exponential to check. The memo makes the reported
2009
- * depth a lower bound once a cycle has been cut, since which edge closes the
2010
- * cycle depends on the order the walk reached it. The cycle is reported as an
2011
- * error of its own, which is the finding worth acting on.
2012
- */
2013
- function analyseSpecGraph(spec) {
2014
- const elements = spec.elements ?? {};
2015
- const cycles = new Set();
2016
- const depthBelow = new Map();
2017
- const onPath = new Set();
2018
- const walkFrom = (start) => {
2019
- const first = elements[start];
2020
- if (!first || depthBelow.has(start))
2021
- return;
2022
- const stack = [
2023
- { key: start, kids: childKeysOf(first), next: 0, deepest: 0 },
2024
- ];
2025
- onPath.add(start);
2026
- while (stack.length > 0) {
2027
- const frame = stack[stack.length - 1];
2028
- if (frame.next < frame.kids.length) {
2029
- const childKey = frame.kids[frame.next++];
2030
- const memo = depthBelow.get(childKey);
2031
- if (memo !== undefined) {
2032
- frame.deepest = Math.max(frame.deepest, memo);
2033
- continue;
2034
- }
2035
- // A child that closes the cycle is not followed — that is what keeps
2036
- // this walk finite — and not counted towards depth either.
2037
- if (onPath.has(childKey)) {
2038
- const from = stack.findIndex((frame) => frame.key === childKey);
2039
- const descends = stack
2040
- .slice(from)
2041
- .some((frame) => descendsIntoItems(elements[frame.key]));
2042
- if (!descends)
2043
- cycles.add(childKey);
2044
- continue;
2045
- }
2046
- const child = elements[childKey];
2047
- // A child the spec never defined is validateSpec's finding, not ours.
2048
- if (!child)
2049
- continue;
2050
- onPath.add(childKey);
2051
- stack.push({
2052
- key: childKey,
2053
- kids: childKeysOf(child),
2054
- next: 0,
2055
- deepest: 0,
2056
- });
2057
- continue;
2058
- }
2059
- stack.pop();
2060
- onPath.delete(frame.key);
2061
- const depth = frame.deepest + 1;
2062
- depthBelow.set(frame.key, depth);
2063
- const parent = stack[stack.length - 1];
2064
- if (parent)
2065
- parent.deepest = Math.max(parent.deepest, depth);
2066
- }
2067
- };
2068
- if (spec.root)
2069
- walkFrom(spec.root);
2070
- const depth = (spec.root ? depthBelow.get(spec.root) : undefined) ?? 0;
2071
- for (const key of Object.keys(elements))
2072
- walkFrom(key);
2073
- return { depth, cycles: [...cycles] };
2074
- }
2075
- /** Report a cycle, naming the element that is its own descendant. */
2076
- function cycleIssue(key) {
2077
- return {
2078
- severity: 'error',
2079
- code: 'cycle',
2080
- elementKey: key,
2081
- 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"}.`,
2082
- };
2083
- }
2084
- /** Report a breached element-count cap. */
2085
- function elementCountIssue(count, limit) {
2086
- return {
2087
- severity: 'error',
2088
- code: 'too_many_elements',
2089
- message: `Spec has ${count} elements, over the limit of ${limit}. Nothing is rendered: renderLimits.maxElements is a cap on the whole spec.`,
2090
- };
2091
- }
2092
- /** Report a breached depth cap. */
2093
- function depthIssue(depth, limit) {
2094
- return {
2095
- severity: 'error',
2096
- code: 'too_deep',
2097
- message: `Spec nests ${depth} levels deep, over the limit of ${limit}. Elements below level ${limit} do not render.`,
2098
- };
2099
- }
2100
- /**
2101
- * Check a spec against a catalog: the component types it names, then the
2102
- * catalog's spec schema, then each element's props against its own
2103
- * component's schema.
2104
- *
2105
- * The props need that pass of their own. Core builds one schema for the whole
2106
- * spec, and it gives an element's props a component's schema only when the
2107
- * catalog has exactly one component; with more, every element's props are an
2108
- * open record, whatever its `type`. So `catalog.validate` holds an element to
2109
- * its shape — `children`, a `type` from the catalog — and not to its props.
2110
- *
2111
- * Whatever that schema did say about the props of an element checked here is
2112
- * dropped: with a one-component catalog it would be reported twice, and it
2113
- * judges expressions this pass knows to leave alone.
2114
- *
2115
- * The schema passes are skipped while a type is unknown. That is an error on
2116
- * its own, and the spec schema would only restate it as a `type` outside the
2117
- * ones it accepts.
2118
- */
2119
- function catalogIssues(spec, catalog) {
2120
- const elements = Object.entries(spec.elements ?? {});
2121
- const known = new Set(catalog.componentNames);
2122
- const unknown = [];
2123
- for (const [key, element] of elements) {
2124
- if (known.has(element.type))
2125
- continue;
2126
- unknown.push({
2127
- severity: 'error',
2128
- code: 'unknown_component',
2129
- elementKey: key,
2130
- message: `Element "${key}" has type "${element.type}", which this catalog does not define. Nothing renders for it.`,
2131
- });
2132
- }
2133
- if (unknown.length > 0)
2134
- return unknown;
2135
- const schemas = new Map();
2136
- for (const [key, element] of elements) {
2137
- const schema = propsSchemaOf(catalog.data, element.type);
2138
- if (schema)
2139
- schemas.set(key, schema);
2307
+ /**
2308
+ * Fire-and-forget entry point for event emission: dispatching is async, but
2309
+ * a template listener has nowhere to await it, so the failure must be
2310
+ * handled here rather than escaping as an unhandled rejection.
2311
+ */
2312
+ fireEvent(eventName) {
2313
+ this.emitEvent(eventName).catch(reportActionError);
2140
2314
  }
2141
- const found = specSchemaIssues(spec, catalog).filter(({ path }) => !(path[0] === 'elements' &&
2142
- typeof path[1] === 'string' &&
2143
- schemas.has(path[1]) &&
2144
- path[2] === 'props'));
2145
- for (const [key, element] of elements) {
2146
- const room = MAX_REPORTED_PROP_ISSUES - found.length;
2147
- if (room <= 0)
2148
- break;
2149
- const schema = schemas.get(key);
2150
- if (schema)
2151
- found.push(...propsIssues(key, element, schema, room));
2315
+ async emitEvent(eventName) {
2316
+ const binding = untracked(this.rawElement)?.on?.[eventName];
2317
+ if (binding)
2318
+ await this.dispatch(binding);
2152
2319
  }
2153
- return found.slice(0, MAX_REPORTED_PROP_ISSUES).map(toSpecCheckIssue);
2154
- }
2155
- /**
2156
- * The props schema a catalog declares for a component type, if it declares
2157
- * one that can be called.
2158
- */
2159
- function propsSchemaOf(data, type) {
2160
- const components = data
2161
- ?.components;
2162
- if (typeof components !== 'object' ||
2163
- components === null ||
2164
- !Object.hasOwn(components, type)) {
2165
- return undefined;
2320
+ /**
2321
+ * Run an `on` or `watch` binding's actions in order. Params are resolved
2322
+ * per action, just before it runs, so a `$state` read in a later action of
2323
+ * the chain sees what an earlier one wrote.
2324
+ */
2325
+ async dispatch(binding) {
2326
+ for (const b of Array.isArray(binding) ? binding : [binding]) {
2327
+ if (!b.params) {
2328
+ await this.actions.execute(b);
2329
+ continue;
2330
+ }
2331
+ const liveCtx = this.liveResolutionCtx();
2332
+ const resolved = {};
2333
+ for (const [key, val] of Object.entries(b.params)) {
2334
+ resolved[key] = resolveActionParam(val, liveCtx);
2335
+ }
2336
+ await this.actions.execute({ ...b, params: resolved });
2337
+ }
2166
2338
  }
2167
- const props = components[type]?.props;
2168
- return typeof props
2169
- ?.safeParse === 'function'
2170
- ? props
2171
- : undefined;
2172
- }
2173
- /** What the catalog's spec schema reported, with paths rooted at the spec. */
2174
- function specSchemaIssues(spec, catalog) {
2175
- try {
2176
- const result = catalog.validate(spec);
2177
- return result.success ? [] : readIssues(result.error);
2339
+ eventHandle(eventName) {
2340
+ const el = this.rawElement();
2341
+ const binding = el?.on?.[eventName];
2342
+ if (!binding) {
2343
+ return { emit: () => { }, shouldPreventDefault: false, bound: false };
2344
+ }
2345
+ const actionBindings = Array.isArray(binding) ? binding : [binding];
2346
+ return {
2347
+ emit: () => this.fireEvent(eventName),
2348
+ shouldPreventDefault: actionBindings.some((b) => b.preventDefault),
2349
+ bound: true,
2350
+ };
2178
2351
  }
2179
- catch (error) {
2180
- return [{ path: [], message: threwMessage('the catalog schema', error) }];
2352
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JrElement, deps: [], target: i0.ɵɵFactoryTarget.Component });
2353
+ 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: `
2354
+ @if (!refusal() && rawElement() && visible() && component()) {
2355
+ @if (devtoolsKey(); as dk) {
2356
+ <span [attr.data-jr-key]="dk" style="display: contents">
2357
+ <ng-container *ngComponentOutlet="component(); injector: outletInjector" />
2358
+ </span>
2359
+ } @else {
2360
+ <ng-container *ngComponentOutlet="component(); injector: outletInjector" />
2361
+ }
2181
2362
  }
2363
+ `, isInline: true, styles: [":host{display:contents}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2182
2364
  }
2365
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JrElement, decorators: [{
2366
+ type: Component,
2367
+ args: [{ selector: 'jr-element', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgComponentOutlet], template: `
2368
+ @if (!refusal() && rawElement() && visible() && component()) {
2369
+ @if (devtoolsKey(); as dk) {
2370
+ <span [attr.data-jr-key]="dk" style="display: contents">
2371
+ <ng-container *ngComponentOutlet="component(); injector: outletInjector" />
2372
+ </span>
2373
+ } @else {
2374
+ <ng-container *ngComponentOutlet="component(); injector: outletInjector" />
2375
+ }
2376
+ }
2377
+ `, styles: [":host{display:contents}\n"] }]
2378
+ }], ctorParameters: () => [], propDecorators: { elementKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "elementKey", required: true }] }] } });
2379
+
2183
2380
  /**
2184
- * What an element's component schema says about its props, leaving out what
2185
- * it says about expressions, with paths rooted at the spec.
2186
- *
2187
- * A schema that throws counts as a rejection, not a crash. Real ones do:
2188
- * `ValidationConfigSchema`, which every Material input's `validation` prop
2189
- * uses, recurses through `enabled` and overflows the stack a thousand
2190
- * `$and`s deep.
2381
+ * The defaults the nearest {@link provideJsonRender} supplies, already merged
2382
+ * with any provider above it.
2191
2383
  */
2192
- function propsIssues(key, element, schema, room) {
2193
- const at = ['elements', key, 'props'];
2194
- let result;
2195
- try {
2196
- result = schema.safeParse(element.props);
2197
- }
2198
- catch (error) {
2199
- return [
2200
- {
2201
- path: at,
2202
- message: threwMessage(`the "${element.type}" schema`, error),
2203
- },
2204
- ];
2205
- }
2206
- if (result.success)
2207
- return [];
2208
- const issues = [];
2209
- for (const { path, message } of readIssues(result.error)) {
2210
- if (issues.length >= room)
2211
- break;
2212
- if (reachesExpression(element.props, path))
2213
- continue;
2214
- issues.push({ path: [...at, ...path], message });
2215
- }
2216
- return issues;
2217
- }
2384
+ const JSON_RENDER_CONFIG = new InjectionToken('JSON_RENDER_CONFIG');
2218
2385
  /**
2219
- * Whether a props issue is about an expression: it sits on one, inside one,
2220
- * or on a value holding one.
2386
+ * Set renderer defaults once instead of on every `<json-render>`:
2221
2387
  *
2222
- * `{"$state": "/title"}` where the schema wants a string is not a defect.
2223
- * Core resolves every such value before the component sees it, so the schema
2224
- * would be judging something that never renders. Any `$`-prefixed key counts
2225
- * — directive names must start with `$`, and so would an expression added to
2226
- * core later. A value merely holding one is left alone too, since a union or
2227
- * a refinement over it ran on the unresolved value.
2388
+ * ```ts
2389
+ * bootstrapApplication(App, { providers: [provideJsonRender({ registry })] });
2390
+ * ```
2228
2391
  *
2229
- * The props object itself is not an expression: core resolves the values
2230
- * inside it, never the object.
2231
- */
2232
- function reachesExpression(props, path) {
2233
- let value = props;
2234
- for (const segment of path) {
2235
- if (typeof value !== 'object' || value === null)
2236
- return false;
2237
- value = value[segment];
2238
- if (isExpression(value))
2239
- return true;
2240
- }
2241
- return holdsExpression(value);
2392
+ * ```html
2393
+ * <json-render [spec]="m.spec" />
2394
+ * ```
2395
+ *
2396
+ * Pass a function to build the defaults with `inject()`, for handlers that
2397
+ * call your services:
2398
+ *
2399
+ * ```ts
2400
+ * provideJsonRender(() => {
2401
+ * const orders = inject(OrderService);
2402
+ * return { registry, handlers: { cancelOrder: (p) => orders.cancel(p['id']) } };
2403
+ * });
2404
+ * ```
2405
+ *
2406
+ * Works in application, route and component `providers`. A provider below
2407
+ * another extends it with the same rules an element input does: its own
2408
+ * values replace, its named vocabularies merge.
2409
+ */
2410
+ function provideJsonRender(config) {
2411
+ return [
2412
+ {
2413
+ provide: JSON_RENDER_CONFIG,
2414
+ useFactory: () => {
2415
+ const outer = inject(JSON_RENDER_CONFIG, {
2416
+ optional: true,
2417
+ skipSelf: true,
2418
+ });
2419
+ const own = typeof config === 'function' ? config() : config;
2420
+ return outer ? mergeJsonRenderConfig(outer, own) : own;
2421
+ },
2422
+ },
2423
+ ];
2242
2424
  }
2243
- /** A value core resolves at render time — or will, once it knows the key. */
2244
- function isExpression(value) {
2245
- return (typeof value === 'object' &&
2246
- value !== null &&
2247
- !Array.isArray(value) &&
2248
- Object.keys(value).some((key) => key.startsWith('$')));
2425
+ /** `inner` over `outer`, by the rules {@link JsonRenderConfig} describes. */
2426
+ function mergeJsonRenderConfig(outer, inner) {
2427
+ const merged = { ...outer };
2428
+ for (const [key, value] of Object.entries(inner)) {
2429
+ if (value !== undefined && value !== null) {
2430
+ merged[key] = value;
2431
+ }
2432
+ }
2433
+ merged.handlers = mergeByName(outer.handlers, inner.handlers);
2434
+ merged.functions = mergeByName(outer.functions, inner.functions);
2435
+ merged.validationFunctions = mergeByName(outer.validationFunctions, inner.validationFunctions);
2436
+ merged.directives = mergeDirectives(outer.directives, inner.directives);
2437
+ return merged;
2249
2438
  }
2250
2439
  /**
2251
- * Whether anything nested inside `value` is an expression.
2440
+ * Two name → value maps as one, `inner` winning a name both define. Returns
2441
+ * whichever is given when only one is, so an unmerged map keeps its identity.
2252
2442
  *
2253
- * Iterative, and it remembers what it has seen, for the same reason as
2254
- * {@link analyseSpecGraph}: props are as attacker-shaped as the rest of the
2255
- * spec, and a check that overflowed or looped on them would be a second way
2256
- * to crash.
2443
+ * @internal
2257
2444
  */
2258
- function holdsExpression(value) {
2259
- const pending = [value];
2260
- const seen = new Set();
2261
- while (pending.length > 0) {
2262
- const current = pending.pop();
2263
- if (typeof current !== 'object' || current === null)
2264
- continue;
2265
- if (seen.has(current))
2266
- continue;
2267
- seen.add(current);
2268
- for (const child of Object.values(current)) {
2269
- if (isExpression(child))
2270
- return true;
2271
- pending.push(child);
2272
- }
2273
- }
2274
- return false;
2445
+ function mergeByName(outer, inner) {
2446
+ if (!outer)
2447
+ return inner ?? undefined;
2448
+ if (!inner)
2449
+ return outer;
2450
+ return { ...outer, ...inner };
2275
2451
  }
2276
2452
  /**
2277
- * Read what a schema reported.
2453
+ * Two directive lists as one, `inner` winning a name both define — the
2454
+ * directive registry keeps the last definition of a name.
2278
2455
  *
2279
- * Read defensively rather than typed against the schema library: the shape
2280
- * below (`issues`, each with `path` and `message`) is what every version of
2281
- * it has produced, and a renderer that threw while explaining a bad spec
2282
- * would be the worse failure.
2456
+ * @internal
2283
2457
  */
2284
- function readIssues(error) {
2285
- const raw = error?.issues;
2286
- if (!Array.isArray(raw))
2287
- return [];
2288
- return raw.map((entry) => {
2289
- const item = entry;
2290
- const path = item?.path;
2291
- const message = item?.message;
2292
- return {
2293
- path: Array.isArray(path) ? path : [],
2294
- message: typeof message === 'string' ? message : 'is not valid',
2295
- };
2296
- });
2297
- }
2298
- function threwMessage(schema, error) {
2299
- const reason = error instanceof Error ? ` (${error.name}: ${error.message})` : '';
2300
- return `${schema} threw instead of answering${reason}, so this counts as rejected`;
2301
- }
2302
- function toSpecCheckIssue({ path, message }) {
2303
- // Paths are rooted at the spec, so `elements.<key>.props.<prop>` names the
2304
- // element in its second segment.
2305
- const elementKey = path[0] === 'elements' && typeof path[1] === 'string' ? path[1] : undefined;
2306
- const where = path.length > 0 ? path.map(String).join('.') : 'spec';
2307
- return {
2308
- severity: 'error',
2309
- code: 'invalid_props',
2310
- ...(elementKey === undefined ? {} : { elementKey }),
2311
- message: `${where}: ${message}`,
2312
- };
2458
+ function mergeDirectives(outer, inner) {
2459
+ if (!outer)
2460
+ return inner ?? undefined;
2461
+ if (!inner)
2462
+ return outer;
2463
+ return [...outer, ...inner];
2313
2464
  }
2314
2465
 
2315
2466
  const CLEAN = {
@@ -2461,8 +2612,11 @@ function reportSpecCheck(check, mode) {
2461
2612
  class JsonRenderer {
2462
2613
  /** The UI spec to render (may be partial while streaming). */
2463
2614
  spec = input.required(...(ngDevMode ? [{ debugName: "spec" }] : /* istanbul ignore next */ []));
2464
- /** Component registry mapping catalog type names to Angular components. */
2465
- registry = input.required(...(ngDevMode ? [{ debugName: "registry" }] : /* istanbul ignore next */ []));
2615
+ /**
2616
+ * Component registry mapping catalog type names to Angular components.
2617
+ * Required unless a {@link provideJsonRender} above supplies one.
2618
+ */
2619
+ registry = input(undefined, ...(ngDevMode ? [{ debugName: "registry" }] : /* istanbul ignore next */ []));
2466
2620
  /** Whether the spec is currently loading/streaming. */
2467
2621
  loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
2468
2622
  /** Fallback component for unknown types. */
@@ -2479,7 +2633,7 @@ class JsonRenderer {
2479
2633
  * The check is skipped while `loading` is true: a spec that is still
2480
2634
  * arriving is expected to reference children that have not streamed in yet.
2481
2635
  */
2482
- validate = input('off', ...(ngDevMode ? [{ debugName: "validate" }] : /* istanbul ignore next */ []));
2636
+ validate = input(undefined, ...(ngDevMode ? [{ debugName: "validate" }] : /* istanbul ignore next */ []));
2483
2637
  /**
2484
2638
  * Caps on what the spec may cost the browser: `maxElements`, `maxDepth` and
2485
2639
  * `maxRepeatItems`. Unset by default, and each field is independently
@@ -2501,8 +2655,14 @@ class JsonRenderer {
2501
2655
  * `$bindState`, `$template`, a directive — has no value until render time,
2502
2656
  * so it is not checked.
2503
2657
  *
2504
- * Only read when `validate` is on, and — like the rest of the check —
2505
- * skipped while `loading`.
2658
+ * The check is read only when `validate` is on, and — like the rest of
2659
+ * it — skipped while `loading`.
2660
+ *
2661
+ * While `loading`, the catalog also gates mounting, whatever `validate` is:
2662
+ * an element whose props do not yet pass its component's schema shows its
2663
+ * registry entry's `fallback`, or nothing, instead of a component with half
2664
+ * its props. Once an element has mounted it stays mounted, and when
2665
+ * `loading` ends every element mounts.
2506
2666
  */
2507
2667
  catalog = input(null, ...(ngDevMode ? [{ debugName: "catalog" }] : /* istanbul ignore next */ []));
2508
2668
  /**
@@ -2530,33 +2690,52 @@ class JsonRenderer {
2530
2690
  stateStore;
2531
2691
  /** The action dispatcher of this renderer. */
2532
2692
  actions;
2693
+ /**
2694
+ * The nearest {@link provideJsonRender}'s defaults. Every option input
2695
+ * falls through to these when left `null` or `undefined`; see
2696
+ * {@link JsonRenderConfig} for the merge rules.
2697
+ */
2698
+ config = inject(JSON_RENDER_CONFIG, { optional: true }) ?? {};
2699
+ mode = computed(() => this.validate() ?? this.config.validate ?? 'off', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
2700
+ limits = computed(() => this.renderLimits() ?? this.config.renderLimits ?? null, ...(ngDevMode ? [{ debugName: "limits" }] : /* istanbul ignore next */ []));
2701
+ specCatalog = computed(() => this.catalog() ?? this.config.catalog ?? null, ...(ngDevMode ? [{ debugName: "specCatalog" }] : /* istanbul ignore next */ []));
2533
2702
  /**
2534
2703
  * The spec as checked, and what the check found. While `loading`, the mode
2535
2704
  * is forced off: the spec is still arriving, and the missing children it
2536
2705
  * refers to are the normal state of a stream, not a defect.
2537
2706
  */
2538
- checked = computed(() => checkSpec(this.spec(), this.loading() ? 'off' : this.validate(), {
2539
- limits: this.renderLimits(),
2540
- catalog: this.catalog(),
2707
+ checked = computed(() => checkSpec(this.spec(), this.loading() ? 'off' : this.mode(), {
2708
+ limits: this.limits(),
2709
+ catalog: this.specCatalog(),
2541
2710
  }), ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
2542
2711
  constructor() {
2543
2712
  const root = inject(JsonRenderRootContext);
2544
2713
  // The fixed spec, not the input: a `visible` the check moved out of
2545
2714
  // `props` has to be the one the tree renders, or the fix is cosmetic.
2546
2715
  root.spec = computed(() => this.checked().spec, ...(ngDevMode ? [{ debugName: "spec" }] : /* istanbul ignore next */ []));
2547
- root.registry = this.registry;
2716
+ const config = this.config;
2717
+ // Read only once an element needs a component, like a required input:
2718
+ // a renderer with no spec yet has no use for one.
2719
+ root.registry = computed(() => {
2720
+ const registry = this.registry() ?? config.registry;
2721
+ if (!registry) {
2722
+ throw new Error('[ngx-json-render] <json-render> has no registry. Bind [registry], or supply one for the whole app with provideJsonRender({ registry }).');
2723
+ }
2724
+ return registry;
2725
+ }, ...(ngDevMode ? [{ debugName: "registry" }] : /* istanbul ignore next */ []));
2548
2726
  root.loading = this.loading;
2549
- root.fallback = this.fallback;
2550
- root.limits = this.renderLimits;
2727
+ root.fallback = computed(() => this.fallback() ?? config.fallback ?? null, ...(ngDevMode ? [{ debugName: "fallback" }] : /* istanbul ignore next */ []));
2728
+ root.limits = this.limits;
2729
+ root.catalog = this.specCatalog;
2551
2730
  root.store = this.store;
2552
2731
  root.initialState = computed(() => this.state() ?? this.spec()?.state ?? {}, ...(ngDevMode ? [{ debugName: "initialState" }] : /* istanbul ignore next */ []));
2553
- root.handlers = this.handlers;
2554
- root.onAction = this.onAction;
2555
- root.navigate = this.navigate;
2556
- root.validationFunctions = this.validationFunctions;
2557
- root.functions = this.functions;
2732
+ root.handlers = computed(() => mergeByName(config.handlers, this.handlers()), ...(ngDevMode ? [{ debugName: "handlers" }] : /* istanbul ignore next */ []));
2733
+ root.onAction = computed(() => this.onAction() ?? config.onAction ?? null, ...(ngDevMode ? [{ debugName: "onAction" }] : /* istanbul ignore next */ []));
2734
+ root.navigate = computed(() => this.navigate() ?? config.navigate ?? null, ...(ngDevMode ? [{ debugName: "navigate" }] : /* istanbul ignore next */ []));
2735
+ root.validationFunctions = computed(() => mergeByName(config.validationFunctions, this.validationFunctions()), ...(ngDevMode ? [{ debugName: "validationFunctions" }] : /* istanbul ignore next */ []));
2736
+ root.functions = computed(() => mergeByName(config.functions, this.functions()), ...(ngDevMode ? [{ debugName: "functions" }] : /* istanbul ignore next */ []));
2558
2737
  root.directiveRegistry = computed(() => {
2559
- const definitions = this.directives();
2738
+ const definitions = mergeDirectives(config.directives, this.directives());
2560
2739
  return definitions ? createDirectiveRegistry(definitions) : undefined;
2561
2740
  }, ...(ngDevMode ? [{ debugName: "directiveRegistry" }] : /* istanbul ignore next */ []));
2562
2741
  root.emitStateChange = (changes) => this.stateChange.emit(changes);
@@ -2572,7 +2751,25 @@ class JsonRenderer {
2572
2751
  effect(() => {
2573
2752
  if (this.loading())
2574
2753
  return;
2575
- reportSpecCheck(this.checked(), this.validate());
2754
+ reportSpecCheck(this.checked(), this.mode());
2755
+ });
2756
+ // With validation off, a `root` that names no element renders nothing and
2757
+ // nothing says why — the first mistake a hand-written spec tends to have.
2758
+ // Skipped while loading (a stream sends `/root` before its element) and
2759
+ // under `warn`/`strict`, where the check already reports `root_not_found`.
2760
+ // Once per root key, not once per change detection run.
2761
+ const warnedRoots = new Set();
2762
+ effect(() => {
2763
+ if (this.loading() || this.mode() !== 'off')
2764
+ return;
2765
+ const spec = this.checked().spec;
2766
+ if (!spec?.root)
2767
+ return;
2768
+ const root = spec.root;
2769
+ if (spec.elements?.[root] || warnedRoots.has(root))
2770
+ return;
2771
+ warnedRoots.add(root);
2772
+ console.warn(`[ngx-json-render] root "${root}" is not in elements; nothing is rendered.`);
2576
2773
  });
2577
2774
  }
2578
2775
  rootKey = computed(() => {
@@ -2585,7 +2782,7 @@ class JsonRenderer {
2585
2782
  // A spec whose errors survived the fixes does not render under `strict`.
2586
2783
  // Reporting it and drawing half of it anyway is the behaviour the mode
2587
2784
  // exists to refuse.
2588
- if (this.validate() === 'strict' && checked.hasErrors)
2785
+ if (this.mode() === 'strict' && checked.hasErrors)
2589
2786
  return null;
2590
2787
  const spec = checked.spec;
2591
2788
  return spec?.root && spec.elements?.[spec.root] ? spec.root : null;
@@ -2624,7 +2821,7 @@ class JsonRenderer {
2624
2821
  });
2625
2822
  }, ...(ngDevMode ? [{ debugName: "confirmInjector" }] : /* istanbul ignore next */ []));
2626
2823
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JsonRenderer, deps: [], target: i0.ɵɵFactoryTarget.Component });
2627
- 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: [
2824
+ 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: false, 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: [
2628
2825
  JsonRenderRootContext,
2629
2826
  JsonRenderStateService,
2630
2827
  JsonRenderValidationService,
@@ -2673,7 +2870,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2673
2870
  }
2674
2871
  }
2675
2872
  `, styles: [":host{display:contents}\n"] }]
2676
- }], 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"] }] } });
2873
+ }], ctorParameters: () => [], propDecorators: { spec: [{ type: i0.Input, args: [{ isSignal: true, alias: "spec", required: true }] }], registry: [{ type: i0.Input, args: [{ isSignal: true, alias: "registry", required: false }] }], 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"] }] } });
2677
2874
 
2678
2875
  /**
2679
2876
  * Provides the repeat scope (item, index, absolute base path) to the elements
@@ -2878,8 +3075,13 @@ function defineRegistry(catalog, options) {
2878
3075
  // Build the component registry, attaching slot metadata from the catalog.
2879
3076
  const registry = {};
2880
3077
  if (options.components) {
2881
- for (const [name, component] of Object.entries(options.components)) {
2882
- const entry = { component };
3078
+ for (const [name, value] of Object.entries(options.components)) {
3079
+ const entry = typeof value === 'function'
3080
+ ? { component: value }
3081
+ : {
3082
+ component: value.component,
3083
+ ...(value.fallback ? { fallback: value.fallback } : {}),
3084
+ };
2883
3085
  const slots = catalogComponents?.[name]?.slots;
2884
3086
  if (slots)
2885
3087
  entry.slots = slots;
@@ -3809,5 +4011,5 @@ function injectChatUI(options) {
3809
4011
  * Generated bundle index. Do not edit.
3810
4012
  */
3811
4013
 
3812
- 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, ɵregisterStreamObserver };
4014
+ export { CONFIRM_CONTEXT, ELEMENT_KEY, JR_CONFIRM_DIALOG, JR_CONFIRM_LABELS, JSON_RENDER_CONFIG, 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, provideJsonRender, ɵregisterStreamObserver };
3813
4015
  //# sourceMappingURL=ngx-json-render.mjs.map