Skip to content

Instantly share code, notes, and snippets.

@mklabs
Created November 1, 2011 21:41
Show Gist options
  • Save mklabs/1332010 to your computer and use it in GitHub Desktop.
Save mklabs/1332010 to your computer and use it in GitHub Desktop.
Cake bin wrapper - load cake tasks from tasks/ dir
fs = require 'fs'
path = require 'path'
{EventEmitter} = require 'events'
colors = require 'colors'
# ### Options
# Options are handled using coffeescript optparser,
# You can define the option with short and long flags,
# and it will be made available in the options object.
options = require './options'
# helpers
{extend, load, error} = require './helper'
# change working directory if dirname is defined (mainly usefull for the bin usage)
process.chdir options.dirname if options.dirname
# ### Logger
# Logs are handled via winston with cli mode and a default level set to
# input. The log level is set by the `-l` or `--loglevel` cli options:
#
# silly, input, verbose, prompt, info, data, help, warn, debug, error
#
log = require('./log')(options)
# ### config
# merge the local config with global object for this module, so that
# interpolation works as expected (todo: clarify configuration)
configfile = path.join process.cwd(), '.cheesecake'
if path.existsSync(configfile)
config = JSON.parse fs.readFileSync(configfile, 'utf8')
extend global, config
# ### gem
# the event emitter used along tasks to handle some asynchronous stuff, gem for global EventEmitter. Basically,
# this is the main mediator that tasks listen to `end:` events to know wheter thay can be executed. Each tasks to
# notify that their async work is done simply emit an `end` event on the local EventEmitter of the task (third argument).
global.gem = gem = new EventEmitter
# ### task monkey-patch
#
# To provide a tasks-scopped EventEmitter and enable some async stuff and task ordering.
#
_task = global.task
# `_tasks` is the internal cache, stored as `taskname: status` where status turns false
# once the end event is emitted. Tasks should not be runned more than once, even if multiple
# tasks `invoke()`-d them.
_tasks = {}
global.task = task = (name, description, action) ->
description = description.grey
_task name, description, (options) ->
em = new EventEmitter()
# a local `EventEmitter` is created and passed in as a second parameter to tasks' functions.
#
# Namely provides a few logging helpers:
#
# em.emit 'log', 'Something to log'
# em.emit 'warn', 'Something to warn'
# em.emit 'error', 'Error to log, and exit program'
# em.emit 'data', {foo: 'bar'}
#
# The special `end` event allows tasks to run asynchronously and still be able to depends on each other. Once ended,
# a task notify its status to the global EventEmitter by emitting an `end:taskname` event.
.on('error', (err) -> log.error 'error occured'.red.bold; error err)
.on('warn', (err) -> log.warn err)
.on('silly', log.silly.bind log, "#{name} » ".magenta)
.on('log', log.input.bind log, "#{name} » ".magenta)
.on('data', log.inspect.bind log)
.on('end', (results) ->
log.info "✔ end:#{name}".green
log.silly log.inspector(results) if results
gem.emit "end:#{name}", results
_tasks[name] = 'done'
)
state = _tasks[name]
# This (simple) async system and task dependency ensures that a task is only executed once. We emit the
# end event and prevent action call if the task is already done.
return gem.emit "end:#{name}" if state is 'done'
# set the task state to pending, will turn done once the task emiter
# emit the end event
_tasks[name] = 'pending'
log.verbose "start #{name} » ".grey
# invoke the task if the task is unknown yet
action.call @, options, em unless state
# ### cake init
#
# A simple task to create basic configuration file
#
task 'init', 'Create a basic configuration file', (options, em) ->
# ideally, main props will get prompted along the way
output = JSON.stringify {foobar: 'foobar'}, null, 2
fs.writeFileSync path.join(process.cwd(), '.cheesecake'), output
em.emit 'end'
# ### cake config
# Show configuration for key
#
# cake config
# cake --k dir config
# cake --key paths config
#
task 'config', 'Show configuration for key', (options, em) ->
conf = config[options.key]
em.emit 'warn', "No #{options.key} in config".yellow.bold if not conf
em.emit 'data', conf or config
plugins = path.resolve(path.join options.dirname, 'tasks')
if plugins isnt path.join(__dirname, 'tasks') and path.existsSync plugins
fs.readdirSync(path.join(options.dirname, 'tasks'))
.filter((file) -> fs.statSync(path.join(options.dirname, 'tasks', file)).isFile() && !/^\./.test(file) )
.forEach (load(options.dirname, log))
#!/usr/bin/env node
var path = require('path'),
args = process.argv.slice(2),
last = args.slice(-1)[0];
if(args.length) {
// handle no task case, make sure to output the cake help.
process.argv.splice(2, 0, '--dirname', process.cwd());
}
// only allow cli use and build process when a local `.cheesecake` file exist
// This prevents unwanted build trigger on the current working directory.
if(last && !~['config', 'init'].indexOf(last) && !path.existsSync(path.join(process.cwd(), '.cheesecake'))) {
throw new Error('Cli usage requires a local .cheesecake file. Type cheesecake init to create one.');
}
process.chdir(__dirname);
require('coffee-script/lib/cake').run();
# External dependency
coffee = require 'coffee-script'
fs = require 'fs'
path = require 'path'
# ## extend
# Extend a source object with the properties of another object (shallow copy).
exports.extend = extend = (object, properties) ->
for key, val of properties
object[key] = val
object
# ### error() handler
#
# return error err if err
# return error new Error(':((') if err
exports.error = error = (err) ->
console.error ' ✗ '.red + (err.message || err).red
console.trace err
process.exit 1
# ## load
#
# Load a task file, written in js or cs, and evaluate its content
# in a new VM context. Meant to be used as a forEach helper.
exports.load = (dirname, log) ->
return (file) ->
script = fs.readFileSync path.join(dirname, 'tasks', file), 'utf8'
# tasks may be written in pure JS or coffee. Takes care of coffee compile if needed.
script = if /\.coffee$/.test(file) then coffee.compile script else script
# load and compile
log.silly "Import #{file}"
run script, path.join(dirname, 'tasks', file)
# Borrowed to coffee-script: CoffeeScript way of loading and compiling, correctly
# setting `__filename`, `__dirname`, and relative `require()`.
# https://github.com/jashkenas/coffee-script/blob/master/src/coffee-script.coffee#L54-75
exports.run = run = (code, filename) ->
mainModule = require.main
# Set the filename.
mainModule.filename = fs.realpathSync filename
# Clear the module cache.
mainModule.moduleCache and= {}
# Assign paths for node_modules loading
if process.binding('natives').module
{Module} = require 'module'
mainModule.paths = Module._nodeModulePaths path.dirname filename
# Compile.
mainModule._compile code, mainModule.filename
winston = require 'winston'
eyes = require 'eyes'
levels = winston.config.cli.levels
# setup logger
module.exports = (options) ->
logger = new winston.Logger
transports: [
new winston.transports.Console({ level: options.loglevel || 'input' })
]
logger.inspector = eyes.inspector
stream: null
styles: # Styles applied to stdout
all: null, # Overall style applied to everything
label: 'underline', # Inspection labels, like 'array' in `array: [1, 2, 3]`
other: 'inverted', # Objects which don't have a literal representation, such as functions
key: 'cyan', # The keys in object literals, like 'a' in `{a: 1}`
special: 'grey', # null, undefined...
number: 'blue', # 0, 1, 2...
bool: 'yellow', # true false
regexp: 'green' # /\d+/
string: 'green' # strings...
logger.inspect = (o) ->
result = logger.inspector(o)
logger.data line for line in result.split('\n')
logger.cli()
winston = require 'winston'
optparse = require 'coffee-script/lib/optparse'
levels = winston.config.cli.levels
# options parsing, needs this to be able to parse command line arguments and used them outside of a cake task
# mainly for the loglevel options
switches = [
['-l', '--loglevel [level]', 'What level of logs to report. Values → ' + Object.keys(levels).join(', ') + ' or silent'],
['-k', '--key [key]', 'Key configuration for config task']
['-d', '--dirname [dir]', 'directory from which the Cakefile is meant to run (mainly usefull for the bin usage)']
]
oparse = new optparse.OptionParser switches
options = oparse.parse process.argv.slice(2)
# still call option to make cake keep track of these options too
option.apply this, opt for opt in switches
if options.loglevel and options.loglevel isnt 'silent' and levels[options.loglevel] is undefined
throw new Error('Unrecognized loglevel option: ' + options.loglevel + ' \n instead of → ' + Object.keys(levels).join(', '))
# export parsed options from cli
module.exports = options
{
"author": "mklabs",
"name": "cheesecake",
"description": "cheesecake",
"version": "0.0.1",
"bin": "./cheesecake",
"dependencies": {
"colors": "0.5.x",
"winston": "~0.5.5",
"eyes": "~0.1.6",
"coffee-script": "1.1.2"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment