@emeryld/rrroutes-contract 2.1.11 → 2.1.13

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,4 +1,4 @@
1
- import { z, ZodTypeAny } from 'zod';
1
+ import { z, ZodType } from 'zod';
2
2
  /** Supported HTTP verbs for the routes DSL. */
3
3
  export type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';
4
4
  /** Declarative description of a multipart upload field. */
@@ -16,13 +16,13 @@ export type NodeCfg = {
16
16
  /** Per-method configuration merged with inherited node config. */
17
17
  export type MethodCfg = {
18
18
  /** Zod schema describing the request body. */
19
- bodySchema?: ZodTypeAny;
19
+ bodySchema?: ZodType;
20
20
  /** Zod schema describing the query string. */
21
- querySchema?: ZodTypeAny;
21
+ querySchema?: ZodType;
22
22
  /** Zod schema describing path params (overrides inferred params). */
23
- paramsSchema?: ZodTypeAny;
23
+ paramsSchema?: ZodType;
24
24
  /** Zod schema describing the response payload. */
25
- outputSchema?: ZodTypeAny;
25
+ outputSchema?: ZodType;
26
26
  /** Multipart upload definitions for the route. */
27
27
  bodyFiles?: FileField[];
28
28
  /** Marks the route as an infinite feed (enables cursor helpers). */
@@ -102,13 +102,13 @@ export declare function buildCacheKey<L extends AnyLeaf>(args: {
102
102
  query?: InferQuery<L>;
103
103
  }): readonly [HttpMethod, string, {}];
104
104
  /** Infer params either from the explicit params schema or from the path literal. */
105
- export type InferParams<L extends AnyLeaf> = L['cfg']['paramsSchema'] extends ZodTypeAny ? z.infer<L['cfg']['paramsSchema']> : ExtractParamsFromPath<L['path']>;
105
+ export type InferParams<L extends AnyLeaf> = L['cfg']['paramsSchema'] extends ZodType ? z.infer<L['cfg']['paramsSchema']> : ExtractParamsFromPath<L['path']>;
106
106
  /** Infer query shape from a Zod schema when present. */
107
- export type InferQuery<L extends AnyLeaf> = L['cfg']['querySchema'] extends ZodTypeAny ? z.infer<L['cfg']['querySchema']> : never;
107
+ export type InferQuery<L extends AnyLeaf> = L['cfg']['querySchema'] extends ZodType ? z.infer<L['cfg']['querySchema']> : never;
108
108
  /** Infer request body shape from a Zod schema when present. */
109
- export type InferBody<L extends AnyLeaf> = L['cfg']['bodySchema'] extends ZodTypeAny ? z.infer<L['cfg']['bodySchema']> : never;
109
+ export type InferBody<L extends AnyLeaf> = L['cfg']['bodySchema'] extends ZodType ? z.infer<L['cfg']['bodySchema']> : never;
110
110
  /** Infer handler output shape from a Zod schema. Defaults to unknown. */
111
- export type InferOutput<L extends AnyLeaf> = L['cfg']['outputSchema'] extends ZodTypeAny ? z.infer<L['cfg']['outputSchema']> : unknown;
111
+ export type InferOutput<L extends AnyLeaf> = L['cfg']['outputSchema'] extends ZodType ? z.infer<L['cfg']['outputSchema']> : unknown;
112
112
  /** Render a type as if it were a simple object literal. */
113
113
  export type Prettify<T> = {
114
114
  [K in keyof T]: T[K];
@@ -1,2 +1,6 @@
1
1
  import { AnyLeaf } from "../core/routesV3.core";
2
- export declare function renderLeafDocsHTML(leaves: AnyLeaf[]): string;
2
+ interface RenderOptions {
3
+ cspNonce?: string;
4
+ }
5
+ export declare function renderLeafDocsHTML(leaves: AnyLeaf[], options?: RenderOptions): string;
6
+ export {};
@@ -0,0 +1,15 @@
1
+ import * as z from "zod";
2
+ export type SerializableSchemaNode = {
3
+ kind: string;
4
+ optional?: boolean;
5
+ nullable?: boolean;
6
+ description?: string;
7
+ properties?: Record<string, SerializableSchemaNode>;
8
+ element?: SerializableSchemaNode;
9
+ union?: SerializableSchemaNode[];
10
+ literal?: unknown;
11
+ enumValues?: string[];
12
+ };
13
+ type ZodAny = z.ZodTypeAny;
14
+ export declare function introspectSchema(schema: ZodAny | undefined): SerializableSchemaNode | undefined;
15
+ export {};
@@ -1,9 +1,14 @@
1
1
  import { AnyLeaf, MethodCfg } from "../core/routesV3.core";
2
- type SerializableMethodCfg = Pick<MethodCfg, 'description' | 'summary' | 'docsGroup' | 'tags' | 'deprecated' | 'stability' | 'feed' | 'docsMeta'> & {
2
+ import { SerializableSchemaNode } from "./schemaIntrospection";
3
+ type SerializableMethodCfg = Pick<MethodCfg, "description" | "summary" | "docsGroup" | "tags" | "deprecated" | "stability" | "feed" | "docsMeta"> & {
3
4
  hasBody: boolean;
4
5
  hasQuery: boolean;
5
6
  hasParams: boolean;
6
7
  hasOutput: boolean;
8
+ bodySchema?: SerializableSchemaNode;
9
+ querySchema?: SerializableSchemaNode;
10
+ paramsSchema?: SerializableSchemaNode;
11
+ outputSchema?: SerializableSchemaNode;
7
12
  };
8
13
  export type SerializableLeaf = {
9
14
  method: string;
package/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -299,6 +309,130 @@ function defineSocketEvents(config, events) {
299
309
  return { config, events };
300
310
  }
301
311
 
312
+ // src/docs/schemaIntrospection.ts
313
+ var z3 = __toESM(require("zod"), 1);
314
+ function getDef(schema) {
315
+ if (!schema || typeof schema !== "object") return void 0;
316
+ const anySchema = schema;
317
+ return anySchema._zod?.def ?? anySchema._def;
318
+ }
319
+ function getDescription(schema) {
320
+ const anyZ = z3;
321
+ const registry = anyZ.globalRegistry?.get ? anyZ.globalRegistry.get(schema) : void 0;
322
+ if (registry && typeof registry.description === "string") {
323
+ return registry.description;
324
+ }
325
+ const def = getDef(schema);
326
+ if (def && typeof def.description === "string") {
327
+ return def.description;
328
+ }
329
+ return void 0;
330
+ }
331
+ function unwrap(schema) {
332
+ let s = schema;
333
+ let optional = false;
334
+ let nullable = false;
335
+ const ZodEffectsCtor = z3.ZodEffects;
336
+ while (true) {
337
+ if (ZodEffectsCtor && s instanceof ZodEffectsCtor) {
338
+ const def = getDef(s) || {};
339
+ const sourceType = typeof s.sourceType === "function" ? s.sourceType() : def.schema;
340
+ if (!sourceType) break;
341
+ s = sourceType;
342
+ continue;
343
+ }
344
+ if (s instanceof z3.ZodOptional) {
345
+ optional = true;
346
+ const def = getDef(s);
347
+ s = def && def.innerType || s;
348
+ continue;
349
+ }
350
+ if (s instanceof z3.ZodNullable) {
351
+ nullable = true;
352
+ const def = getDef(s);
353
+ s = def && def.innerType || s;
354
+ continue;
355
+ }
356
+ if (s instanceof z3.ZodDefault) {
357
+ const def = getDef(s);
358
+ s = def && def.innerType || s;
359
+ continue;
360
+ }
361
+ break;
362
+ }
363
+ return { base: s, optional, nullable };
364
+ }
365
+ function introspectSchema(schema) {
366
+ if (!schema) return void 0;
367
+ const { base, optional, nullable } = unwrap(schema);
368
+ const def = getDef(base);
369
+ const node = {
370
+ kind: inferKind(base),
371
+ optional: optional || void 0,
372
+ nullable: nullable || void 0,
373
+ description: getDescription(base)
374
+ };
375
+ if (base instanceof z3.ZodObject) {
376
+ const rawShape = base.shape ?? (def && typeof def.shape === "function" ? def.shape() : def?.shape);
377
+ const shape = typeof rawShape === "function" ? rawShape() : rawShape ?? {};
378
+ const props = {};
379
+ for (const key of Object.keys(shape)) {
380
+ const child = shape[key];
381
+ const childNode = introspectSchema(child);
382
+ if (childNode) props[key] = childNode;
383
+ }
384
+ node.properties = props;
385
+ }
386
+ if (base instanceof z3.ZodArray) {
387
+ const inner = def && def.element || def && def.type || void 0;
388
+ if (inner) {
389
+ node.element = introspectSchema(inner);
390
+ }
391
+ }
392
+ if (base instanceof z3.ZodUnion) {
393
+ const options = def && def.options || [];
394
+ node.union = options.map((opt) => introspectSchema(opt)).filter(Boolean);
395
+ }
396
+ if (base instanceof z3.ZodLiteral) {
397
+ if (def) {
398
+ if (Array.isArray(def.values)) {
399
+ node.literal = def.values.length === 1 ? def.values[0] : def.values.slice();
400
+ } else {
401
+ node.literal = def.value;
402
+ }
403
+ }
404
+ }
405
+ if (base instanceof z3.ZodEnum) {
406
+ if (def) {
407
+ if (Array.isArray(def.values)) {
408
+ node.enumValues = def.values.slice();
409
+ } else if (def.entries && typeof def.entries === "object") {
410
+ node.enumValues = Object.values(def.entries).map(
411
+ (v) => String(v)
412
+ );
413
+ }
414
+ }
415
+ }
416
+ return node;
417
+ }
418
+ function inferKind(schema) {
419
+ if (schema instanceof z3.ZodString) return "string";
420
+ if (schema instanceof z3.ZodNumber) return "number";
421
+ if (schema instanceof z3.ZodBoolean) return "boolean";
422
+ if (schema instanceof z3.ZodBigInt) return "bigint";
423
+ if (schema instanceof z3.ZodDate) return "date";
424
+ if (schema instanceof z3.ZodArray) return "array";
425
+ if (schema instanceof z3.ZodObject) return "object";
426
+ if (schema instanceof z3.ZodUnion) return "union";
427
+ if (schema instanceof z3.ZodLiteral) return "literal";
428
+ if (schema instanceof z3.ZodEnum) return "enum";
429
+ if (schema instanceof z3.ZodRecord) return "record";
430
+ if (schema instanceof z3.ZodTuple) return "tuple";
431
+ if (schema instanceof z3.ZodUnknown) return "unknown";
432
+ if (schema instanceof z3.ZodAny) return "any";
433
+ return "unknown";
434
+ }
435
+
302
436
  // src/docs/serializer.ts
303
437
  function serializeLeaf(leaf) {
304
438
  const cfg = leaf.cfg;
@@ -319,22 +453,17 @@ function serializeLeaf(leaf) {
319
453
  hasBody: !!cfg.bodySchema || !!cfg.bodyFiles?.length,
320
454
  hasQuery: !!cfg.querySchema,
321
455
  hasParams: !!cfg.paramsSchema,
322
- hasOutput: !!cfg.outputSchema
456
+ hasOutput: !!cfg.outputSchema,
457
+ bodySchema: introspectSchema(cfg.bodySchema),
458
+ querySchema: introspectSchema(cfg.querySchema),
459
+ paramsSchema: introspectSchema(cfg.paramsSchema),
460
+ outputSchema: introspectSchema(cfg.outputSchema)
323
461
  }
324
462
  };
325
463
  }
326
464
 
327
465
  // src/docs/docs.ts
328
- function renderLeafDocsHTML(leaves) {
329
- const docsLeaves = leaves.map(serializeLeaf);
330
- const leafJson = JSON.stringify(docsLeaves).replace(/</g, "\\u003c");
331
- return `<!DOCTYPE html>
332
- <html lang="en">
333
- <head>
334
- <meta charset="utf-8" />
335
- <title>API Routes</title>
336
- <meta name="viewport" content="width=device-width, initial-scale=1" />
337
- <style>
466
+ var styles = `
338
467
  :root {
339
468
  --bg: #020617;
340
469
  --bg-elevated: #020617;
@@ -363,6 +492,7 @@ function renderLeafDocsHTML(leaves) {
363
492
  -webkit-font-smoothing: antialiased;
364
493
  }
365
494
 
495
+
366
496
  .page {
367
497
  min-height: 100vh;
368
498
  padding: 24px;
@@ -727,12 +857,121 @@ function renderLeafDocsHTML(leaves) {
727
857
  border: 1px dashed rgba(148, 163, 184, 0.5);
728
858
  background: rgba(15, 23, 42, 0.9);
729
859
  }
860
+ .schema-section {
861
+ margin-top: 8px;
862
+ font-size: 11px;
863
+ border-top: 1px dashed rgba(148, 163, 184, 0.5);
864
+ padding-top: 6px;
865
+ }
866
+
867
+ .schema-details {
868
+ margin-top: 6px;
869
+ border-radius: 10px;
870
+ border: 1px solid rgba(148, 163, 184, 0.4);
871
+ background: rgba(15, 23, 42, 0.95);
872
+ padding: 8px 10px;
873
+ }
874
+
875
+ .schema-details summary {
876
+ cursor: pointer;
877
+ list-style: none;
878
+ font-size: 11px;
879
+ color: var(--text-muted);
880
+ }
881
+
882
+ .schema-details summary::-webkit-details-marker {
883
+ display: none;
884
+ }
885
+
886
+ .schema-details summary::before {
887
+ content: "\u25B8";
888
+ display: inline-block;
889
+ margin-right: 6px;
890
+ font-size: 9px;
891
+ }
892
+
893
+ .schema-details[open] summary::before {
894
+ content: "\u25BE";
895
+ }
896
+
897
+ .schema-table {
898
+ width: 100%;
899
+ border-collapse: collapse;
900
+ margin-top: 6px;
901
+ font-size: 11px;
902
+ }
903
+
904
+ .schema-table th,
905
+ .schema-table td {
906
+ padding: 3px 4px;
907
+ border-bottom: 1px solid rgba(30, 64, 175, 0.35);
908
+ text-align: left;
909
+ }
910
+
911
+ .schema-table th {
912
+ font-weight: 500;
913
+ color: var(--text-muted);
914
+ }
915
+
916
+ .schema-field-name {
917
+ font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
918
+ Consolas, "Liberation Mono", "Courier New", monospace;
919
+ }
920
+
921
+ .schema-type {
922
+ color: var(--accent);
923
+ font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
924
+ Consolas, "Liberation Mono", "Courier New", monospace;
925
+ }
926
+
927
+ .schema-required {
928
+ font-size: 10px;
929
+ text-transform: uppercase;
930
+ letter-spacing: 0.12em;
931
+ padding: 1px 5px;
932
+ border-radius: 999px;
933
+ border: 1px solid rgba(52, 211, 153, 0.7);
934
+ color: #bbf7d0;
935
+ background: rgba(22, 163, 74, 0.1);
936
+ }
937
+
938
+ .schema-optional {
939
+ font-size: 10px;
940
+ text-transform: uppercase;
941
+ letter-spacing: 0.12em;
942
+ padding: 1px 5px;
943
+ border-radius: 999px;
944
+ border: 1px solid rgba(148, 163, 184, 0.6);
945
+ color: var(--text-muted);
946
+ background: rgba(15, 23, 42, 0.95);
947
+ }
948
+
949
+ .schema-meta {
950
+ margin-top: 6px;
951
+ font-size: 10px;
952
+ color: var(--text-muted);
953
+ white-space: pre-wrap;
954
+ word-break: break-word;
955
+ }
956
+ `;
957
+ function renderLeafDocsHTML(leaves, options = {}) {
958
+ const docsLeaves = leaves.map(serializeLeaf);
959
+ const leafJson = JSON.stringify(docsLeaves).replace(/</g, "\\u003c");
960
+ const nonceAttr = options.cspNonce ? ` nonce="${options.cspNonce}"` : "";
961
+ return `<!DOCTYPE html>
962
+ <html lang="en">
963
+ <head>
964
+ <meta charset="utf-8" />
965
+ <title>API Routes</title>
966
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
967
+ <style${nonceAttr}>
968
+ ${styles}
730
969
  </style>
731
970
  </head>
732
971
  <body>
733
972
  <div class="page">
734
973
  <div class="shell">
735
- <header class="header">
974
+ <header class="header">
736
975
  <div class="header-main">
737
976
  <div class="title-row">
738
977
  <div class="title">
@@ -746,8 +985,7 @@ function renderLeafDocsHTML(leaves) {
746
985
  </div>
747
986
  <div id="stats" class="stats"></div>
748
987
  </header>
749
-
750
- <div class="layout">
988
+ <div class="layout">
751
989
  <aside class="sidebar">
752
990
  <h2>Filters</h2>
753
991
 
@@ -784,9 +1022,102 @@ function renderLeafDocsHTML(leaves) {
784
1022
  </div>
785
1023
  </div>
786
1024
 
787
- <script id="leaf-data" type="application/json">${leafJson}</script>
788
- <script>
1025
+ <script id="leaf-data" type="application/json"${nonceAttr}>${leafJson}</script>
1026
+ <script${nonceAttr}>
789
1027
  (function () {
1028
+
1029
+ function typeLabel(node) {
1030
+ if (!node) return "unknown";
1031
+ var base = node.kind || "unknown";
1032
+ if (node.literal !== undefined) {
1033
+ return JSON.stringify(node.literal);
1034
+ }
1035
+ if (node.enumValues && node.enumValues.length) {
1036
+ return "enum[" + node.enumValues.join(" | ") + "]";
1037
+ }
1038
+ if (node.kind === "array" && node.element) {
1039
+ return "Array<" + typeLabel(node.element) + ">";
1040
+ }
1041
+ if (node.kind === "union" && node.union && node.union.length) {
1042
+ return node.union.map(typeLabel).join(" | ");
1043
+ }
1044
+ return base;
1045
+ }
1046
+
1047
+ function renderObjectRows(node, prefix) {
1048
+ if (!node || node.kind !== "object" || !node.properties) return "";
1049
+ var rows = "";
1050
+ Object.keys(node.properties).forEach(function (key) {
1051
+ var child = node.properties[key];
1052
+ var fullName = prefix ? prefix + "." + key : key;
1053
+ var required = !child.optional;
1054
+ rows +=
1055
+ "<tr>" +
1056
+ '<td class="schema-field-name">' +
1057
+ escapeHtml(fullName) +
1058
+ "</td>" +
1059
+ '<td class="schema-type">' +
1060
+ escapeHtml(typeLabel(child)) +
1061
+ "</td>" +
1062
+ "<td>" +
1063
+ (required
1064
+ ? '<span class="schema-required">required</span>'
1065
+ : '<span class="schema-optional">optional</span>') +
1066
+ "</td>" +
1067
+ "<td>" +
1068
+ (child.description
1069
+ ? escapeHtml(String(child.description))
1070
+ : "") +
1071
+ "</td>" +
1072
+ "</tr>";
1073
+
1074
+ // Recursively show nested objects, arrays-of-objects, etc.
1075
+ if (child.kind === "object") {
1076
+ rows += renderObjectRows(child, fullName);
1077
+ } else if (child.kind === "array" && child.element && child.element.kind === "object") {
1078
+ rows += renderObjectRows(
1079
+ child.element,
1080
+ fullName + "[]."
1081
+ );
1082
+ }
1083
+ });
1084
+ return rows;
1085
+ }
1086
+
1087
+ function renderSchemaSection(label, node) {
1088
+ if (!node) return "";
1089
+ // For non-object schemas, just show a one-line type.
1090
+ var isObject = node.kind === "object" && node.properties;
1091
+ var content = "";
1092
+
1093
+ if (isObject) {
1094
+ var rows = renderObjectRows(node, "");
1095
+ content =
1096
+ '<table class="schema-table">' +
1097
+ "<thead><tr>" +
1098
+ "<th>Field</th><th>Type</th><th></th><th>Description</th>" +
1099
+ "</tr></thead>" +
1100
+ "<tbody>" +
1101
+ rows +
1102
+ "</tbody></table>";
1103
+ } else {
1104
+ content =
1105
+ '<div class="schema-meta">' +
1106
+ "<strong>Type:</strong> " +
1107
+ escapeHtml(typeLabel(node)) +
1108
+ "</div>";
1109
+ }
1110
+
1111
+ return (
1112
+ '<div class="schema-section">' +
1113
+ "<div><strong>" +
1114
+ escapeHtml(label) +
1115
+ "</strong></div>" +
1116
+ content +
1117
+ "</div>"
1118
+ );
1119
+ }
1120
+
790
1121
  function escapeHtml(str) {
791
1122
  return String(str)
792
1123
  .replace(/&/g, '&amp;')
@@ -882,40 +1213,40 @@ function renderLeafDocsHTML(leaves) {
882
1213
  }
883
1214
 
884
1215
  function routeToHTML(route) {
885
- var method = String(route.method || '').toUpperCase();
886
- var methodClass = 'method-' + String(route.method || '').toLowerCase();
1216
+ var method = String(route.method || "").toUpperCase();
1217
+ var methodClass = "method-" + String(route.method || "").toLowerCase();
887
1218
 
888
1219
  var cfg = route.cfg || {};
889
- var group = cfg.docsGroup || 'Ungrouped';
1220
+ var group = cfg.docsGroup || "Ungrouped";
890
1221
 
891
- var summary = cfg.summary || cfg.description || '';
1222
+ var summary = cfg.summary || cfg.description || "";
892
1223
  var description =
893
1224
  cfg.description && cfg.summary !== cfg.description
894
1225
  ? cfg.description
895
- : '';
1226
+ : "";
896
1227
 
897
1228
  var tags = Array.isArray(cfg.tags) ? cfg.tags : [];
898
1229
  var tagHtml = tags
899
1230
  .map(function (tag) {
900
1231
  var t = String(tag);
901
- var extraClass = t === 'not-implemented' ? ' tag-not-impl' : '';
902
- return '<span class="tag' + extraClass + '">' + escapeHtml(t) + '</span>';
1232
+ var extraClass = t === "not-implemented" ? " tag-not-impl" : "";
1233
+ return '<span class="tag' + extraClass + '">' + escapeHtml(t) + "</span>";
903
1234
  })
904
- .join('');
1235
+ .join("");
905
1236
 
906
- var metaBadges = '';
1237
+ var metaBadges = "";
907
1238
  if (cfg.feed) {
908
1239
  metaBadges += '<span class="badge badge-feed">Feed</span>';
909
1240
  }
910
- if (cfg.deprecated || cfg.stability === 'deprecated') {
1241
+ if (cfg.deprecated || cfg.stability === "deprecated") {
911
1242
  metaBadges += '<span class="badge badge-deprecated">Deprecated</span>';
912
- } else if (cfg.stability && cfg.stability !== 'stable') {
1243
+ } else if (cfg.stability && cfg.stability !== "stable") {
913
1244
  metaBadges +=
914
1245
  '<span class="badge badge-' +
915
1246
  escapeHtml(cfg.stability) +
916
1247
  '">' +
917
1248
  escapeHtml(String(cfg.stability)) +
918
- '</span>';
1249
+ "</span>";
919
1250
  }
920
1251
 
921
1252
  var schemaPills = [];
@@ -926,8 +1257,48 @@ function renderLeafDocsHTML(leaves) {
926
1257
 
927
1258
  var schemaHtml =
928
1259
  schemaPills.length > 0
929
- ? '<div class="schema-row">' + schemaPills.join('') + '</div>'
930
- : '';
1260
+ ? '<div class="schema-row">' + schemaPills.join("") + "</div>"
1261
+ : "";
1262
+
1263
+ // NEW: full expandable details including field-level schemas + docsMeta
1264
+ var hasDetailedSchemas =
1265
+ cfg.bodySchema || cfg.querySchema || cfg.paramsSchema || cfg.outputSchema;
1266
+ var detailsInner = "";
1267
+
1268
+ if (cfg.bodySchema) {
1269
+ detailsInner += renderSchemaSection("Request body", cfg.bodySchema);
1270
+ }
1271
+ if (cfg.querySchema) {
1272
+ detailsInner += renderSchemaSection("Query params", cfg.querySchema);
1273
+ }
1274
+ if (cfg.paramsSchema) {
1275
+ detailsInner += renderSchemaSection("Path params", cfg.paramsSchema);
1276
+ }
1277
+ if (cfg.outputSchema) {
1278
+ detailsInner += renderSchemaSection("Response", cfg.outputSchema);
1279
+ }
1280
+
1281
+ if (cfg.docsMeta) {
1282
+ try {
1283
+ var metaJson = JSON.stringify(cfg.docsMeta, null, 2);
1284
+ detailsInner +=
1285
+ '<div class="schema-section">' +
1286
+ "<div><strong>Metadata</strong></div>" +
1287
+ '<div class="schema-meta">' +
1288
+ escapeHtml(metaJson) +
1289
+ "</div>" +
1290
+ "</div>";
1291
+ } catch (e) {}
1292
+ }
1293
+
1294
+ var detailsHtml = "";
1295
+ if (hasDetailedSchemas || cfg.docsMeta) {
1296
+ detailsHtml =
1297
+ '<details class="schema-details">' +
1298
+ "<summary>Inspect schemas & metadata</summary>" +
1299
+ detailsInner +
1300
+ "</details>";
1301
+ }
931
1302
 
932
1303
  return (
933
1304
  '<article class="route-card">' +
@@ -936,31 +1307,33 @@ function renderLeafDocsHTML(leaves) {
936
1307
  methodClass +
937
1308
  '">' +
938
1309
  method +
939
- '</span>' +
1310
+ "</span>" +
940
1311
  '<code class="route-path">' +
941
- escapeHtml(route.path || '') +
942
- '</code>' +
1312
+ escapeHtml(route.path || "") +
1313
+ "</code>" +
943
1314
  (group
944
- ? '<span class="group-label">' + escapeHtml(String(group)) + '</span>'
945
- : '') +
946
- '</header>' +
1315
+ ? '<span class="group-label">' + escapeHtml(String(group)) + "</span>"
1316
+ : "") +
1317
+ "</header>" +
947
1318
  '<div class="route-tags">' +
948
1319
  tagHtml +
949
1320
  metaBadges +
950
- '</div>' +
1321
+ "</div>" +
951
1322
  (summary
952
- ? '<p class="route-summary">' + escapeHtml(String(summary)) + '</p>'
953
- : '') +
1323
+ ? '<p class="route-summary">' + escapeHtml(String(summary)) + "</p>"
1324
+ : "") +
954
1325
  (description
955
1326
  ? '<p class="route-description">' +
956
1327
  escapeHtml(String(description)) +
957
- '</p>'
958
- : '') +
1328
+ "</p>"
1329
+ : "") +
959
1330
  schemaHtml +
960
- '</article>'
1331
+ (detailsHtml ? detailsHtml : "") +
1332
+ "</article>"
961
1333
  );
962
1334
  }
963
1335
 
1336
+
964
1337
  function applyFilters() {
965
1338
  var query = searchInput.value.toLowerCase().trim();
966
1339