lib/pkg.js

Summary

Maintainability
C
7 hrs
Test Coverage
var path = require('path')
var fs = require('fs')
var spawn = require('child_process').spawn
var cache = require('npm-cache-filename')
var log = require('npmlog')
var rimraf = require('rimraf')
var download = require('download')
var tmp = path.join(require('os').tmpdir(), 'cache')
var mkdirp = require('mkdirp')
var pack = require('tar-pack').pack
var unpack = require('tar-pack').unpack
var loadJsonFile = require('load-json-file')
var writeJsonFile = require('write-json-file')

function NapaPkg (url, name, opts) {
  if (!(this instanceof NapaPkg)) return new NapaPkg(url, name, opts)
  var self = this
  opts = opts || {}
  this.cwd = opts.cwd || process.cwd()
  this.log = opts.log || log
  if (opts['log-level']) this.log.level = opts['log-level']
  this._mock = opts._mock
  this.ref = opts.ref
  this.url = url
  this.name = name
  this.installTo = path.join(this.cwd, 'node_modules', this.name)
  this.useCache = (typeof opts.cache === 'undefined') || opts.cache !== false
  this.cacheTo = cache(
    typeof opts['cache-path'] !== 'string'
      ? tmp
      : path.resolve(this.cwd, opts['cache-path']),
    this.url
  )
  this._napaResolvedKey = '_napaResolved'
  this.saveToPkgJson = opts.save

  Object.defineProperty(self, 'installed', {
    get: function () {
      var existing = path.join(self.installTo, 'package.json')
      return (fs.existsSync(existing) && require(existing)[self._napaResolvedKey] === url)
    }
  })
  Object.defineProperty(self, 'cached', {
    get: function () { return fs.existsSync(self.cacheTo) }
  })
}
module.exports = NapaPkg

NapaPkg.prototype.install = function (done) {
  var self = this
  done = done || function () {}

  // Save to package.json
  if (self.saveToPkgJson) self.save()

  // Do nothing if already installed
  if (self.installed) return done()

  function cb (err) {
    if (err) return done(err.message)
    self.writePackageJson(function (err) {
      if (err) return done(err.message)
      if (self.useCache) {
        self.cache(done)
      } else {
        return done()
      }
    })
  }

  function cacheInstall () {
    if (typeof self._mock === 'function') {
      self._mock(['cache', self.url, self.name])
    } else {
      self.log.info('cache', '%s into %s', self.url, self.name)
      fs.createReadStream(self.cacheTo)
        .pipe(unpack(self.installTo, cb))
    }
  }

  function gitInstall () {
    var args = ['clone', '--depth', '1', '-q', (self.url.replace('git+', '')), self.installTo]
    var cmd = ['git', args]
    if (self.ref) args.splice(1, 2)
    if (typeof self._mock === 'function') {
      self._mock(cmd)
    } else {
      self.log.info('git', '%s into %s', self.url, self.name)
      var git = spawn.apply(spawn, cmd)
      git.stderr.on('data', log.error)
      git.on('close', function (code, signal) {
        var checkout
        if (code) return cb(code, signal)
        if (self.ref) {
          checkout = spawn('git', ['checkout', self.ref], {cwd: self.installTo})
          checkout.stderr.on('data', log.info)
          checkout.on('close', function () {
            rimraf(path.resolve(self.installTo, '.git'), cb)
          })
        } else {
          rimraf(path.resolve(self.installTo, '.git'), cb)
        }
      })
    }
  }

  function downloadInstall () {
    if (typeof self._mock === 'function') {
      self._mock(['download', self.url, self.installTo])
    } else {
      self.log.info('download', '%s into %s', self.url, self.name)
      download(self.url, self.installTo, { extract: true, strip: 1 })
        .then(function () { cb() }, cb)
    }
  }

  // is this a git repo url?
  var gitUrls = ['git+', 'git://']
  var githubRepoUrls = /github\.com(?:\/[^/]+){2}($|#)/

  // Determine which type of install we would like
  rimraf(self.installTo, function (err) {
    if (err) log.error(err.message)
    if (self.useCache && fs.existsSync(self.cacheTo)) {
      return cacheInstall()
    } else if (gitUrls.indexOf(self.url.slice(0, 4)) !== -1) {
      return gitInstall()
    } else {
      if (githubRepoUrls.test(self.url)) {
        return gitInstall()
      }
      return downloadInstall()
    }
  })
}

// Caches a locally installed package
NapaPkg.prototype.cache = function (done) {
  var self = this
  if (!this.installed) return done()
  mkdirp(path.dirname(self.cacheTo), function (err) {
    if (err) return done(err)
    var dest = fs.createWriteStream(self.cacheTo)
    pack(self.installTo, {ignoreFiles: []})
      .pipe(dest)
      .on('close', done)
  })
}

// TODO: Replace this with metamorph and ability to override
NapaPkg.prototype.writePackageJson = function (done) {
  var filepath = path.join(this.installTo, 'package.json')
  var pkg = null
  if (!fs.existsSync(filepath)) {
    pkg = {
      name: this.name,
      version: '0.0.0',
      description: '-',
      repository: {type: 'git', url: '-'},
      readme: '-'
    }
  } else {
    pkg = require(filepath)
  }
  pkg[this._napaResolvedKey] = this.url
  fs.writeFile(filepath, JSON.stringify(pkg, null, 2), done)
}

// Save to package.json
NapaPkg.prototype.save = function () {
  var self = this
  var pkgJson = path.join(self.cwd, 'package.json')

  try {
    // Load package.json
    var json = loadJsonFile.sync(pkgJson)

    var exists = false
    if (json.napa === undefined) json.napa = {}
    if (json.napa[self.name] !== undefined) exists = true
    json.napa[self.name] = self.url

    // Write to package.json
    if (!exists) writeJsonFile.sync(pkgJson, json, { indent: 2 })
  } catch (err) {
    if (err) return self.log.error('save', 'Unable to save %s to package.json', self.name)
  }

  if (!exists) self.log.info('save', '%s to package.json', self.name)
}