Permalink
Browse files

initial commit, working search command

  • Loading branch information...
0 parents commit 0ec60bfd1ed40748b42602573b33982a808bffc5 @rvagg rvagg committed Jan 28, 2012
@@ -0,0 +1 @@
+node_modules
@@ -0,0 +1,3 @@
+#!/usr/bin/env node
+
+require('../lib/main').exec(process.argv)
@@ -0,0 +1,46 @@
+function UnknownMainError (message) {
+ Error.call(this)
+ Error.captureStackTrace(this, arguments.callee)
+ this.message = message
+ this.name = 'UnknownMainError'
+}
+UnknownMainError.prototype.__proto__ = Error.prototype
+
+var nopt = require('nopt')
+
+ , knownOptions = {
+ 'output': String
+ , 'use': String
+ , 'max': Number
+ , 'sandbox': Array
+ , 'noop': Boolean
+ , 'silent': Boolean
+ , 'help': Boolean
+ , 'sans': Boolean
+ , 'debug': Boolean
+ }
+
+ , shorthandOptions = {
+ 'o': 'output'
+ , 'u': 'use'
+ , 'x': 'noop'
+ , 's': 'silent'
+ }
+
+ , knownMains = [ 'help', 'build', 'refresh', 'info', 'search', 'compile' ]
+
+ , parse = function (argv) {
+ var parsed = nopt(knownOptions, shorthandOptions, argv)
+ , main = parsed.argv.remain[0]
+ , remaining = parsed.argv.remain.slice(1)
+
+ if (knownMains.indexOf(main) == -1)
+ throw new UnknownMainError('Unknown main command "' + main + '"')
+
+ return {
+ main: main
+ , remaining: remaining
+ }
+ }
+
+module.exports.parse = parse
@@ -0,0 +1,79 @@
+var extend = require('./util').extend
+ , searchUtil = require('./main-search-util')
+ , Output = require('./output')
+
+ , SearchOutput = extend(Output, { // inherit from Output
+
+ test: function () {
+ console.log('test', this.outfd, this.debug)
+ }
+
+ , searchInit: function () {
+ this.statusMsg('Searching NPM registry...')
+ this.log()
+ }
+
+ , searchNoResults: function () {
+ this.warnMsg('Sorry, we couldn\'t find anything. :(')
+ }
+
+ , searchError: function (err) {
+ this.repositoryError(err, 'Something went wrong searching NPM')
+ }
+
+ , searchResults: function (results) {
+ if (results.primary) {
+ this.heading('Ender tagged results:')
+ results.primary.forEach(function (item) {
+ this.processItem(item, results.terms)
+ }.bind(this))
+ }
+
+ if (results.secondary) {
+ var meta = results.secondaryTotal > results.secondary.length
+ ? results.secondary.length + ' of ' + results.secondaryTotal
+ : ''
+ this.heading('NPM general results:', meta)
+ results.secondary.forEach(function (item) {
+ this.processItem(item, results.terms)
+ }.bind(this))
+ }
+ }
+
+ , processItem: function (item, terms) {
+ var reg = new RegExp('(' + terms.map(function (item) {
+ return searchUtil.escapeRegExp(item)
+ }).join('|') + ')', 'ig')
+ , maintainers = ''
+ , title = item.name
+ , last
+
+ if (item.description)
+ title += ' - ' + item.description.substring(0, 80) + (item.description.length > 80 ? '...' : '')
+
+ this.log('+ ' + title.replace(reg, '$1'.cyan))
+
+ if (item.maintainers && item.maintainers.length) {
+ item.maintainers = item.maintainers.map(function (maintainer) {
+ return maintainer.replace(/^=/, '@')
+ })
+
+ if (item.maintainers.length > 1) {
+ last = item.maintainers.splice(-1)[0]
+ maintainers = item.maintainers.join(', ')
+ maintainers += ' & ' + last
+ } else {
+ maintainers = item.maintainers[0]
+ }
+ }
+
+ this.log(' by ' + maintainers.replace(reg, '$1'.cyan) + '\n')
+ }
+
+ , create: function (outfd, debug) {
+ return Object.create(this).init(outfd, debug)
+ }
+
+ })
+
+module.exports = SearchOutput
@@ -0,0 +1,38 @@
+ // make a string regex friendly by escaping regex characters
+ // Credit: XRegExp 0.6.1 (c) 2007-2008 Steven Levithan <http://stevenlevithan.com/regex/xregexp/> MIT License
+var escapeRegExp = function (string) {
+ return string.replace(/[-[\]{}()*+?.\\^$|,#\s]/g, function (match) {
+ return '\\' + match
+ })
+ }
+
+ // given a regex, an array of objects and an array of properties prioritised, populate a ranked
+ // array with objects whose properties match the regex in priority order. elements get removed
+ // from source array when they are put into the priority array.
+ // array: [ { a: '1;, b: '2' }, { a: '3', b: '4' } ]
+ // ranked: [] (effective return)
+ // priority: [ 'a', 'b' ] (properties in 'array' elements)
+ , sortByRegExp = function (regex, array, ranked, priority) {
+ for (var i = 0; i < priority.length; i++) {
+ var p = priority[i]
+ for (var j = 0; j < array.length; j++) {
+ if (typeof array[j][p] == 'string' && regex.test(array[j][p])) {
+ ranked.push(array.splice(j, 1)[0])
+ j--
+ } else if (array[j][p] && typeof array[j][p] != 'string') {
+ for (var m = 0; m < array[j][p].length; m++) {
+ if (regex.test(array[j][p][m])) {
+ ranked.push(array.splice(j, 1)[0])
+ j--
+ break
+ }
+ }
+ }
+ }
+ }
+ }
+
+module.exports = {
+ escapeRegExp: escapeRegExp
+ , sortByRegExp: sortByRegExp
+}
@@ -0,0 +1,77 @@
+var repository = require('./repository')
+ , escapeRegExp = require('./main-search-util').escapeRegExp
+ , sortByRegExp = require('./main-search-util').sortByRegExp
+ , defaultMax = 8
+
+ , exec = function (args, out) {
+ var terms = args.remaining
+ , max = args.max || defaultMax
+ , handler = handle.bind(null, terms, max, out)
+
+ out && out.searchInit()
+
+ repository.setup(function (err) {
+ if (err)
+ return out && out.repositoryLoadError(err)
+
+ repository.search(terms, handler)
+ })
+ }
+
+ , handle = function (terms, max, out, err, data) {
+ var primary = []
+ , secondary = []
+
+ if (err)
+ return out && out.searchError(err)
+
+ repository.packup()
+
+ if (data) {
+ Object.keys(data).forEach(function (id) {
+ var d = data[id]
+ if (d.keywords)
+ (d.keywords.indexOf('ender') == -1 ? secondary : primary).push(d)
+ })
+ }
+
+ if (!primary.length && !secondary.length)
+ return out && out.searchNoResults()
+
+ if (primary.length)
+ primary = rankRelevance(terms, primary)
+
+ out && out.searchResults({
+ terms: terms
+ , max: max
+ , primary: primary.length ? primary : null
+ , secondaryTotal: secondary.length
+ , secondary: secondary.length && (max - primary.length > 0)
+ ? rankRelevance(terms, secondary).slice(0, max - primary.length)
+ : null
+ })
+ }
+
+ , rankRelevance = function (args, data) {
+ var sorted = []
+ , priority = [ 'name', 'keywords', 'description' ]
+ , args = args.map(function (arg) { return escapeRegExp(arg) })
+ , regex
+
+ // args as exact phrase for name
+ regexp = new RegExp('^' + args.join('\\s') + '$')
+ sortByRegExp(regexp, data, sorted, [ 'name' ])
+
+ // args as phrase anywhere
+ regexp = new RegExp('\\b' + args.join('\\s') + '\\b', 'i')
+ sortByRegExp(regexp, data, sorted, priority)
+
+ // args as keywords anywhere (ex: useful for case when express matches expresso)
+ regexp = new RegExp('\\b' + args.join('\\b\|\\b') + '\\b', 'i')
+ sortByRegExp(regexp, data, sorted, priority)
+
+ // we don't really care about relevance at this point :P
+ return sorted.concat(data)
+ }
+
+module.exports.exec = exec
@@ -0,0 +1,11 @@
+var argsParse = require('./args-parse')
+
+ , exec = function (argv) {
+ var args = argsParse.parse(argv)
+ , exe = args && require('./main-' + args.main)
+ , out = args && require('./main-' + args.main + '-output').create(require('util'))
+
+ exe && exe.exec(args, out)
+ }
+
+module.exports.exec = exec
@@ -0,0 +1,56 @@
+var colors = require('colors')
+
+ , Output = {
+
+ init: function (out, isDebug) {
+ this.out = out // an object with a 'print' method, like `require('util')`
+ this.isDebug = isDebug
+ return this
+ }
+
+ , print: function (string) {
+ this.out && this.out.print(string)
+ }
+
+ // generic method, like console.log, should avoid in favour of more specific 'views'
+ , log: function (string) {
+ if (typeof string != 'undefined')
+ this.print(string)
+ this.print('\n')
+ }
+
+ , debug: function (string) {
+ this.isDebug && this.print('DEBUG: ' + String(string) + '\n')
+ }
+
+ , statusMsg: function (string) {
+ this.log(string.grey)
+ }
+
+ , warnMsg: function (string) {
+ this.log(string.grey)
+ }
+
+ , repositoryError: function (err, msg) {
+ if (this.isDebug)
+ throw err
+
+ this.log(msg.red)
+ }
+
+ , repositoryLoadError: function (err) {
+ this.repositoryError(err, 'Something went wrong trying to load NPM!')
+ }
+
+ , heading: function (string, meta) {
+ this.log(string.yellow + (meta ? (' (' + meta + ')').grey : ''))
+ this.log(string.replace(/./g, '-'))
+ }
+
+ , create: function () {
+ return Object.create(this)
+ }
+
+}
+
+module.exports = Output
@@ -0,0 +1,77 @@
+function RepositorySetupError (message) {
+ Error.call(this)
+ Error.captureStackTrace(this, arguments.callee)
+ this.message = message
+ this.name = 'RepositorySetupError'
+}
+RepositorySetupError.prototype.__proto__ = Error.prototype
+
+var npm = require('npm')
+ , fs = require('fs')
+ , net = require('net')
+ , colors = require('colors')
+ , util = require('./util')
+
+ , isSetup = false
+ , sessionFile
+ , sessionStream
+
+ , generateTempFile = function (callback) {
+ sessionFile = util.tmpDir + '/ender_npm_' + process.pid + '.' + (+new Date())
+ fs.open(sessionFile, 'w', '0644', callback)
+ }
+
+ // must be called at the start of an NPM session
+ , setup = function (callback) {
+ if (isSetup)
+ return callback && callback()
+
+ generateTempFile(function (err, fd) {
+ if (err)
+ return callback(err)
+
+ sessionStream = new net.Stream(fd)
+ var config = {
+ logfd: sessionStream
+ , outfd: sessionStream
+ }
+
+ npm.load(config, function (err) {
+ if (!err)
+ isSetup = true
+
+ callback.apply(null, arguments)
+ })
+ })
+ }
+
+ // must be called at the end of an NPM session
+ , packup = function (wasError, callback) {
+ if (!isSetup)
+ return callback && callback()
+
+ isSetup = false
+
+ sessionStream.on('close', function () {
+ if (!wasError)
+ return fs.unlink(sessionFile, callback)
+
+ callback && callback()
+ sessionStream = null
+ })
+
+ sessionStream.destroySoon()
+ }
+
+ , search = function (keywords, callback) {
+ if (!isSetup)
+ throw new RepositorySetupError('repository.setup() has not been called')
+
+ npm.commands.search(keywords, callback)
+ }
+
+module.exports = {
+ search: search
+ , setup: setup
+ , packup: packup
+}
Oops, something went wrong.

0 comments on commit 0ec60bf

Please sign in to comment.