Skip to content
This repository has been archived by the owner on Jun 15, 2021. It is now read-only.

Commit

Permalink
initial commit, working search command
Browse files Browse the repository at this point in the history
  • Loading branch information
rvagg committed Jan 28, 2012
0 parents commit 0ec60bf
Show file tree
Hide file tree
Showing 20 changed files with 952 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -0,0 +1 @@
node_modules
3 changes: 3 additions & 0 deletions bin/ender
@@ -0,0 +1,3 @@
#!/usr/bin/env node

require('../lib/main').exec(process.argv)
46 changes: 46 additions & 0 deletions lib/args-parse.js
@@ -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
79 changes: 79 additions & 0 deletions lib/main-search-output.js
@@ -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
38 changes: 38 additions & 0 deletions lib/main-search-util.js
@@ -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
}
77 changes: 77 additions & 0 deletions lib/main-search.js
@@ -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
11 changes: 11 additions & 0 deletions lib/main.js
@@ -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
56 changes: 56 additions & 0 deletions lib/output.js
@@ -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
77 changes: 77 additions & 0 deletions lib/repository.js
@@ -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
}

0 comments on commit 0ec60bf

Please sign in to comment.