UNPKG

tsrpc-cli

Version:
1,125 lines (1,058 loc) 123 kB
#!/usr/bin/env node /*! * TSRPC Command-line Interface v2.4.5 * ----------------------------------------- * MIT LICENSE * KingWorks (C) Copyright 2022 * https://github.com/k8w/tsrpc-cli */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var chalk = require('chalk'); require('k8w-extend-native'); var minimist = require('minimist'); var path = require('path'); var console$1 = require('console'); var fs$1 = require('fs-extra'); var glob = require('glob'); var tsbufferProtoGenerator = require('tsbuffer-proto-generator'); var fs = require('fs'); require('ts-node/register'); var childProcess = require('child_process'); var ora = require('ora'); var inquirer = require('inquirer'); var tsbuffer = require('tsbuffer'); var chokidar = require('chokidar'); var tsbufferSchema = require('tsbuffer-schema'); var tsbufferValidator = require('tsbuffer-validator'); var typescriptFormatter = require('typescript-formatter'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var chalk__default = /*#__PURE__*/_interopDefaultLegacy(chalk); var minimist__default = /*#__PURE__*/_interopDefaultLegacy(minimist); var path__default = /*#__PURE__*/_interopDefaultLegacy(path); var fs__default$1 = /*#__PURE__*/_interopDefaultLegacy(fs$1); var glob__default = /*#__PURE__*/_interopDefaultLegacy(glob); var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs); var childProcess__default = /*#__PURE__*/_interopDefaultLegacy(childProcess); var ora__default = /*#__PURE__*/_interopDefaultLegacy(ora); var inquirer__default = /*#__PURE__*/_interopDefaultLegacy(inquirer); var chokidar__default = /*#__PURE__*/_interopDefaultLegacy(chokidar); /** * 用于 `tsrpc.config.ts` 的代码模板 * @public */ class CodeTemplate { /** * 自公共基类继承的 Ptl */ static getExtendedPtl(baseFile = 'src/shared/protocols/base.ts', baseReq = 'BaseRequest', baseRes = 'BaseResponse', baseConf = 'BaseConf') { return (ptlBaseName, ptlPath, ptlDir) => { const importPath = path__default["default"].relative(path__default["default"].dirname(ptlPath), path__default["default"].resolve(baseFile.replace(/\.ts$/, ''))).replace(/\\/g, '/'); return `import { ${baseReq}, ${baseRes}, ${baseConf} } from "${importPath.startsWith('.') ? importPath : `./${importPath}`}"; export interface Req${ptlBaseName} extends ${baseReq} { } export interface Res${ptlBaseName} extends ${baseRes} { } export const conf: ${baseConf} = { }`; }; } /** * 自公共基类继承的 Msg */ static getExtendedMsg(baseFile = 'src/shared/protocols/base.ts', baseMsg = 'BaseMessage', baseConf = 'BaseConf') { return (msgBaseName, msgPath, msgDir) => `import { ${baseMsg}, ${baseConf} } from "./${path__default["default"].relative(path__default["default"].dirname(msgPath), path__default["default"].resolve(baseFile.replace(/\.ts$/, ''))).replace(/\\/g, '/')}"; export interface Msg${msgBaseName} extends ${baseMsg} { } export const conf: ${baseConf} = { }`; } } /** * 默认 Ptl */ CodeTemplate.defaultPtl = ptlBaseName => `export interface Req${ptlBaseName} { } export interface Res${ptlBaseName} { } `; /** * 默认 Msg */ CodeTemplate.defaultMsg = msgBaseName => `export interface Msg${msgBaseName} { } `; /** * 默认 Api */ CodeTemplate.defaultApi = (apiBaseName, apiFileDir, ptlFileDir) => `import { ApiCall } from "tsrpc"; import { Req${apiBaseName}, Res${apiBaseName} } from "${path__default["default"].relative(apiFileDir, ptlFileDir).replace(/\\/g, '/')}/Ptl${apiBaseName}"; export default async function (call: ApiCall<Req${apiBaseName}, Res${apiBaseName}>) { // TODO call.error('API Not Implemented'); }`; const i18nEnUs = { welcome: 'https://npmjs.com/tsrpc\nWelcome to TSRPC utilities V${version}', help: ` Usage: --- [Recommended] Use Via Config File --- tsrpc-cli proto --config tsrpc.config.ts tsrpc-cli api --config tsrpc.config.ts tsrpc-cli sync --config tsrpc.config.ts tsrpc-cli link --config tsrpc.config.ts tsrpc-cli dev --config tsrpc.config.ts tsrpc-cli build --config tsrpc.config.ts tsrpc-cli doc --config tsrpc.config.ts ---------- Use Via CLI Params ---------- tsrpc-cli init Init "tsrpc.config.ts" and scripts in package.json tsrpc-cli proto <options> Generate proto file -i, --input <file> Input TS file (support glob expression) It would generate all exported types -o, --output <file> Output file (or print to CLI) -c, --compatible <file> Compatible mode, compatible to old proto (=output by default) --new Generate fresh new proto (no compatible) --ugly Output as ugly JSON (no indent and smaller) --verbose Show debug info --ignore <glob> Files to be ignored from --input tsrpc-cli api <options> Generate TSRPC API implementations -i, --input <file> Proto file path (proto.ts or proto.json) -o, --output <folder> Output api folder path tsrpc-cli sync Sync directory --from <dir> Source path --to <dir> Target path (copy and set as read-only) tsrpc-cli link <options> Create symlink (cross all operating system) --from <dir> Source path --to <dir> Target path for created symlink tsrpc-cli dev <options> Run local dev server --config <file> Path of config file --entry <file> Entry file,default is "src/index.ts" tsrpc-cli build <options> Build the server project --config <file> Path of config file tsrpc-cli doc <options> Generate API documents (Swagger/OpenAPI, Markdown, TSAPI) -i, --input <folder> The path of protocols folder -o, --output <folder> The path of output documents folder --verbose Show debug info --ignore <glob> Files to be ignored from --input Buffer Utilities: tsrpc-cli encode <options> [exp] Encode a JS expression or a file (content is JS expression) [exp] Expression to encode (e.g. "123" "new Uint8Array([1,2,3])") -p, --proto <file> Proto file to use -s, --schema <id> SchemaID (filePath/TypeName) -i, --input <file> Input file path, alternative to [exp] -o, --output <file> Output file path (or print to CLI) --verbose Show debug info tsrpc-cli decode <options> [binstr] Decode buffer [binstr] Buffer to decode, hex string, like "0F A2 E3" -p, --proto <file> Proto file -s, --schema <id> SchemaID (filePath/TypeName) -i, --input <file> Input file path, alternative to [binstr] -o, --output <file> Output file path (or print to CLI) --verbose Show debug info tsrpc-cli validate <options> [exp] Validate if a JS expression is valid to a schema [exp] Expression to validate (e.g. "123" "new Uint8Array([1,2,3])") -p, --proto <file> Proto file to use -s, --schema <id> SchemaID (filePath/TypeName) -i, --input <file> Input file path, alternative to [exp] tsrpc-cli show <file> Show a binary file as hex string `.trim(), example: ` Example: tsrpc-cli dev --entry src/xxx.ts Run local dev server tsrpc-cli build Build the server project tsrpc-cli doc Generate API document `.trim(), errCmd: 'Error command, use "tsrpc-cli -h" to see more help info.', missingParam: 'Missing parameter ${param}, use "tsrpc-cli -h" to see more help info.', shouldBeDir: '${path} should be a directory', protoSucc: '✔ ServiceProto generated to: ${output}', protoFailed: (output) => `⨯ Generate ServiceProto failed, please check if there is any TypeScript compile error : ${output}`, fileNotExists: 'File not exists: ${file}', fileOpenError: 'Failed to open file: ${file}', jsParsedError: 'Failed to parse JS expression from: ${file}', invalidProtoExt: '旧 ServiceProto 格式非法,仅支持 .ts 和 .json 文件: ${file}', protoParsedError: 'Failed to parse old proto: ${file}', expParsedError: 'Invalid JS expression', or: 'or', and: 'and', encodeSucc: '✔ Encoded succ to: ${output}', decodeSucc: '✔ Decoded succ to: ${output}', apiSucc: '✔ Api${apiName} generated: ${apiPath}', allApiSucc: '✔ Success,${newCount} new API generated', validateSucc: '✔ Validate succ', validateFail: '⨯ Validate fail: ${msg}', error: ' ERROR ', success: ' SUCCESS ', helpGuide: 'Use "tsrpc-cli -h" to see more help info.', compatibleError: 'Failed to keep compatible with old proto: \n\t|- ${innerError}', canOptimizeByNew: (oldProtoPath) => `Redundancy in ServiceProto is detected, delete '${oldProtoPath}' manually to optimize this, but it would cause incompatibility between the latest and older protocol. `, dirNotExists: 'Directory not exists: ${dir}', codeError: 'Build TypeScript failed, please fix code error', ifUpdateProto: 'A protocol change is detected, do you need to regenerate ServiceProto?', ifSyncNow: 'Execute "npm run sync" after generated successfully?', syncFailed: 'Execute "npm run sync" failed, you can manually finish syncing.', deleteConfirm: '${target}\nis existed already,delete it and continue?', canceled: 'CANCELED', confInvalid: 'Invalid TSRPC config file: ${path}', missingConfigItem: (itemName) => `Missing '${itemName}' in config file`, nothingSyncConf: `'sync' is not configurated in the config file`, syncedSucc: '✔ Synced successfully', allSyncedSucc: '✔ All Synced successfully', copy: 'Copy Files:', link: 'Create Symlink:', junction: 'Create Junction:', linkedSucc: '✔ Linked successfully', allLinkedSucc: '✔ All linked successfully', elevatingForLink: 'Asking for authorization to create symlink', linkFailed: 'Authorization to create Symlink failed. Please select "Yes" in the authorization dialog: ', linkRetry: 'Retry', linkJunction: 'Create Junction instead (Not recommended)', devServerRestarting: '\n------ Recompiling & Restarting ------\n', startDevServer: '✔ Start Dev Server:', devServerStopped: '⨯ Dev Server Stopped', protoNotReady: '⨯ Dev Server cannot start,please fix ServiceProto firstly', buildClean: (outDir) => `Clean '${outDir}'`, buildTsc: `Compile TypeScript`, buildCopyFiles: `Copy Files`, buildSucc: 'Build Successfully!', docOpenApiSucc: (path) => `OpenAPI generated successfully: ${chalk__default["default"].cyan(path)}`, docTsapiSucc: (path) => `TSAPI generated successfully: ${chalk__default["default"].cyan(path)}`, docMdSucc: (path) => `Markdown generated successfully: ${chalk__default["default"].cyan(path)}`, fileAlreadyExists: (path) => `${path} already exists`, npmNotInited: `package.json not exists,please run ${chalk__default["default"].cyan('npm init')} first`, initSucc: (path) => `✔ Init successfully: ${path}`, }; const i18nZhCn = { welcome: 'https://npmjs.com/tsrpc\n欢迎进入 TSRPC 实用工具 V${version}', help: ` 使用说明: ---- [推荐] 通过配置文件使用 ---- tsrpc-cli proto --config tsrpc.config.ts tsrpc-cli api --config tsrpc.config.ts tsrpc-cli sync --config tsrpc.config.ts tsrpc-cli link --config tsrpc.config.ts tsrpc-cli dev --config tsrpc.config.ts tsrpc-cli build --config tsrpc.config.ts tsrpc-cli doc --config tsrpc.config.ts ------- 通过命令行参数使用 ------- tsrpc-cli init 初始化 tsrpc.config.ts 文件和 package.json scripts tsrpc-cli proto <options> 生成TSRPC Proto文件 -i, --input <folder> 用来生成Proto的协议文件夹路径 -o, --output <file> 输出的文件路径,不指定将直接输出到命令行 -o XXX.ts 和 -o XXX.json 将对应输出两种不同的格式 -c, --compatible <file> 兼容模式:要兼容的旧Proto文件的路径(默认同output) --new 不兼容旧版,生成全新的Proto文件 --ugly 输出为可读性较差但体积更小压缩格式 --verbose 显示调试信息 --ignore <glob> 从--input范围中要忽略的文件,Glob 表达式 支持传入多个,例如 --ignore "AAA" --ignore "BBB" tsrpc-cli api <options> 自动生成TSRPC API实现 -i, --input <file> Proto文件的路径 -o, --output <folder> 输出的API文件夹路径 tsrpc-cli sync <options> 同步文件夹内容,初始化 Symlink 或只读复制文件 --from <dir> 要同步的源文件夹 --to <dir> 要同步到的目标位置(只读复制) tsrpc-cli link <options> 在目标位置创建到源的 Symlink,以实现自动同步 --from <dir> 要同步的源文件夹 --to <dir> 创建 Symlink 的目标位置 tsrpc-cli dev <options> 启动本地开发服务器,当源代码变更时自动重启 --config <file> 配置文件路径 --entry <file> 程序入口点,默认 "src/index.ts" tsrpc-cli build <options> 构建 TSRPC 后端项目 --config <file> 配置文件路径 tsrpc-cli doc <options> 生成多种格式的 API 接口文档 (如 Swagger/OpenAPI、Markdown、TSAPI) -i, --input <folder> 协议文件夹路径 -o, --output <folder> 输出文档的文件夹路径 --verbose 显示调试信息 --ignore <glob> 从--input范围中要忽略的文件,Glob 表达式 支持传入多个,例如 --ignore "AAA" --ignore "BBB" 二进制调试工具: tsrpc-cli encode <options> [exp] 编码JS表达式 [exp] 要编码的值(JS表达式,例如"123" "new Uint8Array([1,2,3])") -p, --proto <file> 编码要使用的Proto文件 -s, --schema <id> 编码要使用的SchemaID -i, --input <file> 输入为文件,不可与[exp]同用(文件内容为JS表达式) -o, --output <file> 输出的文件路径,不指定将直接输出到命令行 --verbose 显示调试信息 tsrpc-cli decode <options> [binstr] 解码二进制数据 [binstr] 要解码的二进制数据的字符串表示,如"0F A2 E3 F2 D9" -p, --proto <file> 解码要使用的Proto文件 -s, --schema <id> 解码要使用的SchemaID -i, --input <file> 输入为文件,不可与[binstr]同用 -o, --output <file> 输出的文件路径,不指定将直接输出到命令行 --verbose 显示调试信息 tsrpc-cli validate <options> [exp] 验证JSON数据 [exp] 要验证的值(JS表达式,例如"123" "new Uint8Array([1,2,3])") -p, --proto <file> 验证要使用的Proto文件 -s, --schema <id> 验证要使用的SchemaID -i, --input <file> 输入为文件,不可与[exp]同用(文件内容为JS表达式) tsrpc-cli show <file> 打印二进制文件内容 `.trim(), example: ` 使用示例: tsrpc-cli dev --entry src/xxx.ts 启动本地开发服务器 tsrpc-cli build 构建后端项目 tsrpc-cli doc 生成 API 接口文档 `.trim(), errCmd: '命令格式有误,键入 tsrpc-cli -h 以查看帮助。', missingParam: '缺少 ${param} 参数,键入 tsrpc-cli -h 以查看更多信息。', shouldBeDir: '${path} 应当为一个文件夹', protoSucc: '✔ ServiceProto 已生成到:${output}', protoFailed: (output) => `⨯ ServiceProto 生成失败,请检查 TypeScript 代码是否编译报错: ${output}`, fileNotExists: '文件不存在:${file}', fileOpenError: '文件打开失败: ${file}', jsParsedError: 'JS表达式解析失败: ${file}', invalidProtoExt: '旧 ServiceProto 格式非法,仅支持 .ts 和 .json 文件: ${file}', protoParsedError: '旧 ServiceProto 文件解析失败: ${file}', expParsedError: '表达式解析失败', or: '或', and: '和', encodeSucc: '✔ 编码结果已生成到:${output}', decodeSucc: '✔ 解码结果已生成到:${output}', apiSucc: '✔ Api${apiName} 生成成功: ${apiPath}', allApiSucc: '✔ 完成,共生成 ${newCount} 个新的 API 文件。', validateSucc: '✔ 验证通过', validateFail: '⨯ 验证不通过: ${msg}', error: ' 错误 ', success: ' 成功 ', helpGuide: '键入 tsrpc-cli -h 查看更多帮助信息', compatibleError: '兼容旧Proto失败: ${innerError}', canOptimizeByNew: (oldProtoPath) => `检测到协议中可优化的冗余信息,删除 '${oldProtoPath}' 即可优化,但将导致新旧协议的不兼容。`, dirNotExists: '文件夹不存在: ${dir}', codeError: 'TypeScript 构建失败,请检查代码报错', ifUpdateProto: '检测到协议变更,是否重新生成 ServiceProto?', ifSyncNow: '生成后执行同步(npm run sync)吗?', syncFailed: '执行 "npm run sync" 失败, 你可以手动完成同步。', deleteConfirm: '${target}\n目标已经存在,是否删除再继续?', canceled: '已取消', confInvalid: '配置文件解析失败: ${path}', missingConfigItem: (itemName) => `配置文件中缺少配置项 '${itemName}'`, nothingSyncConf: '配置文件中没有配置 sync 项目', syncedSucc: '✔ 同步完成', allSyncedSucc: '✔ 已完成所有同步', copy: '复制文件:', link: '创建 Symlink:', junction: '创建 Junction:', linkedSucc: '✔ Symlink 创建成功', allLinkedSucc: '✔ 已完成所有 Symlink 创建', elevatingForLink: '正在获取创建 Symlink 所需的授权', linkFailed: '创建 Symlink 授权失败,请在授权弹框选择 “是” 以继续:', linkRetry: '重 试', linkJunction: '改为创建 Junction(不推荐)', devServerRestarting: '\n———— Dev Server 自动重启中 ————\n', startDevServer: '✔ 启动本地服务:', devServerStopped: '⨯ Dev Server 已停止运行', protoNotReady: '⨯ Dev Server 未启动,请先修复 ServiceProto 问题', buildClean: (outDir) => `清空目录 '${outDir}'`, buildTsc: `编译 TypeScript`, buildCopyFiles: `复制文件`, buildSucc: '构建成功!', docOpenApiSucc: (path) => `OpenAPI 已成成到:${chalk__default["default"].cyan(path)}`, docTsapiSucc: (path) => `TSAPI 已成成到:${chalk__default["default"].cyan(path)}`, docMdSucc: (path) => `Markdown 已成成到:${chalk__default["default"].cyan(path)}`, fileAlreadyExists: (path) => `${path} 已经存在`, npmNotInited: `package.json 不存在,请先执行 ${chalk__default["default"].cyan('npm init')} 初始化`, initSucc: (path) => `✔ 初始化成功:${path}`, }; // 根据系统语言判断中英文 const i18n = require('os-locale').sync() === 'zh-CN' ? i18nZhCn : i18nEnUs; // export const i18n = zhCN; function showLogo() { console.log(chalk__default["default"].green(` 88888888888 .d8888b. 8888888b. 8888888b. .d8888b. 888 d88P Y88b 888 Y88b 888 Y88b d88P Y88b 888 Y88b. 888 888 888 888 888 888 888 "Y888b. 888 d88P 888 d88P 888 888 "Y88b. 8888888P" 8888888P" 888 888 "888 888 T88b 888 888 888 888 Y88b d88P 888 T88b 888 Y88b d88P 888 "Y8888P" 888 T88b 888 "Y8888P" ------------------------------------------------------------------------ `)); } function formatStr(str, data) { for (let key in data) { str = str.replace(`\${${key}}`, data[key]); } return str; } function error(str, data) { if (data) { str = formatStr(str, data); } return new Error(str); } function colorJson(json) { return JSON.colorStringify(json, null, 2); } function hex2Bin(hexStr) { return Buffer.from(new Uint8Array(hexStr.trim().split(/\s+/).map(v => parseInt('0x' + v)))); } function buf2Hex(buf) { let arr = []; buf.forEach(v => { let char = v.toString(16).toUpperCase(); if (char.length === 1) { char = '0' + char; } arr.push(char); }); return arr.join(' '); } /** * 引入 TS 模块,但不缓存 */ function importTS(modulePath, ignoreTypeError) { modulePath = path__default["default"].resolve(modulePath); if (!fs__default["default"].existsSync(modulePath)) { throw error(i18n.fileNotExists, { file: modulePath }); } let module; // 忽略类型报错 if (ignoreTypeError) { let oldEnv = process.env['TS_NODE_TRANSPILE_ONLY']; process.env['TS_NODE_TRANSPILE_ONLY'] = 'true'; try { module = require(modulePath); } finally { process.env['TS_NODE_TRANSPILE_ONLY'] = oldEnv; } } else { module = require(modulePath); } delete require.cache[modulePath]; return module; } function importTsrpcConfig(modulePath) { var _a, _b; let module = importTS(modulePath, true); let conf = (_b = (_a = module['default']) !== null && _a !== void 0 ? _a : module['conf']) !== null && _b !== void 0 ? _b : module['config']; if (!conf) { throw error(i18n.confInvalid, { path: path__default["default"].resolve(modulePath) }); } return conf; } class ProtoUtil { static async loadServiceProto(filepath, logger, useRegExpWhenTsError = true) { if (await fs__default$1["default"].access(filepath).catch(e => true)) { logger === null || logger === void 0 ? void 0 : logger.error(formatStr(i18n.fileNotExists, { file: path__default["default"].resolve(filepath) })); return undefined; } if (filepath.endsWith('.ts')) { // 首先尝试通过 ts-node 直接解析 let module; try { module = importTS(path__default["default"].resolve(filepath)); } catch (e) { } if (module === null || module === void 0 ? void 0 : module.serviceProto) { return module.serviceProto; } // ts-node 解析失败:由于上面检测过,文件必定存在,所以此时应该是 serviceProto.ts 编译报错 // 尝试通过字符串匹配方式解析 ServiceProto if (useRegExpWhenTsError) { // Read file let fileContent = await fs__default$1["default"].readFile(filepath, 'utf-8').catch(); if (fileContent) { // Match ServiceProto by RegExp let match = fileContent.match(/export const serviceProto: ServiceProto<ServiceType> = (\{[\s\S]+\});/); if (match) { try { let proto = JSON.parse(match[1]); return proto; } catch { } } } } // 解析失败 logger === null || logger === void 0 ? void 0 : logger.error(formatStr(i18n.protoParsedError, { file: path__default["default"].resolve(filepath) })); return undefined; } else if (filepath.endsWith('.json')) { // 打开OldFile let fileContent = (await fs__default$1["default"].readFile(filepath)).toString(); try { return JSON.parse(fileContent); } catch (e) { logger === null || logger === void 0 ? void 0 : logger.error(formatStr(i18n.protoParsedError, { file: path__default["default"].resolve(filepath) })); return undefined; } } else { logger === null || logger === void 0 ? void 0 : logger.warn(formatStr(i18n.invalidProtoExt, { file: path__default["default"].resolve(filepath) })); return undefined; } } static async parseProtoAndSchema(proto, schemaId) { // #region 解析Proto if (!proto) { throw error(i18n.missingParam, { param: '--proto' }); } if (!schemaId) { throw error(i18n.missingParam, { param: '--schema' }); } let serviceProto; try { serviceProto = await ProtoUtil.loadServiceProto(proto); } catch (e) { throw error(e.message); } if (!serviceProto) { throw error(i18n.fileOpenError, { file: path__default["default"].resolve(proto) }); } return { proto: serviceProto.types, schemaId: schemaId }; // #endregion } /** * 生成 ServiceProto * @param options * @returns */ static async generateServiceProto(options) { var _a, _b; const oldProto = (_a = options.oldProto) === null || _a === void 0 ? void 0 : _a.proto; let errMsgs = []; // 标准化路径(抹平系统差异) const protocolDir = options.protocolDir.replace(/\\/g, '/').replace(/\/+$/, ''); // 只能填写文件夹 不支持通配符 if (!(await fs__default$1["default"].stat(options.protocolDir)).isDirectory()) { throw error(i18n.shouldBeDir, { path: protocolDir }); } // 查找所有目标 Ptl 和 Msg 文件,输出标准化的相对路径 let fileList = glob__default["default"].sync(protocolDir + '/**/{Ptl,Msg}?*.ts', { ignore: options.ignore, }).map(v => path__default["default"].relative(protocolDir, v).replace(/\\/g, '/')); // 临时切换working dir // let originalCwd = process.cwd(); // process.chdir(protocolDir); // 生成 types (TSBufferSchema) const EXP_DIR_TYPE_NAME = /^(.+\/)?(Ptl|Msg)([^\.\/\\]+)\.ts$/; try { var typeProto = await new tsbufferProtoGenerator.TSBufferProtoGenerator({ verbose: options.verbose, baseDir: protocolDir, customSchemaIds: ['mongodb/ObjectId', 'mongodb/ObjectID', 'bson/ObjectId', 'bson/ObjectID'], keepComment: options.keepComment, resolveModule: options.resolveModule }).generate(fileList, { compatibleResult: oldProto === null || oldProto === void 0 ? void 0 : oldProto.types, filter: info => { let infoPath = info.path.replace(/\\/g, '/'); let match = infoPath.match(EXP_DIR_TYPE_NAME); if (!match) { return false; } if (match[2] === 'Ptl') { return info.name === 'Req' + match[3] || info.name === 'Res' + match[3]; } else { return info.name === 'Msg' + match[3]; } }, logger: options.verbose ? console : undefined }); } catch (e) { e.message = (e.message.startsWith('⨯') ? '' : '⨯ ') + e.message; throw e; } // 生成 services let services = []; for (let filepath of fileList) { let match = filepath.match(EXP_DIR_TYPE_NAME); let typePath = filepath.replace(/^\.\//, '').replace(/\.ts$/, ''); // 解析conf let tsModule = importTS(path__default["default"].resolve(protocolDir, filepath)); let conf = tsModule.conf; // Ptl 检测 Req 和 Res 类型齐全 if (match[2] === 'Ptl') { let req = typePath + '/Req' + match[3]; let res = typePath + '/Res' + match[3]; if (typeProto[req] && typeProto[res]) { services.push({ id: services.length, name: (match[1] || '') + match[3], type: 'api', conf: conf }); } else { !typeProto[res] && errMsgs.push(chalk__default["default"].red(`⨯ Missing type ${chalk__default["default"].cyan(`Res${match[3]}`)} at ${chalk__default["default"].cyan(filepath)}`)); !typeProto[req] && errMsgs.push(chalk__default["default"].red(`⨯ Missing type ${chalk__default["default"].cyan(`Req${match[3]}`)} at ${chalk__default["default"].cyan(filepath)}`)); } } // Msg 检测Msg类型在 else { let msg = typePath + '/Msg' + match[3]; if (typeProto[msg]) { services.push({ id: services.length, name: (match[1] || '') + match[3], type: 'msg', conf: conf }); } else { errMsgs.push(chalk__default["default"].red(`⨯ Missing type ${chalk__default["default"].cyan(`Msg${match[3]}`)} at ${chalk__default["default"].cyan}`)); } } } // 有 Missing 报错,不生成 if (errMsgs.length) { throw new Error(errMsgs.join('\n')); } // 检测可优化的 ID 冗余 let canOptimizeByNew = false; tsbufferProtoGenerator.EncodeIdUtil.onGenCanOptimized = () => { canOptimizeByNew = true; }; // EncodeID 兼容 OldProto let encodeIds = tsbufferProtoGenerator.EncodeIdUtil.genEncodeIds(services.map(v => v.type + v.name), oldProto === null || oldProto === void 0 ? void 0 : oldProto.services.map(v => ({ key: v.type + v.name, id: v.id }))); for (let item of encodeIds) { services.find(v => item.key.startsWith(v.type) && v.name === item.key.substr(v.type.length)).id = item.id; } let version = oldProto === null || oldProto === void 0 ? void 0 : oldProto.version; // 只有在旧 Proto 存在,同时协议内容变化的情况下,才更新版本号 if (oldProto && JSON.stringify({ types: oldProto.types, services: oldProto.services }) !== JSON.stringify({ types: typeProto, services: services })) { version = (oldProto.version || 0) + 1; } // 创建新 Proto let newProto = { version: version, services: services, types: typeProto }; // process.chdir(originalCwd); if (options.checkOptimize && canOptimizeByNew && ((_b = options.oldProto) === null || _b === void 0 ? void 0 : _b.path)) { console.warn(chalk__default["default"].yellow(i18n.canOptimizeByNew(path__default["default"].resolve(options.oldProto.path)) + '\n')); } return { newProto: newProto, isChanged: newProto.version !== (oldProto === null || oldProto === void 0 ? void 0 : oldProto.version) }; } static async outputProto(options, logger) { if (options.noEmitWhenNoChange) { // TS 报错也算需要重新生成 let oldProto = await this.loadServiceProto(options.newProtoPath, undefined, false); if (oldProto && JSON.stringify(oldProto) === JSON.stringify(options.proto)) { return { emited: false }; } } // TS if (options.newProtoPath.endsWith('.ts')) { let imports = {}; let apis = []; let msgs = []; // 防止重名 let usedNames = {}; let getAsName = (name) => { while (usedNames[name]) { let match = name.match(/(^.*)\_(\d+)$/); if (match) { let seq = parseInt(match[2]) + 1; name = match[1] + '_' + seq; } else { name = name + '_1'; } } usedNames[name] = 1; return name; }; let addImport = (path, srcNames) => { let asNames = srcNames.map(v => getAsName(v)); imports[path] = srcNames.map((v, i) => ({ srcName: v, asName: asNames[i] && asNames[i] !== v ? asNames[i] : undefined })); return asNames; }; for (let svc of options.proto.services) { let match = svc.name.replace(/\\/g, '/').match(/^(.*\/)*([^\/]+)$/); if (!match) { throw new Error(`Invalid svc name: ${svc.name}`); } let lastName = match[2]; let importPath = path__default["default"].relative(path__default["default"].dirname(options.newProtoPath), path__default["default"].join(options.protocolDir, (match[1] || '') + (svc.type === 'api' ? 'Ptl' : 'Msg') + lastName)).replace(/\\/g, '/'); if (!importPath.startsWith('.')) { importPath = './' + importPath; } if (svc.type === 'api') { let op = addImport(importPath, ['Req' + lastName, 'Res' + lastName]); apis.push({ name: svc.name, importPath: importPath, req: op[0], res: op[1] }); } else { let op = addImport(importPath, ['Msg' + lastName]); msgs.push({ name: svc.name, importPath: importPath, msg: op[0] }); } } let importStr = Object.entries(imports) .map(v => `import { ${v[1].map(w => w.asName ? `${w.srcName} as ${w.asName}` : w.srcName).join(', ')} } from '${v[0]}';`) .join('\n'); let apiStr = apis.map(v => ` ${JSON.stringify(v.name)}: { req: ${v.req}, res: ${v.res} }`).join(',\n'); let msgStr = msgs.map(v => ` ${JSON.stringify(v.name)}: ${v.msg}`).join(',\n'); let fileContent = ` import { ServiceProto } from 'tsrpc-proto'; ${importStr} export interface ServiceType { api: { ${apiStr} }, msg: { ${msgStr} } } export const serviceProto: ServiceProto<ServiceType> = ${JSON.stringify(options.proto, null, 4)}; `.trim(); await fs__default$1["default"].ensureDir(path__default["default"].dirname(options.newProtoPath)); await fs__default$1["default"].writeFile(options.newProtoPath, fileContent); } // JSON else { await fs__default$1["default"].ensureDir(path__default["default"].dirname(options.newProtoPath)); await fs__default$1["default"].writeFile(options.newProtoPath, options.ugly ? JSON.stringify(options.proto) : JSON.stringify(options.proto, null, 2)); } logger === null || logger === void 0 ? void 0 : logger.log(chalk__default["default"].green(formatStr(i18n.protoSucc, { output: path__default["default"].resolve(options.newProtoPath) }))); return { emited: true }; } static async loadOldProtoByConfigItem(confItem, verbose) { var _a; // old let oldProtoPath = (_a = confItem.compatible) !== null && _a !== void 0 ? _a : confItem.output; let oldProto; if (oldProtoPath) { oldProto = await ProtoUtil.loadServiceProto(oldProtoPath, verbose ? console : undefined); } verbose && console.log(`oldProtoPath: ${oldProtoPath}, hasOldProto=${!!oldProto}`); return oldProto ? { proto: oldProto, path: oldProtoPath } : undefined; } static async genProtoByConfigItem(confItem, old, verbose, checkOptimize, noEmitWhenNoChange, keepComment) { // new try { var resGenProto = await ProtoUtil.generateServiceProto({ protocolDir: confItem.ptlDir, oldProto: old, ignore: confItem.ignore, verbose: verbose, checkOptimize: checkOptimize, resolveModule: confItem.resolveModule }); verbose && console.log(`Proto generated succ, start to write output file...`); } catch (e) { console.error(chalk__default["default"].red(e.message)); throw new Error(i18n.protoFailed(confItem.output)); } // output await ProtoUtil.outputProto({ protocolDir: confItem.ptlDir, newProtoPath: confItem.output, proto: resGenProto.newProto, noEmitWhenNoChange: noEmitWhenNoChange }, console); verbose && console.log(`Finish: ${confItem.output}...`); return resGenProto.newProto; } static toJsonSchema() { } } async function cmdApi(options) { var _a; if (options.config) { if (!options.config.proto) { throw new Error(i18n.missingConfigItem('proto')); } let newCount = 0; for (let conf of options.config.proto) { if (!conf.apiDir) { continue; } let proto = await ProtoUtil.loadServiceProto(conf.output, options.config.verbose ? console : undefined); if (!proto) { console.warn(chalk__default["default"].yellow(formatStr(i18n.protoParsedError, { file: path__default["default"].resolve(conf.output) }))); continue; } let res = await genApiFilesByProto({ proto: proto, ptlDir: conf.ptlDir, apiDir: conf.apiDir, template: (_a = conf.newApiTemplate) !== null && _a !== void 0 ? _a : CodeTemplate.defaultApi }, console); newCount += res.length; } console.log(chalk__default["default"].green(formatStr(i18n.allApiSucc, { newCount: '' + newCount }))); } else { if (!options.input) { throw console$1.error(i18n.missingParam, { param: 'input' }); } if (!options.output) { throw console$1.error(i18n.missingParam, { param: 'output' }); } let proto = await ProtoUtil.loadServiceProto(options.input); if (!proto) { throw console$1.error(i18n.protoParsedError, { file: options.input }); } let res = await genApiFilesByProto({ proto: proto, ptlDir: path__default["default"].dirname(options.input), apiDir: options.output, template: undefined }, console); console.log(chalk__default["default"].green(formatStr(i18n.allApiSucc, { newCount: '' + res.length }))); } } function getApiFileInfo(apiSvcName, apiDir, ptlDir) { let apiBaseName = apiSvcName.match(/\w+$/)[0]; /** a/b/c/Test apiBaseName='Test' apiBasePath='a/b/c/' */ let apiBasePath = apiSvcName.substr(0, apiSvcName.length - apiBaseName.length); /** API src files dir */ let apiFileDir = path__default["default"].join(apiDir, apiBasePath); /** API src .ts file pathname */ let apiFilePath = path__default["default"].join(apiFileDir, `Api${apiBaseName}.ts`); /** Ptl src files dir */ let ptlFileDir = path__default["default"].join(ptlDir, apiBasePath); return { apiBaseName, apiBasePath, apiFileDir, apiFilePath, ptlFileDir }; } async function genApiFilesByProto(options, logger) { var _a; let apis = options.proto.services.filter(v => v.type === 'api'); let generatedFiles = []; for (let api of apis) { let { apiBaseName, apiFilePath, apiFileDir, ptlFileDir } = getApiFileInfo(api.name, options.apiDir, options.ptlDir); await genNewApiFile(apiBaseName, apiFilePath, apiFileDir, ptlFileDir, (_a = options.template) !== null && _a !== void 0 ? _a : CodeTemplate.defaultApi); generatedFiles.push({ apiFilePath: apiFilePath, apiBaseName: apiBaseName }); logger === null || logger === void 0 ? void 0 : logger.log(chalk__default["default"].green(formatStr(i18n.apiSucc, { apiPath: apiFilePath, apiName: apiBaseName }))); } return generatedFiles; } async function genNewApiFile(apiBaseName, apiFilePath, apiFileDir, ptlFileDir, template) { /** Files exists already, skip */ if (!await fs__default$1["default"].access(apiFilePath).catch(() => true)) { return; } await fs__default$1["default"].ensureDir(path__default["default"].dirname(apiFilePath)); await fs__default$1["default"].writeFile(apiFilePath, template(apiBaseName, apiFileDir, ptlFileDir), { encoding: 'utf-8' }); } class CliUtil { static doing(text, doingPostFix = '...') { if (this.currentDoingText !== undefined) { this.spinner.stop(); } this.currentDoingText = text; this.spinner.text = chalk__default["default"].yellow(`${text}${doingPostFix}\n`); this.spinner.start(); } static done(succ = true, text) { if (this.currentDoingText !== undefined) { text = `${text !== null && text !== void 0 ? text : this.currentDoingText}`; succ ? this.spinner.succeed(chalk__default["default"].green(text)) : this.spinner.fail(chalk__default["default"].red(text)); this.currentDoingText = undefined; } } static clear() { this.spinner.stop(); this.currentDoingText = undefined; } } CliUtil.spinner = ora__default["default"](''); async function cmdLink(options) { var _a; if (options.config) { if (!((_a = options.config.sync) === null || _a === void 0 ? void 0 : _a.length)) { console.log(chalk__default["default"].yellow(i18n.nothingSyncConf)); return; } let linkConfs = options.config.sync.filter(v => v.type === 'symlink'); await ensureSymlinks(linkConfs.map(v => ({ src: v.from, dst: v.to })), console); console.log(chalk__default["default"].green(i18n.allLinkedSucc)); } else if (options.elevate) { let confs = JSON.parse(decodeURIComponent(options.elevate)); await ensureSymlinks(confs, console, true); } else { // Validate options if (!options.from) { throw error(i18n.missingParam, { param: 'from' }); } if (!options.to) { throw error(i18n.missingParam, { param: 'to' }); } await ensureSymlinks([{ src: options.from, dst: options.to }], console); console.log(chalk__default["default"].green(i18n.linkedSucc)); } } async function ensureSymlinks(confs, logger, isElevate) { // 通过 elevate 运行的结果,undefined 代表尚未通过 elevate 运行 let elevateResult; let createJunction; let isAllSucc = true; for (let conf of confs) { let { src, dst } = conf; src = path__default["default"].resolve(src); dst = path__default["default"].resolve(dst); if (elevateResult) { logger === null || logger === void 0 ? void 0 : logger.log(chalk__default["default"].green(`✔ ${createJunction ? i18n.junction : i18n.link} ${src} -> ${dst}`)); continue; } // 检查 dst 上级目录存在 const dstParent = path__default["default"].dirname(dst); if (!(await fs__default$1["default"].access(dstParent).then(() => true).catch(() => false))) { logger === null || logger === void 0 ? void 0 : logger.log(chalk__default["default"].yellow(`✘ ${i18n.link} ${src} -> ${dst}\n |- ${formatStr(i18n.dirNotExists, { dir: dstParent })}`)); isAllSucc = false; continue; } await fs__default$1["default"].ensureDir(src); // 已经创建过同款,不需要重复创建 if (await isSymbolicLink(dst, src)) { logger === null || logger === void 0 ? void 0 : logger.log(chalk__default["default"].green(`✔ ${createJunction ? i18n.junction : i18n.link} ${src} -> ${dst}`)); continue; } // 没有现成的,重建(重建前先清理) await fs__default$1["default"].rm(dst, { recursive: true, force: true }); // 按相对目录创建 const relativeSrc = path__default["default"].relative(dstParent, src); // Try first time let err = await fs__default$1["default"].symlink(relativeSrc, dst, createJunction ? 'junction' : 'dir').catch(e => e); // Windows 下无权限,尝试提权 if (!isElevate && process.platform === 'win32' && (err === null || err === void 0 ? void 0 : err.code) === 'EPERM') { // 尚未尝试过提权,提权重试 while (elevateResult === undefined) { // 提权执行 let elevateCmd = `"${path__default["default"].resolve(resPath, 'elevate.cmd')}" "${process.execPath}" ${process.execArgv.join(' ')} "${process.argv[1]}" link --elevate="${encodeURIComponent(JSON.stringify(confs))}"`; await new Promise(rs => { childProcess.exec(elevateCmd).on('close', rs); }); // 由于 elevate 的调起机制,无法获取返回结果 // 所以通过轮询来检测,最多等待 5 秒 CliUtil.doing(i18n.elevatingForLink); let startTime = Date.now(); while (Date.now() - startTime <= 5000) { // 普通重试,验证提权结果 elevateResult = await isSymbolicLink(dst, src); if (elevateResult) { err = undefined; break; } await new Promise(rs => setTimeout(rs, 500)); } CliUtil.done(!!elevateResult); // 提权失败,询问是否创建 junction if (!elevateResult) { let answer = (await inquirer__default["default"].prompt({ type: 'list', message: chalk__default["default"].yellow(i18n.linkFailed), choices: [ { name: i18n.linkRetry, value: 'retry' }, { name: i18n.linkJunction, value: 'junction' }, ], name: 'res' })).res; // 清除结果,重试 if (answer === 'retry') { elevateResult = undefined; } // 改为创建 Junction· else { createJunction = true; } } } // 提权失败,创建 junction if (elevateResult === false && createJunction) { err = await fs__default$1["default"].symlink(relativeSrc, dst, 'junction').catch(e => e); } } // Fail if (err) { throw err; } // Success logger === null || logger === void 0 ? void 0 : logger.log(chalk__default["default"].green(`✔ ${createJunction ? i18n.junction : i18n.link} ${src} -> ${dst}`)); } return { isAllSucc: isAllSucc }; } async function isSymbolicLink(dst, src) { // dst 已存在 Symlink if (await fs__default$1["default"].lstat(dst).then(v => v.isSymbolicLink()).catch(e => false)) { // 检查 symlink 的 destination 是否正确 const oldSymlinkPath = path__default["default"].resolve(path__default["default"].dirname(dst), await fs__default$1["default"].readlink(dst)); // 是同款 if (oldSymlinkPath === src) { return true; } } return false; } async function cmdSync(options) { var _a; if (options.config) { if (!((_a = options.config.sync) === null || _a === void 0 ? void 0 : _a.length)) { console.log(chalk__default["default"].yellow(i18n.nothingSyncConf)); return; } await syncByConfig(options.config.sync, options.config.verbose ? console : undefined); console.log(chalk__default["default"].green(i18n.allSyncedSucc)); } else { // Validate options if (!options.from) { throw error(i18n.missingParam, { param: 'from' }); } if (!options.to) { throw error(i18n.missingParam, { param: 'to' }); } if (await fs__default$1["default"].access(options.from).catch(() => true)) { throw error(i18n.dirNotExists, { dir: path__default["default"].resolve(options.from) }); } CliUtil.doing(`${i18n.copy} '${path__default["default"].resolve(options.from)}' -> '${path__default["default"].resolve(options.to)}'`); await copyDirReadonly(options.from, options.to, true, true, options.verbose ? console : undefined); CliUtil.done(true); console.log(chalk__default["default"].green(i18n.syncedSucc)); } } async function syncByConfig(syncConfig, logger) { var _a; if (!syncConfig.length) { return; } // Copy for (let item of syncConfig) { if (item.type === 'copy') { CliUtil.doing(`${i18n.copy} '${item.from}' -> '${item.to}'`); aw