Skip to content

Commit

Permalink
feat: 添加api辅助生成脚本
Browse files Browse the repository at this point in the history
  • Loading branch information
zoswing authored and ostli committed Feb 18, 2022
1 parent 3136b4e commit 176fe90
Show file tree
Hide file tree
Showing 6 changed files with 278 additions and 0 deletions.
8 changes: 8 additions & 0 deletions package.json
Expand Up @@ -31,16 +31,21 @@
"example": "cd example && npm run dev",
"publish:npm":"node scripts/publish.js",
"gc": "git-cz",
"gen": "node scripts/gen/index.js",
"release": "standard-version",
"clean": "rimraf lib es typings",
"linkdev": "node scripts/dev.js"
},
"devDependencies": {
"@babel/core": "^7.15.0",
"@babel/generator": "^7.17.0",
"@babel/parser": "^7.17.0",
"@babel/plugin-proposal-class-properties": "^7.14.5",
"@babel/plugin-transform-runtime": "^7.15.0",
"@babel/preset-env": "^7.15.0",
"@babel/preset-typescript": "^7.15.0",
"@babel/traverse": "^7.17.0",
"@babel/types": "^7.17.0",
"@commitlint/cli": "^13.1.0",
"@commitlint/config-conventional": "^13.1.0",
"@rollup/plugin-babel": "^5.3.0",
Expand All @@ -62,9 +67,12 @@
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-jest": "^24.4.0",
"eslint-plugin-prettier": "^3.4.0",
"handlebars": "^4.7.7",
"husky": "^7.0.1",
"inquirer": "^8.2.0",
"jest": "^25.3.0",
"lint-staged": "^11.1.2",
"lodash.clonedeep": "^4.5.0",
"prettier": "^2.3.2",
"readline-sync": "^1.4.10",
"rimraf": "^3.0.2",
Expand Down
26 changes: 26 additions & 0 deletions scripts/gen/index.js
@@ -0,0 +1,26 @@
const inquirer = require('inquirer');
const genSDK = require('./sdk');

const promptList = [
{
type: 'input',
message: '请输入API名称(如:mute,多个单词使用驼峰形式,如:channelPermission):',
name: 'name',
validate: function (val) {
if (val.match(/^\w+$/g)) {
return true;
}
return '请输入合法的API名称';
},
},
];

inquirer
.prompt(promptList)
.then((answers) => {
const { name } = answers;
genSDK(name);
})
.catch((err) => {
console.log(err);
});
208 changes: 208 additions & 0 deletions scripts/gen/sdk.js
@@ -0,0 +1,208 @@
const Handlebars = require('handlebars');
const { upperFirst, getKebabCase } = require('./utils');
const fs = require('fs');
const path = require('path');
const chalk = require('chalk');
const parser = require('@babel/parser');
const t = require('@babel/types');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const cloneDeep = require('lodash.clonedeep');

// AST操作参考文档
// https://babeljs.io/docs/en/babel-types
// https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md#toc-pushing-a-variable-declaration-to-a-parent-scope

let apiClassName; // 类 名称
let apiClassType; // 类 类型
let methodName; // 方法名称 默认一个
let uriName; // 接口url名称
let uriValue = 'xxx'; // 接口url值
let apiFileName; // 新增文件名
let apiInstanceName; // 示例名字

/**
* 根据模板生成文件内容并写入对应文件
*
* @param {*} tplFileName
* @param {*} targetFilePath
* @param {*} tplArgs
*/
const genFileByTemplate = (tplFileName, targetFilePath, tplArgs) => {
try {
const templateContent = fs.readFileSync(path.join(__dirname, `./template/${tplFileName}`));
const template = Handlebars.compile(templateContent.toString());
const apiFileContent = template(tplArgs);
fs.existsSync(targetFilePath);
fs.writeFileSync(targetFilePath, apiFileContent);
} catch (error) {
console.log(chalk.red(`从模板${tplFileName}生成文件失败`));
console.log(error);
}
};

/**
* 传入文件路径,读取文件内容 生成ast,支持修改ast,最后根据ast生成代码
*
* @param {*} filePath
* @param {*} patch
*/
const genCodeByAST = (filePath, patch) => {
const fileContent = fs.readFileSync(filePath);
let ast = parser.parse(fileContent.toString(), {
sourceType: 'module',
plugins: ['typescript'],
});
traverse(ast, {
enter(path) {
patch(path);
},
});
const newContent = generate(ast, {
// retainFunctionParens:true,
comments: false,
}).code;
fs.writeFileSync(filePath, newContent);
};

/**
* 新增文件
* @param {*} name
*/
const genApiFiles = (name) => {
// 1. src/openapi/v1下文件生成
const apiFilePath = path.join(__dirname, `../../src/openapi/v1/${apiFileName}.ts`);
genFileByTemplate('api.handlebars', apiFilePath, {
className: apiClassName,
classType: apiClassType,
methodName,
uriName,
});
// 2. src/types/openapi/v1类型文件生成
const typeFilePath = path.join(__dirname, `../../src/types/openapi/v1/${apiFileName}.ts`);
genFileByTemplate('type.handlebars', typeFilePath, {
className: apiClassName,
classType: apiClassType,
methodName,
});
};

/**
* 更新 src/openapi/v1/resource.ts 文件
* 新增 apiURI 映射
*/
const pathResourceFile = () => {
const resourceFilePath = path.resolve(__dirname, '../../src/openapi/v1/resource.ts');
genCodeByAST(resourceFilePath, (path) => {
if (path.isObjectExpression()) {
const newNode = cloneDeep(path.node.properties[0]);
newNode.key.name = uriName;
newNode.value.value = uriValue;
path.node.properties.push(newNode);
}
});
};

/**
* 更新src/types/openapi/index.ts文件
* 类型定义补充
*/
const patchTypeDefinitionFile = () => {
const typeFilePath = path.resolve(__dirname, '../../src/types/openapi/index.ts');
genCodeByAST(typeFilePath, (path) => {
// import语句
if (path.isImportDeclaration() && path.getNextSibling().isExportNamedDeclaration()) {
let importNode = t.importDeclaration(
[t.importSpecifier(t.identifier(apiClassType), t.identifier(apiClassType))],
t.stringLiteral(`./v1/${apiFileName}`),
);
path.insertBefore(importNode);
}
// IOpenAPI属性
if (path.isIdentifier({ name: 'IOpenAPI' }) && path.parent.type === 'TSInterfaceDeclaration') {
path.parentPath
.get('body')
.pushContainer(
'body',
t.tSPropertySignature(
t.identifier(apiInstanceName),
t.tsTypeAnnotation(t.tsTypeReference(t.identifier(apiClassType))),
),
);
}
// 添加导出
if (path.type === 'ExportAllDeclaration' && !path.getNextSibling().node) {
path.insertBefore(t.exportAllDeclaration(t.stringLiteral(`./v1/${apiFileName}`)));
}
});
};

/**
* 更新 src/openapi/v1/openapi.ts 文件
* API类定义补充
*/
const patchDefinitionFile = () => {
const definitionFilePath = path.resolve(__dirname, '../../src/openapi/v1/openapi.ts');
genCodeByAST(definitionFilePath, (path) => {
// 导入类名
if (path.isImportDeclaration() && path.node.source.value === 'resty-client') {
path.insertAfter(
t.importDeclaration(
[t.importDefaultSpecifier(t.identifier(apiClassName))],
t.stringLiteral(`./${apiFileName}`),
),
);
}
// 导入类型
if (path.isImportDeclaration() && path.node.source.value === '@src/types') {
path.node.specifiers.push(t.importSpecifier(t.identifier(apiClassType), t.identifier(apiClassType)));
}
// 成员变量声明
if (path.isClassProperty() && path.getNextSibling().isClassMethod()) {
let newNode = cloneDeep(path.node);
newNode.key.name = apiInstanceName;
newNode.typeAnnotation.typeAnnotation.typeName.name = apiClassType;
path.insertBefore(newNode);
}
// api实例对象创建
if (path.isClassMethod() && path.node.key.name === 'register') {
path
.get('body')
.pushContainer(
'body',
t.expressionStatement(
t.assignmentExpression(
'=',
t.memberExpression(t.identifier('client'), t.identifier(apiInstanceName)),
t.newExpression(t.identifier(apiClassName), [
t.memberExpression(t.thisExpression(), t.identifier('request')),
t.memberExpression(t.thisExpression(), t.identifier('config')),
]),
),
),
);
console.log(10);
}
});
};

/**
* 更新各种需要补充的文件
*/
const patchApiFiles = () => {
pathResourceFile();
patchTypeDefinitionFile();
patchDefinitionFile();
};

module.exports = (name) => {
apiClassName = upperFirst(name);
apiClassType = `${apiClassName}API`;
methodName = name;
uriName = `${methodName}URI`;
apiFileName = getKebabCase(name);
apiInstanceName = `${name}Api`;

genApiFiles(name);
patchApiFiles();
};
21 changes: 21 additions & 0 deletions scripts/gen/template/api.handlebars
@@ -0,0 +1,21 @@
import { Config, OpenAPIRequest, {{classType}} } from '@src/types';
import { RestyResponse } from 'resty-client';
import { getURL } from './resource';

export default class {{className}} implements {{classType}} {
public request: OpenAPIRequest;
public config: Config;
constructor(request: OpenAPIRequest, config: Config) {
this.request = request;
this.config = config;
}

// TODO:添加注释,补全内容,补全类型
public {{methodName}}(): Promise<RestyResponse<any>> {
const options = {
method: 'GET' as const,
url: getURL('{{uriName}}'),
};
return this.request(options);
}
}
9 changes: 9 additions & 0 deletions scripts/gen/template/type.handlebars
@@ -0,0 +1,9 @@
import { RestyResponse } from 'resty-client';

/**
* ============= {{className}} 接口 =============
*/
export interface {{classType}} {
{{methodName}}: () => Promise<RestyResponse<any>>;
}

6 changes: 6 additions & 0 deletions scripts/gen/utils.js
@@ -0,0 +1,6 @@
module.exports.upperFirst = (str) => str.replace(str[0], str[0].toUpperCase());

module.exports.getKebabCase = (str) =>
str.replace(/[A-Z]/g, (item) => {
return '-' + item.toLowerCase();
});

0 comments on commit 176fe90

Please sign in to comment.