UNPKG

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
"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