@emeryld/rrroutes-contract 2.7.4 → 2.7.6
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 +19 -1
- package/bin/rrroutes-export-finalized-leaves.mjs +14 -0
- package/dist/export/defaultViewerTemplate.d.ts +1 -1
- package/dist/export/exportFinalizedLeaves.cli.d.ts +2 -0
- package/dist/export/exportFinalizedLeaves.d.ts +14 -0
- package/dist/export/extractLeafSourceByAst.d.ts +30 -0
- package/dist/export/index.d.ts +1 -0
- package/dist/index.cjs +540 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +539 -38
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -2
- package/tools/finalized-leaves-viewer.html +272 -138
package/dist/index.mjs
CHANGED
|
@@ -42,9 +42,9 @@ function collectNestedFieldSuggestions(shape, prefix = []) {
|
|
|
42
42
|
}
|
|
43
43
|
return suggestions;
|
|
44
44
|
}
|
|
45
|
-
function compilePath(
|
|
46
|
-
if (!params) return
|
|
47
|
-
const withParams =
|
|
45
|
+
function compilePath(path4, params) {
|
|
46
|
+
if (!params) return path4;
|
|
47
|
+
const withParams = path4.replace(/:([A-Za-z0-9_]+)/g, (_, k) => {
|
|
48
48
|
const v = params[k];
|
|
49
49
|
if (v === void 0 || v === null) throw new Error(`Missing param :${k}`);
|
|
50
50
|
return String(v);
|
|
@@ -119,8 +119,8 @@ function buildLowProfileLeaf(leaf) {
|
|
|
119
119
|
}
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
|
-
var keyOf = (method,
|
|
123
|
-
const key = `${method.toUpperCase()} ${
|
|
122
|
+
var keyOf = (method, path4, encodeSafe) => {
|
|
123
|
+
const key = `${method.toUpperCase()} ${path4}`;
|
|
124
124
|
return encodeSafe ? encodeURIComponent(key) : key;
|
|
125
125
|
};
|
|
126
126
|
|
|
@@ -296,8 +296,8 @@ function joinPaths(parent, child) {
|
|
|
296
296
|
if (!trimmedChild) return trimmedParent;
|
|
297
297
|
return `${trimmedParent}/${trimmedChild}`;
|
|
298
298
|
}
|
|
299
|
-
function assertDynamicLayerUniqueness(
|
|
300
|
-
const segments =
|
|
299
|
+
function assertDynamicLayerUniqueness(path4, dynamicLayerMap) {
|
|
300
|
+
const segments = path4.split("/").filter(Boolean);
|
|
301
301
|
if (segments.length === 0) return;
|
|
302
302
|
for (let i = 0; i < segments.length; i++) {
|
|
303
303
|
const segment = segments[i];
|
|
@@ -617,45 +617,45 @@ function normalizeType(schema) {
|
|
|
617
617
|
return "unknown";
|
|
618
618
|
}
|
|
619
619
|
}
|
|
620
|
-
function isNonEmptyPath(
|
|
621
|
-
return typeof
|
|
620
|
+
function isNonEmptyPath(path4) {
|
|
621
|
+
return typeof path4 === "string" && path4.length > 0;
|
|
622
622
|
}
|
|
623
|
-
function setNode(out,
|
|
624
|
-
if (!isNonEmptyPath(
|
|
625
|
-
out[
|
|
623
|
+
function setNode(out, path4, schema, inherited) {
|
|
624
|
+
if (!isNonEmptyPath(path4)) return;
|
|
625
|
+
out[path4] = {
|
|
626
626
|
type: normalizeType(schema),
|
|
627
627
|
nullable: inherited.nullable || Boolean(schema.nullable),
|
|
628
628
|
optional: inherited.optional || Boolean(schema.optional)
|
|
629
629
|
};
|
|
630
630
|
}
|
|
631
|
-
function flattenInto(out, schema,
|
|
632
|
-
if (!schema || !isNonEmptyPath(
|
|
631
|
+
function flattenInto(out, schema, path4, inherited) {
|
|
632
|
+
if (!schema || !isNonEmptyPath(path4)) return;
|
|
633
633
|
const nextInherited = {
|
|
634
634
|
optional: inherited.optional || Boolean(schema.optional),
|
|
635
635
|
nullable: inherited.nullable || Boolean(schema.nullable)
|
|
636
636
|
};
|
|
637
637
|
if (schema.kind === "union" && Array.isArray(schema.union) && schema.union.length > 0) {
|
|
638
638
|
schema.union.forEach((option, index) => {
|
|
639
|
-
const optionPath = `${
|
|
639
|
+
const optionPath = `${path4}-${index + 1}`;
|
|
640
640
|
flattenInto(out, option, optionPath, inherited);
|
|
641
641
|
});
|
|
642
642
|
return;
|
|
643
643
|
}
|
|
644
|
-
setNode(out,
|
|
644
|
+
setNode(out, path4, schema, inherited);
|
|
645
645
|
if (schema.kind === "object" && schema.properties) {
|
|
646
646
|
for (const [key, child] of Object.entries(schema.properties)) {
|
|
647
|
-
const childPath = `${
|
|
647
|
+
const childPath = `${path4}.${key}`;
|
|
648
648
|
flattenInto(out, child, childPath, nextInherited);
|
|
649
649
|
}
|
|
650
650
|
return;
|
|
651
651
|
}
|
|
652
652
|
if (schema.kind === "array" && schema.element) {
|
|
653
|
-
flattenInto(out, schema.element, `${
|
|
653
|
+
flattenInto(out, schema.element, `${path4}[]`, nextInherited);
|
|
654
654
|
}
|
|
655
655
|
}
|
|
656
|
-
function flattenSerializableSchema(schema,
|
|
656
|
+
function flattenSerializableSchema(schema, path4) {
|
|
657
657
|
const out = {};
|
|
658
|
-
flattenInto(out, schema,
|
|
658
|
+
flattenInto(out, schema, path4, { optional: false, nullable: false });
|
|
659
659
|
return out;
|
|
660
660
|
}
|
|
661
661
|
function isSerializedLeaf(value) {
|
|
@@ -676,7 +676,7 @@ function flattenLeafSchemas(leaf) {
|
|
|
676
676
|
|
|
677
677
|
// src/export/exportFinalizedLeaves.ts
|
|
678
678
|
import fs from "fs/promises";
|
|
679
|
-
import
|
|
679
|
+
import path2 from "path";
|
|
680
680
|
import { spawn } from "child_process";
|
|
681
681
|
|
|
682
682
|
// src/export/defaultViewerTemplate.ts
|
|
@@ -731,6 +731,13 @@ var DEFAULT_VIEWER_TEMPLATE = `<!doctype html>
|
|
|
731
731
|
} else {
|
|
732
732
|
statusEl.textContent = 'Loaded baked payload with ' + payload.leaves.length + ' routes.'
|
|
733
733
|
|
|
734
|
+
const toHref = (source) => {
|
|
735
|
+
if (!source || !source.file) return null
|
|
736
|
+
const normalizedPath = String(source.file).replace(/\\\\/g, '/')
|
|
737
|
+
const prefix = normalizedPath.startsWith('/') ? 'file://' : 'file:///'
|
|
738
|
+
return prefix + encodeURI(normalizedPath)
|
|
739
|
+
}
|
|
740
|
+
|
|
734
741
|
payload.leaves.forEach((leaf) => {
|
|
735
742
|
const details = document.createElement('details')
|
|
736
743
|
const summary = document.createElement('summary')
|
|
@@ -739,7 +746,63 @@ var DEFAULT_VIEWER_TEMPLATE = `<!doctype html>
|
|
|
739
746
|
const pre = document.createElement('pre')
|
|
740
747
|
pre.textContent = JSON.stringify(leaf, null, 2)
|
|
741
748
|
|
|
749
|
+
const source = payload.sourceByLeaf && payload.sourceByLeaf[leaf.key]
|
|
750
|
+
let sourceWrap = null
|
|
751
|
+
if (source) {
|
|
752
|
+
sourceWrap = document.createElement('div')
|
|
753
|
+
sourceWrap.className = 'meta'
|
|
754
|
+
|
|
755
|
+
const definitionHref = toHref(source.definition)
|
|
756
|
+
if (definitionHref) {
|
|
757
|
+
const label = document.createElement('div')
|
|
758
|
+
const link = document.createElement('a')
|
|
759
|
+
link.href = definitionHref
|
|
760
|
+
link.target = '_blank'
|
|
761
|
+
link.rel = 'noopener noreferrer'
|
|
762
|
+
link.textContent =
|
|
763
|
+
'definition: ' +
|
|
764
|
+
source.definition.file +
|
|
765
|
+
':' +
|
|
766
|
+
source.definition.line +
|
|
767
|
+
':' +
|
|
768
|
+
source.definition.column
|
|
769
|
+
label.appendChild(link)
|
|
770
|
+
sourceWrap.appendChild(label)
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (source.schemas && typeof source.schemas === 'object') {
|
|
774
|
+
Object.entries(source.schemas).forEach(([name, schema]) => {
|
|
775
|
+
if (!schema) return
|
|
776
|
+
const href = toHref(schema)
|
|
777
|
+
const row = document.createElement('div')
|
|
778
|
+
if (href) {
|
|
779
|
+
const link = document.createElement('a')
|
|
780
|
+
link.href = href
|
|
781
|
+
link.target = '_blank'
|
|
782
|
+
link.rel = 'noopener noreferrer'
|
|
783
|
+
link.textContent =
|
|
784
|
+
name +
|
|
785
|
+
': ' +
|
|
786
|
+
(schema.sourceName || schema.tag || '<anonymous>') +
|
|
787
|
+
' (' +
|
|
788
|
+
schema.file +
|
|
789
|
+
':' +
|
|
790
|
+
schema.line +
|
|
791
|
+
':' +
|
|
792
|
+
schema.column +
|
|
793
|
+
')'
|
|
794
|
+
row.appendChild(link)
|
|
795
|
+
} else {
|
|
796
|
+
row.textContent = name + ': ' + (schema.sourceName || schema.tag || '<anonymous>')
|
|
797
|
+
}
|
|
798
|
+
sourceWrap.appendChild(row)
|
|
799
|
+
})
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
}
|
|
803
|
+
|
|
742
804
|
details.appendChild(summary)
|
|
805
|
+
if (sourceWrap) details.appendChild(sourceWrap)
|
|
743
806
|
details.appendChild(pre)
|
|
744
807
|
resultsEl.appendChild(details)
|
|
745
808
|
})
|
|
@@ -749,6 +812,393 @@ var DEFAULT_VIEWER_TEMPLATE = `<!doctype html>
|
|
|
749
812
|
</html>
|
|
750
813
|
`;
|
|
751
814
|
|
|
815
|
+
// src/export/extractLeafSourceByAst.ts
|
|
816
|
+
import path from "path";
|
|
817
|
+
import ts from "typescript";
|
|
818
|
+
var SCHEMA_KEYS = [
|
|
819
|
+
"bodySchema",
|
|
820
|
+
"querySchema",
|
|
821
|
+
"paramsSchema",
|
|
822
|
+
"outputSchema",
|
|
823
|
+
"outputMetaSchema",
|
|
824
|
+
"queryExtensionSchema"
|
|
825
|
+
];
|
|
826
|
+
var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete"]);
|
|
827
|
+
function toLocation(node) {
|
|
828
|
+
const sourceFile = node.getSourceFile();
|
|
829
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(
|
|
830
|
+
node.getStart(sourceFile)
|
|
831
|
+
);
|
|
832
|
+
return {
|
|
833
|
+
file: path.resolve(sourceFile.fileName),
|
|
834
|
+
line: line + 1,
|
|
835
|
+
column: character + 1
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
function trimPreview(text, max = 80) {
|
|
839
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
840
|
+
return normalized.length > max ? `${normalized.slice(0, max)}...` : normalized;
|
|
841
|
+
}
|
|
842
|
+
function unwrapExpression(expression) {
|
|
843
|
+
let current = expression;
|
|
844
|
+
while (true) {
|
|
845
|
+
if (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isTypeAssertionExpression(current) || ts.isNonNullExpression(current) || ts.isSatisfiesExpression(current)) {
|
|
846
|
+
current = current.expression;
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
return current;
|
|
852
|
+
}
|
|
853
|
+
function getTextName(name) {
|
|
854
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
855
|
+
return name.text;
|
|
856
|
+
}
|
|
857
|
+
return void 0;
|
|
858
|
+
}
|
|
859
|
+
function joinPaths2(parent, child) {
|
|
860
|
+
if (!parent) return child;
|
|
861
|
+
if (!child) return parent;
|
|
862
|
+
const trimmedParent = parent.endsWith("/") ? parent.replace(/\/+$/, "") : parent;
|
|
863
|
+
const trimmedChild = child.startsWith("/") ? child.replace(/^\/+/, "") : child;
|
|
864
|
+
if (!trimmedChild) return trimmedParent;
|
|
865
|
+
return `${trimmedParent}/${trimmedChild}`;
|
|
866
|
+
}
|
|
867
|
+
function buildLeafKey(method, leafPath) {
|
|
868
|
+
return `${method.toUpperCase()} ${leafPath}`;
|
|
869
|
+
}
|
|
870
|
+
function normalizeResourceBase(raw) {
|
|
871
|
+
if (raw === "") return "";
|
|
872
|
+
if (raw.startsWith("/") || raw.startsWith(":") || raw.startsWith("*")) {
|
|
873
|
+
return raw;
|
|
874
|
+
}
|
|
875
|
+
return `/${raw}`;
|
|
876
|
+
}
|
|
877
|
+
function getModuleSymbol(checker, sourceFile) {
|
|
878
|
+
return checker.getSymbolAtLocation(sourceFile);
|
|
879
|
+
}
|
|
880
|
+
function getAliasedSymbolIfNeeded(checker, symbol) {
|
|
881
|
+
if ((symbol.flags & ts.SymbolFlags.Alias) !== 0) {
|
|
882
|
+
try {
|
|
883
|
+
return checker.getAliasedSymbol(symbol);
|
|
884
|
+
} catch {
|
|
885
|
+
return symbol;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return symbol;
|
|
889
|
+
}
|
|
890
|
+
function findSourceFile(program, filePath) {
|
|
891
|
+
const wanted = path.resolve(filePath);
|
|
892
|
+
const normalizedWanted = path.normalize(wanted);
|
|
893
|
+
return program.getSourceFiles().find((file) => path.normalize(path.resolve(file.fileName)) === normalizedWanted);
|
|
894
|
+
}
|
|
895
|
+
function expressionFromDeclaration(declaration) {
|
|
896
|
+
if (!declaration) return void 0;
|
|
897
|
+
if (ts.isVariableDeclaration(declaration)) {
|
|
898
|
+
return declaration.initializer;
|
|
899
|
+
}
|
|
900
|
+
if (ts.isExportAssignment(declaration)) {
|
|
901
|
+
return declaration.expression;
|
|
902
|
+
}
|
|
903
|
+
if (ts.isPropertyAssignment(declaration)) {
|
|
904
|
+
return declaration.initializer;
|
|
905
|
+
}
|
|
906
|
+
if (ts.isShorthandPropertyAssignment(declaration)) {
|
|
907
|
+
return declaration.name;
|
|
908
|
+
}
|
|
909
|
+
if (ts.isBindingElement(declaration)) {
|
|
910
|
+
return declaration.initializer;
|
|
911
|
+
}
|
|
912
|
+
return void 0;
|
|
913
|
+
}
|
|
914
|
+
function resolveIdentifierExpression(identifier, ctx) {
|
|
915
|
+
const symbol = ctx.checker.getSymbolAtLocation(identifier);
|
|
916
|
+
if (!symbol) return void 0;
|
|
917
|
+
const target = getAliasedSymbolIfNeeded(ctx.checker, symbol);
|
|
918
|
+
const declaration = target.declarations?.[0];
|
|
919
|
+
return expressionFromDeclaration(declaration);
|
|
920
|
+
}
|
|
921
|
+
function resolveExportExpression(sourceFile, exportName, checker) {
|
|
922
|
+
const moduleSymbol = getModuleSymbol(checker, sourceFile);
|
|
923
|
+
if (!moduleSymbol) return void 0;
|
|
924
|
+
const exports = checker.getExportsOfModule(moduleSymbol);
|
|
925
|
+
const explicit = exports.find((entry) => entry.getName() === exportName);
|
|
926
|
+
if (explicit) {
|
|
927
|
+
const declaration = getAliasedSymbolIfNeeded(checker, explicit).declarations?.[0];
|
|
928
|
+
return expressionFromDeclaration(declaration);
|
|
929
|
+
}
|
|
930
|
+
const defaultExport = exports.find((entry) => entry.getName() === "default");
|
|
931
|
+
if (!defaultExport) return void 0;
|
|
932
|
+
const defaultDecl = getAliasedSymbolIfNeeded(checker, defaultExport).declarations?.[0];
|
|
933
|
+
const defaultExpr = expressionFromDeclaration(defaultDecl);
|
|
934
|
+
if (!defaultExpr) return void 0;
|
|
935
|
+
const resolved = unwrapExpression(defaultExpr);
|
|
936
|
+
if (!ts.isObjectLiteralExpression(resolved)) return void 0;
|
|
937
|
+
for (const property of resolved.properties) {
|
|
938
|
+
if (!ts.isPropertyAssignment(property)) continue;
|
|
939
|
+
const propertyName = getTextName(property.name);
|
|
940
|
+
if (propertyName !== exportName) continue;
|
|
941
|
+
return property.initializer;
|
|
942
|
+
}
|
|
943
|
+
return void 0;
|
|
944
|
+
}
|
|
945
|
+
function maybeObjectLiteral(expression, ctx) {
|
|
946
|
+
if (!expression) return void 0;
|
|
947
|
+
const resolved = unwrapExpression(expression);
|
|
948
|
+
if (ts.isObjectLiteralExpression(resolved)) return resolved;
|
|
949
|
+
if (ts.isIdentifier(resolved)) {
|
|
950
|
+
const target = resolveIdentifierExpression(resolved, ctx);
|
|
951
|
+
if (!target) return void 0;
|
|
952
|
+
return maybeObjectLiteral(target, ctx);
|
|
953
|
+
}
|
|
954
|
+
return void 0;
|
|
955
|
+
}
|
|
956
|
+
function collectSchemaExpressionsFromObject(objectLiteral, ctx) {
|
|
957
|
+
const schemas = {};
|
|
958
|
+
for (const property of objectLiteral.properties) {
|
|
959
|
+
if (ts.isSpreadAssignment(property)) {
|
|
960
|
+
const spreadObject = maybeObjectLiteral(property.expression, ctx);
|
|
961
|
+
if (!spreadObject) continue;
|
|
962
|
+
Object.assign(schemas, collectSchemaExpressionsFromObject(spreadObject, ctx));
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
if (ts.isPropertyAssignment(property)) {
|
|
966
|
+
const key = getTextName(property.name);
|
|
967
|
+
if (!key || !SCHEMA_KEYS.includes(key)) continue;
|
|
968
|
+
schemas[key] = property.initializer;
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
972
|
+
const key = property.name.text;
|
|
973
|
+
if (!SCHEMA_KEYS.includes(key)) continue;
|
|
974
|
+
schemas[key] = property.name;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return schemas;
|
|
978
|
+
}
|
|
979
|
+
function extractSchemaExpressions(cfgExpression, ctx) {
|
|
980
|
+
const objectLiteral = maybeObjectLiteral(cfgExpression, ctx);
|
|
981
|
+
if (!objectLiteral) return {};
|
|
982
|
+
return collectSchemaExpressionsFromObject(objectLiteral, ctx);
|
|
983
|
+
}
|
|
984
|
+
function getNearestVariableName(node) {
|
|
985
|
+
let current = node;
|
|
986
|
+
while (current) {
|
|
987
|
+
if (ts.isVariableDeclaration(current) && ts.isIdentifier(current.name)) {
|
|
988
|
+
return current.name.text;
|
|
989
|
+
}
|
|
990
|
+
current = current.parent;
|
|
991
|
+
}
|
|
992
|
+
return void 0;
|
|
993
|
+
}
|
|
994
|
+
function evaluateBranchExpression(expression, ctx) {
|
|
995
|
+
const resolved = unwrapExpression(expression);
|
|
996
|
+
if (ts.isIdentifier(resolved)) {
|
|
997
|
+
const valueExpr = resolveIdentifierExpression(resolved, ctx);
|
|
998
|
+
if (!valueExpr) return void 0;
|
|
999
|
+
return evaluateBranchExpression(valueExpr, ctx);
|
|
1000
|
+
}
|
|
1001
|
+
if (!ts.isCallExpression(resolved)) return void 0;
|
|
1002
|
+
const call = resolved;
|
|
1003
|
+
if (ts.isIdentifier(call.expression) && call.expression.text === "resource") {
|
|
1004
|
+
const firstArg = call.arguments[0];
|
|
1005
|
+
if (!firstArg || !ts.isStringLiteralLike(firstArg)) return void 0;
|
|
1006
|
+
return { base: normalizeResourceBase(firstArg.text), leaves: [] };
|
|
1007
|
+
}
|
|
1008
|
+
if (!ts.isPropertyAccessExpression(call.expression)) return void 0;
|
|
1009
|
+
const owner = call.expression.expression;
|
|
1010
|
+
const method = call.expression.name.text;
|
|
1011
|
+
const branch = evaluateBranchExpression(owner, ctx);
|
|
1012
|
+
if (!branch) return void 0;
|
|
1013
|
+
if (method === "with") {
|
|
1014
|
+
return branch;
|
|
1015
|
+
}
|
|
1016
|
+
if (HTTP_METHODS.has(method)) {
|
|
1017
|
+
const cfgExpression = call.arguments[0];
|
|
1018
|
+
const schemas = extractSchemaExpressions(cfgExpression, ctx);
|
|
1019
|
+
const nextLeaf = {
|
|
1020
|
+
method,
|
|
1021
|
+
path: branch.base,
|
|
1022
|
+
definitionNode: call.expression.name,
|
|
1023
|
+
schemas
|
|
1024
|
+
};
|
|
1025
|
+
return {
|
|
1026
|
+
...branch,
|
|
1027
|
+
leaves: [...branch.leaves, nextLeaf]
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
if (method === "sub") {
|
|
1031
|
+
const mountedLeaves = call.arguments.flatMap(
|
|
1032
|
+
(arg) => evaluateLeavesFromExpression(arg, ctx)
|
|
1033
|
+
);
|
|
1034
|
+
const prefixed = mountedLeaves.map((leaf) => ({
|
|
1035
|
+
...leaf,
|
|
1036
|
+
path: joinPaths2(branch.base, leaf.path)
|
|
1037
|
+
}));
|
|
1038
|
+
return {
|
|
1039
|
+
...branch,
|
|
1040
|
+
leaves: [...branch.leaves, ...prefixed]
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
return void 0;
|
|
1044
|
+
}
|
|
1045
|
+
function evaluateLeavesFromExpression(expression, ctx) {
|
|
1046
|
+
const resolved = unwrapExpression(expression);
|
|
1047
|
+
const key = resolved.getStart(resolved.getSourceFile());
|
|
1048
|
+
if (ctx.visitedExpressionStarts.has(key)) {
|
|
1049
|
+
return [];
|
|
1050
|
+
}
|
|
1051
|
+
ctx.visitedExpressionStarts.add(key);
|
|
1052
|
+
try {
|
|
1053
|
+
if (ts.isIdentifier(resolved)) {
|
|
1054
|
+
const valueExpr = resolveIdentifierExpression(resolved, ctx);
|
|
1055
|
+
if (!valueExpr) return [];
|
|
1056
|
+
return evaluateLeavesFromExpression(valueExpr, ctx);
|
|
1057
|
+
}
|
|
1058
|
+
if (ts.isArrayLiteralExpression(resolved)) {
|
|
1059
|
+
const leaves = [];
|
|
1060
|
+
for (const element of resolved.elements) {
|
|
1061
|
+
if (ts.isSpreadElement(element)) {
|
|
1062
|
+
leaves.push(...evaluateLeavesFromExpression(element.expression, ctx));
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
leaves.push(...evaluateLeavesFromExpression(element, ctx));
|
|
1066
|
+
}
|
|
1067
|
+
return leaves;
|
|
1068
|
+
}
|
|
1069
|
+
if (ts.isCallExpression(resolved)) {
|
|
1070
|
+
if (ts.isIdentifier(resolved.expression)) {
|
|
1071
|
+
const callName = resolved.expression.text;
|
|
1072
|
+
if (callName === "finalize") {
|
|
1073
|
+
const arg = resolved.arguments[0];
|
|
1074
|
+
if (!arg) return [];
|
|
1075
|
+
return evaluateLeavesFromExpression(arg, ctx);
|
|
1076
|
+
}
|
|
1077
|
+
if (callName === "mergeArrays") {
|
|
1078
|
+
return resolved.arguments.flatMap(
|
|
1079
|
+
(arg) => evaluateLeavesFromExpression(arg, ctx)
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (ts.isPropertyAccessExpression(resolved.expression)) {
|
|
1084
|
+
const prop = resolved.expression.name.text;
|
|
1085
|
+
if (prop === "done") {
|
|
1086
|
+
const branch2 = evaluateBranchExpression(resolved.expression.expression, ctx);
|
|
1087
|
+
return branch2?.leaves ?? [];
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
const branch = evaluateBranchExpression(resolved, ctx);
|
|
1092
|
+
return branch?.leaves ?? [];
|
|
1093
|
+
} finally {
|
|
1094
|
+
ctx.visitedExpressionStarts.delete(key);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
function resolveSchemaMetadata(expression, ctx) {
|
|
1098
|
+
const resolved = unwrapExpression(expression);
|
|
1099
|
+
if (ts.isIdentifier(resolved)) {
|
|
1100
|
+
const symbol = ctx.checker.getSymbolAtLocation(resolved);
|
|
1101
|
+
const target = symbol ? getAliasedSymbolIfNeeded(ctx.checker, symbol) : void 0;
|
|
1102
|
+
const declaration = target?.declarations?.[0];
|
|
1103
|
+
const locationNode = declaration ? ts.isVariableDeclaration(declaration) ? declaration.name : declaration : resolved;
|
|
1104
|
+
const sourceName = declaration && ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name) ? declaration.name.text : resolved.text;
|
|
1105
|
+
return {
|
|
1106
|
+
...toLocation(locationNode),
|
|
1107
|
+
sourceName,
|
|
1108
|
+
tag: declaration ? void 0 : "<anonymous>"
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
if (ts.isPropertyAccessExpression(resolved) || ts.isElementAccessExpression(resolved)) {
|
|
1112
|
+
return {
|
|
1113
|
+
...toLocation(resolved),
|
|
1114
|
+
sourceName: trimPreview(resolved.getText()),
|
|
1115
|
+
tag: "<expression>"
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
return {
|
|
1119
|
+
...toLocation(resolved),
|
|
1120
|
+
sourceName: trimPreview(resolved.getText()),
|
|
1121
|
+
tag: "<inline>"
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
function parseTsConfig(cwd, tsconfigPath) {
|
|
1125
|
+
const resolvedTsconfig = tsconfigPath ? path.resolve(cwd, tsconfigPath) : ts.findConfigFile(cwd, ts.sys.fileExists, "tsconfig.json");
|
|
1126
|
+
if (!resolvedTsconfig) {
|
|
1127
|
+
return void 0;
|
|
1128
|
+
}
|
|
1129
|
+
const read = ts.readConfigFile(resolvedTsconfig, ts.sys.readFile);
|
|
1130
|
+
if (read.error) {
|
|
1131
|
+
throw new Error(ts.flattenDiagnosticMessageText(read.error.messageText, "\n"));
|
|
1132
|
+
}
|
|
1133
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
1134
|
+
read.config,
|
|
1135
|
+
ts.sys,
|
|
1136
|
+
path.dirname(resolvedTsconfig),
|
|
1137
|
+
void 0,
|
|
1138
|
+
resolvedTsconfig
|
|
1139
|
+
);
|
|
1140
|
+
if (parsed.errors.length > 0) {
|
|
1141
|
+
throw new Error(
|
|
1142
|
+
parsed.errors.map((entry) => ts.flattenDiagnosticMessageText(entry.messageText, "\n")).join("\n")
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
return { resolvedTsconfig, parsed };
|
|
1146
|
+
}
|
|
1147
|
+
function extractLeafSourceByAst({
|
|
1148
|
+
modulePath,
|
|
1149
|
+
exportName,
|
|
1150
|
+
tsconfigPath,
|
|
1151
|
+
cwd = process.cwd()
|
|
1152
|
+
}) {
|
|
1153
|
+
const parsedConfig = parseTsConfig(cwd, tsconfigPath);
|
|
1154
|
+
if (!parsedConfig) {
|
|
1155
|
+
return { sourceByLeaf: {} };
|
|
1156
|
+
}
|
|
1157
|
+
const program = ts.createProgram({
|
|
1158
|
+
rootNames: parsedConfig.parsed.fileNames,
|
|
1159
|
+
options: parsedConfig.parsed.options
|
|
1160
|
+
});
|
|
1161
|
+
const checker = program.getTypeChecker();
|
|
1162
|
+
const moduleFile = findSourceFile(program, path.resolve(cwd, modulePath));
|
|
1163
|
+
if (!moduleFile) {
|
|
1164
|
+
return {
|
|
1165
|
+
sourceByLeaf: {},
|
|
1166
|
+
tsconfigPath: parsedConfig.resolvedTsconfig
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
const exportedExpression = resolveExportExpression(moduleFile, exportName, checker);
|
|
1170
|
+
if (!exportedExpression) {
|
|
1171
|
+
return {
|
|
1172
|
+
sourceByLeaf: {},
|
|
1173
|
+
tsconfigPath: parsedConfig.resolvedTsconfig
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
const ctx = {
|
|
1177
|
+
checker,
|
|
1178
|
+
visitedExpressionStarts: /* @__PURE__ */ new Set()
|
|
1179
|
+
};
|
|
1180
|
+
const evaluatedLeaves = evaluateLeavesFromExpression(exportedExpression, ctx);
|
|
1181
|
+
const sourceByLeaf = {};
|
|
1182
|
+
for (const leaf of evaluatedLeaves) {
|
|
1183
|
+
const key = buildLeafKey(leaf.method, leaf.path);
|
|
1184
|
+
const definition = {
|
|
1185
|
+
...toLocation(leaf.definitionNode),
|
|
1186
|
+
symbolName: getNearestVariableName(leaf.definitionNode)
|
|
1187
|
+
};
|
|
1188
|
+
const schemas = {};
|
|
1189
|
+
for (const schemaKey of SCHEMA_KEYS) {
|
|
1190
|
+
const schemaExpression = leaf.schemas[schemaKey];
|
|
1191
|
+
if (!schemaExpression) continue;
|
|
1192
|
+
schemas[schemaKey] = resolveSchemaMetadata(schemaExpression, ctx);
|
|
1193
|
+
}
|
|
1194
|
+
sourceByLeaf[key] = { definition, schemas };
|
|
1195
|
+
}
|
|
1196
|
+
return {
|
|
1197
|
+
sourceByLeaf,
|
|
1198
|
+
tsconfigPath: parsedConfig.resolvedTsconfig
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
|
|
752
1202
|
// src/export/exportFinalizedLeaves.ts
|
|
753
1203
|
function isRegistry(value) {
|
|
754
1204
|
return typeof value === "object" && value !== null && "all" in value && "byKey" in value;
|
|
@@ -756,10 +1206,11 @@ function isRegistry(value) {
|
|
|
756
1206
|
function getLeaves(input) {
|
|
757
1207
|
return isRegistry(input) ? input.all : input;
|
|
758
1208
|
}
|
|
759
|
-
function buildMeta() {
|
|
1209
|
+
function buildMeta(sourceExtraction) {
|
|
760
1210
|
return {
|
|
761
1211
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
762
1212
|
description: "Finalized RRRoutes leaves export with serialized schemas and flattened schema paths for downstream processing.",
|
|
1213
|
+
sourceExtraction,
|
|
763
1214
|
fieldCatalog: {
|
|
764
1215
|
leaf: ["key", "method", "path", "cfg"],
|
|
765
1216
|
cfg: [
|
|
@@ -816,20 +1267,20 @@ ${htmlTemplate}`;
|
|
|
816
1267
|
}
|
|
817
1268
|
async function resolveViewerTemplatePath(viewerTemplateFile) {
|
|
818
1269
|
if (viewerTemplateFile) {
|
|
819
|
-
const resolved =
|
|
1270
|
+
const resolved = path2.resolve(viewerTemplateFile);
|
|
820
1271
|
await fs.access(resolved);
|
|
821
1272
|
return resolved;
|
|
822
1273
|
}
|
|
823
1274
|
const candidates = [
|
|
824
|
-
|
|
1275
|
+
path2.resolve(
|
|
825
1276
|
process.cwd(),
|
|
826
1277
|
"node_modules/@emeryld/rrroutes-contract/tools/finalized-leaves-viewer.html"
|
|
827
1278
|
),
|
|
828
|
-
|
|
1279
|
+
path2.resolve(
|
|
829
1280
|
process.cwd(),
|
|
830
1281
|
"tools/finalized-leaves-viewer.html"
|
|
831
1282
|
),
|
|
832
|
-
|
|
1283
|
+
path2.resolve(
|
|
833
1284
|
process.cwd(),
|
|
834
1285
|
"packages/contract/tools/finalized-leaves-viewer.html"
|
|
835
1286
|
)
|
|
@@ -844,8 +1295,8 @@ async function resolveViewerTemplatePath(viewerTemplateFile) {
|
|
|
844
1295
|
return void 0;
|
|
845
1296
|
}
|
|
846
1297
|
async function writeJsonExport(payload, outFile) {
|
|
847
|
-
const resolved =
|
|
848
|
-
await fs.mkdir(
|
|
1298
|
+
const resolved = path2.resolve(outFile);
|
|
1299
|
+
await fs.mkdir(path2.dirname(resolved), { recursive: true });
|
|
849
1300
|
await fs.writeFile(resolved, `${JSON.stringify(payload, null, 2)}
|
|
850
1301
|
`, "utf8");
|
|
851
1302
|
return resolved;
|
|
@@ -854,13 +1305,13 @@ async function writeBakedHtmlExport(payload, htmlFile, viewerTemplateFile) {
|
|
|
854
1305
|
const templatePath = await resolveViewerTemplatePath(viewerTemplateFile);
|
|
855
1306
|
const template = templatePath ? await fs.readFile(templatePath, "utf8") : DEFAULT_VIEWER_TEMPLATE;
|
|
856
1307
|
const baked = injectPayloadIntoViewerHtml(template, payload);
|
|
857
|
-
const resolved =
|
|
858
|
-
await fs.mkdir(
|
|
1308
|
+
const resolved = path2.resolve(htmlFile);
|
|
1309
|
+
await fs.mkdir(path2.dirname(resolved), { recursive: true });
|
|
859
1310
|
await fs.writeFile(resolved, baked, "utf8");
|
|
860
1311
|
return resolved;
|
|
861
1312
|
}
|
|
862
1313
|
async function openHtmlInBrowser(filePath) {
|
|
863
|
-
const resolved =
|
|
1314
|
+
const resolved = path2.resolve(filePath);
|
|
864
1315
|
const platform = process.platform;
|
|
865
1316
|
if (platform === "darwin") {
|
|
866
1317
|
spawn("open", [resolved], { detached: true, stdio: "ignore" }).unref();
|
|
@@ -904,10 +1355,46 @@ async function exportFinalizedLeaves(input, options = {}) {
|
|
|
904
1355
|
const schemaFlatByLeaf = Object.fromEntries(
|
|
905
1356
|
serializedLeaves.map((leaf) => [leaf.key, flattenLeafSchemas(leaf)])
|
|
906
1357
|
);
|
|
1358
|
+
const sourceByLeaf = {};
|
|
1359
|
+
let sourceExtraction;
|
|
1360
|
+
if (options.includeSource) {
|
|
1361
|
+
const modulePath = options.sourceModulePath;
|
|
1362
|
+
const exportName = options.sourceExportName ?? "leaves";
|
|
1363
|
+
if (modulePath) {
|
|
1364
|
+
const extracted = extractLeafSourceByAst({
|
|
1365
|
+
modulePath,
|
|
1366
|
+
exportName,
|
|
1367
|
+
tsconfigPath: options.tsconfigPath
|
|
1368
|
+
});
|
|
1369
|
+
const allowedLeafKeys = new Set(serializedLeaves.map((leaf) => leaf.key));
|
|
1370
|
+
for (const [key, source] of Object.entries(extracted.sourceByLeaf)) {
|
|
1371
|
+
if (allowedLeafKeys.has(key)) {
|
|
1372
|
+
sourceByLeaf[key] = source;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
sourceExtraction = {
|
|
1376
|
+
mode: "ast",
|
|
1377
|
+
enabled: true,
|
|
1378
|
+
modulePath: path2.resolve(modulePath),
|
|
1379
|
+
exportName,
|
|
1380
|
+
tsconfigPath: extracted.tsconfigPath,
|
|
1381
|
+
resolvedLeafCount: Object.keys(sourceByLeaf).length
|
|
1382
|
+
};
|
|
1383
|
+
} else {
|
|
1384
|
+
sourceExtraction = {
|
|
1385
|
+
mode: "ast",
|
|
1386
|
+
enabled: false,
|
|
1387
|
+
exportName,
|
|
1388
|
+
tsconfigPath: options.tsconfigPath ? path2.resolve(options.tsconfigPath) : void 0,
|
|
1389
|
+
resolvedLeafCount: 0
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
907
1393
|
const payload = {
|
|
908
|
-
_meta: buildMeta(),
|
|
1394
|
+
_meta: buildMeta(sourceExtraction),
|
|
909
1395
|
leaves: serializedLeaves,
|
|
910
|
-
schemaFlatByLeaf
|
|
1396
|
+
schemaFlatByLeaf,
|
|
1397
|
+
sourceByLeaf: options.includeSource && Object.keys(sourceByLeaf).length > 0 ? sourceByLeaf : void 0
|
|
911
1398
|
};
|
|
912
1399
|
if (options.outFile || options.htmlFile) {
|
|
913
1400
|
await writeFinalizedLeavesExport(payload, {
|
|
@@ -921,14 +1408,19 @@ async function exportFinalizedLeaves(input, options = {}) {
|
|
|
921
1408
|
}
|
|
922
1409
|
|
|
923
1410
|
// src/export/exportFinalizedLeaves.cli.ts
|
|
924
|
-
import
|
|
1411
|
+
import path3 from "path";
|
|
925
1412
|
import { pathToFileURL } from "url";
|
|
926
1413
|
function parseFinalizedLeavesCliArgs(argv) {
|
|
927
1414
|
const args = /* @__PURE__ */ new Map();
|
|
1415
|
+
let withSource = false;
|
|
928
1416
|
for (let i = 0; i < argv.length; i += 1) {
|
|
929
1417
|
const key = argv[i];
|
|
930
1418
|
if (key === "--") continue;
|
|
931
1419
|
if (!key.startsWith("--")) continue;
|
|
1420
|
+
if (key === "--with-source") {
|
|
1421
|
+
withSource = true;
|
|
1422
|
+
continue;
|
|
1423
|
+
}
|
|
932
1424
|
const value = argv[i + 1];
|
|
933
1425
|
if (!value || value.startsWith("--")) {
|
|
934
1426
|
throw new Error(`Missing value for ${key}`);
|
|
@@ -943,14 +1435,16 @@ function parseFinalizedLeavesCliArgs(argv) {
|
|
|
943
1435
|
return {
|
|
944
1436
|
modulePath,
|
|
945
1437
|
exportName: args.get("--export") ?? "leaves",
|
|
946
|
-
outFile: args.get("--out") ?? "finalized-leaves.export.json"
|
|
1438
|
+
outFile: args.get("--out") ?? "finalized-leaves.export.json",
|
|
1439
|
+
withSource,
|
|
1440
|
+
tsconfigPath: args.get("--tsconfig") ?? void 0
|
|
947
1441
|
};
|
|
948
1442
|
}
|
|
949
1443
|
async function loadFinalizedLeavesInput({
|
|
950
1444
|
modulePath,
|
|
951
1445
|
exportName
|
|
952
1446
|
}) {
|
|
953
|
-
const resolvedModule =
|
|
1447
|
+
const resolvedModule = path3.resolve(process.cwd(), modulePath);
|
|
954
1448
|
const mod = await import(pathToFileURL(resolvedModule).href);
|
|
955
1449
|
const value = mod[exportName] ?? (mod.default && mod.default[exportName]);
|
|
956
1450
|
if (!value) {
|
|
@@ -961,10 +1455,16 @@ async function loadFinalizedLeavesInput({
|
|
|
961
1455
|
async function runExportFinalizedLeavesCli(argv) {
|
|
962
1456
|
const args = parseFinalizedLeavesCliArgs(argv);
|
|
963
1457
|
const input = await loadFinalizedLeavesInput(args);
|
|
964
|
-
const payload = await exportFinalizedLeaves(input, {
|
|
1458
|
+
const payload = await exportFinalizedLeaves(input, {
|
|
1459
|
+
outFile: args.outFile,
|
|
1460
|
+
includeSource: args.withSource,
|
|
1461
|
+
tsconfigPath: args.tsconfigPath,
|
|
1462
|
+
sourceModulePath: args.modulePath,
|
|
1463
|
+
sourceExportName: args.exportName
|
|
1464
|
+
});
|
|
965
1465
|
return {
|
|
966
1466
|
payload,
|
|
967
|
-
outFile:
|
|
1467
|
+
outFile: path3.resolve(args.outFile)
|
|
968
1468
|
};
|
|
969
1469
|
}
|
|
970
1470
|
export {
|
|
@@ -977,6 +1477,7 @@ export {
|
|
|
977
1477
|
createSchemaIntrospector,
|
|
978
1478
|
defineSocketEvents,
|
|
979
1479
|
exportFinalizedLeaves,
|
|
1480
|
+
extractLeafSourceByAst,
|
|
980
1481
|
finalize,
|
|
981
1482
|
flattenLeafSchemas,
|
|
982
1483
|
flattenSerializableSchema,
|