Created
November 1, 2011 21:41
-
-
Save mklabs/1332010 to your computer and use it in GitHub Desktop.
Cake bin wrapper - load cake tasks from tasks/ dir
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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