59naga/babel-plugin-add-module-exports

View on GitHub
src/index.js

Summary

Maintainability
A
3 hrs
Test Coverage
// 1. find to `exports.default`
// 2. find to all Expression(`exports.default`, `exports.foo` etc)
// 3. add `module.exports` if exists only `exports.default` assignment
// The above works after executing `preset-env`(transform-es2015-modules-*) in `Plugin.post`

module.exports = ({ template }) => {
  let pluginOptions

  function addModuleExportsDefaults(path) {
    const finder = new ExportsFinder(path)
    if (!finder.isOnlyExportsDefault()) {
      return
    }
    if (finder.isAmd()) {
      return
    }
    const rootPath = finder.getRootPath()

    // HACK: `path.node.body.push` instead of path.pushContainer(due doesn't work in Plugin.post)
    rootPath.node.body.push(template('module.exports = exports.default')())
    if (pluginOptions.addDefaultProperty) {
      rootPath.node.body.push(template('module.exports.default = exports.default')())
    }
  }

  const ExportsDefaultVisitor = {
    CallExpression(path) {
      if (!path.get('callee').matchesPattern('Object.defineProperty')) {
        return
      }

      const [identifier, prop] = path.get('arguments')
      const objectName = identifier.get('name').node
      const propertyName = prop.get('value').node

      if ((objectName === 'exports' || objectName === '_exports') && propertyName === 'default') {
        addModuleExportsDefaults(path)
      }
    },
    AssignmentExpression(path) {
      if (
        path.get('left').matchesPattern('exports.default') ||
        path.get('left').matchesPattern('_exports.default')
      ) {
        addModuleExportsDefaults(path)
      }
    }
  }

  return {
    visitor: {
      Program(path, state) {
        // HACK: can't get plugin options in Plugin.post
        pluginOptions = state.opts
      }
    },
    post(fileMap) {
      fileMap.path.traverse(ExportsDefaultVisitor)
    }
  }
}

class ExportsFinder {
  constructor(exportsDefaultPath) {
    this.path = exportsDefaultPath
    this.hasExportsDefault = false
    this.hasExportsNamed = false
    this.hasModuleExports = false
  }

  getRootPath() {
    return this.path.findParent(path => {
      return path.key === 'body' || !path.parentPath
    })
  }

  isOnlyExportsDefault() {
    this.getRootPath()
      .get('body')
      .forEach(path => {
        if (path.isVariableDeclaration()) {
          this.findExports(path.get('declarations.0'), 'init')
        } else if (
          path.isExpressionStatement() &&
          path.get('expression').isAssignmentExpression()
        ) {
          this.findExports(path)
        } else {
          this.findExportsInCallExpression(path)
        }
      })
    return this.hasExportsDefault && !this.hasExportsNamed && !this.hasModuleExports
  }

  findExports(path, property = 'expression') {
    // Not `exports.anything`, skip
    if (
      !path.get(`${property}`).node ||
      !path.get(`${property}.left`).node ||
      !path.get(`${property}.left.object`).node
    ) {
      return
    }

    const objectName = path.get(`${property}.left.object.name`).node
    const propertyName = path.get(`${property}.left.property.name`).node
    if (objectName === 'exports' || objectName === '_exports') {
      if (propertyName === 'default') {
        this.hasExportsDefault = true
      } else if (propertyName !== '__esModule') {
        this.hasExportsNamed = true
      }
    }
    if (`${objectName}.${propertyName}` === 'module.exports') {
      this.hasModuleExports = true
    }
  }

  findExportsInCallExpression(path) {
    const self = this
    path.traverse({
      CallExpression(path) {
        if (!path.get('callee').matchesPattern('Object.defineProperty')) {
          return
        }

        const [identifier, prop] = path.get('arguments')
        const objectName = identifier.get('name').node
        const propertyName = prop.get('value').node

        if (
          (objectName === 'exports' || objectName === '_exports') &&
          propertyName !== '__esModule'
        ) {
          if (propertyName === 'default') {
            self.hasExportsDefault = true
          } else {
            self.hasExportsNamed = true
          }
        }
      }
    })
  }

  isAmd() {
    const rootPath = this.getRootPath()
    const hasntAmdRoot = !(rootPath.parentPath && rootPath.parentPath.parentPath)
    if (hasntAmdRoot) {
      return false
    }

    const amdRoot = rootPath.parentPath.parentPath
    if (!amdRoot.isCallExpression()) {
      return false
    }
    if (amdRoot.get('callee.name').node === 'define') {
      return true
    }
    return false
  }
}