@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 +92 -0
- package/dist/export/defaultViewerTemplate.d.ts +1 -0
- package/dist/export/exportFinalizedLeaves.d.ts +13 -1
- package/dist/export/index.d.ts +1 -0
- package/dist/index.cjs +179 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +178 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/tools/finalized-leaves-viewer.html +739 -0
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
|
|
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>;
|
package/dist/export/index.d.ts
CHANGED
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
|
-
|
|
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,
|
|
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,
|