import * as fs from 'fs'; import * as path from 'path'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; /** * Utility class for writing formatted code with proper indentation */ class LineWriter { private lines: string[] = []; private indentLevel: number = 0; private indentStr: string = ' '; // 4 spaces /** * Write a line with current indentation */ writeLine(line: string = ''): void { if (line.trim() === '') { this.lines.push(''); } else { this.lines.push(this.indentStr.repeat(this.indentLevel) + line); } } /** * Write text without adding a new line */ write(text: string): void { if (this.lines.length === 0) { this.lines.push(''); } const lastIndex = this.lines.length - 1; if (this.lines[lastIndex] === '') { this.lines[lastIndex] = this.indentStr.repeat(this.indentLevel) + text; } else { this.lines[lastIndex] += text; } } /** * Increase indentation level */ indent(): void { this.indentLevel++; } /** * Decrease indentation level */ outdent(): void { if (this.indentLevel > 0) { this.indentLevel--; } } /** * Get the generated content as a string */ toString(): string { return this.lines.join('\n'); } /** * Clear all content and reset indentation */ clear(): void { this.lines = []; this.indentLevel = 0; } } /** * Interface definitions based on the metaModel schema */ interface MetaModel { metaData: MetaData; requests: Request[]; notifications: Notification[]; structures: Structure[]; enumerations: Enumeration[]; typeAliases: TypeAlias[]; } interface MetaData { version: string; } interface Request { method: string; result: Type; messageDirection: MessageDirection; params?: Type | Type[]; partialResult?: Type; errorData?: Type; registrationOptions?: Type; registrationMethod?: string; documentation?: string; since?: string; proposed?: boolean; deprecated?: string; } interface Notification { method: string; messageDirection: MessageDirection; params?: Type | Type[]; registrationOptions?: Type; registrationMethod?: string; documentation?: string; since?: string; proposed?: boolean; deprecated?: string; } interface Structure { name: string; properties: Property[]; extends?: Type[]; mixins?: Type[]; documentation?: string; since?: string; proposed?: boolean; deprecated?: string; } interface Property { name: string; type: Type; optional?: boolean; documentation?: string; since?: string; proposed?: boolean; deprecated?: string; } interface Enumeration { name: string; type: EnumerationType; values: EnumerationEntry[]; supportsCustomValues?: boolean; documentation?: string; since?: string; proposed?: boolean; deprecated?: string; } interface EnumerationEntry { name: string; value: string | number; documentation?: string; since?: string; proposed?: boolean; deprecated?: string; } interface EnumerationType { kind: 'base'; name: 'string' | 'integer' | 'uinteger'; } interface TypeAlias { name: string; type: Type; documentation?: string; since?: string; proposed?: boolean; deprecated?: string; } type MessageDirection = 'clientToServer' | 'serverToClient' | 'both'; type Type = | BaseType | ReferenceType | ArrayType | MapType | AndType | OrType | TupleType | StructureLiteralType | StringLiteralType | IntegerLiteralType | BooleanLiteralType; interface BaseType { kind: 'base'; name: | 'URI' | 'DocumentUri' | 'integer' | 'uinteger' | 'decimal' | 'RegExp' | 'string' | 'boolean' | 'null'; } interface ReferenceType { kind: 'reference'; name: string; } interface ArrayType { kind: 'array'; element: Type; } interface MapType { kind: 'map'; key: Type; value: Type; } interface AndType { kind: 'and'; items: Type[]; } interface OrType { kind: 'or'; items: Type[]; } interface TupleType { kind: 'tuple'; items: Type[]; } interface StructureLiteralType { kind: 'literal'; value: StructureLiteral; } interface StructureLiteral { properties: Property[]; documentation?: string; since?: string; proposed?: boolean; deprecated?: string; } interface StringLiteralType { kind: 'stringLiteral'; value: string; } interface IntegerLiteralType { kind: 'integerLiteral'; value: number; } interface BooleanLiteralType { kind: 'booleanLiteral'; value: boolean; } /** * TypeScript types generator for LSP client */ class LSPTypesGenerator { private writer = new LineWriter(); constructor( private readonly metaModelPath: string, private readonly outPath: string ) { } /** * Load and parse the metaModel.json file */ private loadMetaModel(): MetaModel { const content = fs.readFileSync(this.metaModelPath, 'utf-8'); return JSON.parse(content) as MetaModel; } /** * Convert Type to TypeScript type string */ private typeToTypeScript(type: Type): string { switch (type.kind) { case 'base': switch (type.name) { case 'string': case 'DocumentUri': case 'URI': return 'string'; case 'integer': case 'uinteger': case 'decimal': return 'number'; case 'boolean': return 'boolean'; case 'null': return 'null'; case 'RegExp': return 'RegExp'; default: return 'any'; } case 'reference': return type.name; case 'array': return `(${this.typeToTypeScript(type.element)})[]`; case 'map': return `{ [key: ${this.typeToTypeScript(type.key)}]: ${this.typeToTypeScript( type.value )} }`; case 'and': return type.items.map((item) => this.typeToTypeScript(item)).join(' & '); case 'or': return type.items.map((item) => this.typeToTypeScript(item)).join(' | '); case 'tuple': return `[${type.items.map((item) => this.typeToTypeScript(item)).join(', ')}]`; case 'literal': return this.structureLiteralToTypeScript(type.value); case 'stringLiteral': return `'${type.value}'`; case 'integerLiteral': return type.value.toString(); case 'booleanLiteral': return type.value.toString(); default: return 'any'; } } /** * Convert structure literal to TypeScript interface */ private structureLiteralToTypeScript(literal: StructureLiteral): string { const properties = literal.properties.map((prop) => { const optional = prop.optional ? '?' : ''; return `${prop.name}${optional}: ${this.typeToTypeScript(prop.type)}`; }); return `{\n ${properties.join(';\n ')}\n}`; } /** * Generate TypeScript interface for a structure */ private generateStructure(structure: Structure): void { if (structure.documentation) { this.writer.writeLine('/**'); this.writer.writeLine(` * ${structure.documentation.replace(/\n/g, '\n * ')}`); this.writer.writeLine(' */'); } // Build extends clause combining extends and mixins const allParents: string[] = []; if (structure.extends && structure.extends.length > 0) { allParents.push(...structure.extends.map((type) => this.typeToTypeScript(type))); } if (structure.mixins && structure.mixins.length > 0) { allParents.push(...structure.mixins.map((type) => this.typeToTypeScript(type))); } const extendsClause = allParents.length > 0 ? ` extends ${allParents.join(', ')}` : ''; this.writer.writeLine(`export interface ${structure.name}${extendsClause} {`); this.writer.indent(); // Add properties for (const property of structure.properties) { if (property.documentation) { this.writer.writeLine('/**'); this.writer.writeLine(` * ${property.documentation.replace(/\n/g, '\n * ')}`); this.writer.writeLine(' */'); } const optional = property.optional ? '?' : ''; this.writer.writeLine( `${property.name}${optional}: ${this.typeToTypeScript(property.type)};` ); } this.writer.outdent(); this.writer.writeLine('}'); this.writer.writeLine(''); } /** * Generate TypeScript enum for an enumeration */ private generateEnumeration(enumeration: Enumeration): void { if (enumeration.documentation) { this.writer.writeLine('/**'); this.writer.writeLine(` * ${enumeration.documentation.replace(/\n/g, '\n * ')}`); this.writer.writeLine(' */'); } this.writer.writeLine(`export enum ${enumeration.name} {`); this.writer.indent(); for (let i = 0; i < enumeration.values.length; i++) { const entry = enumeration.values[i]; if (entry.documentation) { this.writer.writeLine('/**'); this.writer.writeLine(` * ${entry.documentation.replace(/\n/g, '\n * ')}`); this.writer.writeLine(' */'); } const isLast = i === enumeration.values.length - 1; const comma = isLast ? '' : ','; if (typeof entry.value === 'string') { this.writer.writeLine(`${entry.name} = '${entry.value}'${comma}`); } else { this.writer.writeLine(`${entry.name} = ${entry.value}${comma}`); } } this.writer.outdent(); this.writer.writeLine('}'); this.writer.writeLine(''); } /** * Generate TypeScript type alias */ private generateTypeAlias(typeAlias: TypeAlias): void { if (typeAlias.documentation) { this.writer.writeLine('/**'); this.writer.writeLine(` * ${typeAlias.documentation.replace(/\n/g, '\n * ')}`); this.writer.writeLine(' */'); } this.writer.writeLine( `export type ${typeAlias.name} = ${this.typeToTypeScript(typeAlias.type)};` ); this.writer.writeLine(''); } /** * Generate the Capability class */ private generateCapabilityClass(): void { this.writer.writeLine('/**'); this.writer.writeLine( ' * Represents a capability with its associated method and registration options type' ); this.writer.writeLine(' */'); this.writer.writeLine('export class Capability {'); this.writer.indent(); this.writer.writeLine('constructor(public readonly method: string) {}'); this.writer.outdent(); this.writer.writeLine('}'); this.writer.writeLine(''); } /** * Generate the capabilities map */ private generateCapabilitiesMap(metaModel: MetaModel): void { this.writer.writeLine('/**'); this.writer.writeLine(' * Map of all LSP capabilities with their registration options'); this.writer.writeLine(' */'); this.writer.writeLine('export const capabilities = {'); this.writer.indent(); // Collect all requests and notifications with registration options const itemsWithRegistration: Array<{ method: string; registrationOptions?: Type }> = []; for (const request of metaModel.requests) { if (request.registrationOptions) { itemsWithRegistration.push({ method: request.method, registrationOptions: request.registrationOptions }); } } for (const notification of metaModel.notifications) { if (notification.registrationOptions) { itemsWithRegistration.push({ method: notification.method, registrationOptions: notification.registrationOptions }); } } // Generate capability entries for (const item of itemsWithRegistration) { const methodIdentifier = this.methodToIdentifier(item.method); const registrationType = item.registrationOptions ? this.typeToTypeScript(item.registrationOptions) : 'unknown'; this.writer.writeLine( `${methodIdentifier}: new Capability<${registrationType}>('${item.method}'),` ); } this.writer.outdent(); this.writer.writeLine('};'); this.writer.writeLine(''); } /** * Convert LSP method name to valid JavaScript identifier */ private methodToIdentifier(method: string): string { const parts = method .replace(/\$/g, '') // Remove $ characters .split('/') // Split on forward slashes .filter((part) => part.length > 0); // Remove empty parts return parts .map((part, index) => { // Convert kebab-case to camelCase for each part const camelCase = part.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); // Capitalize first letter of all parts except the first non-empty part return index === 0 ? camelCase : camelCase.charAt(0).toUpperCase() + camelCase.slice(1); }) .join(''); } /** * Generate the API contract object */ private generateApiContract(metaModel: MetaModel): void { this.writer.writeLine('/**'); this.writer.writeLine(' * LSP API Contract'); this.writer.writeLine(' */'); this.writer.writeLine('export const api = contract({'); this.writer.indent(); this.writer.writeLine('name: "LSP",'); // Helper function to generate request entries const generateRequest = (request: Request, isOptional: boolean = false) => { const methodIdentifier = this.methodToIdentifier(request.method); const paramsType = this.getParamsType(request.params); const resultType = this.typeToTypeScript(request.result); if (request.documentation) { this.writer.writeLine('/**'); this.writer.writeLine(` * ${request.documentation.replace(/\n/g, '\n * ')}`); this.writer.writeLine(' */'); } const optional = isOptional ? '.optional()' : ''; this.writer.writeLine( `${methodIdentifier}: unverifiedRequest<${paramsType}, ${resultType}>({ method: "${request.method}" })${optional},` ); }; // Helper function to generate notification entries const generateNotification = (notification: Notification) => { const methodIdentifier = this.methodToIdentifier(notification.method); const paramsType = this.getParamsType(notification.params); if (notification.documentation) { this.writer.writeLine('/**'); this.writer.writeLine(` * ${notification.documentation.replace(/\n/g, '\n * ')}`); this.writer.writeLine(' */'); } this.writer.writeLine( `${methodIdentifier}: unverifiedNotification<${paramsType}>({ method: "${notification.method}" }),` ); }; // Server section this.writer.writeLine('server: {'); this.writer.indent(); // Server requests (sent from client to server) for (const request of metaModel.requests) { if (request.messageDirection === 'clientToServer' || request.messageDirection === 'both') { generateRequest(request, true); } } // Server notifications (sent from client to server) for (const notification of metaModel.notifications) { if ( notification.messageDirection === 'clientToServer' || notification.messageDirection === 'both' ) { generateNotification(notification); } } this.writer.outdent(); this.writer.writeLine('},'); // Client section this.writer.writeLine('client: {'); this.writer.indent(); // Client requests (handled by server) for (const request of metaModel.requests) { if (request.messageDirection === 'serverToClient' || request.messageDirection === 'both') { generateRequest(request, true); // serverToClient requests are optional } } // Client notifications (sent from server to client) for (const notification of metaModel.notifications) { if ( notification.messageDirection === 'serverToClient' || notification.messageDirection === 'both' ) { generateNotification(notification); } } this.writer.outdent(); this.writer.writeLine('}'); this.writer.outdent(); this.writer.writeLine('});'); this.writer.writeLine(''); } /** * Helper method to get parameter type */ private getParamsType(params?: Type | Type[]): string { if (!params) { return 'void'; } if (Array.isArray(params)) { const paramTypes = params.map((p) => this.typeToTypeScript(p)); return `[${paramTypes.join(', ')}]`; } else { return this.typeToTypeScript(params); } } /** * Generate the complete TypeScript types */ generate(): void { const metaModel = this.loadMetaModel(); this.writer.clear(); this.writer.writeLine('// Generated TypeScript definitions for LSP'); this.writer.writeLine(`// Protocol version: ${metaModel.metaData.version}`); this.writer.writeLine('// This file is auto-generated. Do not edit manually.'); this.writer.writeLine(''); // Import contract types from @hediet/json-rpc this.writer.writeLine('import {'); this.writer.indent(); this.writer.writeLine('contract,'); this.writer.writeLine('Contract,'); this.writer.writeLine('unverifiedRequest,'); this.writer.writeLine('unverifiedNotification,'); this.writer.outdent(); this.writer.writeLine('} from "@hediet/json-rpc";'); this.writer.writeLine(''); // Generate enumerations for (const enumeration of metaModel.enumerations) { this.generateEnumeration(enumeration); } // Generate type aliases for (const typeAlias of metaModel.typeAliases) { this.generateTypeAlias(typeAlias); } // Generate structures for (const structure of metaModel.structures) { this.generateStructure(structure); } // Generate Capability class this.generateCapabilityClass(); // Generate capabilities map this.generateCapabilitiesMap(metaModel); // Generate API contract this.generateApiContract(metaModel); // Write types file const outDir = path.dirname(this.outPath); if (!fs.existsSync(outDir)) { fs.mkdirSync(outDir, { recursive: true }); } fs.writeFileSync(this.outPath, this.writer.toString()); console.log(`Generated LSP types file: ${this.outPath}`); } } // Run the generator if (require.main === module) { const argv = yargs(hideBin(process.argv)) .option('input', { alias: 'i', type: 'string', description: 'Path to metaModel.json', default: path.join(__dirname, '..', 'spec', 'metaModel.json') }) .option('output', { alias: 'o', type: 'string', description: 'Output file', default: path.join(__dirname, '..', 'src', 'types.ts') }) .help() .parseSync(); const generator = new LSPTypesGenerator(argv.input, argv.output); generator.generate(); }