@emeryld/rrroutes-contract 2.7.1 → 2.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -365,6 +365,8 @@ import { exportFinalizedLeaves } from '@emeryld/rrroutes-contract'
365
365
 
366
366
  const payload = await exportFinalizedLeaves(registry, {
367
367
  outFile: './finalized-leaves.export.json',
368
+ htmlFile: './finalized-leaves-viewer.baked.html',
369
+ openOnFinish: true,
368
370
  })
369
371
  ```
370
372
 
@@ -373,3 +375,93 @@ const payload = await exportFinalizedLeaves(registry, {
373
375
  - `_meta`: export/documentation metadata
374
376
  - `leaves`: contract-native serialized leaves
375
377
  - `schemaFlatByLeaf`: flattened schema map per leaf
378
+
379
+ `htmlFile` writes a self-contained viewer HTML with the export payload baked in (no file picker needed).
380
+ `viewerTemplateFile` optionally points to a custom viewer HTML template instead of the default bundled viewer.
381
+ `openOnFinish` opens the generated `htmlFile` in your default browser after write completes.
382
+
383
+ ### Custom `viewerTemplateFile`
384
+
385
+ Use `viewerTemplateFile` when you want your own branded/layout HTML while still baking export data directly into the page.
386
+
387
+ Behavior:
388
+
389
+ - If omitted, RRRoutes uses the bundled `finalized-leaves-viewer.html` template.
390
+ - Resolution order: package-bundled viewer, then local repo paths (`tools/...`, `packages/contract/tools/...`), then built-in string fallback.
391
+ - If provided, RRRoutes reads your template and injects a script like:
392
+ - `window.__FINALIZED_LEAVES_PAYLOAD = {...}`
393
+ - If your template contains this marker comment, payload is injected exactly there:
394
+ - `<!--__FINALIZED_LEAVES_BAKED_PAYLOAD__-->`
395
+ - If marker is missing, payload script is inserted before `</body>` (or prepended if no `</body>` exists).
396
+
397
+ Minimal custom template example:
398
+
399
+ ```html
400
+ <!doctype html>
401
+ <html>
402
+ <head>
403
+ <meta charset="UTF-8" />
404
+ <title>My Leaves Viewer</title>
405
+ </head>
406
+ <body>
407
+ <h1>My API Routes</h1>
408
+ <div id="app"></div>
409
+
410
+ <!--__FINALIZED_LEAVES_BAKED_PAYLOAD__-->
411
+
412
+ <script>
413
+ const payload = window.__FINALIZED_LEAVES_PAYLOAD
414
+ document.getElementById('app').textContent = payload
415
+ ? `Loaded ${payload.leaves.length} leaves`
416
+ : 'No baked payload found'
417
+ </script>
418
+ </body>
419
+ </html>
420
+ ```
421
+
422
+ Runtime usage with custom template:
423
+
424
+ ```ts
425
+ await exportFinalizedLeaves(registry, {
426
+ htmlFile: './dist/leaves-viewer.html',
427
+ viewerTemplateFile: './tools/my-viewer-template.html',
428
+ openOnFinish: true,
429
+ })
430
+ ```
431
+
432
+ ### Viewer HTML (searchable UI)
433
+
434
+ A simple local viewer is included at:
435
+
436
+ - `packages/contract/tools/finalized-leaves-viewer.html`
437
+
438
+ How to use:
439
+
440
+ 1. Generate an export JSON with `export:finalized-leaves`.
441
+ 2. Open the HTML file in your browser.
442
+ 3. Load the JSON file using the file picker.
443
+ 4. Use the single search textbox + field checkboxes to filter routes.
444
+
445
+ Each result is rendered as a collapsible block with title `METHOD path`.
446
+
447
+ To access it from your project:
448
+
449
+ - Quick local use: open the HTML file directly.
450
+ - Team/shared use: serve it as a static file (Express example):
451
+
452
+ ```ts
453
+ import express from 'express'
454
+ import path from 'node:path'
455
+
456
+ const app = express()
457
+
458
+ app.use(
459
+ '/tools/finalized-leaves-viewer',
460
+ express.static(
461
+ path.resolve(process.cwd(), 'packages/contract/tools'),
462
+ ),
463
+ )
464
+
465
+ app.listen(3000)
466
+ // open http://localhost:3000/tools/finalized-leaves-viewer/finalized-leaves-viewer.html
467
+ ```
@@ -0,0 +1 @@
1
+ export declare const DEFAULT_VIEWER_TEMPLATE = "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Finalized Leaves Viewer</title>\n <style>\n :root {\n --bg: #f5f7fb;\n --surface: #ffffff;\n --border: #d6ddea;\n --text: #172033;\n --muted: #5b6680;\n --accent: #1858c6;\n }\n body {\n margin: 0;\n font-family: 'Iosevka Web', 'SFMono-Regular', Menlo, Consolas, monospace;\n color: var(--text);\n background: linear-gradient(180deg, var(--bg), #eef2fa);\n }\n .wrap { max-width: 1100px; margin: 0 auto; padding: 20px; }\n .card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 14px; }\n .meta { color: var(--muted); font-size: 12px; }\n #results { margin-top: 12px; display: grid; gap: 8px; }\n details { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 8px 10px; }\n summary { cursor: pointer; font-weight: 700; color: var(--accent); }\n pre { margin: 10px 0 0; overflow: auto; border: 1px solid var(--border); border-radius: 8px; padding: 10px; background: #fafcff; }\n </style>\n </head>\n <body>\n <div class=\"wrap\">\n <h1>Finalized Leaves Viewer (Baked)</h1>\n <div class=\"card\">\n <div id=\"status\" class=\"meta\">Waiting for baked payload...</div>\n </div>\n <div id=\"results\"></div>\n </div>\n\n <!--__FINALIZED_LEAVES_BAKED_PAYLOAD__-->\n\n <script>\n const statusEl = document.getElementById('status')\n const resultsEl = document.getElementById('results')\n const payload = window.__FINALIZED_LEAVES_PAYLOAD\n\n if (!payload || !Array.isArray(payload.leaves)) {\n statusEl.textContent = 'No baked payload found in this HTML file.'\n } else {\n statusEl.textContent = 'Loaded baked payload with ' + payload.leaves.length + ' routes.'\n\n payload.leaves.forEach((leaf) => {\n const details = document.createElement('details')\n const summary = document.createElement('summary')\n summary.textContent = String(leaf.method || '').toUpperCase() + ' ' + (leaf.path || '')\n\n const pre = document.createElement('pre')\n pre.textContent = JSON.stringify(leaf, null, 2)\n\n details.appendChild(summary)\n details.appendChild(pre)\n resultsEl.appendChild(details)\n })\n }\n </script>\n </body>\n</html>\n";
@@ -25,6 +25,18 @@ export type FinalizedLeavesExport = {
25
25
  };
26
26
  export type ExportFinalizedLeavesOptions = SerializeLeafContractOptions & {
27
27
  outFile?: string;
28
+ htmlFile?: string;
29
+ viewerTemplateFile?: string;
30
+ openOnFinish?: boolean;
28
31
  };
29
- export declare function writeFinalizedLeavesExport(payload: FinalizedLeavesExport, outFile: string): Promise<string>;
32
+ export type WriteFinalizedLeavesExportOptions = {
33
+ outFile?: string;
34
+ htmlFile?: string;
35
+ viewerTemplateFile?: string;
36
+ openOnFinish?: boolean;
37
+ };
38
+ export declare function writeFinalizedLeavesExport(payload: FinalizedLeavesExport, outFileOrOptions: string | WriteFinalizedLeavesExportOptions): Promise<{
39
+ outFile?: string;
40
+ htmlFile?: string;
41
+ }>;
30
42
  export declare function exportFinalizedLeaves(input: ExportFinalizedLeavesInput, options?: ExportFinalizedLeavesOptions): Promise<FinalizedLeavesExport>;
@@ -3,3 +3,4 @@ export * from './serializeLeafContract';
3
3
  export * from './flattenSchema';
4
4
  export * from './exportFinalizedLeaves';
5
5
  export * from './exportFinalizedLeaves.cli';
6
+ export * from './defaultViewerTemplate';
package/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ DEFAULT_VIEWER_TEMPLATE: () => DEFAULT_VIEWER_TEMPLATE,
33
34
  buildCacheKey: () => buildCacheKey,
34
35
  buildLowProfileLeaf: () => buildLowProfileLeaf,
35
36
  clearSchemaIntrospectionHandlers: () => clearSchemaIntrospectionHandlers,
@@ -741,6 +742,79 @@ function flattenLeafSchemas(leaf) {
741
742
  // src/export/exportFinalizedLeaves.ts
742
743
  var import_promises = __toESM(require("fs/promises"), 1);
743
744
  var import_node_path = __toESM(require("path"), 1);
745
+ var import_node_child_process = require("child_process");
746
+
747
+ // src/export/defaultViewerTemplate.ts
748
+ var DEFAULT_VIEWER_TEMPLATE = `<!doctype html>
749
+ <html lang="en">
750
+ <head>
751
+ <meta charset="UTF-8" />
752
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
753
+ <title>Finalized Leaves Viewer</title>
754
+ <style>
755
+ :root {
756
+ --bg: #f5f7fb;
757
+ --surface: #ffffff;
758
+ --border: #d6ddea;
759
+ --text: #172033;
760
+ --muted: #5b6680;
761
+ --accent: #1858c6;
762
+ }
763
+ body {
764
+ margin: 0;
765
+ font-family: 'Iosevka Web', 'SFMono-Regular', Menlo, Consolas, monospace;
766
+ color: var(--text);
767
+ background: linear-gradient(180deg, var(--bg), #eef2fa);
768
+ }
769
+ .wrap { max-width: 1100px; margin: 0 auto; padding: 20px; }
770
+ .card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 14px; }
771
+ .meta { color: var(--muted); font-size: 12px; }
772
+ #results { margin-top: 12px; display: grid; gap: 8px; }
773
+ details { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 8px 10px; }
774
+ summary { cursor: pointer; font-weight: 700; color: var(--accent); }
775
+ pre { margin: 10px 0 0; overflow: auto; border: 1px solid var(--border); border-radius: 8px; padding: 10px; background: #fafcff; }
776
+ </style>
777
+ </head>
778
+ <body>
779
+ <div class="wrap">
780
+ <h1>Finalized Leaves Viewer (Baked)</h1>
781
+ <div class="card">
782
+ <div id="status" class="meta">Waiting for baked payload...</div>
783
+ </div>
784
+ <div id="results"></div>
785
+ </div>
786
+
787
+ <!--__FINALIZED_LEAVES_BAKED_PAYLOAD__-->
788
+
789
+ <script>
790
+ const statusEl = document.getElementById('status')
791
+ const resultsEl = document.getElementById('results')
792
+ const payload = window.__FINALIZED_LEAVES_PAYLOAD
793
+
794
+ if (!payload || !Array.isArray(payload.leaves)) {
795
+ statusEl.textContent = 'No baked payload found in this HTML file.'
796
+ } else {
797
+ statusEl.textContent = 'Loaded baked payload with ' + payload.leaves.length + ' routes.'
798
+
799
+ payload.leaves.forEach((leaf) => {
800
+ const details = document.createElement('details')
801
+ const summary = document.createElement('summary')
802
+ summary.textContent = String(leaf.method || '').toUpperCase() + ' ' + (leaf.path || '')
803
+
804
+ const pre = document.createElement('pre')
805
+ pre.textContent = JSON.stringify(leaf, null, 2)
806
+
807
+ details.appendChild(summary)
808
+ details.appendChild(pre)
809
+ resultsEl.appendChild(details)
810
+ })
811
+ }
812
+ </script>
813
+ </body>
814
+ </html>
815
+ `;
816
+
817
+ // src/export/exportFinalizedLeaves.ts
744
818
  function isRegistry(value) {
745
819
  return typeof value === "object" && value !== null && "all" in value && "byKey" in value;
746
820
  }
@@ -786,13 +860,109 @@ function buildMeta() {
786
860
  }
787
861
  };
788
862
  }
789
- async function writeFinalizedLeavesExport(payload, outFile) {
863
+ var BAKED_PAYLOAD_MARKER = "<!--__FINALIZED_LEAVES_BAKED_PAYLOAD__-->";
864
+ function escapePayloadForInlineScript(payload) {
865
+ return JSON.stringify(payload).replace(/<\//g, "<\\/").replace(/<!--/g, "<\\!--");
866
+ }
867
+ function injectPayloadIntoViewerHtml(htmlTemplate, payload) {
868
+ const payloadScript = `${BAKED_PAYLOAD_MARKER}
869
+ <script id="finalized-leaves-baked-payload">window.__FINALIZED_LEAVES_PAYLOAD = ${escapePayloadForInlineScript(
870
+ payload
871
+ )};</script>`;
872
+ if (htmlTemplate.includes(BAKED_PAYLOAD_MARKER)) {
873
+ return htmlTemplate.replace(BAKED_PAYLOAD_MARKER, payloadScript);
874
+ }
875
+ if (htmlTemplate.includes("</body>")) {
876
+ return htmlTemplate.replace("</body>", `${payloadScript}
877
+ </body>`);
878
+ }
879
+ return `${payloadScript}
880
+ ${htmlTemplate}`;
881
+ }
882
+ async function resolveViewerTemplatePath(viewerTemplateFile) {
883
+ if (viewerTemplateFile) {
884
+ const resolved = import_node_path.default.resolve(viewerTemplateFile);
885
+ await import_promises.default.access(resolved);
886
+ return resolved;
887
+ }
888
+ const candidates = [
889
+ import_node_path.default.resolve(
890
+ process.cwd(),
891
+ "node_modules/@emeryld/rrroutes-contract/tools/finalized-leaves-viewer.html"
892
+ ),
893
+ import_node_path.default.resolve(
894
+ process.cwd(),
895
+ "tools/finalized-leaves-viewer.html"
896
+ ),
897
+ import_node_path.default.resolve(
898
+ process.cwd(),
899
+ "packages/contract/tools/finalized-leaves-viewer.html"
900
+ )
901
+ ];
902
+ for (const candidate of candidates) {
903
+ try {
904
+ await import_promises.default.access(candidate);
905
+ return candidate;
906
+ } catch {
907
+ }
908
+ }
909
+ return void 0;
910
+ }
911
+ async function writeJsonExport(payload, outFile) {
790
912
  const resolved = import_node_path.default.resolve(outFile);
791
913
  await import_promises.default.mkdir(import_node_path.default.dirname(resolved), { recursive: true });
792
914
  await import_promises.default.writeFile(resolved, `${JSON.stringify(payload, null, 2)}
793
915
  `, "utf8");
794
916
  return resolved;
795
917
  }
918
+ async function writeBakedHtmlExport(payload, htmlFile, viewerTemplateFile) {
919
+ const templatePath = await resolveViewerTemplatePath(viewerTemplateFile);
920
+ const template = templatePath ? await import_promises.default.readFile(templatePath, "utf8") : DEFAULT_VIEWER_TEMPLATE;
921
+ const baked = injectPayloadIntoViewerHtml(template, payload);
922
+ const resolved = import_node_path.default.resolve(htmlFile);
923
+ await import_promises.default.mkdir(import_node_path.default.dirname(resolved), { recursive: true });
924
+ await import_promises.default.writeFile(resolved, baked, "utf8");
925
+ return resolved;
926
+ }
927
+ async function openHtmlInBrowser(filePath) {
928
+ const resolved = import_node_path.default.resolve(filePath);
929
+ const platform = process.platform;
930
+ if (platform === "darwin") {
931
+ (0, import_node_child_process.spawn)("open", [resolved], { detached: true, stdio: "ignore" }).unref();
932
+ return;
933
+ }
934
+ if (platform === "win32") {
935
+ (0, import_node_child_process.spawn)("cmd", ["/c", "start", "", resolved], {
936
+ detached: true,
937
+ stdio: "ignore"
938
+ }).unref();
939
+ return;
940
+ }
941
+ (0, import_node_child_process.spawn)("xdg-open", [resolved], { detached: true, stdio: "ignore" }).unref();
942
+ }
943
+ async function writeFinalizedLeavesExport(payload, outFileOrOptions) {
944
+ const options = typeof outFileOrOptions === "string" ? { outFile: outFileOrOptions } : outFileOrOptions;
945
+ const written = {};
946
+ if (options.outFile) {
947
+ written.outFile = await writeJsonExport(payload, options.outFile);
948
+ }
949
+ if (options.htmlFile) {
950
+ written.htmlFile = await writeBakedHtmlExport(
951
+ payload,
952
+ options.htmlFile,
953
+ options.viewerTemplateFile
954
+ );
955
+ }
956
+ if (options.openOnFinish) {
957
+ if (!written.htmlFile) {
958
+ throw new Error(
959
+ "openOnFinish requires htmlFile. Provide htmlFile to open the baked viewer."
960
+ );
961
+ }
962
+ await openHtmlInBrowser(written.htmlFile);
963
+ }
964
+ return written;
965
+ }
796
966
  async function exportFinalizedLeaves(input, options = {}) {
797
967
  const leaves = getLeaves(input);
798
968
  const serializedLeaves = serializeLeavesContract(leaves, options);
@@ -804,8 +974,13 @@ async function exportFinalizedLeaves(input, options = {}) {
804
974
  leaves: serializedLeaves,
805
975
  schemaFlatByLeaf
806
976
  };
807
- if (options.outFile) {
808
- await writeFinalizedLeavesExport(payload, options.outFile);
977
+ if (options.outFile || options.htmlFile) {
978
+ await writeFinalizedLeavesExport(payload, {
979
+ outFile: options.outFile,
980
+ htmlFile: options.htmlFile,
981
+ viewerTemplateFile: options.viewerTemplateFile,
982
+ openOnFinish: options.openOnFinish
983
+ });
809
984
  }
810
985
  return payload;
811
986
  }
@@ -859,6 +1034,7 @@ async function runExportFinalizedLeavesCli(argv) {
859
1034
  }
860
1035
  // Annotate the CommonJS export names for ESM import in node:
861
1036
  0 && (module.exports = {
1037
+ DEFAULT_VIEWER_TEMPLATE,
862
1038
  buildCacheKey,
863
1039
  buildLowProfileLeaf,
864
1040
  clearSchemaIntrospectionHandlers,