开发中遇到一个需求:测试环境的接口是 test.xxx.cn/, 正式的接口是 xxx.cn/, 为了防止在正式环境中访问测试接口,ci 中进行了代码匹配,如果代码段中存在 test.xxx.cn 就会抛错误。但是会存在一些误查的问题,本篇文章是记录该问题的解决方案。
效果
- 转换前
// @ci-ignore
const url = 'test.abc.cn'
- 转换后
const url = ['t','e', 's', 't', '.', 'a', 'b', 'c', '.', 'c', 'n'].join('')
思路
- 遍历文件,如果存在符合的注释节点,继续便利,否则跳过
- 筛选出符合条件的节点
- 判断节点属性
- 节点是模版字符串 -> 转换为普通字符串
- 节点是普通字符串 -> 进行模版转换
- 节点是其他 -> 遍历子节点的字符串
源码
const { declare } = require('@babel/helper-plugin-utils')
const t = require('@babel/types')
const CI_IGNORE = '@ci-ignore'
/**
* 把字符串转换为 对应的表达式
* @example
* 'abc' => ['a','b','c',].join('')
*/
function stringToStatement(str) {
let statement = '['
str.split('').forEach(s => {
statement += `'${s}',`
})
statement += "].join('')"
console.log('CI-IGNORE:', str, '=>', statement)
return statement
}
function buildConcatCallExpressions(items) {
let avail = true
return items.reduce(function(left, right) {
let canBeInserted = t.isLiteral(right)
if (!canBeInserted && avail) {
canBeInserted = true
avail = false
}
if (canBeInserted && t.isCallExpression(left)) {
left.arguments.push(right)
return left
}
return t.callExpression(t.memberExpression(left, t.identifier('concat')), [
right
])
})
}
/**
* 检查是否存在符合要求的注释
*/
function existComments(comments) {
return !!comments.find(item => item.value.includes(CI_IGNORE))
}
/**
* 将模版字符串转换为普通字符串,
* 参考:transform-template-literals 官方插件
* @param {*} params
*/
function transformTemplateLiterals(path) {
const nodes = []
const ignoreToPrimitiveHint = false
const expressions = path.get('expressions')
let index = 0
for (const elem of path.node.quasis) {
if (elem.value.cooked) {
nodes.push(t.stringLiteral(elem.value.cooked))
}
if (index < expressions.length) {
const expr = expressions[index++]
const node = expr.node
if (!t.isStringLiteral(node, { value: '' })) {
nodes.push(node)
}
}
}
// since `+` is left-to-right associative
// ensure the first node is a string if first/second isn't
if (
!t.isStringLiteral(nodes[0]) &&
!(ignoreToPrimitiveHint && t.isStringLiteral(nodes[1]))
) {
nodes.unshift(t.stringLiteral(''))
}
let root = nodes[0]
if (ignoreToPrimitiveHint) {
for (let i = 1; i < nodes.length; i++) {
root = t.binaryExpression('+', root, nodes[i])
}
} else if (nodes.length > 1) {
root = buildConcatCallExpressions(nodes)
}
path.replaceWith(root)
}
function transformStringLiteralToStatement(path, api) {
/**
* 增加 isNew 字段, 防止处理自己生成的代码段(死循环)
*/
if (path.node.isNew) {
return
}
/**
* 禁用默认占位符
* https://github.com/babel/babel/issues/8067
*/
path.replaceWith(
api.template.statement(stringToStatement(path.node.value), {
placeholderPattern: false
})()
)
path.traverse({
StringLiteral(path) {
path.node.isNew = true
}
})
}
const ciIgnorePlugin = declare((api, options) => {
return {
name: 'plugin-ci-ignore',
visitor: {
// 遍历文件
Program(path, state) {
if (existComments(path.container.comments)) {
path.traverse({
// 遍历节点
enter(path, state) {
if (
path.node &&
path.node.leadingComments &&
existComments(path.node.leadingComments)
) {
if (path.node.type === 'TemplateLiteral') {
transformTemplateLiterals(path)
}
if (path.node.type === 'StringLiteral') {
transformStringLiteralToStatement(path, api)
}
path.traverse({
TemplateLiteral: transformTemplateLiterals,
StringLiteral: path =>
transformStringLiteralToStatement(path, api)
})
}
}
})
}
}
}
}
})
module.exports = ciIgnorePlugin