API Docs for: 0.5.8
Show:

File: node_modules/mojito/lib/app/commands/compile.js

/*
 * Copyright (c) 2011-2013, Yahoo! Inc.  All rights reserved.
 * Copyrights licensed under the New BSD License.
 * See the accompanying LICENSE file for terms.
 */


/*jslint anon:true, nomen:true, stupid:true, continue:true */
'use strict';

var libpath = require('path'),
    libfs = require('fs'),
    existsSync = libfs.existsSync || libpath.existsSync,
    mkdirP = require('mkdirp').sync,

    // paths
    BASE = libpath.resolve(__dirname, '../../../') + '/',
    libutils = require(BASE + 'lib/management/utils'),
    Mojito = require(BASE + 'lib/mojito'),
    Store = require(BASE + 'lib/store'),

    // private compilation function container
    compile = {},
    // private messages, to be printed upon completion
    msgs = [],

    compileType,

    // private functions
    rmdirR,
    getContentFromUrl,
    removeFile,
    clean,
    everything,

    // private class
    YuiModuleCacheWriter,

    // public exports
    usage,
    options,
    run,
    YuiModuleCacher,
    Y = libutils.getYUIInstance({
        'async-queue': null,
        'json-parse': null,
        'json-stringify': null,
        'mojito-hb': BASE + 'lib/app/addons/view-engines/hb.server.js',
        'mojito-mu': BASE + 'lib/app/addons/view-engines/mu.server.js'
    });

usage = [
    'DEPRECATED: the compile command is deprecated.',
    '',
    'mojito compile {options} {type}',
    'OPTIONS: ',
    '  --remove     remove all compiled files, instead of creating them',
    '   -r          short for --remove',
    '  --app        perform the action for the app-level items (if applicable)',
    '   -a          short for --app',
    '  --everything compile everything possible',
    '   -e          short for --everything',
    '  --clean      clean up all compiled modules',
    '   -c          short for --clean',
    '  --verbose    for verbose output',
    '   -v          short for --verbose',
    '  --port       if a server is started, specify the port',
    '   -p          short for --port',
    '',
    'TYPES: ',
    '  all        performs all the other types',
    '  inlinecss  creates files for inlining the css into the page',
    '  json       reads specs, definitions, and defaults and compiles them into JS',
    '  views      precompiles the views to speed rendering\n'
].join('\n');

options = [
    {
        shortName: 'r',
        longName: 'remove',
        hasValue: false
    },
    {
        shortName: 'c',
        longName: 'clean',
        hasValue: false
    },
    {
        shortName: 'o',
        longName: 'core',
        hasValue: false
    },
    {
        shortName: 'e',
        longName: 'everything',
        hasValue: false
    },
    {
        shortName: 'v',
        longName: 'verbose',
        hasValue: false
    },
    {
        shortName: 'p',
        longName: 'port',
        hasValue: true
    },
    {
        shortName: 'a',
        longName: 'app',
        hasValue: false
    }
];


/**
 * Returns details on how to make inline CSS for mojits.
 *
 * This example comes from (a modified) GSG5.
 * [ {
 *      mojitName: 'FlickrDetail',
 *      yuiModuleName: 'inlinecss/FlickrDetail',
 *      dest: '/blah/mojits/FlickrDetail/autoload/compiled' +
 *          '/css.iphone.client.js',
 *      srcs: {
 *          '/static/FlickrDetail/assets/index.css': true,
 *          '/static/FlickrDetail/assets/message.css': true
 *   }
 * ]
 *
 * @method getInlineCssMojits
 * @param store {string} resource store
 * @param env {string} "client" or "server"
 * @param context {object} runtime context
 * @return {array} object describing where to put the inline CSS file and what it should contain
 */
function getInlineCssMojits(store, env, context) {
    var m,
        mojit,
        mojits,
        mojitRes,
        r,
        res,
        ress,
        selector,
        dest,
        srcs,
        inlines = [];

    mojits = store.listAllMojits();
    for (m = 0; m < mojits.length; m += 1) {
        mojit = mojits[m];

        mojitRes = store.getResourceVersions({type: 'mojit', name: mojit});
        mojitRes = mojitRes[0];
        if ('mojito' === mojitRes.source.pkg.name) {
            // don't write framework-provided inlinecss into the framework directory
            continue;
        }

        // TODO:  This isn't quite right, since multiple contexts might map to
        // posls with the same lead selector.
        selector = store.selector.getPOSLFromContext(context)[0];

        srcs = {};
        ress = store.getResourceVersions({mojit: mojit});
        for (r = 0; r < ress.length; r += 1) {
            res = ress[r];
            if ((res.type === 'asset') && (res.subtype === 'css')) {
                srcs[res.url] = true;
            }
        }
        dest = 'autoload/compiled/inlinecss' + ('*' === selector ? '' : '.' +
            selector) + '.common.js';
        dest = libpath.join(mojitRes.source.fs.fullPath, dest);
        if (Object.keys(srcs).length) {
            inlines.push({
                mojitName: mojit,
                yuiModuleName: 'inlinecss/' + mojit,
                dest: dest,
                srcs: srcs
            });
        }
    } // for each mojit

    return inlines;
}


/**
 * Creates the Resource Store object.
 * @private
 * @method MakeStore
 * @param {Object} cfg Configuration for the resource store.
 * @return {ResourceStore} the new resource store object
 */
function makeStore(cfg) {
    var store;
    store = Store.createStore(cfg);
    return store;
}


run = function(params, options, callback) {
    var cwd = process.cwd(),
        displayResults,
        type,
        context = {};

    // TODO: don't assign to a parameter.
    options = options || {};

    libutils.isMojitoApp(cwd, exports.usage, true);

    if (options.context) {
        // TODO: parseURL.
        context = options.context;
    }

    displayResults = function(err) {
        libutils.log('');
        msgs.forEach(function(msg) {
            libutils.log(msg);
        });
        libutils.log('');
        libutils.warn('DEPRECATED the compile command is deprecated.');
        callback(err);
    };

    if (options.clean) {
        return clean(context, options, displayResults);
    }

    if (options.everything) {
        compileType = 'all';
        return everything(context, options, displayResults);
    }

    compileType = type = params.shift();

    if (!type) {
        libutils.error('Please provide the type of compilation you want.',
            exports.usage, true);
    }
    if (!compile[type]) {
        libutils.error("Unknown type '" + type + "'", exports.usage, true);
    }

    compile[type](context, options, displayResults);
};


/**
 * Compiles all assets.
 * @param {object} context The context.
 * @param {object} options Command options.
 * @param {function} callback Function to invoke upon completion.
 */
compile.all = function(context, options, callback) {
    var actions = [
        'inlinecss',
        'views',
        'json'
    ];

    if (options.remove) {
        actions = [
            'json',
            'inlinecss',
            'views'
        ];
    }

    function runOne() {
        var action = actions.shift();

        if (!action) {
            return callback();
        }
        if (options.verbose) {
            libutils.log('executing -- ' + action.toUpperCase() + ' --');
        }
        compile[action](context, options, function(err) {
            if (err) {
                return callback(err);
            }
            if (options.verbose) {
                libutils.log('done -- ' + action.toUpperCase() + ' --\n\n');
            }
            runOne();
        });
    }
    runOne();
};


/**
 * Compiles css assets.
 * @param {object} context The context.
 * @param {object} options Command options.
 * @param {function} callback Function to invoke upon completion.
 * @return {object} The return value from any optional callback function.
 */
compile.inlinecss = function(context, options, callback) {
    /*jslint regexp:true */
    var app,
        action = options.remove ? 'Removed' : 'Created',
        processed = 0,
        cwd = process.cwd(),
        urlMatcher = /url\(([^)]+)\)/g,
        inlines,
        inlineNext;

    app = Mojito.createServer({
        port: options.port || 11111,
        verbose: options.verbose
    });

    if (options.app) {
        libutils.warn('Creating app-level inline css not supported\n');
        return callback();
    }

    inlineNext = function(store, cb) {
        var inline = inlines.shift(),
            mojitName,
            yuiModuleCacheWriter,
            inliner,
            shortDest,
            i,
            url,
            storeURLs,
            fs2url = {},
            srcKey,
            srcDir,
            total,
            count = 0;

        // if there are no more inlines to process, we'll end here
        if (!inline) {
            return cb();
        }

        mojitName = inline.mojitName;

        // need a reverse mapping
        storeURLs = store.getAllURLs();
        for (url in storeURLs) {
            if (storeURLs.hasOwnProperty(url)) {
                fs2url[storeURLs[url]] = url;
            }
        }

        shortDest = inline.dest;
        if (cwd === shortDest.substr(0, cwd.length)) {
            shortDest = shortDest.substr(cwd.length + 1);
        }

        // for CSS inline file removal:
        if (options.remove) {
            if (removeFile(inline.dest)) {
                if (options.verbose) {
                    libutils.log('Removed: ' + inline.dest);
                }
                processed += 1;
                // on to the next one
            }
            inlineNext(store, cb);
            return;
        }

        // for CSS inline file creation:
        total = Object.keys(inline.srcs).length;

        // start of the script body
        yuiModuleCacheWriter = new YuiModuleCacheWriter(inline.yuiModuleName,
            inline.dest, options);

        inliner = function(srcKey) {
            // go to the running application to get the live content for
            // this CSS URL
            getContentFromUrl(app, srcKey, function(content) {
                var contentString;

                // update all the content's url() blocks with proper
                // paths
                content = content.replace(urlMatcher, function(whole,
                                                               url) {
                    var fs;

                    if ('"' === url.substr(0, 1)) {
                        url = url.substr(1, url.length - 2);
                    }
                    if ("'" === url.substr(0, 1)) {
                        url = url.substr(1, url.length - 2);
                    }
                    if ('/' === url.substr(0, 1)) {
                        return whole;
                    }
                    if ('#' === url.substr(0, 1)) {
                        return whole;
                    }
                    if ('http' === url.substr(0, 4)) {
                        return whole;
                    }
                    if ('data:' === url.substr(0, 5)) {
                        return whole;
                    }
                    srcDir = libpath.dirname(inline.srcs[srcKey]);
                    fs = libpath.join(srcDir, url);
                    if (!fs2url[fs]) {
                        libutils.warn('couldn\'t normalize url(' + url +
                            ')(' + fs + ') in ' + inline.srcs[srcKey]);
                        return whole;
                    }
                    return 'url(' + fs2url[fs] + ')';
                });

                // remove newlines
                contentString = content.replace(/\n/g, '');

                // inject into the body the CSS for this inline file
                yuiModuleCacheWriter.createNamespace(
                    'compiled.css.inline'
                ).cache(srcKey, contentString);

                // when we're done with all CSS files, close off the
                // file and write it before going on to the next
                // inline
                count += 1;
                if (count === total) {
                    // end of the script body
                    if (yuiModuleCacheWriter.write()) {
                        processed += 1;
                    }
                    inlineNext(store, cb);
                }
            }); // getContentFromUrl()
        };

        // add all the contents of the script body for inline CSS
        for (srcKey in inline.srcs) {
            if (inline.srcs.hasOwnProperty(srcKey)) {
                inliner(srcKey);
            }
        }
    };  // inlineNext

    app.listen(null, null, function(err, appInstance) {
        var store;

        if (err) {
            libutils.error(err);
        } else {
            store = appInstance.store;
            inlines = getInlineCssMojits(store, 'client', context);

            libutils.log((options.remove ? 'Removing' : 'Creating') +
                ' inline css...');

            inlineNext(store, function(err) {
                try {
                    app.close();
                } catch (err2) {
                    libutils.warn('(app server was not running) ' + err2);
                }

                if (err) {
                    callback(err);
                } else {
                    msgs.push(action + ' compiled inline CSS YUI modules for ' +
                        processed + ' mojits.');
                    callback();
                }
            });
        }
    });

};


/**
 * Compiles views for the application.
 * @param {object} context The context.
 * @param {object} options Command options.
 * @param {function} callback Function to invoke upon completion.
 * @return {object} The return value from any optional callback function.
 */
compile.views = function(context, options, callback) {
    var self = this,
        cwd = process.cwd(),
        store = makeStore({root: cwd}),
        compiledFilename = '/autoload/compiled/views.common.js',
        mojits,
        renderer,
        renderedView,
        action = options.remove ? 'Removed' : 'Created',
        processed = 0,
        mojit,
        view,
        source,
        engine,
        mojitViews = {},
        compilerQueue = new Y.AsyncQueue();

    // there are no views in the app, so no need to do this
    if (options.app) {
        libutils.warn('Compiling app-level views not supported\n');
        return callback();
    }

    libutils.log((options.remove ? 'Removing compiled' : 'Compiling') +
        ' views...');

    if (options.mojit) {
        mojits = [ options.mojit ];
    } else {
        mojits = store.listAllMojits();
    }

    Y.Array.each(mojits, function(mojitName) {
        var mojitRes,
            outputFilepath,
            mojitNs = mojitName.replace(/\./g, '_'),
            yuiModuleCacheWriter,
            viewName,
            compileFunction;

        mojitRes = store.getResourceVersions({type: 'mojit', name: mojitName});
        if (!mojitRes || !mojitRes.length) {
            callback('Unknown mojit "' + options.mojit + '"');
            return;
        }
        mojitRes = mojitRes[0];

        outputFilepath = libpath.join(mojitRes.source.fs.fullPath, 'autoload/compiled/views.common.js');

        if ('mojito' === mojitRes.source.pkg.name) {
            // don't write framework-provided views into the framework directory
            return;
        }

        if (options.remove) {
            if (removeFile(outputFilepath)) {
                if (options.verbose) {
                    libutils.log('Removed: ' + outputFilepath);
                }
                processed += 1;
            }
            return;
        }

        yuiModuleCacheWriter = new YuiModuleCacheWriter('views/' + mojitName,
            outputFilepath, options);

        mojit = store.getMojitTypeDetails('server', context, mojitName);

        if (mojit.views) {
            compileFunction = function (renderer, source, mojitNs, viewName) {
                renderer.compiler(source, function (err, templateObj) {
                    renderedView = Y.JSON.parse(templateObj.toString());
                    yuiModuleCacheWriter.createNamespace('compiled.' +
                        mojitNs + '.views').cache(viewName,
                        renderedView);
                });
            };
            // Check each view for a template and engine
            for (viewName in mojit.views) {
                if (mojit.views.hasOwnProperty(viewName)) {
                    view = mojit.views[viewName];
                    // Check if there is a view to build
                    if (view['content-path'] && view.engine) {
                        if (!mojitViews[mojitName]) {
                            mojitViews[mojitName] = {};
                        }
                        // We have a view to compile
                        source = view['content-path'];
                        engine = view.engine;

                        if (Y.mojito.addons.viewEngines[engine]) {
                            renderer = new (Y.mojito.addons.viewEngines[engine])();
                            compilerQueue.add(Y.bind(compileFunction, self, renderer, source, mojitNs, viewName));
                        } else {
                            libutils.warn('engine not supported: ' + engine);
                        }
                    }
                }
            }
        }
        compilerQueue.add(function () {
            if (yuiModuleCacheWriter.write()) {
                processed += 1;
            }
        });

    });
    compilerQueue.add(function () {
        msgs.push(action + ' compiled view YUI modules for ' + processed +
            ' mojits.');
        callback();
    });
    compilerQueue.run();
};


/**
 * Compiles json resources.
 * @param {object} context The context.
 * @param {object} options Command options.
 * @param {function} callback Function to invoke upon completion.
 * @return {object} The return value from any optional callback function.
 */
compile.json = function(context, options, callback) {
    var cwd = process.cwd(),
        store = makeStore({root: cwd}),
        app = Mojito.createServer({
            port: options.port || 11111,
            verbose: options.verbose,
            appConfig: {}
        }),
        staticPrefix = '/',
        count = 0,
        processed = 0,
        total = 0,
        action = options.remove ? 'Removed' : 'Created',
        compiledFilename = 'autoload/compiled/json.common.js',
        processNextMojit,
        processSpecs,
        processNextSpec,
        processDefinitionJSON,
        specs,
        jsonOpts = {
            headers: { 'x-mojito-header': 'tunnel' },
            method: 'POST'
        },
        mojits,
        mojitNames;

    // there are no json configs in the app, so no need to do this
    if (options.app) {
        libutils.warn('Compiling app-level json not supported\n');
        return callback();
    }

    if (options.mojit) {
        mojitNames = [ options.mojit ];
    } else {
        mojitNames = store.listAllMojits();
    }
    total = mojitNames.length;

    processSpecs = function(newSpecs, mojitName, store, yuiModuleCacheWriter,
            cb) {
        // look for mojit specs
        specs = newSpecs;
        processNextSpec(mojitName, yuiModuleCacheWriter, cb);
    };

    processNextSpec = function(mojitName, yuiModuleCacheWriter, cb) {
        var fullSpecName = specs.shift(),
            parts,
            mName,
            mojitNs,
            specName,
            specUrl;

        if (!fullSpecName) {
            return cb();
        }

        if (options.verbose) {
            libutils.log('\tprocessing spec... ' + fullSpecName);
        }

        parts = fullSpecName.split(':');
        mName = parts[0];
        mojitNs = mName.replace(/\./g, '_');
        specName = parts[1] || 'default';

        if (!mojitName || mName === mojitName) {
            /* NOTE_1:  During the resource store redesign, it was noticed
             * that this branch is never called, which STRONGLY suggests that
             * this feature was never used.
             */
            specUrl = '/' + mName + '/specs/' + specName + '.json';

            if (options.verbose) {
                libutils.log('found spec (' + specName + ') for ' + mName);
            }
            getContentFromUrl(app, specUrl, jsonOpts, function(spec) {
                yuiModuleCacheWriter.createNamespace('compiled.' + mojitNs +
                    '.specs').cache(specName, Y.JSON.parse(spec));
                processNextSpec(mojitName, yuiModuleCacheWriter, cb);
            });
        } else {
            processNextSpec(mojitName, yuiModuleCacheWriter, cb);
        }
    };

    processDefinitionJSON = function(mojitRes, store, yuiModuleCacheWriter,
            cb) {
        var mojitName = mojitRes.name,
            processFullDefinition,
            processPreloadDefinitions;

        processFullDefinition = function(mojitName, ymcw, cb) {
            // TODO:  probably want to use mojitRes.url instead
            var url = staticPrefix + mojitName + '/definition.json';

            getContentFromUrl(app, url, jsonOpts, function(definition) {
                var defObj = Y.JSON.parse(definition);
                ymcw.createNamespace('compiled.' +
                    mojitName.replace(/\./g, '_') + '.definitions').cache(
                    'definition',
                    defObj
                );
                cb();
            });
        };
        processPreloadDefinitions = function(mojitNames, ymcw, cb) {
            var continuation = function() {
                if (mojitNames.length) {
                    processFullDefinition(mojitNames.shift(), ymcw,
                        continuation);
                } else {
                    cb();
                }
            };

            processFullDefinition(mojitNames.shift(), ymcw,
                continuation);
        };

        /*
         * The resource store doesn't respond well if you call
         * store._getMojitConfig() a bunch of times in a row synchronously.
         * Setting it inside of a setTimeout seems to keep it happy.
         */
        setTimeout(function() {
            if (options.verbose) {
                libutils.log('looking for definition.json for ' + mojitName);
            }

            var specsToPreload = [],
                mojitNamesToPreload = [],
                path,
                definition;

            path = libpath.join(mojitRes.source.fs.fullPath, 'definition.json');

            // TODO:  use commandline context instead?
            definition = store.config.readConfigYCB(path, {});

            if (Object.keys(definition).length > 0) {
                if (definition.preload) {
                    if (options.verbose) {
                        libutils.log('processing preload mojits for ' + mojitName);
                    }

                    definition.preload.forEach(function(toPreload) {
                        Object.keys(store._appConfigStatic.specs).forEach(
                            function(fullSpecName) {
                                var mName = fullSpecName.split(':').shift();
                                if (mName === toPreload) {
                                    specsToPreload.push(fullSpecName);
                                }
                            }
                        );
                    });

                    if (options.verbose) {
                        libutils.log('processing the preload specs for ' +
                            mojitName);
                        libutils.log(specsToPreload.join(', '));
                    }

                    specsToPreload.forEach(function(stp) {
                        mojitNamesToPreload.push(stp.split(':')[0]);
                    });

                    processSpecs(specsToPreload, null, store,
                        yuiModuleCacheWriter, function() {
                            processPreloadDefinitions(mojitNamesToPreload,
                                yuiModuleCacheWriter,
                                function() {
                                    processFullDefinition(mojitName,
                                        yuiModuleCacheWriter,
                                        cb);
                                });
                        });
                } else {
                    processFullDefinition(mojitName, yuiModuleCacheWriter, cb);
                }
            } else {
                cb();
            }
        }, 10);
    };

    processNextMojit = function(store, cb) {
        var mojitName = mojitNames.shift(),
            mojitRes,
            outputFilepath,
            theCloser,
            yuiModuleCacheWriter;

        if (!mojitName) {
            return cb();
        }

        if (options.verbose) {
            libutils.log('Processing mojit... ' + mojitName);
        }

        count += 1;

        mojitRes = store.getResourceVersions({type: 'mojit', name: mojitName});
        if (!mojitRes || !mojitRes.length) {
            return processNextMojit(store, cb);
        }
        mojitRes = mojitRes[0];

        if ('mojito' === mojitRes.source.pkg.name) {
            // don't write framework-provided json into the framework directory
            return processNextMojit(store, cb);
        }

        outputFilepath = libpath.join(mojitRes.source.fs.fullPath, compiledFilename);

        if (options.remove) {
            if (removeFile(outputFilepath)) {
                processed += 1;
            }
            return processNextMojit(store, cb);
        }

        yuiModuleCacheWriter = new YuiModuleCacheWriter('json/' + mojitName,
            outputFilepath, options);

        theCloser = function() {
            if (yuiModuleCacheWriter.write()) {
                processed += 1;
            }
            processNextMojit(store, cb);
        };

        // look for definitions
        // TODO:  really only need to do this after all the mojits are processed
        processDefinitionJSON(mojitRes, store, yuiModuleCacheWriter,
            function() {
                // look for specs
                var appConfig = store.getStaticAppConfig(),
                    specs = [];
                if (appConfig.specs) {
                    specs = Object.keys(store._appConfigStatic.specs);
                }
                processSpecs(specs, mojitRes.name, store, yuiModuleCacheWriter,
                    theCloser);
            });
    };

    // start up the server
    app.listen(null, null, function(err, appInst) {
        if (err) {
            libutils.error(err);
            return;
        }

        libutils.log((options.remove ? 'Removing compiled' : 'Compiling') +
            ' json...');

        processNextMojit(store, function() {
            try {
                app.close();
            } catch (err2) {
                libutils.info('(app server was not running) ' + err2);
            }
            msgs.push(action + ' compiled JSON YUI modules for ' + processed +
                ' mojits.');
            callback();
        });
    });
};


clean = function(context, options, cb) {
    libutils.warn('Cleaning all compiled files!');
    options.remove = true;
    options.app = true;
    compile.all(context, options, function() {
        options.app = false;
        compile.all(context, options, cb);
    });
};


everything = function(context, options, cb) {
    options.app = false;
    options.remove = false;
    compile.all(context, options, function() {
        options.app = true;
        compile.all(context, options, cb);
    });
};


rmdirR = function(path) {
    var files = libfs.readdirSync(path),
        i,
        currFile;

    /* Loop through and delete everything in the sub-tree after checking it */
    for (i = 0; i < files.length; i += 1) {
        currFile = libfs.statSync(path + '/' + files[i]);

        if (currFile.isDirectory()) {
            // Recursive function back to the beginning
            rmdirR(path + '/' + files[i]);
        } else if (currFile.isSymbolicLink()) {
            // Unlink symlinks
            libfs.unlinkSync(path + '/' + files[i]);
        } else {
            // Assume it's a file - perhaps a try/catch belongs here?
            libfs.unlinkSync(path + '/' + files[i]);
        }
    }

    /*
     * Now that we know everything in the sub-tree has been deleted,
     * we can delete the main directory. Huzzah for the shopkeep.
     */
    return libfs.rmdirSync(path);
};


getContentFromUrl = function(app, url, opts, callback) {
    if (typeof opts === 'function') {
        callback = opts;
        opts = {};
    }
    app.getWebPage(url, opts, function(err, url, content) {
        if (err) {
            libutils.error('FAILED to get ' + url);
            libutils.error(err);
        } else {
            callback(content);
        }
    });
};


removeFile = function(file) {
    if (existsSync(file)) {
        try {
            libfs.unlinkSync(file);
        } catch (err) {
            return false;
        }
        return true;
    }
    return false;
};


/*
 * Used for writing out YUI modules containing cached content
 */
YuiModuleCacher = function(name) {
    this.moduleName = name;
    this.namespaces = {};
};


/**
 * Creates and returns a namespace object with a cache() function. Call this
 * cache(key, val) to cache stuff.
 * For example:
 *     var ymc = new YuiModuleCacher('happy');
 *     ymc.createNamespace('one.two').cache('foo', {bar: 'bar'});
 *     ymc.dump();
 * Produces:
 *
 * YUI.add('happy', function(Y, NAME) {
 *     YUI.namespace("_mojito._cache.compiled.one.two");
 *     YUI._mojito._cache.compiled.one.two["foo"] = {"bar": "bar"};
 * });
 * @param {string} ns A namespace name.
 * @return {Object} The namespace.
 */
YuiModuleCacher.prototype.createNamespace = function(ns) {
    if (!this.namespaces[ns]) {
        this.namespaces[ns] = {
            _c: {},
            cache: function(k, v) {
                this._c[k] = v;
            }
        };
    }
    return this.namespaces[ns];
};


/**
 * return the YUI module text.
 * @return {string} The dump string of the namespace cache.
 */
YuiModuleCacher.prototype.dump = function() {
    var s = 'YUI.add("' + this.moduleName + '", function(Y, NAME) {\n',
        namespaces = this.namespaces;

    Object.keys(namespaces).forEach(function(ns) {
        s += '    YUI.namespace("_mojito._cache.' + ns + '");\n';
        s += '    YUI._mojito._cache.' + ns + ' = ' +
            Y.JSON.stringify(namespaces[ns]._c) + ';\n';
    });
    s += '});\n';
    return s;
};


/**
 * Extends YuiModuleCacher and adds ability to write files as well
 * @param {string} name YUI module name.
 * @param {string} file output file path.
 * @param {object} options same options given to compile command.
 */
YuiModuleCacheWriter = function(name, file, options) {
    YuiModuleCacher.call(this, name);
    this.destinationFile = file;
    this.opts = options || {};
};

/**
 * Create a clean prototype instance.
 */
YuiModuleCacheWriter.prototype = libutils.heir(YuiModuleCacher.prototype);


/**
 * Patch the constructor slot.
 */
YuiModuleCacheWriter.prototype.constructor = YuiModuleCacheWriter;


/**
 * Writes out to a file synchronously.
 * @return {Boolean} True if a file was written.
 */
YuiModuleCacheWriter.prototype.write = function() {
    var file = this.destinationFile,
        namespaces = this.namespaces,
        output = this.dump();

    // only write the file if there is something to write
    if (Object.keys(namespaces).length > 0) {
        try {
            mkdirP(libpath.dirname(file), parseInt('755', 8));
            if (this.opts.verbose) {
                libutils.log('writing file: ' + file);
            }
            libfs.writeFileSync(file, output, 'utf8');
            if (this.opts.verbose) {
                libutils.log('Created: ' + file);
            }
            return true;
        } catch (err) {
            libutils.error('Error writing file: ' + file);
            libutils.error(err);
            return false;
        }
    }
    return false;
};


/**
 * Standard usage string export.
 */
exports.usage = usage;


/**
 * Standard options list export.
 */
exports.options = options;


/**
 * Standard run method hook export.
 */
exports.run = run;


/**
 * Export the module itself so it's accessible.
 */
exports.YuiModuleCacher = YuiModuleCacher;


exports.test = {
    getInlineCssMojits: getInlineCssMojits
};