index.js 6.61 KB
var esutils = require('esutils')
var groupProps = require('./lib/group-props')
var mustUseProp = require('./lib/must-use-prop')

var isInsideJsxExpression = function (t, path) {
  if (!path.parentPath) {
    return false
  }
  if (t.isJSXExpressionContainer(path.parentPath)) {
    return true
  }
  return isInsideJsxExpression(t, path.parentPath)
}

module.exports = function (babel) {
  var t = babel.types

  return {
    inherits: require('babel-plugin-syntax-jsx'),
    visitor: {
      JSXNamespacedName (path) {
        throw path.buildCodeFrameError(
          'Namespaced tags/attributes are not supported. JSX is not XML.\n' +
          'For attributes like xlink:href, use xlinkHref instead.'
        )
      },
      JSXElement: {
        exit (path, file) {
          // turn tag into createElement call
          var callExpr = buildElementCall(path.get('openingElement'), file)
          if (path.node.children.length) {
            // add children array as 3rd arg
            callExpr.arguments.push(t.arrayExpression(path.node.children))
            if (callExpr.arguments.length >= 3) {
              callExpr._prettyCall = true
            }
          }
          path.replaceWith(t.inherits(callExpr, path.node))
        }
      },
      'Program' (path) {
        path.traverse({
          'ObjectMethod|ClassMethod' (path) {
            const params = path.get('params')
            // do nothing if there is (h) param
            if (params.length && params[0].node.name === 'h') {
              return
            }
            // do nothing if there is no JSX inside
            const jsxChecker = {
              hasJsx: false
            }
            path.traverse({
              JSXElement () {
                this.hasJsx = true
              }
            }, jsxChecker)
            if (!jsxChecker.hasJsx) {
              return
            }
            // do nothing if this method is a part of JSX expression
            if (isInsideJsxExpression(t, path)) {
              return
            }
            const isRender = path.node.key.name === 'render'
            // inject h otherwise
            path.get('body').unshiftContainer('body', t.variableDeclaration('const', [
              t.variableDeclarator(
                t.identifier('h'),
                (
                  isRender
                    ? t.memberExpression(
                      t.identifier('arguments'),
                      t.numericLiteral(0),
                      true
                    )
                    : t.memberExpression(
                      t.thisExpression(),
                      t.identifier('$createElement')
                    )
                )
              )
            ]))
          },
          JSXOpeningElement (path) {
            const tag = path.get('name').node.name
            const attributes = path.get('attributes')
            const typeAttribute = attributes.find(attributePath => attributePath.node.name && attributePath.node.name.name === 'type')
            const type = typeAttribute && t.isStringLiteral(typeAttribute.node.value) ? typeAttribute.node.value.value : null

            attributes.forEach(attributePath => {
              const attribute = attributePath.get('name')

              if (!attribute.node) {
                return
              }

              const attr = attribute.node.name

              if (mustUseProp(tag, type, attr) && t.isJSXExpressionContainer(attributePath.node.value)) {
                attribute.replaceWith(t.JSXIdentifier(`domProps-${attr}`))
              }
            })
          }
        })
      }
    }
  }

  function buildElementCall (path, file) {
    path.parent.children = t.react.buildChildren(path.parent)
    var tagExpr = convertJSXIdentifier(path.node.name, path.node)
    var args = []

    var tagName
    if (t.isIdentifier(tagExpr)) {
      tagName = tagExpr.name
    } else if (t.isLiteral(tagExpr)) {
      tagName = tagExpr.value
    }

    if (t.react.isCompatTag(tagName)) {
      args.push(t.stringLiteral(tagName))
    } else {
      args.push(tagExpr)
    }

    var attribs = path.node.attributes
    if (attribs.length) {
      attribs = buildOpeningElementAttributes(attribs, file)
      args.push(attribs)
    }
    return t.callExpression(t.identifier('h'), args)
  }

  function convertJSXIdentifier (node, parent) {
    if (t.isJSXIdentifier(node)) {
      if (node.name === 'this' && t.isReferenced(node, parent)) {
        return t.thisExpression()
      } else if (esutils.keyword.isIdentifierNameES6(node.name)) {
        node.type = 'Identifier'
      } else {
        return t.stringLiteral(node.name)
      }
    } else if (t.isJSXMemberExpression(node)) {
      return t.memberExpression(
        convertJSXIdentifier(node.object, node),
        convertJSXIdentifier(node.property, node)
      )
    }
    return node
  }

  /**
   * The logic for this is quite terse. It's because we need to
   * support spread elements. We loop over all attributes,
   * breaking on spreads, we then push a new object containing
   * all prior attributes to an array for later processing.
   */

  function buildOpeningElementAttributes (attribs, file) {
    var _props = []
    var objs = []

    function pushProps () {
      if (!_props.length) return
      objs.push(t.objectExpression(_props))
      _props = []
    }

    while (attribs.length) {
      var prop = attribs.shift()
      if (t.isJSXSpreadAttribute(prop)) {
        pushProps()
        prop.argument._isSpread = true
        objs.push(prop.argument)
      } else {
        _props.push(convertAttribute(prop))
      }
    }

    pushProps()

    objs = objs.map(function (o) {
      return o._isSpread ? o : groupProps(o.properties, t)
    })

    if (objs.length === 1) {
      // only one object
      attribs = objs[0]
    } else if (objs.length) {
      // add prop merging helper
      var helper = file.addImport('babel-helper-vue-jsx-merge-props', 'default', '_mergeJSXProps')
      // spread it
      attribs = t.callExpression(
        helper,
        [t.arrayExpression(objs)]
      )
    }
    return attribs
  }

  function convertAttribute (node) {
    var value = convertAttributeValue(node.value || t.booleanLiteral(true))
    if (t.isStringLiteral(value) && !t.isJSXExpressionContainer(node.value)) {
      value.value = value.value.replace(/\n\s+/g, ' ')
    }
    if (t.isValidIdentifier(node.name.name)) {
      node.name.type = 'Identifier'
    } else {
      node.name = t.stringLiteral(node.name.name)
    }
    return t.inherits(t.objectProperty(node.name, value), node)
  }

  function convertAttributeValue (node) {
    if (t.isJSXExpressionContainer(node)) {
      return node.expression
    } else {
      return node
    }
  }
}