A set of handy helpers for your Cakefiles.
A set of generic high level file operations you don't want to implement yourself, like copying files, CoffeeScript compilation and compile time requiring, minification, and SLOC counting.
Note: Muffin is completely unmaintained. Gulp and grunt seem pretty cool and are both definitely maintained. If you'd like to take muffin over, shoot me an email at [email protected]!
Using npm
, do
npm install muffin-builder
and then require in your file using
muffin = require 'muffin-builder'
Note, muffin used to be called muffin
on NPM, now it's called muffin-builder
. Muffin is too awesome a name to reserve for a thing which isn't really used.
muffin.js
- allows you to define a map of actions to take using certain files, making making Makefile style Cakefiles a bit easier
- takes this map from you as a set of file to action pairs, where you specify the file as a regex pattern (then run on the source tree), and the action as a Javascript function.
- provides a bunch of high level functions to use within those map actions to do real work, like compiling trees of CoffeeScript files
- includes free goodies like file system watching, so that when a file changes the action is re run, the ability to operate on the git stage instead of the current state in the file system, and handy callbacks sprinkled about which deal with weird asynchronous race conditions for you.
First, the main entry point, muffin.run
:
muffin.run(options)
Call this function in your Cake task and pass it some options
. options
is an object containing the keys:
files
: aString
orArray
thereof of files (relative to the Cakefile) which muffin will look at. The string or strings can be single file paths orglob(3)
style paths with*
and**
style wildcards and such.options
: anotherObject
, containing any options passed in from Cake (so you don't have to extend the first object)map
: anotherObject
, who's keys areString
s and values areFunction
s.- Each
key
,value
pair in themap
gets uses as a pattern -> action pair upon the files passed in asoptions.files
. For any files matching the pattern (converted to aRegExp
from the string), the function is called with theRegExp
matches as its only argument.
- Each
before
: accepts aFunction
to be called before any actions are run.after
: accepts aFunction
to be called after all actions have been run the first time, or after a watched file triggers its own action.
Take a look at this example from the muffin.js
Cakefile itself:
# Define a Cake task called build
task 'build', 'compile muffin', (options) ->
# Run a map on all the files in the src directory
muffin.run
files: './src/**/*'
options: options
# For any file matching 'src/muffin.coffee', compile it to 'lib/muffin.js'
map:
'src/muffin.coffee' : (matches) -> muffin.compileScript(matches[0], 'lib/muffin.js', options)
We define a build
task, and in it we run muffin
on all the files anywhere within the src
dir. For all of these files who's filenames match src/muffin.coffee
, which is really just one I guess, we get muffin
to compile it to lib/muffin.js
, according to the options passed to the Cake task.
From a more complex project:
task 'test', 'compile project.js and the tests and run them on the command line', (options) ->
# Require a library for making temporary directories and the qunit command line runner
temp = require 'temp'
runner = require 'qunit'
tmpdir = temp.mkdirSync()
# Track weather or not this is the first time the map has been run
first = false
# Run a map on both the source files and the test files
muffin.run
files: ['./src/**/*.coffee', './tests/**/*.coffee']
options: options
# Compile the project coffeescript and all the tests into a temporary directory
map:
'src/project.coffee' : (matches) -> muffin.compileTree(matches[0], "#{tmpdir}/project.js", muffin.extend {notify: first}, options)
'tests/project/(.+)_test.coffee' : (matches) -> muffin.compileScript(matches[0], "#{tmpdir}/#{matches[1]}_test.js", muffin.extend {notify: first}, options)
'tests/project/test_helper.coffee' : (matches) -> muffin.compileScript(matches[0], "#{tmpdir}/test_helper.js", muffin.extend {notify: first}, options)
after: ->
# After successfull compilation, run the tests in this temporary directory. Note that this after callback
# gets fired after all the actions have completed the first time, and then if in watch mode, after any action
# completes a second time.
first = true
runner.run
code: "#{tmpdir}/project.js"
deps: ["jsdom", "#{tmpdir}/test_helper.js", "./tests/lib/jquery.js"]
tests: glob.globSync("#{tmpdir}/*_test.js")
Below are some notes on the helper functions available for use in the map actions. Note that muffin
internally uses q
, a JavaScript promise library which makes it really easy to interface with the asynchronous scheduling piece of muffin
. If your action function is asynchronous, which it almost always will be, you must return a q.Promise
from your action function. By doing this, muffin
can ensure the after
callbacks do in fact run after all the actions have been completed.
All the functions below return 'q.Promise' objects, so using them as they are used in the examples above works quite well.
If you want to use compound actions (such as reading to a file, transforming it, and then writing it or something equivalently non trivial), have a look at the q
readme. Kris explains the benefits of using promises, and explains how to join, intersect, and chain them so your Cakefiles look simply sublime.
All the methods below accept an options
parameter, which you can just pass in from the Cake task or modify as in the second example. Common options to all methods:
notify
: Wheather or not to notify the user (growl and command line) about the result of the action
The methods:
muffin.readFile(source, options = {})
Reads a file in from the source, returning a promise which resolves to the contents of the file.
muffin.writeFile(target, data, options = {})
Writes the data
to the target
file. Accepts a mode
option, which the file will be chmod
ed to after successful writing if passed.mode
defaults to 644, and must be passed in as a decimal (644) as opposed to octal (0644) number.
muffin.copyFile(source, target, options = {})
Copies the file at source
to target
according to options, which gets passed straight in to readFile
and writeFile
.
muffin.compileScript(source, target, options = {})
Compiles a CoffeeScript file at source
to JavaScript at target
. Accepts bare
as an option, which if true excludes the default closure around the generated JavaScript. bare
is false by default and it's recommended it remains that way.
muffin.compileTree(source, target, options = {})
Compiles a set of CoffeeScript files with a root at source
to JavaScript at target
. This uses the snockets library which allows you to specify dependencies among files using the #= require otherfile
syntax. See the snockets documentation for more information.
muffin.minifyScript(source, options = {})
Minifies a JavaScript file at source
into a min.js
file in the same directory using Uglify.js.
muffin.statFiles(files, options = {})
Logs SLOC counts to the console for all the files included in the files
(Array
) option. Plays nice with both CoffeeScript and JavaScript. Needs (and checks) for a valid Perl > 5.3 installation to count lines. Accepts a fields
option, which is a declarative array of string names of fields to be included in the output. Available fields are filename
, filetype
, sloc
, blank
, comment
, size, and
modified.
statFilescan also compare the given files across refs in git by passing it the
compareand
compareFields` option. It will then prompt the user for the fields to compare, clone and checkout the two versions in a temp directory, and report the different stats in the same table.
muffin.doccoFile(file, options)
Generates Docco documentation for the JavaScript or CoffeeScript source file at file
. Docco without choice writes to a docs
folder.
muffin.notify(filename, message, error = false)
Notifies the user about a filename
. Sends them a message
, with either a good connotation with error = false
or a bad connotation with error = true
.
muffin.exec
Promise wrapper for child process execution. Works the same as the child_process.exec
function, but returns an array of the child_process
and the promise
in the form [child, promise]
. The promise resolves with an array of the child
's stdout
and stderr
, in the form of [stdout, stderr]
. As with child = child_process.exec
, child
may need to have it's input stream ended for the program being executed to exit, which can be done by calling child.stdin.end()
.
muffin.extend(onto, what, this, that ...)
Simple variadic extend implementation which is handy for extending options hashes.
muffin.js
also accepts an options
object to the muffin.map
function, which is usually just passed in right from the options in the Cake task. This allows you to expose the inbuilt muffin options listed below:
- 'watch': Monitor any files who match in the map and rerun their action when they change.
- 'commit': Use the git stage if one exists instead of the current files in the directory. This is useful when you want to only stage some changes and commit them.
- 'compare': Compare a set of files across two git refs.
statFiles
only.
You can use something like this to expose these options in Cake:
option '-w', '--watch', 'continue to watch the files and rebuild them when they change'
option '-c', '--commit', 'operate on the git index instead of the working tree'
option '-m', '--compare', 'compare across git refs, stats task only.'
and then just ensure you pass in the options when you declare a task:
task 'build', 'build my super project', (options) ->
muffin.run
files: ...
options: options
map: ...
That's it!
The annotated source code for muffin can be found here.
Copyright (C) 2012 by Jaded Pixel Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.