h2non/resolve-tree

View on GitHub
index.js

Summary

Maintainability
B
4 hrs
Test Coverage
const fs = require('fs')
const fw = require('fw')
const path = require('path')
const resolve = require('resolve')
const assign = require('object-assign')

// List of packages that doesn't have a main module.
// You can extend the map via mutation, or send a PR to add custom packages.
const resolutions = {
  'babel-runtime': 'babel-runtime/core-js',
  'mz': 'mz/fs',
  'spdx-exceptions': 'spdx-exceptions/index.json',
  'semantic-ui': 'sematic-ui/package.json',
  'timers-ext': 'timers-ext/package.json',
  'unicode': 'unicode/package.json'
}

// Export API
exports.resolutions = resolutions
exports.byName = resolveByName
exports.packages = resolveByName
exports.manifest = manifest
exports.flatten = flatten
exports.flattenMap = flattenMap

function manifest (meta, params, cb) {
  if (typeof params === 'function') { cb = params; params = {} }

  const opts = setOptions(params)
  const deps = readDependencies(meta, opts)

  resolveByName(deps, opts, cb)
}

function resolveByName (names, params, cb, lookups) {
  lookups = lookups || []

  if (typeof params === 'function') { cb = params; params = {} }
  if (typeof names === 'string') names = [ names ]

  const opts = setOptions(params)
  const pkgs = mapPackages(names, opts, lookups)

  lookupPackages(pkgs, opts, lookups, cb)
}

function flatten (tree, buf) {
  return tree
  .filter(exists)
  .reduce(function (buf, pkg) {
    buf.push(pkg)
    if (Array.isArray(pkg.dependencies)) {
      flatten(pkg.dependencies, buf)
    }
    return buf
  }, buf || [])
}

function flattenMap (tree, field) {
  const mapper = typeof field === 'function' ? field : function (pkg) {
    return pkg[field || 'manifest']
  }
  return flatten(tree).filter(notEmpty).map(mapper)
}

function lookupPackages (pkgs, opts, lookups, cb) {
  fw.each(pkgs, resolvePackage(lookups), function (err, pkgs) {
    if (err) return cb(err)
    resolveDependencies(filter(pkgs), opts, lookups, resolve)
  })

  function resolve (err, pkgs) {
    if (err || !pkgs) return cb(err, [])
    cb(null, filter(pkgs))
  }

  function filter (pkgs) {
    return pkgs.filter(exists)
  }
}

function findPredecessor (lookups, pkg) {
  const predecessor = lookups.reduce(function (match, lookup) {
    if (match) return match
    if (lookup === pkg) return null
    if (lookup.root === pkg.root) return lookup
  }, null)

  if (predecessor) {
    return assign({ repeated: true }, predecessor)
  }

  return null
}

function resolvePackage (lookups) {
  return function (pkg, next) {
    const name = resolutions.hasOwnProperty(pkg.name)
      ? resolutions[pkg.name]
      : path.join(pkg.name, 'package.json')

    // Resolve package via require.resolve() algorithm
    resolve(name, { basedir: pkg.basedir }, function (err, main) {
      if (err && pkg.optional) {
        return next()
      }
      if (err) {
        return next(err)
      }
      resolveManifest(main, pkg, lookups, next)
    })
  }
}

function resolveManifest (main, pkg, lookups, next) {
  const base = path.dirname(main)
  findMainfest(base, function (err, manifestPath) {
    if (err) return next(new Error('Cannot find package.json for package: ' + pkg.name))

    const manifest = readJSON(manifestPath)
    if (!manifest) return next(new Error('Bad formed JSON: ' + manifestPath))

    // Attach package required fields
    pkg.main = main
    pkg.manifest = manifestPath
    pkg.root = path.dirname(manifestPath)
    pkg.meta = manifest
    pkg.version = manifest.version

    // Detect if it is a redundant dependency
    const predecessor = findPredecessor(lookups, pkg)
    if (predecessor) return next(null, predecessor)

    next(null, pkg)
  })
}

function resolveDependencies (pkgs, opts, lookups, cb) {
  fw.each(pkgs, function (pkg, next) {
    const deps = readDependencies(pkg.meta, opts)
    if (!deps.length) return next(null, pkg)

    // Overwrite options for the next lookup
    const options = assign({}, opts)
    options.basedir = path.dirname(pkg.manifest)

    // If circular, just continue with it
    if (pkg.repeated) return next(null, pkg)

    // Resolve package child dependencies
    resolveByName(deps, options, childDependencies(pkg, next), lookups)
  }, cb)
}

function childDependencies (pkg, next) {
  return function (err, deps) {
    if (err) return next(err)
    next(err, attachDependencies(pkg, deps))
  }
}

function attachDependencies (pkg, deps) {
  pkg.dependencies = deps.filter(function (pkg) {
    return pkg && typeof pkg === 'object'
  })
  return pkg
}

function findMainfest (base, cb) {
  const file = path.join(base, 'package.json')

  fs.exists(file, function (exists) {
    if (exists) return cb(null, file)

    const parent = path.join(base, '..')
    if (isRoot(parent)) return cb(new Error('Cannot find package.json'))

    // Find package in parent directory
    findMainfest(parent, cb)
  })
}

function readDependencies (manifest, opts) {
  return opts.lookups.reduce(function (buf, type) {
    const deps = Object.keys(manifest[type] || {})
    .map(function (name) {
      return {
        name: name,
        optional: isOptional(manifest, type, name)
      }
    })
    return buf.concat(deps)
  }, [])
}

function isOptional (manifest, type, name) {
  return type === 'optionalDependencies' ||
    Object.prototype.hasOwnProperty.call(manifest.optionalDependencies || {}, name)
}

function mapPackages (pkgs, opts, lookups) {
  return pkgs
  .map(function (pkg) {
    if (typeof pkg === 'string') {
      return { name: pkg }
    }
    return pkg
  })
  .map(function (pkg) {
    const basedir = opts.basedir
    const pkgPath = path.join(basedir, 'node_modules', pkg.name, 'package.json')

    const meta = {
      name: pkg.name,
      manifest: pkgPath,
      basedir: basedir,
      optional: pkg.optional === true
    }

    lookups.push(meta)
    return meta
  })
}

function setOptions (params) {
  const cwd = process.cwd()
  const defaults = {
    lookups: ['dependencies'],
    basedir: cwd,
    root: cwd
  }
  return assign(defaults, params)
}

function readJSON (path) {
  try {
    return require(path)
  } catch (e) {
    return false
  }
}

function isRoot (dir) {
  return path.dirname(dir) === '/'
}

function notEmpty (x) {
  return x != null
}

function exists (pkg) {
  return pkg && pkg.root
}

// ---- Sync ----

// Export Sync API
exports.byNameSync = resolveByNameSync
exports.packagesSync = resolveByNameSync
exports.manifestSync = manifestSync

function manifestSync (meta, params) {
  if (typeof params === 'undefined') { params = {} }

  const opts = setOptions(params)
  const deps = readDependencies(meta, opts)

  return resolveByNameSync(deps, opts)
}

function resolveByNameSync (names, params, lookups) {
  lookups = lookups || []

  if (typeof params === 'undefined') { params = {} }
  if (typeof names === 'string') names = [ names ]

  const opts = setOptions(params)
  const pkgs = mapPackages(names, opts, lookups)

  return lookupPackagesSync(pkgs, opts, lookups)
}

function lookupPackagesSync (pkgs, opts, lookups, cb) {
  pkgs = pkgs.map(function (pkg) {
    return resolvePackageSync(lookups)(pkg)
  })
  const resolved = resolveDependenciesSync(filter(pkgs), opts, lookups)
  return filter(resolved)

  function filter (pkgs) {
    return pkgs.filter(exists)
  }
}

function resolvePackageSync (lookups) {
  return function (pkg) {
    const name = resolutions.hasOwnProperty(pkg.name)
      ? resolutions[pkg.name]
      : path.join(pkg.name, 'package.json')

    // Resolve package via require.resolve() algorithm
    try {
      const main = resolve.sync(name, { basedir: pkg.basedir })
      return resolveManifestSync(main, pkg, lookups)
    } catch (err) {
      if (err && pkg.optional) {
        return
      }
      throw err
    }
  }
}

function resolveManifestSync (main, pkg, lookups) {
  const base = path.dirname(main)
  var manifestPath
  try {
    manifestPath = findMainfestSync(base)
  } catch (err) {
    throw new Error('Cannot find package.json for package: ' + pkg.name)
  }

  const manifest = readJSON(manifestPath)
  if (!manifest) throw new Error('Bad formed JSON: ' + manifestPath)

  // Attach package required fields
  pkg.main = main
  pkg.manifest = manifestPath
  pkg.root = path.dirname(manifestPath)
  pkg.meta = manifest
  pkg.version = manifest.version

  // Detect if it is a redundant dependency
  const predecessor = findPredecessor(lookups, pkg)
  if (predecessor) return predecessor

  return pkg
}

function findMainfestSync (base) {
  const file = path.join(base, 'package.json')

  const exists = fs.existsSync(file)
  if (exists) return file

  const parent = path.join(base, '..')
  if (isRoot(parent)) throw new Error('Cannot find package.json')

  // Find package in parent directory
  return findMainfestSync(parent)
}

function resolveDependenciesSync (pkgs, opts, lookups) {
  return pkgs.map(function (pkg) {
    const deps = readDependencies(pkg.meta, opts)
    if (!deps.length) return pkg

    // Overwrite options for the next lookup
    const options = assign({}, opts)
    options.basedir = path.dirname(pkg.manifest)

    // If circular, just continue with it
    if (pkg.repeated) return pkg

    // Resolve package child dependencies
    const resolved = resolveByNameSync(deps, options, lookups)
    return childDependenciesSync(pkg)(resolved)
  })
}

function childDependenciesSync (pkg) {
  return function (deps) {
    return attachDependencies(pkg, deps)
  }
}