@arcteninc/core 0.0.46 → 0.0.47
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/package.json +2 -1
- package/scripts/cli-extract-types-auto.ts +119 -650
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcteninc/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.47",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"ajv": "^8.17.1",
|
|
47
47
|
"react": "^19.2.0",
|
|
48
48
|
"react-dom": "^19.2.0",
|
|
49
|
+
"ts-json-schema-generator": "^2.4.0",
|
|
49
50
|
"typescript": "^5.9.3",
|
|
50
51
|
"vite": "^7.1.12",
|
|
51
52
|
"vite-plugin-dts": "^4.5.4"
|
|
@@ -9,6 +9,25 @@ import * as fs from 'fs';
|
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import { glob } from 'glob';
|
|
11
11
|
|
|
12
|
+
// Lazy import for ts-json-schema-generator (optional - falls back to custom serializer if not available)
|
|
13
|
+
let tsJsonSchemaGeneratorModule: any = null;
|
|
14
|
+
let generatorAvailable = false;
|
|
15
|
+
|
|
16
|
+
async function ensureGeneratorLoaded(): Promise<boolean> {
|
|
17
|
+
if (generatorAvailable) return true;
|
|
18
|
+
if (tsJsonSchemaGeneratorModule !== null) return false; // Already tried and failed
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
// @ts-ignore - ts-json-schema-generator is optional
|
|
22
|
+
tsJsonSchemaGeneratorModule = await import('ts-json-schema-generator');
|
|
23
|
+
generatorAvailable = true;
|
|
24
|
+
return true;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
tsJsonSchemaGeneratorModule = false; // Mark as failed
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
12
31
|
// JSON Schema compatible format
|
|
13
32
|
interface JsonSchemaProperty {
|
|
14
33
|
type?: string | string[]; // Can be string, array of strings, or omitted for anyOf/oneOf/$ref
|
|
@@ -553,237 +572,36 @@ function resolveImportPath(
|
|
|
553
572
|
return null;
|
|
554
573
|
}
|
|
555
574
|
|
|
556
|
-
// Constants for JSON Schema validation
|
|
557
|
-
// These are actual constants from the JSON Schema specification
|
|
558
|
-
const VALID_JSON_SCHEMA_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'object', 'array', 'null']);
|
|
559
|
-
|
|
560
|
-
// Safety limit for serialization depth (only used as a last resort)
|
|
561
|
-
// Note: Cycle detection via 'visited' Set is the primary protection against infinite recursion
|
|
562
|
-
// This is only a fallback safety net for edge cases
|
|
563
|
-
const MAX_SERIALIZATION_DEPTH = 50; // Increased from 10 - legitimate types can be deeply nested
|
|
564
|
-
|
|
565
|
-
// Note: MAX_TOOL_NAME_LENGTH was removed - we now use proper AST node type checks
|
|
566
|
-
// instead of string length heuristics for better accuracy
|
|
567
|
-
|
|
568
|
-
/**
|
|
569
|
-
* Type-safe accessors for TypeScript internal APIs
|
|
570
|
-
* These use type guards to safely access properties that aren't in the public API
|
|
571
|
-
*/
|
|
572
|
-
|
|
573
|
-
interface BooleanLiteralType extends ts.Type {
|
|
574
|
-
intrinsicName: 'true' | 'false';
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
interface NumberLiteralType extends ts.Type {
|
|
578
|
-
value: number;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
interface StringLiteralType extends ts.Type {
|
|
582
|
-
value: string;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
/**
|
|
586
|
-
* Get boolean literal value from a TypeScript type
|
|
587
|
-
*/
|
|
588
|
-
function getBooleanLiteralValue(type: ts.Type): boolean | null {
|
|
589
|
-
if (type.flags & ts.TypeFlags.BooleanLiteral) {
|
|
590
|
-
const boolType = type as BooleanLiteralType;
|
|
591
|
-
return boolType.intrinsicName === 'true';
|
|
592
|
-
}
|
|
593
|
-
return null;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
575
|
/**
|
|
597
|
-
*
|
|
576
|
+
* Try to get type name and source file for use with ts-json-schema-generator
|
|
598
577
|
*/
|
|
599
|
-
function
|
|
600
|
-
if (type.flags & ts.TypeFlags.NumberLiteral) {
|
|
601
|
-
const numType = type as NumberLiteralType;
|
|
602
|
-
return numType.value;
|
|
603
|
-
}
|
|
604
|
-
return null;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
/**
|
|
608
|
-
* Get string literal value from a TypeScript type
|
|
609
|
-
*/
|
|
610
|
-
function getStringLiteralValue(type: ts.Type): string | null {
|
|
611
|
-
if (type.flags & ts.TypeFlags.StringLiteral) {
|
|
612
|
-
const strType = type as StringLiteralType;
|
|
613
|
-
return strType.value;
|
|
614
|
-
}
|
|
615
|
-
return null;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
/**
|
|
619
|
-
* Get type ID safely (for cycle detection)
|
|
620
|
-
* Uses TypeScript's internal API - this is necessary for cycle detection
|
|
621
|
-
* but we wrap it in a function to centralize the unsafe access
|
|
622
|
-
*/
|
|
623
|
-
function getTypeId(type: ts.Type): number | undefined {
|
|
624
|
-
// TypeScript's Type interface doesn't expose 'id' in public API,
|
|
625
|
-
// but it's available internally and needed for cycle detection.
|
|
626
|
-
// This is a known limitation when working with TypeScript's compiler API.
|
|
627
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
628
|
-
return (type as any).id;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
/**
|
|
632
|
-
* Get a stable name for a type to use in $defs
|
|
633
|
-
* Returns the type name if it's a named type (interface, type alias, class)
|
|
634
|
-
* Otherwise returns a generated name based on type ID
|
|
635
|
-
*/
|
|
636
|
-
function getTypeNameForDefs(
|
|
578
|
+
function getTypeInfoForGenerator(
|
|
637
579
|
type: ts.Type,
|
|
638
580
|
checker: ts.TypeChecker
|
|
639
|
-
): string | null {
|
|
581
|
+
): { typeName: string; sourceFile: string } | null {
|
|
640
582
|
const symbol = type.getSymbol();
|
|
641
|
-
if (symbol)
|
|
642
|
-
const name = symbol.getName();
|
|
643
|
-
if (name && name.length > 0) {
|
|
644
|
-
// Check if it's a named type (not a primitive)
|
|
645
|
-
const declarations = symbol.getDeclarations();
|
|
646
|
-
if (declarations && declarations.length > 0) {
|
|
647
|
-
// Check if it's a type alias, interface, or class
|
|
648
|
-
for (const decl of declarations) {
|
|
649
|
-
if (
|
|
650
|
-
ts.isTypeAliasDeclaration(decl) ||
|
|
651
|
-
ts.isInterfaceDeclaration(decl) ||
|
|
652
|
-
ts.isClassDeclaration(decl)
|
|
653
|
-
) {
|
|
654
|
-
return name;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
// Fallback: try to get a name from typeToString
|
|
662
|
-
const typeString = checker.typeToString(type);
|
|
663
|
-
// If it's a simple type name (not a complex expression), use it
|
|
664
|
-
if (typeString && !typeString.includes('|') && !typeString.includes('&') &&
|
|
665
|
-
!typeString.includes('<') && !typeString.includes('(') &&
|
|
666
|
-
typeString.length < 50 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(typeString)) {
|
|
667
|
-
return typeString;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
return null;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* Serialize a literal type (boolean, number, or string literal) to JSON Schema
|
|
675
|
-
* Returns null if the type is not a literal
|
|
676
|
-
*/
|
|
677
|
-
function serializeLiteralType(type: ts.Type): JsonSchemaProperty | null {
|
|
678
|
-
const boolValue = getBooleanLiteralValue(type);
|
|
679
|
-
if (boolValue !== null) {
|
|
680
|
-
return { type: 'boolean', const: boolValue };
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
const numValue = getNumberLiteralValue(type);
|
|
684
|
-
if (numValue !== null) {
|
|
685
|
-
return { type: 'number', const: numValue };
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
const strValue = getStringLiteralValue(type);
|
|
689
|
-
if (strValue !== null) {
|
|
690
|
-
return { type: 'string', const: strValue };
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
return null;
|
|
694
|
-
}
|
|
583
|
+
if (!symbol) return null;
|
|
695
584
|
|
|
696
|
-
/**
|
|
697
|
-
* Check if a type is a type reference (named type like FilterTreeNode)
|
|
698
|
-
* Uses TypeScript API to determine if it's a named type reference
|
|
699
|
-
*/
|
|
700
|
-
function isTypeReference(type: ts.Type, checker: ts.TypeChecker): boolean {
|
|
701
|
-
const symbol = type.getSymbol();
|
|
702
|
-
if (!symbol) return false;
|
|
703
|
-
|
|
704
585
|
const name = symbol.getName();
|
|
705
|
-
if (name.length === 0) return
|
|
706
|
-
|
|
707
|
-
// Check if it's a primitive JSON Schema type (should not be treated as type reference)
|
|
708
|
-
if (VALID_JSON_SCHEMA_TYPES.has(name.toLowerCase())) {
|
|
709
|
-
return false;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// Check if it's a named type (has declarations and is not a primitive)
|
|
586
|
+
if (!name || name.length === 0) return null;
|
|
587
|
+
|
|
713
588
|
const declarations = symbol.getDeclarations();
|
|
714
|
-
if (!declarations || declarations.length === 0) return
|
|
715
|
-
|
|
716
|
-
//
|
|
717
|
-
// This is more reliable than regex pattern matching
|
|
589
|
+
if (!declarations || declarations.length === 0) return null;
|
|
590
|
+
|
|
591
|
+
// Find the first declaration that's a type alias, interface, or class
|
|
718
592
|
for (const decl of declarations) {
|
|
719
593
|
if (
|
|
720
594
|
ts.isTypeAliasDeclaration(decl) ||
|
|
721
595
|
ts.isInterfaceDeclaration(decl) ||
|
|
722
|
-
ts.isClassDeclaration(decl)
|
|
723
|
-
ts.isEnumDeclaration(decl)
|
|
596
|
+
ts.isClassDeclaration(decl)
|
|
724
597
|
) {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
return false;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
/**
|
|
736
|
-
* Check if a schema is a fallback 'any' (not a real schema)
|
|
737
|
-
* Uses type checking instead of string length heuristics
|
|
738
|
-
*/
|
|
739
|
-
function isFallbackAnySchema(
|
|
740
|
-
schema: JsonSchemaProperty,
|
|
741
|
-
type: ts.Type,
|
|
742
|
-
checker: ts.TypeChecker
|
|
743
|
-
): boolean {
|
|
744
|
-
// Empty schema {} means "any value" in JSON Schema draft 2020-12
|
|
745
|
-
// If schema is empty (no keys), it's a fallback "any" schema
|
|
746
|
-
if (Object.keys(schema).length === 0) {
|
|
747
|
-
return true;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// If it has properties, anyOf, or items, it's a real schema
|
|
751
|
-
if (schema.properties || schema.anyOf || schema.items) {
|
|
752
|
-
return false;
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
// If it has a type property (and it's not 'any'), it's a real schema
|
|
756
|
-
if (schema.type && schema.type !== 'any') {
|
|
757
|
-
return false;
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
// Check if it's actually a type reference we couldn't resolve
|
|
761
|
-
// (not a primitive, not an object we processed, etc.)
|
|
762
|
-
return isTypeReference(type, checker);
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
/**
|
|
766
|
-
* Resolve type references (type aliases, interfaces) to their actual types
|
|
767
|
-
*/
|
|
768
|
-
function resolveTypeReference(
|
|
769
|
-
type: ts.Type,
|
|
770
|
-
checker: ts.TypeChecker
|
|
771
|
-
): ts.Type | null {
|
|
772
|
-
const symbol = type.getSymbol();
|
|
773
|
-
if (!symbol) return null;
|
|
774
|
-
|
|
775
|
-
const declarations = symbol.getDeclarations();
|
|
776
|
-
if (!declarations || declarations.length === 0) return null;
|
|
777
|
-
|
|
778
|
-
// Try to get the actual type from the declaration
|
|
779
|
-
for (const decl of declarations) {
|
|
780
|
-
if (ts.isTypeAliasDeclaration(decl) && decl.type) {
|
|
781
|
-
// Resolve the type alias
|
|
782
|
-
return checker.getTypeFromTypeNode(decl.type);
|
|
783
|
-
}
|
|
784
|
-
if (ts.isInterfaceDeclaration(decl)) {
|
|
785
|
-
// Get the interface type
|
|
786
|
-
return checker.getTypeAtLocation(decl);
|
|
598
|
+
const sourceFile = decl.getSourceFile();
|
|
599
|
+
if (sourceFile) {
|
|
600
|
+
return {
|
|
601
|
+
typeName: name,
|
|
602
|
+
sourceFile: sourceFile.fileName,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
787
605
|
}
|
|
788
606
|
}
|
|
789
607
|
|
|
@@ -791,454 +609,104 @@ function resolveTypeReference(
|
|
|
791
609
|
}
|
|
792
610
|
|
|
793
611
|
/**
|
|
794
|
-
* Serialize a TypeScript type to JSON Schema format
|
|
795
|
-
* Returns: { isOptional, schema }
|
|
612
|
+
* Serialize a TypeScript type to JSON Schema format using ts-json-schema-generator
|
|
613
|
+
* Returns: { isOptional, schema } or null if generator not available
|
|
614
|
+
* Note: This is async and should only be called at top level
|
|
796
615
|
*/
|
|
797
|
-
function
|
|
616
|
+
async function serializeTypeWithGenerator(
|
|
798
617
|
type: ts.Type,
|
|
799
618
|
checker: ts.TypeChecker,
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
defs?: Record<string, JsonSchemaProperty
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
const
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
const typeId = getTypeId(type);
|
|
812
|
-
|
|
813
|
-
// Handle recursive types with $defs support
|
|
814
|
-
if (typeId !== undefined && visited.has(typeId)) {
|
|
815
|
-
// Check if this is a named type that we can reference
|
|
816
|
-
if (defs) {
|
|
817
|
-
const typeName = getTypeNameForDefs(type, checker);
|
|
818
|
-
if (typeName) {
|
|
819
|
-
// If the definition already exists, use $ref
|
|
820
|
-
if (defs[typeName]) {
|
|
821
|
-
return { isOptional: false, schema: { $ref: `#/$defs/${typeName}` } };
|
|
822
|
-
}
|
|
823
|
-
// If we're currently building this definition, use $ref (circular reference)
|
|
824
|
-
if (defsVisited.has(typeId)) {
|
|
825
|
-
return { isOptional: false, schema: { $ref: `#/$defs/${typeName}` } };
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
// Fallback: empty schema for anonymous recursive types
|
|
830
|
-
return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
|
|
619
|
+
program: ts.Program,
|
|
620
|
+
configPath: string | undefined,
|
|
621
|
+
defs?: Record<string, JsonSchemaProperty>
|
|
622
|
+
): Promise<{ isOptional: boolean; schema: JsonSchemaProperty } | null> {
|
|
623
|
+
// Try to load generator if not already loaded
|
|
624
|
+
const generatorLoaded = await ensureGeneratorLoaded();
|
|
625
|
+
if (!generatorLoaded || !tsJsonSchemaGeneratorModule) {
|
|
626
|
+
return null;
|
|
831
627
|
}
|
|
832
628
|
|
|
833
|
-
|
|
834
|
-
if (
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
if (type.flags & ts.TypeFlags.Undefined || type.flags & ts.TypeFlags.Void) {
|
|
851
|
-
return { isOptional: true, schema: { type: 'null' } };
|
|
852
|
-
}
|
|
629
|
+
const typeInfo = getTypeInfoForGenerator(type, checker);
|
|
630
|
+
if (!typeInfo) return null;
|
|
631
|
+
|
|
632
|
+
try {
|
|
633
|
+
// Create generator config
|
|
634
|
+
const generatorConfig: any = {
|
|
635
|
+
path: typeInfo.sourceFile,
|
|
636
|
+
tsconfig: configPath || 'tsconfig.json',
|
|
637
|
+
type: typeInfo.typeName,
|
|
638
|
+
expose: 'all',
|
|
639
|
+
jsDoc: 'extended',
|
|
640
|
+
topRef: true,
|
|
641
|
+
skipTypeCheck: true,
|
|
642
|
+
additionalProperties: false,
|
|
643
|
+
};
|
|
853
644
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
const types = type.types;
|
|
857
|
-
|
|
858
|
-
// Separate null, undefined, and other types
|
|
859
|
-
const hasNull = types.some(t => t.flags & ts.TypeFlags.Null);
|
|
860
|
-
const hasUndefined = types.some(t => t.flags & ts.TypeFlags.Undefined);
|
|
861
|
-
const nonNullUndefinedTypes = types.filter(
|
|
862
|
-
t => !(t.flags & ts.TypeFlags.Null) && !(t.flags & ts.TypeFlags.Undefined)
|
|
863
|
-
);
|
|
864
|
-
|
|
865
|
-
// Check if it's a string literal union (enum) - possibly with null/undefined
|
|
866
|
-
const stringLiterals = nonNullUndefinedTypes
|
|
867
|
-
.map(t => getStringLiteralValue(t))
|
|
868
|
-
.filter((v): v is string => v !== null);
|
|
869
|
-
|
|
870
|
-
// If all non-null/undefined types are string literals, use enum
|
|
871
|
-
if (stringLiterals.length > 0 && stringLiterals.length === nonNullUndefinedTypes.length) {
|
|
872
|
-
const enumSchema: JsonSchemaProperty = { type: 'string', enum: stringLiterals };
|
|
873
|
-
|
|
874
|
-
// If null or undefined is also present, wrap in anyOf
|
|
875
|
-
if (hasNull || hasUndefined) {
|
|
876
|
-
return {
|
|
877
|
-
isOptional: false,
|
|
878
|
-
schema: { anyOf: [enumSchema, { type: 'null' }] }
|
|
879
|
-
};
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
return {
|
|
883
|
-
isOptional: false,
|
|
884
|
-
schema: enumSchema
|
|
885
|
-
};
|
|
886
|
-
}
|
|
645
|
+
const generator = tsJsonSchemaGeneratorModule.createGenerator(generatorConfig);
|
|
646
|
+
const schema = generator.createSchema(typeInfo.typeName);
|
|
887
647
|
|
|
888
|
-
//
|
|
889
|
-
if (
|
|
890
|
-
|
|
891
|
-
return { isOptional: true, schema: result.schema };
|
|
648
|
+
// Extract $defs if present and merge into our defs
|
|
649
|
+
if (schema.$defs && defs) {
|
|
650
|
+
Object.assign(defs, schema.$defs);
|
|
892
651
|
}
|
|
893
652
|
|
|
894
|
-
//
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
// Add null if present
|
|
898
|
-
if (hasNull) {
|
|
899
|
-
anyOf.push({ type: 'null' });
|
|
900
|
-
}
|
|
653
|
+
// Remove $defs from the schema itself (we'll add it at the top level)
|
|
654
|
+
const { $defs, ...schemaWithoutDefs } = schema as any;
|
|
901
655
|
|
|
902
|
-
//
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
}
|
|
656
|
+
// Handle optional (check if type includes undefined)
|
|
657
|
+
const isOptional = type.flags & ts.TypeFlags.Undefined ||
|
|
658
|
+
(type.isUnion() && type.types.some(t => t.flags & ts.TypeFlags.Undefined));
|
|
906
659
|
|
|
907
|
-
// Process non-null/undefined types
|
|
908
|
-
for (const unionType of nonNullUndefinedTypes) {
|
|
909
|
-
// Check if it's a primitive type
|
|
910
|
-
if (unionType.flags & ts.TypeFlags.String) {
|
|
911
|
-
anyOf.push({ type: 'string' });
|
|
912
|
-
} else if (unionType.flags & ts.TypeFlags.Number) {
|
|
913
|
-
anyOf.push({ type: 'number' });
|
|
914
|
-
} else if (unionType.flags & ts.TypeFlags.Boolean) {
|
|
915
|
-
anyOf.push({ type: 'boolean' });
|
|
916
|
-
} else {
|
|
917
|
-
// Try literal types first (using helper function)
|
|
918
|
-
const literalSchema = serializeLiteralType(unionType);
|
|
919
|
-
if (literalSchema) {
|
|
920
|
-
anyOf.push(literalSchema);
|
|
921
|
-
} else if (checker.isArrayType(unionType)) {
|
|
922
|
-
// Handle array types
|
|
923
|
-
const typeArgs = (unionType as ts.TypeReference).typeArguments;
|
|
924
|
-
if (typeArgs && typeArgs.length > 0) {
|
|
925
|
-
const itemResult = serializeType(typeArgs[0], checker, visited, depth + 1, defs, defsVisited);
|
|
926
|
-
anyOf.push({ type: 'array', items: itemResult.schema });
|
|
927
|
-
} else {
|
|
928
|
-
anyOf.push({ type: 'array' });
|
|
929
|
-
}
|
|
930
|
-
} else {
|
|
931
|
-
// Try to serialize the type (handles objects, type references, etc.)
|
|
932
|
-
// This will recursively resolve type aliases and interfaces
|
|
933
|
-
try {
|
|
934
|
-
const refResult = serializeType(unionType, checker, visited, depth + 1, defs, defsVisited);
|
|
935
|
-
|
|
936
|
-
// Check if we got a valid schema (not just 'any' fallback)
|
|
937
|
-
if (!isFallbackAnySchema(refResult.schema, unionType, checker)) {
|
|
938
|
-
anyOf.push(refResult.schema);
|
|
939
|
-
} else {
|
|
940
|
-
// Try to resolve type reference before giving up
|
|
941
|
-
const resolvedType = resolveTypeReference(unionType, checker);
|
|
942
|
-
if (resolvedType) {
|
|
943
|
-
try {
|
|
944
|
-
const resolvedResult = serializeType(resolvedType, checker, visited, depth + 1, defs, defsVisited);
|
|
945
|
-
if (!isFallbackAnySchema(resolvedResult.schema, resolvedType, checker)) {
|
|
946
|
-
anyOf.push(resolvedResult.schema);
|
|
947
|
-
} else {
|
|
948
|
-
anyOf.push({}); // Empty schema = any value (JSON Schema draft 2020-12)
|
|
949
|
-
}
|
|
950
|
-
} catch (error) {
|
|
951
|
-
anyOf.push({}); // Empty schema = any value (JSON Schema draft 2020-12)
|
|
952
|
-
}
|
|
953
|
-
} else {
|
|
954
|
-
// It's a type reference we couldn't fully resolve - use empty schema (any value)
|
|
955
|
-
anyOf.push({}); // Empty schema = any value (JSON Schema draft 2020-12)
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
} catch (error) {
|
|
959
|
-
// Fallback for unresolvable types
|
|
960
|
-
const unionTypeString = checker.typeToString(unionType);
|
|
961
|
-
console.warn(`Warning: Could not serialize union type: ${unionTypeString}`, error);
|
|
962
|
-
anyOf.push({}); // Empty schema = any value (JSON Schema draft 2020-12)
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
// If we only have one type (plus null/undefined), simplify
|
|
969
|
-
if (anyOf.length === 1) {
|
|
970
|
-
return { isOptional: hasNull || hasUndefined, schema: anyOf[0] };
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
// If we have multiple types, use anyOf
|
|
974
|
-
if (anyOf.length > 1) {
|
|
975
|
-
// Remove duplicate null entries
|
|
976
|
-
const uniqueAnyOf = anyOf.filter((schema, index, self) => {
|
|
977
|
-
if (schema.type === 'null') {
|
|
978
|
-
return index === self.findIndex(s => s.type === 'null');
|
|
979
|
-
}
|
|
980
|
-
return true;
|
|
981
|
-
});
|
|
982
|
-
|
|
983
|
-
return {
|
|
984
|
-
isOptional: false,
|
|
985
|
-
schema: { anyOf: uniqueAnyOf }
|
|
986
|
-
};
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// Fallback
|
|
990
660
|
return {
|
|
991
|
-
isOptional:
|
|
992
|
-
schema:
|
|
661
|
+
isOptional: !!isOptional,
|
|
662
|
+
schema: schemaWithoutDefs as JsonSchemaProperty,
|
|
993
663
|
};
|
|
664
|
+
} catch (error) {
|
|
665
|
+
// If generator fails, return null (will result in empty schema)
|
|
666
|
+
return null;
|
|
994
667
|
}
|
|
668
|
+
}
|
|
995
669
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
const types = type.types;
|
|
999
|
-
const allProperties: Record<string, JsonSchemaProperty> = {};
|
|
1000
|
-
const allRequired: string[] = [];
|
|
1001
|
-
|
|
1002
|
-
for (const intersectionType of types) {
|
|
1003
|
-
const result = serializeType(intersectionType, checker, visited, depth + 1, defs, defsVisited);
|
|
1004
|
-
if (result.schema.type === 'object' && result.schema.properties) {
|
|
1005
|
-
Object.assign(allProperties, result.schema.properties);
|
|
1006
|
-
if (result.schema.required) {
|
|
1007
|
-
allRequired.push(...result.schema.required);
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
670
|
+
// Cache for generator results to avoid repeated calls
|
|
671
|
+
const generatorCache = new Map<string, { isOptional: boolean; schema: JsonSchemaProperty }>();
|
|
1011
672
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
673
|
+
/**
|
|
674
|
+
* Serialize a TypeScript type to JSON Schema format using ts-json-schema-generator
|
|
675
|
+
* Only works for named types (interfaces, type aliases, classes)
|
|
676
|
+
* Returns empty schema for anonymous types
|
|
677
|
+
*/
|
|
678
|
+
async function serializeType(
|
|
679
|
+
type: ts.Type,
|
|
680
|
+
checker: ts.TypeChecker,
|
|
681
|
+
program: ts.Program | undefined,
|
|
682
|
+
configPath: string | undefined,
|
|
683
|
+
defs?: Record<string, JsonSchemaProperty>
|
|
684
|
+
): Promise<{ isOptional: boolean; schema: JsonSchemaProperty }> {
|
|
685
|
+
if (!program || !configPath) {
|
|
686
|
+
return { isOptional: false, schema: {} }; // Empty schema = any value
|
|
1022
687
|
}
|
|
1023
688
|
|
|
1024
|
-
|
|
1025
|
-
if (
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
const itemResult = serializeType(typeArgs[0], checker, visited, depth + 1, defs, defsVisited);
|
|
1029
|
-
return {
|
|
1030
|
-
isOptional: false,
|
|
1031
|
-
schema: { type: 'array', items: itemResult.schema }
|
|
1032
|
-
};
|
|
1033
|
-
}
|
|
1034
|
-
return { isOptional: false, schema: { type: 'array' } };
|
|
689
|
+
const typeInfo = getTypeInfoForGenerator(type, checker);
|
|
690
|
+
if (!typeInfo) {
|
|
691
|
+
// Anonymous/inline type - can't serialize with generator
|
|
692
|
+
return { isOptional: false, schema: {} }; // Empty schema = any value
|
|
1035
693
|
}
|
|
1036
694
|
|
|
1037
|
-
//
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
const properties: Record<string, JsonSchemaProperty> = {};
|
|
1044
|
-
const required: string[] = [];
|
|
1045
|
-
const props = checker.getPropertiesOfType(type);
|
|
1046
|
-
|
|
1047
|
-
// Check if this is a named type that should go in $defs
|
|
1048
|
-
const typeName = defs ? getTypeNameForDefs(type, checker) : null;
|
|
1049
|
-
const shouldUseDefs = defs && typeName && props.length > 0;
|
|
1050
|
-
|
|
1051
|
-
if (shouldUseDefs && typeId !== undefined) {
|
|
1052
|
-
// Check if we're already building this definition
|
|
1053
|
-
if (defsVisited.has(typeId)) {
|
|
1054
|
-
// Circular reference - return $ref
|
|
1055
|
-
return { isOptional: false, schema: { $ref: `#/$defs/${typeName}` } };
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
// Check if definition already exists
|
|
1059
|
-
if (defs[typeName]) {
|
|
1060
|
-
return { isOptional: false, schema: { $ref: `#/$defs/${typeName}` } };
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
// Start building the definition
|
|
1064
|
-
defsVisited.add(typeId);
|
|
1065
|
-
// Create placeholder in defs (will be filled below)
|
|
1066
|
-
defs[typeName] = { type: 'object', properties: {}, required: [] };
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
if (props.length > 0) {
|
|
1070
|
-
for (const prop of props) {
|
|
1071
|
-
try {
|
|
1072
|
-
const propDeclaration = prop.valueDeclaration || prop.declarations?.[0];
|
|
1073
|
-
|
|
1074
|
-
if (!propDeclaration) {
|
|
1075
|
-
// Skip properties without declarations
|
|
1076
|
-
continue;
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
const propType = checker.getTypeOfSymbolAtLocation(prop, propDeclaration);
|
|
1080
|
-
const isOptional = (prop.flags & ts.SymbolFlags.Optional) !== 0;
|
|
1081
|
-
|
|
1082
|
-
const propResult = serializeType(propType, checker, visited, depth + 1, defs, defsVisited);
|
|
1083
|
-
properties[prop.name] = propResult.schema;
|
|
1084
|
-
|
|
1085
|
-
// Track required properties
|
|
1086
|
-
if (!isOptional && !propResult.isOptional) {
|
|
1087
|
-
required.push(prop.name);
|
|
1088
|
-
}
|
|
1089
|
-
} catch (error) {
|
|
1090
|
-
// Skip properties that can't be serialized
|
|
1091
|
-
console.warn(`Warning: Could not serialize property ${prop.name}`, error);
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
// Extract index signatures ([key: string]: T)
|
|
1096
|
-
const indexSignatures = checker.getIndexInfosOfType(type);
|
|
1097
|
-
if (indexSignatures && indexSignatures.length > 0) {
|
|
1098
|
-
try {
|
|
1099
|
-
const indexInfo = indexSignatures[0];
|
|
1100
|
-
if (indexInfo.declaration) {
|
|
1101
|
-
const indexType = checker.getTypeAtLocation(indexInfo.declaration);
|
|
1102
|
-
const indexResult = serializeType(indexType, checker, visited, depth + 1, defs, defsVisited);
|
|
1103
|
-
// JSON Schema uses additionalProperties for index signatures
|
|
1104
|
-
if (indexResult.schema.type !== 'any' || indexResult.schema.properties) {
|
|
1105
|
-
// Only set if it's a meaningful type (not just 'any' fallback)
|
|
1106
|
-
if (!isFallbackAnySchema(indexResult.schema, indexType, checker)) {
|
|
1107
|
-
properties['[key: string]'] = indexResult.schema;
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
} catch (error) {
|
|
1112
|
-
// Index signature extraction failed, continue without it
|
|
1113
|
-
console.warn(`Warning: Could not extract index signature`, error);
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
const schema: JsonSchemaProperty = {
|
|
1118
|
-
type: 'object',
|
|
1119
|
-
properties
|
|
1120
|
-
};
|
|
1121
|
-
|
|
1122
|
-
if (required.length > 0) {
|
|
1123
|
-
schema.required = required;
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
// If we're building a definition, update it and return $ref
|
|
1127
|
-
if (shouldUseDefs && typeName && defs) {
|
|
1128
|
-
defs[typeName] = schema;
|
|
1129
|
-
defsVisited.delete(typeId!);
|
|
1130
|
-
return { isOptional: false, schema: { $ref: `#/$defs/${typeName}` } };
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
return { isOptional: false, schema };
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
// Fallback - check type flags to determine if it's a valid JSON Schema type
|
|
1138
|
-
// Valid JSON Schema types: string, number, integer, boolean, object, array, null
|
|
1139
|
-
|
|
1140
|
-
// Check if it's an enum type (TypeScript enum)
|
|
1141
|
-
if (type.flags & ts.TypeFlags.Enum) {
|
|
1142
|
-
try {
|
|
1143
|
-
const symbol = type.getSymbol();
|
|
1144
|
-
if (symbol) {
|
|
1145
|
-
const enumMembers = checker.getPropertiesOfType(type);
|
|
1146
|
-
if (enumMembers.length > 0) {
|
|
1147
|
-
// Extract enum values - enum members are typically string or number literals
|
|
1148
|
-
const values: (string | number)[] = [];
|
|
1149
|
-
for (const member of enumMembers) {
|
|
1150
|
-
const memberDeclaration = member.valueDeclaration || member.declarations?.[0];
|
|
1151
|
-
if (memberDeclaration) {
|
|
1152
|
-
const memberType = checker.getTypeOfSymbolAtLocation(member, memberDeclaration);
|
|
1153
|
-
const strValue = getStringLiteralValue(memberType);
|
|
1154
|
-
if (strValue !== null) {
|
|
1155
|
-
values.push(strValue);
|
|
1156
|
-
} else {
|
|
1157
|
-
const numValue = getNumberLiteralValue(memberType);
|
|
1158
|
-
if (numValue !== null) {
|
|
1159
|
-
values.push(numValue);
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
if (values.length > 0) {
|
|
1165
|
-
// Check if all values are strings or all are numbers
|
|
1166
|
-
const allStrings = values.every(v => typeof v === 'string');
|
|
1167
|
-
const allNumbers = values.every(v => typeof v === 'number');
|
|
1168
|
-
if (allStrings) {
|
|
1169
|
-
return { isOptional: false, schema: { type: 'string', enum: values.filter((v): v is string => typeof v === 'string') } };
|
|
1170
|
-
} else if (allNumbers) {
|
|
1171
|
-
const numberValues = values.filter((v): v is number => typeof v === 'number');
|
|
1172
|
-
return { isOptional: false, schema: { type: 'number', enum: numberValues } };
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
} catch (error) {
|
|
1178
|
-
console.warn(`Warning: Could not extract enum values for type: ${typeString}`, error);
|
|
1179
|
-
}
|
|
1180
|
-
// If we can't extract enum values, use empty schema (any value)
|
|
1181
|
-
return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
// Check if it's a type reference (like InternalFilterType.StringFilter)
|
|
1185
|
-
// Type references that aren't objects/arrays/primitives should use 'any'
|
|
1186
|
-
if (type.flags & ts.TypeFlags.Object) {
|
|
1187
|
-
// Already handled above, but check if it's an empty object type
|
|
1188
|
-
const props = checker.getPropertiesOfType(type);
|
|
1189
|
-
if (props.length === 0) {
|
|
1190
|
-
// Empty object or type reference we can't resolve
|
|
1191
|
-
// Check if typeString suggests it's a named type reference
|
|
1192
|
-
if (typeString && typeString.includes('.')) {
|
|
1193
|
-
return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
// Check if typeString is a valid JSON Schema primitive type
|
|
1199
|
-
const lowerTypeString = typeString?.toLowerCase();
|
|
1200
|
-
|
|
1201
|
-
// If it's a valid JSON Schema type, use it (shouldn't happen here, but safety check)
|
|
1202
|
-
if (lowerTypeString && VALID_JSON_SCHEMA_TYPES.has(lowerTypeString)) {
|
|
1203
|
-
// Type assertion is safe here because VALID_JSON_SCHEMA_TYPES validates the value
|
|
1204
|
-
const validType = lowerTypeString as 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array' | 'null';
|
|
1205
|
-
return {
|
|
1206
|
-
isOptional: false,
|
|
1207
|
-
schema: { type: validType }
|
|
1208
|
-
};
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
// Try to resolve type references before giving up
|
|
1212
|
-
const resolvedType = resolveTypeReference(type, checker);
|
|
1213
|
-
if (resolvedType) {
|
|
1214
|
-
try {
|
|
1215
|
-
const resolvedResult = serializeType(resolvedType, checker, visited, depth + 1, defs, defsVisited);
|
|
1216
|
-
if (!isFallbackAnySchema(resolvedResult.schema, resolvedType, checker)) {
|
|
1217
|
-
return resolvedResult;
|
|
1218
|
-
}
|
|
1219
|
-
} catch (error) {
|
|
1220
|
-
// Resolution failed, continue to fallback
|
|
1221
|
-
}
|
|
695
|
+
// Check cache first
|
|
696
|
+
const cacheKey = `${typeInfo.sourceFile}:${typeInfo.typeName}`;
|
|
697
|
+
if (generatorCache.has(cacheKey)) {
|
|
698
|
+
return generatorCache.get(cacheKey)!;
|
|
1222
699
|
}
|
|
1223
700
|
|
|
1224
|
-
//
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
|
|
1230
|
-
}
|
|
1231
|
-
if (type.flags & ts.TypeFlags.Any) {
|
|
1232
|
-
return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
|
|
701
|
+
// Use generator
|
|
702
|
+
const result = await serializeTypeWithGenerator(type, checker, program, configPath, defs);
|
|
703
|
+
if (result) {
|
|
704
|
+
generatorCache.set(cacheKey, result);
|
|
705
|
+
return result;
|
|
1233
706
|
}
|
|
1234
707
|
|
|
1235
|
-
//
|
|
1236
|
-
//
|
|
1237
|
-
// Empty schema {} means "allow any value" in JSON Schema draft 2020-12
|
|
1238
|
-
return {
|
|
1239
|
-
isOptional: false,
|
|
1240
|
-
schema: {} // Empty schema = any value (JSON Schema draft 2020-12)
|
|
1241
|
-
};
|
|
708
|
+
// Generator failed - return empty schema
|
|
709
|
+
return { isOptional: false, schema: {} }; // Empty schema = any value
|
|
1242
710
|
}
|
|
1243
711
|
|
|
1244
712
|
/**
|
|
@@ -1310,11 +778,13 @@ function extractDefaultValue(
|
|
|
1310
778
|
/**
|
|
1311
779
|
* Extract metadata from a function
|
|
1312
780
|
*/
|
|
1313
|
-
function extractFunctionMetadata(
|
|
781
|
+
async function extractFunctionMetadata(
|
|
1314
782
|
node: ts.FunctionDeclaration | ts.VariableDeclaration,
|
|
1315
783
|
checker: ts.TypeChecker,
|
|
1316
|
-
sourceFile: ts.SourceFile
|
|
1317
|
-
|
|
784
|
+
sourceFile: ts.SourceFile,
|
|
785
|
+
program?: ts.Program,
|
|
786
|
+
configPath?: string
|
|
787
|
+
): Promise<FunctionMetadata | null> {
|
|
1318
788
|
let functionNode: ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression | null = null;
|
|
1319
789
|
let functionName: string;
|
|
1320
790
|
|
|
@@ -1435,10 +905,9 @@ function extractFunctionMetadata(
|
|
|
1435
905
|
}
|
|
1436
906
|
}
|
|
1437
907
|
|
|
1438
|
-
// Use
|
|
1439
|
-
//
|
|
1440
|
-
const
|
|
1441
|
-
const result = serializeType(type, checker, visited, 0, defs, defsVisited);
|
|
908
|
+
// Use ts-json-schema-generator for named types only
|
|
909
|
+
// Anonymous types are not supported - agents can't use them anyway
|
|
910
|
+
const result = await serializeType(type, checker, program, configPath, defs);
|
|
1442
911
|
propSchema = result.schema;
|
|
1443
912
|
isOptional = result.isOptional;
|
|
1444
913
|
} else {
|
|
@@ -1579,7 +1048,7 @@ async function autoDiscoverAndExtract(projectRoot: string, outputPath: string) {
|
|
|
1579
1048
|
if (sourceFile) {
|
|
1580
1049
|
const result = findFunctionDefinition(toolName, sourceFile, program);
|
|
1581
1050
|
if (result) {
|
|
1582
|
-
const metadata = extractFunctionMetadata(result.node, checker, result.sourceFile);
|
|
1051
|
+
const metadata = await extractFunctionMetadata(result.node, checker, result.sourceFile, program, configPath);
|
|
1583
1052
|
if (metadata) {
|
|
1584
1053
|
functionsMap[toolName] = metadata;
|
|
1585
1054
|
discoveredFrom.push(path.relative(projectRoot, result.sourceFile.fileName));
|