require 'coffee-script/register'
_ = require 'underscore'
path = require 'path'
fs = require 'fs'
logger = require 'winston'
stream = require 'stream'
promise = require 'node-promise'
{inspect} = require 'util'
This file is part of the Mixco framework.
- View me on a static web
- View me on GitHub
This module implements the meat of the mixco
script that takes Mixco
scripts and compiles them such that they can be used inside Mixxx.
require 'coffee-script/register'
_ = require 'underscore'
path = require 'path'
fs = require 'fs'
logger = require 'winston'
stream = require 'stream'
promise = require 'node-promise'
{inspect} = require 'util'
First, we find out what is the name of the script that we are running.
MIXCO = path.basename process.argv[1]
We use the package.json
data to get the script metadata.
packageJsonPath = path.join __dirname, "..", "package.json"
package_ = JSON.parse fs.readFileSync packageJsonPath
MIXCO_VERSION = package_.version
MIXCO_AUTHOR = package_.author
MIXCO_DESCRIPTION = package_.description
MIXCO_HOMEPAGE = package_.homepage
And then we define some defaults.
MIXCO_DEFAULT_OUTPUT_DIR = path.join ".", "mixco-output"
MIXCO_DEFAULT_INPUTS = [ "." ]
MIXCO_EXT_GLOBS = [
"*.mixco.js"
"*.mixco.coffee"
"*.mixco.litcoffee"
]
The colors
library allows us print colored output. We shall not
name colors explicitly through our script, but instead only used the
theme names defined here.
colors = require 'colors/safe'
colors.setTheme
data: 'yellow'
The args function parses the command line arguments and returns an
object containing the options and arguments, as parsed. It will also
output and exit when passed —help
, —version
, etc…
args = ->
_.defaults (require "argp"
.createParser once: true
.allowUndefinedArguments()
.usages [ "", "#{MIXCO} [options] [<input>...]" ]
.on "argument", (argv, argument) ->
argv.inputs ?= []
argv.inputs.push argument
.body()
.text MIXCO_DESCRIPTION
.text "\n
This program can compile all the <input> Mixco scripts
into .js and .xml files that can be used inside Mixxx.
Mixco scripts have one of the following extensions:
#{MIXCO_EXT_GLOBS.join ', '}. When no <input> is
passed, it will compile all scripts in the current
directory. When an <input> is a directory, all scripts
found in it will be compiled."
.text()
.text "Options:"
.option
short: "o"
long: "output"
description: "Directory where to put the generated files
Default: #{MIXCO_DEFAULT_OUTPUT_DIR}"
metavar: "PATH"
default: MIXCO_DEFAULT_OUTPUT_DIR
.option
short: "r"
long: "recursive"
description: "Recursively look for scripts in input directories"
.option
short: "w"
long: "watch"
description: "Watch scripts for changes and recompile them"
.option
short: "T"
long: "self-test"
description: "Test the framework before compilation"
.option
short: "t"
long: "test"
description: "Test the input scripts before compilation"
.option
long: "factory"
description: "Compile the scripts that come with Mixco"
.option
long: "fatal-tests"
description: "Make process fail when tests fail"
.help()
.option
short: "V"
long: "verbose"
description: "Print more output"
.version(MIXCO_VERSION)
.text "\nMore info and bug reports at: <#{MIXCO_HOMEPAGE}>"
.argv()),
inputs: MIXCO_DEFAULT_INPUTS
The sources function takes a list of inputs, as passed by the
user, and returns a list of gulp
enabled globs that can be passed to
gulp.src
sources = (inputs, recursive) ->
_.flatten inputs.map (input) ->
stat = fs.statSync input
if stat.isDirectory()
MIXCO_EXT_GLOBS.map (glob) ->
if recursive
path.join input, "**", glob
else
path.join input, glob
else
[ input ]
The tasks function will, given the gulp sources and an output
directory, define all the gulp
tasks. It returns the gulp module
itself.
tasks = (sources, output, opts={}) ->
gulp = require 'gulp'
cached = require 'gulp-cached'
rename = require 'gulp-rename'
testError = ->
logger.error arguments...
if opts.fatal_tests
process.exit 1
gulp.task 'self-test', ->
if opts.self_test
mocha = require 'gulp-mocha'
specs = path.join __dirname, '..', 'test', 'mixco', '*.spec.coffee'
logger.info "testing framework:", colors.data specs
gulp.src specs, read: false
.pipe mocha()
.on 'error', testError
gulp.task 'test', ['self-test'], ->
if opts.test
mocha = require 'gulp-mocha'
specs = path.join __dirname, '..', 'test', 'scripts.spec.coffee'
logger.info "testing input scripts:", colors.data specs
process.env.MIXCO_TEST_INPUTS = sources.join ':'
gulp.src specs, read: false
.pipe mocha()
.on 'error', -> testError
gulp.task 'scripts', ['test'], ->
ext = ".output.js"
gulp.src sources
.pipe cached 'scripts'
.pipe changed output, ext
.pipe browserified()
.pipe rename extname: ext
.pipe gulp.dest output
.pipe logging "generated"
gulp.task 'mappings', ['test'], ->
ext = ".output.midi.xml"
gulp.src sources
.pipe cached 'sources'
.pipe changed output, ext
.pipe xmlMapped()
.pipe rename extname: ext
.pipe gulp.dest output
.pipe logging "generated"
gulp.task 'build', [ 'scripts', 'mappings' ]
gulp.task 'watch', ['build'], -> gulp.watch sources, [ 'build' ]
gulp
We define a couple of helpers to extract parts of a path pointing to a Mixco script file.
moduleName = (scriptPath) ->
path.join (path.dirname scriptPath),
path.basename scriptPath, path.extname scriptPath
scriptName = (scriptPath) ->
path.basename (moduleName scriptPath), ".mixco"
logging = (str) ->
through = require 'through2'
through.obj (file, enc, next) ->
logger.info "#{str}:", colors.data file.path
next null, file
changed = (dest, ext) ->
changed_ = require 'gulp-changed'
changed_ dest,
extension: ext
hasChanged: (stream, next, file, path) ->
fs.stat path, (err, stat) ->
if err or file.stat.mtime > stat.mtime
stream.push file
else
logger.info "up to date:", colors.data path
do next
The xmlMapped gulpy plugin generates the .midi.xml
Mixxx
controller mapping files.
consume = (stream) ->
result = new promise.Promise
chunks = []
stream.on 'data', (chunk) ->
chunks.push chunk
stream.on 'end', ->
result.resolve Buffer.concat chunks
stream.on 'error', (err) ->
result.reject error
result
fork_ = (what, args) ->
childp = require 'child_process'
proc = childp.fork what, args, silent: true
stdoutResult = consume proc.stdout
exitResult = new promise.Promise
proc.on 'error', (err) ->
logger.error err
next err, null
proc.on 'exit', (code) ->
if code == 0
exitResult.resolve code
else
exitResult.reject new Error "Exit code: #{code}"
promise.allOrNone stdoutResult, exitResult
xmlMapped = ->
through = require 'through2'
through.obj (file, enc, next) ->
moduleName_ = moduleName file.path
scriptName_ = scriptName file.path
logger.debug "compiling mapping for:", colors.data moduleName_
logger.debug " module:", colors.data moduleName_
logger.debug " script:", colors.data scriptName_
fork_ file.path, [ "-g" ]
.then ([data, _]) ->
file.contents = data
next null, file
, (err) ->
logger.error "Error while generating mapping from:",
colors.data file.path
logger.error err
The browserified() function returns a gulpy plugin that compiles a
Mixco script (which is a NodeJS script) into a standalone bundle that
can be loaded inside Mixxx. It also transforms it from Coffee-Script
to JavaScript if necessary, and packages dependencies transparently
(e.g underscore). This means that a Mixco script can be split across
multiple files. It is recommended to only use the .mixco.*
extension for the main script, where mixco.script.register
is called.
browserified = ->
browserify = require 'browserify'
through = require 'through2'
globby = require 'globby'
thisdir = path.dirname module.filename
exclude = globby.sync [ path.join thisdir, "*.litcoffee" ]
through.obj (file, enc, next) ->
moduleName_ = moduleName file.path
scriptName_ = scriptName file.path
logger.debug "compiling script for:", colors.data file.path
logger.debug " module:", colors.data moduleName_
logger.debug " script:", colors.data scriptName_
prepend = new Buffer """
/*
* File generated with Mixco framework version: #{MIXCO_VERSION}
* More info at: <#{MIXCO_HOMEPAGE}>
*/
\nMIXCO_SCRIPT_FILENAME = '#{file.path}';\n\n
"""
append = new Buffer """
\n#{scriptName_} = require('#{moduleName_}').#{scriptName_};
/* End of Mixco generated script */
"""
finish = (err, res) ->
if err
logger.error 'Error while generating script for:',
colors.data file.path
logger.error err
else
file.contents = Buffer.concat [
prepend, res, append ]
next err, file
bundler = browserify (toStream "require('#{file.path}');"),
extensions: [ ".js", ".coffee", ".litcoffee"]
exclude.reduce ((b, fname) -> b.exclude fname), bundler
.exclude 'coffee-script/register'
.require moduleName_
.bundle finish
The main function finally implements the meat of the command line script. It parses the arguments, sets up the logger and starts the appropiate task.
exports.main = ->
argv = args()
logger.cli()
logger.level = if argv.verbose then 'debug' else 'info'
logger.debug "console arguments:\n", colors.data inspect argv
logger.info "inputs:", colors.data argv['inputs']
logger.info "output directory:", colors.data argv['output']
if argv['factory']
argv['inputs'].push path.join __dirname, '..', 'script'
srcs = sources argv['inputs'], argv['recursive']
logger.debug "gulp sources:", colors.data srcs
gulp = tasks srcs, argv.output,
self_test: argv['self-test']
test: argv['test']
fatal_tests: argv['fatal-tests']
task = if argv['watch'] then 'watch' else 'build'
gulp.start task
Oh, and there is this utility to create a stream from a plain string.
class StringStream extends stream.Readable
constructor: (@str) ->
super
_read: (size) ->
@push @str
@push null
toStream = (str) -> new StringStream str