moduleVisitor.js 3.75 KB
'use strict'
exports.__esModule = true

/**
 * Returns an object of node visitors that will call
 * 'visitor' with every discovered module path.
 *
 * todo: correct function prototype for visitor
 * @param  {Function(String)} visitor [description]
 * @param  {[type]} options [description]
 * @return {object}
 */
exports.default = function visitModules(visitor, options) {
  // if esmodule is not explicitly disabled, it is assumed to be enabled
  options = Object.assign({ esmodule: true }, options)

  let ignoreRegExps = []
  if (options.ignore != null) {
    ignoreRegExps = options.ignore.map(p => new RegExp(p))
  }

  function checkSourceValue(source, importer) {
    if (source == null) return //?

    // handle ignore
    if (ignoreRegExps.some(re => re.test(source.value))) return

    // fire visitor
    visitor(source, importer)
  }

  // for import-y declarations
  function checkSource(node) {
    checkSourceValue(node.source, node)
  }

  // for esmodule dynamic `import()` calls
  function checkImportCall(node) {
    if (node.callee.type !== 'Import') return
    if (node.arguments.length !== 1) return

    const modulePath = node.arguments[0]
    if (modulePath.type !== 'Literal') return
    if (typeof modulePath.value !== 'string') return

    checkSourceValue(modulePath, node)
  }

  // for CommonJS `require` calls
  // adapted from @mctep: http://git.io/v4rAu
  function checkCommon(call) {
    if (call.callee.type !== 'Identifier') return
    if (call.callee.name !== 'require') return
    if (call.arguments.length !== 1) return

    const modulePath = call.arguments[0]
    if (modulePath.type !== 'Literal') return
    if (typeof modulePath.value !== 'string') return

    checkSourceValue(modulePath, call)
  }

  function checkAMD(call) {
    if (call.callee.type !== 'Identifier') return
    if (call.callee.name !== 'require' &&
        call.callee.name !== 'define') return
    if (call.arguments.length !== 2) return

    const modules = call.arguments[0]
    if (modules.type !== 'ArrayExpression') return

    for (let element of modules.elements) {
      if (element.type !== 'Literal') continue
      if (typeof element.value !== 'string') continue

      if (element.value === 'require' ||
          element.value === 'exports') continue // magic modules: http://git.io/vByan

      checkSourceValue(element, element)
    }
  }

  const visitors = {}
  if (options.esmodule) {
    Object.assign(visitors, {
      'ImportDeclaration': checkSource,
      'ExportNamedDeclaration': checkSource,
      'ExportAllDeclaration': checkSource,
      'CallExpression': checkImportCall,
    })
  }

  if (options.commonjs || options.amd) {
    const currentCallExpression = visitors['CallExpression']
    visitors['CallExpression'] = function (call) {
      if (currentCallExpression) currentCallExpression(call)
      if (options.commonjs) checkCommon(call)
      if (options.amd) checkAMD(call)
    }
  }

  return visitors
}

/**
 * make an options schema for the module visitor, optionally
 * adding extra fields.
 */
function makeOptionsSchema(additionalProperties) {
  const base =  {
    'type': 'object',
    'properties': {
      'commonjs': { 'type': 'boolean' },
      'amd': { 'type': 'boolean' },
      'esmodule': { 'type': 'boolean' },
      'ignore': {
        'type': 'array',
        'minItems': 1,
        'items': { 'type': 'string' },
        'uniqueItems': true,
      },
    },
    'additionalProperties': false,
  }

  if (additionalProperties){
    for (let key in additionalProperties) {
      base.properties[key] = additionalProperties[key]
    }
  }

  return base
}
exports.makeOptionsSchema = makeOptionsSchema

/**
 * json schema object for options parameter. can be used to build
 * rule options schema object.
 * @type {Object}
 */
exports.optionsSchema = makeOptionsSchema()