typescript-runtime-schemas
Version:
A TypeScript schema generation tool that extracts Zod schemas from TypeScript source files with runtime validation support. Generate validation schemas directly from your existing TypeScript types with support for computed types and constraint-based valid
531 lines • 20.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TypeParser = void 0;
exports.parseTypeFromSource = parseTypeFromSource;
const ts_morph_1 = require("ts-morph");
const fs_1 = require("fs");
const path_1 = require("path");
/**
* TypeParser class that can parse TypeScript types with constraints
* and convert them to schema objects
*/
class TypeParser {
constructor(sourceCode) {
this.project = new ts_morph_1.Project({
compilerOptions: {
target: ts_morph_1.ScriptTarget.ES2020,
module: ts_morph_1.ModuleKind.CommonJS,
strict: true,
},
useInMemoryFileSystem: true,
});
this.sourceFile = this.project.createSourceFile('temp.ts', sourceCode);
}
/**
* Parse a type by name and return its schema
*/
parseType(typeName) {
// Find the type declaration
const typeDeclaration = this.findTypeDeclaration(typeName);
if (!typeDeclaration) {
throw new Error(`Type ${typeName} not found`);
}
return this.extractPropertiesFromType(typeDeclaration);
}
/**
* Find a type declaration (type alias or interface) by name
*/
findTypeDeclaration(typeName) {
// Look for type alias
const typeAlias = this.sourceFile.getTypeAlias(typeName);
if (typeAlias) {
return typeAlias;
}
// Look for interface
const interfaceDecl = this.sourceFile.getInterface(typeName);
if (interfaceDecl) {
return interfaceDecl;
}
return null;
}
/**
* Extract properties from a type declaration
*/
extractPropertiesFromType(typeDecl) {
const schema = {};
let typeNode;
if (ts_morph_1.Node.isTypeAliasDeclaration(typeDecl)) {
typeNode = typeDecl.getTypeNode();
}
else if (ts_morph_1.Node.isInterfaceDeclaration(typeDecl)) {
typeNode = typeDecl;
}
if (!typeNode) {
return schema;
}
// Handle intersection types at the top level (e.g., { id: number } & SupportsRuntimeValidation)
if (ts_morph_1.Node.isIntersectionTypeNode(typeNode)) {
const intersectionTypes = typeNode.getTypeNodes();
for (const intersectionType of intersectionTypes) {
// Look for type literals (object types) in the intersection
if (ts_morph_1.Node.isTypeLiteral(intersectionType)) {
const members = intersectionType.getMembers();
for (const member of members) {
if (ts_morph_1.Node.isPropertySignature(member)) {
const propertyName = member.getName();
if (!propertyName)
continue;
const isOptional = member.hasQuestionToken();
const memberTypeNode = member.getTypeNode();
if (memberTypeNode) {
const propertySchema = this.parsePropertyType(memberTypeNode, !isOptional);
schema[propertyName] = propertySchema;
}
}
}
}
// Handle type references in intersections (like extending other interfaces)
else if (ts_morph_1.Node.isTypeReference(intersectionType)) {
const typeName = intersectionType.getTypeName().getText();
// Skip SupportsRuntimeValidation and constraint types
if (typeName !== 'SupportsRuntimeValidation' &&
!this.isConstraintType(typeName)) {
// Try to resolve the referenced type
const referencedType = this.findTypeDeclaration(typeName);
if (referencedType) {
const referencedSchema = this.extractPropertiesFromType(referencedType);
// Merge properties from referenced type
Object.assign(schema, referencedSchema);
}
}
}
}
return schema;
}
// Handle type literals and interfaces
let members = [];
if (ts_morph_1.Node.isTypeLiteral(typeNode)) {
members = typeNode.getMembers();
}
else if (ts_morph_1.Node.isInterfaceDeclaration(typeNode)) {
members = typeNode.getMembers();
}
for (const member of members) {
if (ts_morph_1.Node.isPropertySignature(member)) {
const propertyName = member.getName();
if (!propertyName)
continue;
const isOptional = member.hasQuestionToken();
const typeNode = member.getTypeNode();
if (typeNode) {
const propertySchema = this.parsePropertyType(typeNode, !isOptional);
schema[propertyName] = propertySchema;
}
}
}
return schema;
}
/**
* Parse a property type node and extract base type and constraints
*/
parsePropertyType(typeNode, required) {
const schema = {
type: 'object',
required,
constraints: {},
};
// Handle intersection types (e.g., string & Min<10> & Max<80> or string[] & MinLength<3>)
if (ts_morph_1.Node.isIntersectionTypeNode(typeNode)) {
const intersectionTypes = typeNode.getTypeNodes();
for (const intersectionType of intersectionTypes) {
// Check if this is a constraint type reference
if (ts_morph_1.Node.isTypeReference(intersectionType)) {
const typeName = intersectionType.getTypeName().getText();
// Check for constraint types
if (this.isConstraintType(typeName)) {
const constraintInfo = this.parseConstraintType(intersectionType);
if (constraintInfo) {
schema.constraints[constraintInfo.name] = constraintInfo.value;
}
}
}
else {
// This might be a base type (including arrays and objects)
const baseType = this.getBaseType(intersectionType);
if (baseType) {
schema.type = baseType;
// Handle array element types
if (baseType === 'array') {
schema.arrayElementType =
this.parseArrayElementType(intersectionType);
}
// Handle nested object types
else if (baseType === 'object') {
schema.properties = this.parseNestedObjectType(intersectionType);
}
}
}
}
// If we didn't find a base type in intersections, try to infer it
if (schema.type === 'object') {
schema.type = this.inferBaseTypeFromConstraints(schema.constraints);
}
}
else {
// Handle simple types (including arrays and nested objects)
const baseType = this.getBaseType(typeNode);
if (baseType) {
schema.type = baseType;
// Handle array element types
if (baseType === 'array') {
schema.arrayElementType = this.parseArrayElementType(typeNode);
}
// Handle nested object types
else if (baseType === 'object') {
schema.properties = this.parseNestedObjectType(typeNode);
}
}
else {
// Check if it's a nested object (type literal)
if (ts_morph_1.Node.isTypeLiteral(typeNode)) {
schema.type = 'object';
schema.properties = this.parseNestedObjectType(typeNode);
}
// Check if it's a reference to another type
else if (ts_morph_1.Node.isTypeReference(typeNode)) {
const typeName = typeNode.getTypeName().getText();
// Try to resolve the referenced type
const referencedType = this.findTypeDeclaration(typeName);
if (referencedType) {
schema.type = 'object';
schema.properties = this.extractPropertiesFromType(referencedType);
}
}
}
}
return schema;
}
/**
* Check if a type name represents a constraint type
* Dynamically loads constraint types from constraint-types.ts
*/
isConstraintType(typeName) {
const constraintTypes = this.getConstraintTypeNames();
return constraintTypes.includes(typeName);
}
/**
* Get constraint type names from constraint-types.ts file
*/
getConstraintTypeNames() {
try {
// Try to load constraint types from the constraint-types.ts file
// First try the compiled .js version, then fall back to .ts
let constraintTypesPath = (0, path_1.join)(__dirname, 'constraint-types.js');
let constraintTypesContent;
try {
constraintTypesContent = (0, fs_1.readFileSync)(constraintTypesPath, 'utf-8');
}
catch {
// Fallback to .ts file (for development)
constraintTypesPath = (0, path_1.join)(__dirname, 'constraint-types.ts');
constraintTypesContent = (0, fs_1.readFileSync)(constraintTypesPath, 'utf-8');
}
// Extract type names using regex
const typeExportRegex = /export type (\w+)(?:<[^>]*>)?\s*=/g;
const constraintTypes = [];
let match;
while ((match = typeExportRegex.exec(constraintTypesContent)) !== null) {
const typeName = match[1];
// Skip base types
if (typeName !== 'SupportsRuntimeValidation' &&
typeName !== 'Constraint') {
constraintTypes.push(typeName);
}
}
return constraintTypes;
}
catch (error) {
// Fallback to hardcoded constraint types if file cannot be loaded
console.warn('Could not load constraint types from constraint-types.ts, using fallback:', error);
return [
'Min',
'Max',
'MinLength',
'MaxLength',
'Regex',
'Email',
'UUID',
'URL',
'Date',
'Integer',
'Positive',
'Negative',
'NonEmpty',
];
}
}
/**
* Parse a constraint type reference and extract its value
*/
parseConstraintType(typeRef) {
if (!ts_morph_1.Node.isTypeReference(typeRef)) {
return null;
}
const typeName = typeRef.getTypeName().getText();
const typeArgs = typeRef.getTypeArguments();
let value;
// Handle constraints with type arguments (like Min<10>, Max<80>)
if (typeArgs.length > 0) {
const firstArg = typeArgs[0];
// Extract the value from the type argument
if (ts_morph_1.Node.isLiteralTypeNode(firstArg)) {
const literal = firstArg.getLiteral();
if (ts_morph_1.Node.isNumericLiteral(literal)) {
// For numeric literals, get the numeric value directly
value = literal.getLiteralValue();
}
else if (ts_morph_1.Node.isStringLiteral(literal)) {
value = literal.getLiteralValue();
}
}
else {
// For simple numeric types, try to parse the text
const argText = firstArg.getText();
const numValue = parseFloat(argText);
if (!isNaN(numValue)) {
value = numValue;
}
else {
value = argText;
}
}
}
else {
// Handle constraints without type arguments (like Email, UUID, etc.)
// These are boolean flags indicating the constraint is present
value = true;
}
// Convert constraint type name to camelCase
const constraintName = this.toCamelCase(typeName);
return {
name: constraintName,
value: value,
};
}
/**
* Convert a constraint type name to camelCase
*/
toCamelCase(str) {
// Handle special cases first
if (str === 'UUID')
return 'uuid';
if (str === 'URL')
return 'url';
// Convert PascalCase to camelCase
return str.charAt(0).toLowerCase() + str.slice(1);
}
/**
* Get the base type from a type node
*/
getBaseType(typeNode) {
const typeText = typeNode.getText().toLowerCase();
if (typeText === 'string') {
return 'string';
}
else if (typeText === 'number') {
return 'number';
}
else if (typeText === 'boolean') {
return 'boolean';
}
else if (typeText.includes('array') || typeText.endsWith('[]')) {
return 'array';
}
return null;
}
/**
* Infer base type from constraints (fallback method)
*/
inferBaseTypeFromConstraints(constraints) {
// If we have min/max constraints, it could be a number or array
if ('min' in constraints || 'max' in constraints) {
// If we also have length constraints, it's likely an array
if ('minLength' in constraints || 'maxLength' in constraints) {
return 'array';
}
return 'number';
}
// If we have length constraints, it could be a string or array
if ('minLength' in constraints || 'maxLength' in constraints) {
return 'array'; // Assume array if we have length constraints without other context
}
// If we have regex constraints, it's likely a string
if ('regex' in constraints) {
return 'string';
}
// Default to string for most constraint types
return 'string';
}
/**
* Parse array element type from an array type node
*/
parseArrayElementType(typeNode) {
// Handle array type reference (e.g., Array<string>)
if (ts_morph_1.Node.isTypeReference(typeNode)) {
const typeName = typeNode.getTypeName().getText();
if (typeName === 'Array') {
const typeArgs = typeNode.getTypeArguments();
if (typeArgs.length > 0) {
return this.parsePropertyType(typeArgs[0], true);
}
}
}
// Handle array type syntax (e.g., string[])
if (ts_morph_1.Node.isArrayTypeNode(typeNode)) {
const elementTypeNode = typeNode.getElementTypeNode();
return this.parsePropertyType(elementTypeNode, true);
}
// Extract element type from array syntax in text
const typeText = typeNode.getText();
if (typeText.endsWith('[]')) {
const elementTypeName = typeText.slice(0, -2);
// Handle primitive types
if (elementTypeName === 'string') {
return { type: 'string', required: true, constraints: {} };
}
else if (elementTypeName === 'number') {
return { type: 'number', required: true, constraints: {} };
}
else if (elementTypeName === 'boolean') {
return { type: 'boolean', required: true, constraints: {} };
}
else {
// Try to resolve as a custom type
const referencedType = this.findTypeDeclaration(elementTypeName);
if (referencedType) {
return {
type: 'object',
required: true,
constraints: {},
properties: this.extractPropertiesFromType(referencedType),
};
}
}
}
return undefined;
}
/**
* Parse nested object type from a type node
*/
parseNestedObjectType(typeNode) {
// Handle type literals (inline object definitions)
if (ts_morph_1.Node.isTypeLiteral(typeNode)) {
const nestedSchema = {};
const members = typeNode.getMembers();
for (const member of members) {
if (ts_morph_1.Node.isPropertySignature(member)) {
const propertyName = member.getName();
if (!propertyName)
continue;
const isOptional = member.hasQuestionToken();
const memberTypeNode = member.getTypeNode();
if (memberTypeNode) {
const propertySchema = this.parsePropertyType(memberTypeNode, !isOptional);
nestedSchema[propertyName] = propertySchema;
}
}
}
return nestedSchema;
}
// Handle type references to other defined types
if (ts_morph_1.Node.isTypeReference(typeNode)) {
const typeName = typeNode.getTypeName().getText();
const referencedType = this.findTypeDeclaration(typeName);
if (referencedType) {
return this.extractPropertiesFromType(referencedType);
}
}
return undefined;
}
}
exports.TypeParser = TypeParser;
/**
* Convenience function to parse a type from the current file
*/
function parseTypeFromSource(typeName, sourceCode) {
const parser = new TypeParser(sourceCode);
return parser.parseType(typeName);
}
/**
* USAGE EXAMPLES:
*
* Basic usage:
* ```typescript
* const parser = new TypeParser(sourceCode);
* const schema = parser.parseType("MyType");
* ```
*
* Using the convenience function:
* ```typescript
* const schema = parseTypeFromSource("MyType", sourceCode);
* ```
*
* SUPPORTED CONSTRAINT TYPES:
* - Min<N>: Minimum value for numbers
* - Max<N>: Maximum value for numbers
* - MinLength<N>: Minimum length for strings and arrays
* - MaxLength<N>: Maximum length for strings and arrays
* - Email: Email validation flag
* - UUID: UUID validation flag
* - URL: URL validation flag
* - And many more...
*
* ARRAY CONSTRAINT SUPPORT:
* Arrays can have length constraints applied:
* ```typescript
* type MyType = {
* tags: string[] & MinLength<1> & MaxLength<10>;
* scores: number[] & MinLength<3>;
* }
* ```
*
* NESTED OBJECT SUPPORT:
* The parser supports nested objects and arrays of objects:
* ```typescript
* type Person = {
* name: string & MinLength<2>;
* contact: {
* email: string & Email;
* phone?: string;
* };
* };
*
* type Company = {
* employees: Person[] & MinLength<1>;
* settings: {
* theme: string;
* limits: {
* maxUsers: number & Max<1000>;
* };
* };
* };
* ```
*
* EXTENSIBILITY:
* To add new constraint types:
* 1. Define the constraint type: `type MyConstraint<T> = Constraint<T>;`
* 2. Add it to the `isConstraintType` method's array
* 3. The constraint name will be automatically converted to camelCase (e.g., MyConstraint -> myConstraint)
*
* The parser automatically handles:
* - Intersection types (string & Min<10> & Max<80>)
* - Array constraints (string[] & MinLength<1> & MaxLength<10>)
* - Nested objects with full constraint support
* - Arrays of objects with element type parsing
* - Type references to other defined types
* - Inline object definitions
* - Optional properties (property?: type)
* - Base type detection (string, number, boolean, array, object)
* - Constraint value extraction from generic parameters
*/
//# sourceMappingURL=type-parser.js.map