@arcteninc/core 0.0.47 → 0.0.49

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcteninc/core",
3
- "version": "0.0.47",
3
+ "version": "0.0.49",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -21,9 +21,11 @@ async function ensureGeneratorLoaded(): Promise<boolean> {
21
21
  // @ts-ignore - ts-json-schema-generator is optional
22
22
  tsJsonSchemaGeneratorModule = await import('ts-json-schema-generator');
23
23
  generatorAvailable = true;
24
+ console.log('✓ ts-json-schema-generator loaded successfully');
24
25
  return true;
25
26
  } catch (error) {
26
27
  tsJsonSchemaGeneratorModule = false; // Mark as failed
28
+ console.warn('⚠️ ts-json-schema-generator not available:', error);
27
29
  return false;
28
30
  }
29
31
  }
@@ -574,6 +576,7 @@ function resolveImportPath(
574
576
 
575
577
  /**
576
578
  * Try to get type name and source file for use with ts-json-schema-generator
579
+ * Handles interfaces, type aliases, classes, and enums
577
580
  */
578
581
  function getTypeInfoForGenerator(
579
582
  type: ts.Type,
@@ -588,12 +591,13 @@ function getTypeInfoForGenerator(
588
591
  const declarations = symbol.getDeclarations();
589
592
  if (!declarations || declarations.length === 0) return null;
590
593
 
591
- // Find the first declaration that's a type alias, interface, or class
594
+ // Find the first declaration that's a type alias, interface, class, or enum
592
595
  for (const decl of declarations) {
593
596
  if (
594
597
  ts.isTypeAliasDeclaration(decl) ||
595
598
  ts.isInterfaceDeclaration(decl) ||
596
- ts.isClassDeclaration(decl)
599
+ ts.isClassDeclaration(decl) ||
600
+ ts.isEnumDeclaration(decl)
597
601
  ) {
598
602
  const sourceFile = decl.getSourceFile();
599
603
  if (sourceFile) {
@@ -662,7 +666,8 @@ async function serializeTypeWithGenerator(
662
666
  schema: schemaWithoutDefs as JsonSchemaProperty,
663
667
  };
664
668
  } catch (error) {
665
- // If generator fails, return null (will result in empty schema)
669
+ // If generator fails, log the error for debugging
670
+ console.warn(`⚠️ ts-json-schema-generator failed for type "${typeInfo.typeName}" from ${typeInfo.sourceFile}:`, error);
666
671
  return null;
667
672
  }
668
673
  }
@@ -670,10 +675,120 @@ async function serializeTypeWithGenerator(
670
675
  // Cache for generator results to avoid repeated calls
671
676
  const generatorCache = new Map<string, { isOptional: boolean; schema: JsonSchemaProperty }>();
672
677
 
678
+ /**
679
+ * Custom serializer for anonymous types, enums, and object types
680
+ * This is a fallback when ts-json-schema-generator can't handle the type
681
+ */
682
+ function serializeTypeCustom(
683
+ type: ts.Type,
684
+ checker: ts.TypeChecker,
685
+ defs?: Record<string, JsonSchemaProperty>,
686
+ visited = new Set<number>(),
687
+ depth = 0
688
+ ): { isOptional: boolean; schema: JsonSchemaProperty } {
689
+ // Prevent infinite recursion
690
+ if (depth > 10) {
691
+ return { isOptional: false, schema: {} };
692
+ }
693
+
694
+ const typeId = (type as any).id;
695
+ if (typeId !== undefined && visited.has(typeId)) {
696
+ return { isOptional: false, schema: {} };
697
+ }
698
+
699
+ if (typeId !== undefined) {
700
+ visited.add(typeId);
701
+ }
702
+
703
+ // Handle primitives first (safety net in case serializeType didn't catch them)
704
+ if (type.flags & ts.TypeFlags.String) {
705
+ return { isOptional: false, schema: { type: 'string' } };
706
+ }
707
+ if (type.flags & ts.TypeFlags.Number) {
708
+ return { isOptional: false, schema: { type: 'number' } };
709
+ }
710
+ if (type.flags & ts.TypeFlags.Boolean) {
711
+ return { isOptional: false, schema: { type: 'boolean' } };
712
+ }
713
+ if (type.flags & ts.TypeFlags.Null) {
714
+ return { isOptional: false, schema: { type: 'null' } };
715
+ }
716
+
717
+ // Handle string/number literals (enum values are often represented as literals)
718
+ if (type.flags & ts.TypeFlags.StringLiteral) {
719
+ const literalType = type as ts.StringLiteralType;
720
+ return { isOptional: false, schema: { type: 'string', const: literalType.value } };
721
+ }
722
+ if (type.flags & ts.TypeFlags.NumberLiteral) {
723
+ const literalType = type as ts.NumberLiteralType;
724
+ return { isOptional: false, schema: { type: 'number', const: literalType.value } };
725
+ }
726
+
727
+ // Handle enums (TypeScript enum types)
728
+ const symbol = type.getSymbol();
729
+ if (symbol) {
730
+ const declarations = symbol.getDeclarations();
731
+ if (declarations && declarations.length > 0) {
732
+ const firstDecl = declarations[0];
733
+ if (ts.isEnumDeclaration(firstDecl)) {
734
+ const enumMembers = firstDecl.members.map(m => {
735
+ if (m.initializer && ts.isStringLiteral(m.initializer)) {
736
+ return m.initializer.text;
737
+ } else if (m.initializer && ts.isNumericLiteral(m.initializer)) {
738
+ return Number(m.initializer.text);
739
+ } else if (m.name && ts.isIdentifier(m.name)) {
740
+ // For enum members without initializers, use the name
741
+ return m.name.text;
742
+ }
743
+ return null;
744
+ }).filter((v): v is string | number => v !== null);
745
+
746
+ if (enumMembers.length > 0) {
747
+ return { isOptional: false, schema: { enum: enumMembers } };
748
+ }
749
+ }
750
+ }
751
+ }
752
+
753
+ // Handle object types with properties
754
+ if (type.flags & ts.TypeFlags.Object) {
755
+ const props = checker.getPropertiesOfType(type);
756
+ if (props.length > 0) {
757
+ const properties: Record<string, JsonSchemaProperty> = {};
758
+ const required: string[] = [];
759
+
760
+ for (const prop of props) {
761
+ const propName = prop.getName();
762
+ const propType = checker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration || prop.declarations?.[0] || checker.getDeclaredTypeOfSymbol(prop).symbol?.valueDeclaration || null as any);
763
+ const isOptional = (prop.flags & ts.SymbolFlags.Optional) !== 0;
764
+
765
+ // Recursively serialize the property type
766
+ const propResult = serializeTypeCustom(propType, checker, defs, new Set(visited), depth + 1);
767
+ properties[propName] = propResult.schema;
768
+
769
+ if (!isOptional && !propResult.isOptional) {
770
+ required.push(propName);
771
+ }
772
+ }
773
+
774
+ return {
775
+ isOptional: false,
776
+ schema: {
777
+ type: 'object',
778
+ properties,
779
+ ...(required.length > 0 && { required })
780
+ }
781
+ };
782
+ }
783
+ }
784
+
785
+ // Fallback to empty schema
786
+ return { isOptional: false, schema: {} };
787
+ }
788
+
673
789
  /**
674
790
  * 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
791
+ * Handles primitives, arrays, and named types (interfaces, type aliases, classes, enums)
677
792
  */
678
793
  async function serializeType(
679
794
  type: ts.Type,
@@ -682,14 +797,124 @@ async function serializeType(
682
797
  configPath: string | undefined,
683
798
  defs?: Record<string, JsonSchemaProperty>
684
799
  ): Promise<{ isOptional: boolean; schema: JsonSchemaProperty }> {
800
+ // Handle primitives first (these don't need the generator)
801
+ if (type.flags & ts.TypeFlags.String) {
802
+ return { isOptional: false, schema: { type: 'string' } };
803
+ }
804
+ if (type.flags & ts.TypeFlags.Number) {
805
+ return { isOptional: false, schema: { type: 'number' } };
806
+ }
807
+ if (type.flags & ts.TypeFlags.Boolean) {
808
+ return { isOptional: false, schema: { type: 'boolean' } };
809
+ }
810
+ if (type.flags & ts.TypeFlags.Null) {
811
+ return { isOptional: false, schema: { type: 'null' } };
812
+ }
813
+ if (type.flags & ts.TypeFlags.Undefined || type.flags & ts.TypeFlags.Void) {
814
+ return { isOptional: true, schema: { type: 'null' } };
815
+ }
816
+
817
+ // Handle string/number literals (enum values are often represented as literals)
818
+ if (type.flags & ts.TypeFlags.StringLiteral) {
819
+ const literalType = type as ts.StringLiteralType;
820
+ return { isOptional: false, schema: { type: 'string', const: literalType.value } };
821
+ }
822
+ if (type.flags & ts.TypeFlags.NumberLiteral) {
823
+ const literalType = type as ts.NumberLiteralType;
824
+ return { isOptional: false, schema: { type: 'number', const: literalType.value } };
825
+ }
826
+
827
+ // Handle arrays
828
+ if (checker.isArrayType(type)) {
829
+ const typeArgs = (type as ts.TypeReference).typeArguments;
830
+ if (typeArgs && typeArgs.length > 0) {
831
+ const itemResult = await serializeType(typeArgs[0], checker, program, configPath, defs);
832
+ return {
833
+ isOptional: false,
834
+ schema: { type: 'array', items: itemResult.schema }
835
+ };
836
+ }
837
+ return { isOptional: false, schema: { type: 'array' } };
838
+ }
839
+
840
+ // Handle union types (like string | null)
841
+ if (type.isUnion()) {
842
+ const types = type.types;
843
+ const hasNull = types.some(t => t.flags & ts.TypeFlags.Null);
844
+ const hasUndefined = types.some(t => t.flags & ts.TypeFlags.Undefined);
845
+ const nonNullUndefinedTypes = types.filter(
846
+ t => !(t.flags & ts.TypeFlags.Null) && !(t.flags & ts.TypeFlags.Undefined)
847
+ );
848
+
849
+ // If it's just T | undefined, make it optional
850
+ if (hasUndefined && nonNullUndefinedTypes.length === 1 && !hasNull) {
851
+ const result = await serializeType(nonNullUndefinedTypes[0], checker, program, configPath, defs);
852
+ return { isOptional: true, schema: result.schema };
853
+ }
854
+
855
+ // Check if union is all string/number literals (enum-like union)
856
+ // This handles cases where TypeScript represents enums as unions of literals
857
+ const allStringLiterals = nonNullUndefinedTypes.every(t => t.flags & ts.TypeFlags.StringLiteral);
858
+ const allNumberLiterals = nonNullUndefinedTypes.every(t => t.flags & ts.TypeFlags.NumberLiteral);
859
+
860
+ if (allStringLiterals && nonNullUndefinedTypes.length > 0) {
861
+ const enumValues = nonNullUndefinedTypes.map(t => {
862
+ const literalType = t as ts.StringLiteralType;
863
+ return literalType.value;
864
+ });
865
+ const schema: JsonSchemaProperty = { enum: enumValues };
866
+ return { isOptional: hasNull || hasUndefined, schema };
867
+ }
868
+
869
+ if (allNumberLiterals && nonNullUndefinedTypes.length > 0) {
870
+ const enumValues = nonNullUndefinedTypes.map(t => {
871
+ const literalType = t as ts.NumberLiteralType;
872
+ return literalType.value;
873
+ });
874
+ const schema: JsonSchemaProperty = { enum: enumValues };
875
+ return { isOptional: hasNull || hasUndefined, schema };
876
+ }
877
+
878
+ // Build anyOf for unions
879
+ if (nonNullUndefinedTypes.length > 0 || hasNull) {
880
+ const anyOf: JsonSchemaProperty[] = [];
881
+ if (hasNull || hasUndefined) {
882
+ anyOf.push({ type: 'null' });
883
+ }
884
+ for (const unionType of nonNullUndefinedTypes) {
885
+ const result = await serializeType(unionType, checker, program, configPath, defs);
886
+ anyOf.push(result.schema);
887
+ }
888
+ if (anyOf.length === 1) {
889
+ return { isOptional: hasNull || hasUndefined, schema: anyOf[0] };
890
+ }
891
+ return { isOptional: false, schema: { anyOf } };
892
+ }
893
+ }
894
+
895
+ // Check if it's an enum - handle with custom serializer first
896
+ const symbol = type.getSymbol();
897
+ if (symbol) {
898
+ const declarations = symbol.getDeclarations();
899
+ if (declarations && declarations.length > 0) {
900
+ const firstDecl = declarations[0];
901
+ if (ts.isEnumDeclaration(firstDecl)) {
902
+ // Use custom serializer for enums (more reliable)
903
+ return serializeTypeCustom(type, checker, defs);
904
+ }
905
+ }
906
+ }
907
+
908
+ // For named types (interfaces, type aliases, classes), try generator first
685
909
  if (!program || !configPath) {
686
- return { isOptional: false, schema: {} }; // Empty schema = any value
910
+ // No program/config - use custom serializer
911
+ return serializeTypeCustom(type, checker, defs);
687
912
  }
688
913
 
689
914
  const typeInfo = getTypeInfoForGenerator(type, checker);
690
915
  if (!typeInfo) {
691
- // Anonymous/inline type - can't serialize with generator
692
- return { isOptional: false, schema: {} }; // Empty schema = any value
916
+ // Anonymous/inline type - use custom serializer
917
+ return serializeTypeCustom(type, checker, defs);
693
918
  }
694
919
 
695
920
  // Check cache first
@@ -698,14 +923,15 @@ async function serializeType(
698
923
  return generatorCache.get(cacheKey)!;
699
924
  }
700
925
 
701
- // Use generator
926
+ // Use generator for named types
702
927
  const result = await serializeTypeWithGenerator(type, checker, program, configPath, defs);
703
928
  if (result) {
704
929
  generatorCache.set(cacheKey, result);
705
930
  return result;
706
931
  }
707
932
 
708
- // Generator failed - return empty schema
933
+ // Generator failed - log and return empty schema
934
+ console.warn(`⚠️ Failed to generate schema for type "${typeInfo.typeName}" from ${typeInfo.sourceFile}. Using empty schema.`);
709
935
  return { isOptional: false, schema: {} }; // Empty schema = any value
710
936
  }
711
937