import * as LibFs from 'mz/fs'; import * as program from 'commander'; import * as LibPath from 'path'; import { lcfirst, mkdir, parseMsgNamesFromProto, parseProto, ProtoFile, ProtoMsgImportInfo, ProtoMsgImportInfos, ProtoParseResult, readProtoList, } from './lib/lib'; import { TplEngine } from './lib/template'; const pkg = require('../../package.json'); program.version(pkg.version) .option('-p, --proto ', 'directory of proto files') .option('-i, --import ', 'third party proto import path: e.g path1,path2,path3', val => val.split(',')) .option('-o, --output ', 'directory to output service codes') .parse(process.argv); const PROTO_DIR = (program as any).proto === undefined ? undefined : LibPath.normalize((program as any).proto); const IMPORTS = (program as any).import === undefined ? [] : (program as any).import; const OUTPUT_DIR = (program as any).output === undefined ? undefined : LibPath.normalize((program as any).output); class ApiClientCLI { private _selfNamespaceList: string[] = []; private _rootProtoFiles: Array = []; private _protoFiles: Array = []; private _serviceInfos: ProtoMsgImportInfos = {}; private _selfServiceInfos: ProtoMsgImportInfos = {}; private _typeInfos: ProtoMsgImportInfos = {}; private _protoTsTypeMap = { double: 'number', float: 'number', int32: 'number', int64: 'number', uint32: 'number', uint64: 'number', sint32: 'number', sint64: 'number', fixed32: 'number', fixed64: 'number', sfixed32: 'number', sfixed64: 'number', string: 'string', bool: 'boolean', }; private _namespaceList: string[]; static instance() { return new ApiClientCLI(); } public async run() { console.log('ApiClientCLI start.'); await this._validate(); await this._loadProtos(); await this._genInfos(); this._filterUselessTypeInfos(); this._filterUselessNamespaces(); this._filterUselessService(); await this._genApiClient(); } private async _validate() { console.log('ApiClientCLI validate.'); if (!PROTO_DIR) { throw new Error('--proto is required'); } if (!OUTPUT_DIR) { throw new Error('--output is required'); } let protoStat = await LibFs.stat(PROTO_DIR); if (!protoStat.isDirectory()) { throw new Error('--proto is not a directory'); } let outputStat = await LibFs.stat(OUTPUT_DIR); if (!outputStat.isDirectory()) { throw new Error('--output is not a directory'); } } private async _loadProtos() { console.log('ApiClientCLI load proto files.'); this._rootProtoFiles = await readProtoList(PROTO_DIR, OUTPUT_DIR); this._selfNamespaceList = this._rootProtoFiles.map(item => item.relativePath); this._protoFiles = this._protoFiles.concat(this._rootProtoFiles); for (let i = 0; i < IMPORTS.length; i++) { this._protoFiles = this._protoFiles.concat(await readProtoList(LibPath.normalize(IMPORTS[i]), OUTPUT_DIR)); } if (this._protoFiles.length === 0) { throw new Error('no proto files found'); } } private async _genInfos() { console.log('ApiClientCLI generate service info and type info.'); // 从 proto 文件中解析出 ProtobufIParserResult 数据 let parseResults = [] as Array; for (let i = 0; i < this._protoFiles.length; i++) { let protoFile = this._protoFiles[i]; if (!protoFile) { continue; } let parseResult = {} as ProtoParseResult; parseResult.result = await parseProto(protoFile); parseResult.protoFile = protoFile; parseResults.push(parseResult); let msgImportInfos = parseMsgNamesFromProto(parseResult.result, protoFile); for (let msgTypeStr in msgImportInfos) { const msgInfo: ProtoMsgImportInfo = msgImportInfos[msgTypeStr]; if (msgInfo.methods && msgInfo.methods.length > 0) { this._serviceInfos[msgTypeStr] = msgInfo; } else { this._typeInfos[msgTypeStr] = msgInfo; } } } // 生成 namespaceList this._namespaceList = [...new Set(Object.keys(this._serviceInfos).map(item => item.split('.')[0]))]; } /** * 生成 type 是请求或者响应类型的列表,示例如下: * ['gateway.UserGetRequest', 'gateway.UserGetResponse'] * @returns {Promise} * @private */ private _genReqOrResTypeList(): string[] { const reqOrResTypeList: string[] = []; for (let serviceInfoName in this._serviceInfos) { const serviceInfo = this._serviceInfos[serviceInfoName]; for (let method of serviceInfo.methods) { reqOrResTypeList.push(method.requestType); reqOrResTypeList.push(method.responseType); } } return reqOrResTypeList; } /** * 过滤不必要的类型 * @returns {Promise} * @private */ private _filterUselessTypeInfos() { let tempTypeInfos: ProtoMsgImportInfos = {}; let reqOrResTypeList = this._genReqOrResTypeList(); for (let typeName in this._typeInfos) { const typeInfo: ProtoMsgImportInfo = this._typeInfos[typeName]; if (this._selfNamespaceList.indexOf(typeInfo.namespace) !== -1 && reqOrResTypeList.indexOf(typeName) !== -1) { tempTypeInfos[typeName] = typeInfo; this._recurFilterTypeInfo(tempTypeInfos, typeInfo); } } this._typeInfos = tempTypeInfos; } /** * 递归过滤不必要的类型 * @param {ProtoMsgImportInfos} tempTypeInfos * @param {ProtoMsgImportInfo} typeInfo * @returns {Promise} * @private */ private _recurFilterTypeInfo(tempTypeInfos: ProtoMsgImportInfos, typeInfo: ProtoMsgImportInfo) { for (let field of typeInfo.fields) { if (!this._protoTsTypeMap[field.fieldType] && !/^(google\.)|(bytes)/.test(field.fieldType)) { tempTypeInfos[field.fieldType] = this._typeInfos[field.fieldType]; this._recurFilterTypeInfo(tempTypeInfos, this._typeInfos[field.fieldType]); } } } /** * 过滤不必要的 namespace * @returns {Promise} * @private */ private _filterUselessNamespaces() { let tempNamespaceSet: Set = new Set(); for (let typeName in this._typeInfos) { let typeInfo = this._typeInfos[typeName]; tempNamespaceSet.add(typeInfo.namespace); } this._namespaceList = [...tempNamespaceSet]; } /** * 过滤不必要的 service * @returns {Promise} * @private */ private _filterUselessService() { let tempServiceMap: ProtoMsgImportInfos = {}; for (let serviceName in this._serviceInfos) { let service = this._serviceInfos[serviceName]; if (this._selfNamespaceList.indexOf(service.namespace) !== -1) { tempServiceMap[serviceName] = service; } } this._selfServiceInfos = tempServiceMap; } private _registerHelpers() { TplEngine.registerHelper('lcfirst', lcfirst); TplEngine.registerHelper('setVar', (varName: string, varValue: string, options): void => { options.data.root[varName] = varValue; return; }); TplEngine.registerHelper('uppercaseAndReplaceUnderline', (v: string): string => { return v.replace(/(^.)|(?:_(.))/g, (v1, v2, v3) => (v2 || v3).toUpperCase()); }); } private async _genApiClient() { let outputDir = LibPath.join(OUTPUT_DIR, 'api_client'); await mkdir(outputDir); let context = { serviceInfos: this._serviceInfos, typeInfos: this._typeInfos, selfNamespaceList: this._selfNamespaceList, protoTsTypeMap: this._protoTsTypeMap, namespaceList: this._namespaceList, selfServiceInfos: this._selfServiceInfos, }; this._registerHelpers(); let tsContent: string = TplEngine.render('client/tsApiClient', context); await LibFs.writeFile(LibPath.join(outputDir, 'ApiClient.ts'), tsContent); let jsContent: string = TplEngine.render('client/jsApiClient', context); await LibFs.writeFile(LibPath.join(outputDir, 'ApiClient.js'), jsContent); } } ApiClientCLI.instance().run().catch((err: Error) => { console.log('err: ', err.message); });