const anyMatch = require('anymatch');
const _ = require('lodash');
const store = require('./store');
const generator = require('@babel/generator').default;
const t = require('@babel/types');
function getNodeValue(node, path) {
if (!node) {
return null;
}
if (t.isStringLiteral(node)) {
return _.get(node, 'value');
}
if (t.isTemplateLiteral(node)) {
const quasiList = _.get(node, 'quasis') || [];
const expressions = _.get(node, 'expressions') || [];
if (quasiList.length === 1 && expressions.length === 0) {
return _.get(quasiList[0], 'value.raw');
}
}
if (t.isIdentifier(node)) {
const varName = _.get(node, 'name');
const scope = path.scope.getBinding(varName);
const varInitNode = _.get(scope, 'path.node.init');
return getNodeValue(varInitNode, path);
}
return generator(node, { minified: true }).code;
}
function checkVisitor(file) {
return file.get('needVisitor');
}
/**
* 将 method 值转大写,若无值,则赋默认值GET
* @param {string} method
* @returns
*/
function upperCaseMethod(method) {
return method ? method.toUpperCase() : 'GET';
}
function save(state, values) {
const { file } = state;
let all = file.get('requests');
all = _.uniqWith([...all, ...values], _.isEqual);
file.set('requests', all);
}
/**
* 获取参数对象中的特定属性
* @param {*} callPath callPath
* @param {*} index 参数下标
* @param {*} prop 属性名
* @returns 属性值
*/
function getObjParamPropValue(callPath, index, prop) {
const callNode = callPath.node;
const argType = _.get(callNode, `arguments.${index}.type`);
if (argType !== 'ObjectExpression') return undefined;
const propNodes = _.get(callNode, `arguments.${index}.properties`);
const find = propNodes.find((pn) => _.get(pn, 'key.name') === prop);
const valueNode = _.get(find, 'value');
// 若是变量取初始值
if (valueNode && valueNode.type === 'Identifier') {
const { name } = valueNode;
const scope = callPath.scope.getBinding(name);
const initNode = _.get(scope, 'path.node.init');
const code = getNodeValue(initNode);
return code;
}
const code = getNodeValue(valueNode);
return code || '';
}
/**
* 从callPath的node中,获取 url 参数
* @param {object} callPath
* @returns
*/
function getUrlFromCallPath(callPath) {
let apiString = '';
const callNode = callPath.node;
const arg0Type = _.get(callNode, 'arguments.0.type');
// 第一个参数为模板字符串,普通字符串,拼接变量字符串
if (['TemplateLiteral', 'StringLiteral', 'BinaryExpression'].includes(arg0Type)) {
const code = getNodeValue(_.get(callNode, 'arguments.0'));
apiString = code;
}
// 第一个参数为变量取初始值
if (arg0Type === 'Identifier') {
const name = _.get(callNode, 'arguments.0.name');
const scope = callPath.scope.getBinding(name);
const apiNode = _.get(scope, 'path.node.init');
const code = getNodeValue(apiNode);
apiString = code;
}
// 第一个参数为对象
if (arg0Type === 'ObjectExpression') {
const code = getObjParamPropValue(callPath, 0, 'url');
apiString = code;
}
return apiString;
}
function getInfoFromCallPath(callPath) {
const getInfoMethodMap = {
// eslint-disable-next-line no-use-before-define
MemberExpression: getMemberTypeInfo,
// eslint-disable-next-line no-use-before-define
CallExpression: getConfigTypeInfo,
};
const nodeType = _.get(callPath, 'node.type');
const getFn = getInfoMethodMap[nodeType];
return getFn(callPath);
}
/**
* 从MemberExpression类型的调用中获取api信息
* @param {object} memberPath
* @returns
*/
function getMemberTypeInfo(memberPath) {
const { node } = memberPath;
const method = _.get(node, 'property.name');
const { parentPath } = memberPath;
const url = getUrlFromCallPath(parentPath);
if (method === 'create') {
const varPath = memberPath.findParent((p) => p.type === 'VariableDeclarator');
const name = _.get(varPath, 'node.id.name');
if (!name) return [];
const bindings = varPath.scope.getBinding(name);
return bindings.referencePaths.reduce((acc, cur) => {
const callPath = cur.parentPath;
const infos = getInfoFromCallPath(callPath);
return acc.concat(infos);
}, []);
}
if (method === 'request') {
const callPath = memberPath.parentPath;
return getInfoFromCallPath(callPath);
}
return [
{
method: upperCaseMethod(method),
url,
},
];
}
/**
* 通过导入的库获取api信息
* @param {*} path import path
* @returns
*/
function getInfos(path) {
const source = _.get(path, 'node.source.value');
const specifiers = _.get(path, 'node.specifiers') || [];
if (source !== 'axios') return [];
const importNames = specifiers
.map((item) => _.get(item, 'local.name'));
return importNames.reduce((iAcc, name) => {
const bindings = path.scope.getBinding(name);
return bindings.referencePaths.reduce((rAcc, bPath) => {
const callPath = bPath.parentPath;
const infos = getInfoFromCallPath(callPath);
return rAcc.concat(infos);
}, iAcc);
}, []);
}
/**
* 获取config配置方式的api信息
* @param {*} path
* @returns
*/
function getConfigTypeInfo(path) {
const { node } = path;
let method = ''; let apiString = '';
apiString = getUrlFromCallPath(path);
if (!apiString) return [];
const arg0Type = _.get(node, 'arguments.0.type');
// 第一个参数为对象
if (arg0Type === 'ObjectExpression') {
method = getObjParamPropValue(path, 0, 'method');
}
// 第二个参数为对象
const arg1Type = _.get(node, 'arguments.1.type');
if (arg1Type === 'ObjectExpression') {
method = getObjParamPropValue(path, 1, 'method');
}
return [{
method: upperCaseMethod(method),
url: apiString,
}];
}
const cache = [];
module.exports = () => ({
pre(file) {
const options = this.opts || {};
const {
include,
exclude,
apiFileMatch,
} = options;
if (cache.includes(this.filename)) {
file.set('needVisitor', false);
return;
}
if (include) {
const isIncludeCurrent = anyMatch(include, this.filename);
if (!isIncludeCurrent) {
file.set('needVisitor', false);
return;
}
}
if (exclude) {
const isExcludeCurrent = anyMatch(exclude, this.filename);
if (isExcludeCurrent) {
file.set('needVisitor', false);
return;
}
}
if (apiFileMatch) {
const isApiFile = anyMatch(apiFileMatch, this.filename);
if (isApiFile) {
file.set('needVisitor', false);
return;
}
}
file.set('needVisitor', true);
cache.push(this.filename);
file.set('requests', []);
},
visitor: {
ImportDeclaration(path, state) {
const isContinue = checkVisitor(state.file);
if (!isContinue) return;
const infos = getInfos(path);
infos.length && save(state, infos);
},
},
post(file) {
const isContinue = checkVisitor(file);
if (!isContinue) return;
const requests = file.get('requests');
requests.length && store.set(this.filename, {
requestList: requests,
});
},
});