[Babel插件] 把字符串拆分 'a,b,c' => ['a', 'b', 'c'].join('')

289 阅读1分钟

开发中遇到一个需求:测试环境的接口是 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('')

思路

  1. 遍历文件,如果存在符合的注释节点,继续便利,否则跳过
  2. 筛选出符合条件的节点
  3. 判断节点属性
    • 节点是模版字符串 -> 转换为普通字符串
    • 节点是普通字符串 -> 进行模版转换
    • 节点是其他 -> 遍历子节点的字符串

image.png

源码

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