UNPKG

rocket

Version:

The rapid development framework for node.js

755 lines (523 loc) 22.8 kB
var async = require('async') , fs = require('fs') , path = require('path') , express = require('express') , jade = require('jade') , child_process = require('child_process') , events = require('events') , EventEmitter = events.EventEmitter , requirejs = require('requirejs') , rimraf = require('rimraf') , wrench = require('wrench') , util = require('util') ; var asyncFs = require('../util/async_fs') , FileEventEmitter = require('../util/file_event_emitter') , tmpdir = require('../util/tmpdir') , config = require('../config') , CLIENT_BUILD_DIR_NAME = config.CLIENT_BUILD_DIR_NAME , CLIENT_BUILD_JSON_FILE_NAME = config.CLIENT_BUILD_JSON_FILE_NAME , CLIENT_STATIC_DIR_NAME = config.CLIENT_STATIC_DIR_NAME , CLIENT_JS_DIR_NAME = config.CLIENT_JS_DIR_NAME , CLIENT_JS_TEMPLATES_DIR_NAME = config.CLIENT_JS_TEMPLATES_DIR_NAME , CLIENT_CSS_DIR_NAME = config.CLIENT_CSS_DIR_NAME , CLIENT_VIEWS_DIR_NAME = config.CLIENT_VIEWS_DIR_NAME , REQUIRE_JS_CONFIG_FILE_NAME = config.REQUIRE_JS_CONFIG_FILE_NAME , JADE_RUNTIME_FILE_NAME = config.JADE_RUNTIME_FILE_NAME , STATIC_FILES_CACHE_LIFETIME = config.STATIC_FILES_CACHE_LIFETIME ; /****************************************************************************** * Compile client views located at `src_path` and saves them in `dst_path`. * * @param src_path {String} The path of the source directory. * @param dst_path {String} The path of the destination directory. * * @param callback {Function} A callback of the form `function(err)` */ function compileViews() { var args = Array.prototype.slice.apply(arguments) , callback = args.pop() , src_path = args.shift() , dst_path = args.shift() , production_paths = args.shift() ; asyncFs.forEachFile( src_path , function (view_filename, top_callback) { var view_path = path.join(src_path, view_filename) , view_dst_path = path.join(dst_path, view_filename + '.js') ; async.waterfall( [ async.apply(fs.stat, view_path) , function(stats, callback) { var view_dst_path_noext = view_dst_path.slice(0, -'.js'.length) ; if (stats.isDirectory()) { async.waterfall( [ async.apply(fs.mkdir, view_dst_path_noext, '0777') , async.apply(compileViews, view_path, view_dst_path_noext) ] , top_callback ); } else if (stats.isFile()) { if (view_filename.slice(-'.jade'.length) === '.jade') { callback(null); } else { fs.symlink(view_path, view_dst_path_noext, top_callback); } } else { callback('UNSUPORTED_VIEW_FS_NODE'); } } , async.apply(fs.readFile, view_path, 'utf8') , function(data, callback) { var options = { client: true } ; if (production_paths) { options.compileDebug = false; production_paths[CLIENT_VIEWS_DIR_NAME + '/' + view_filename] = view_dst_path.substr(0, view_dst_path.length - 3); } try { callback(null, jade.compile(data, options)); } catch(e) { console.log('xxx Error compiling view :', view_filename); throw e; } } , function(f, callback) { var export_string = 'define([\'jade-runtime\'], function() { return (' + f.toString() + ') });' ; fs.writeFile(view_dst_path, export_string, 'utf8', callback); } ] , function(err) { if (err) { throw err; } top_callback(err); } ); } , callback ); } /****************************************************************************** * */ function setupDevModeViews(client_views_path, client_views_tmpdir, callback) { var tree_emitter = asyncFs.watchTree(client_views_path) ; tree_emitter.on('change', function() { async.waterfall( [ function(callback) { client_views_tmpdir.clear(callback); } , function(callback) { compileViews(client_views_path, client_views_tmpdir.path, callback); } ] , function(err) { if (err) { throw(err); } } ); }); compileViews(client_views_path, client_views_tmpdir.path, callback); } /****************************************************************************** * Reads and exports the `require.config.json` file for the controller module * to access. Also wacthes the file for changes and reloads it accordingly. * * @param config_file_path {String} The path of the configuration file * @param callback {Function} A function of the form `function(err)` */ function watchAndExportConfig(config_file_path, callback) { var config_file_emitter = new FileEventEmitter(config_file_path) ; function exportConfig(callback) { fs.readFile(config_file_path, 'utf8', function(err, data) { var json ; if (err) { callback(err); return; } try { json = JSON.parse(data) || {}; } catch (e) { console.log('xxx ERROR parsing `require.config.json`'); throw(e); } json.baseUrl = '/js'; json.paths = json.paths || {}; json.paths['jade-runtime'] = path.join('rocket', 'vendors', JADE_RUNTIME_FILE_NAME.substr(0, JADE_RUNTIME_FILE_NAME.length - 3)); if (!json.paths['now']) json.paths['now'] = '/nowjs/now'; global.require_configuration = json; callback(null); }); } config_file_emitter.on('changes', function() { exportConfig(function(err) { if (err) { throw err; } }); }); exportConfig(callback); } /****************************************************************************** * Optimize the specified directory using RequireJS `r.js` utility, saving the * optimized files in the directory pointed by `dst_tmpdir`. * * @param dir_path {String} The path pointing to the directory containing the * files to be optimized. * * @param dst_tmpdir {ChildTmpdir} A ChildTmpdir defining the directory where * the optimized files are to be stored. * * @param callback {Function} A callback of the form `function(err)` */ function optimizeDir(dir_path, dst_tmpdir, options_tmpdir, callback) { var options_filename = Date.now() + '.js' , require_config_path = path.join(dir_path, REQUIRE_JS_CONFIG_FILE_NAME) , options_file_path = path.join(options_tmpdir.path, options_filename) , options = { appDir : options_tmpdir.path , baseUrl : './' , dir : dst_tmpdir.path , modules : [] // , optimize : 'none' , optimize: 'uglify' } , external_modules = [] , views = false ; async.waterfall( [ function(callback) { fs.readFile(require_config_path, 'utf8', function(err, data) { var config , src ; if (err) { if (err.code !== 'ENOENT') { callback(err); } else { callback(null); } } else { config = JSON.parse(data); if (config.paths) { for (var name in config.paths) { src = config.paths[name]; if (Array.isArray(src) || src.substr(0,4) === 'http') { external_modules.push(name); config.paths[name] = 'empty:'; } } options.paths = config.paths; } config.optimize && (options.optimize = config.optimize); options.uglify2 = config.uglify2; options.generateSourceMaps= config.generateSourceMaps; options.preserveLicenseComments = config.preserveLicenseComments; options.paths = options.paths || {}; options.paths['jade-runtime'] = 'empty:'; external_modules.push('now'); options.paths['now'] = 'empty:'; callback(null); } }); } , function (callback) { compileViews(dir_path, options_tmpdir.path, options.paths, callback); } , async.apply(fs.readdir, dir_path) , function(files, callback) { async.forEach( files , function iterator(filename, callback) { var module_name ; if (filename === REQUIRE_JS_CONFIG_FILE_NAME) { callback(null); return; } var suffix = '_client.js'; if (filename.substr(-suffix.length) === suffix) { module_name = filename.substr(0, filename.length - 3); options.modules.push( { name : module_name , exclude : external_modules } ); } callback(null); } , callback ) } , function (callback) { requirejs.optimize(options, function (buildResponse) { callback(null); }) } , function(callback) { options_tmpdir.clear(callback); } ] , callback ); } /***************************************************************************** * Watches the directory tree under `path` and creates an optimized version * of it, using RequireJS' r.js, and saves it in the directory described by * `dst_tmpdir`, using `options_tmpdir` as working directory. * * The directory is optimized each time a changes in the watched directory tree * is detected. * * @param path {String} The path of the folder to optimize. * * @param dst_tmpdir {Tmpdir} The destination (temporary) directory. * * @param options_tmpdir {Tmpdir} The working temporary directory given as * a `Tmpdir` instance * * @param callback {Function} a callback of the form `function(err)` */ function watchAndOptimize(path, dst_tmpdir, options_tmpdir, callback) { // var tree_event_emitter = asyncFs.watchTree(path); // ; // tree_event_emitter.on('change', function() { // process.stdout.write('\n-- Change in tree detected'); // optimizeDir(path, dst_tmpdir, options_tmpdir, function(err) { // if (err) { throw err; } // }); // }); optimizeDir(path, dst_tmpdir, options_tmpdir, function(err) { if (err) { callback(err); return; } callback(null); }); } /****************************************************************************** * Setups the client related resources. * * It * * sets up a static middleware to serve the webapp `client/static` folder * as `/static` * * * sets up a static middleware to serve the rocket `rocket-js` folder * as `/rocket-js` * * * sets up a static middleware to serve the webapp `client/js` folder as * `/js` (except when in production) * * Moreover, when in production * it * * * optimizes -- with requireJS' `r.js` -- the modules at the root of * `client/js` by packing it with all its dependencies and then uglifying it. * * * serves those modules via a static middleware as `/js` * * Finally, rocket watches the webapp `client/js` directory and updates its * related resources accordingly when new files are found or changes committed. * * @param app {Object} The app object as returned by express, connect, or * rocket. * * @param middlewares {Array<Function||Array<String, Function>>} An array of functions * representing connect/express middlewares -- or -- an array of Array having a route * string as its first element on which to restrict the application of the middleware * that is provided as the second element of the array. * * @param client {Object} The FileEventEmitter object of the `client` * folder. * * @param callback {Function} A callback of the form `function(err)` */ exports.setup = function setup_client() { var args = Array.prototype.slice.call(arguments) , app = args.shift() , middlewares = args.shift() , client = args.shift() , callback = args.pop() , js_dir_path = path.join(client.path, CLIENT_JS_DIR_NAME) , css_dir_path = path.join(client.path, CLIENT_CSS_DIR_NAME) , views_dir_path = path.join(js_dir_path, CLIENT_VIEWS_DIR_NAME) , static_dir_path = path.join(client.path, CLIENT_STATIC_DIR_NAME) , require_js_config_path = path.join(js_dir_path, REQUIRE_JS_CONFIG_FILE_NAME) , client_views_tmpdir = tmpdir.mkuniqueSync() , optimize_js_tmpdir = tmpdir.mkuniqueSync() , optimize_css_tmpdir = tmpdir.mkuniqueSync() , app_dir_path = path.join(client.path, '..') , app_package_json_path = path.join(app_dir_path, 'package.json') , client_build_dir_path = path.join(app_dir_path, CLIENT_BUILD_DIR_NAME) , client_build_json_path = path.join(client_build_dir_path, CLIENT_BUILD_JSON_FILE_NAME) , client_build_js_dir_path = path.join(client_build_dir_path, CLIENT_JS_DIR_NAME) , client_build_css_dir_path = path.join(client_build_dir_path, CLIENT_CSS_DIR_NAME) , js_tree_event_emitter , css_tree_event_emitter , packageInfo , buildClient ; /*** * sets up a static middleware to serve the webapp `client/static` folder * as `/static` */ app.use('/static', express.static(static_dir_path)); function doneCallback() { if (middlewares) { for (var i = 0, ii = middlewares.length; i < ii; i++) { if ( Array.isArray(middlewares[i]) ) { app.use(middlewares[i][0], middlewares[i][1]); } else { app.use(middlewares[i]); } } } callback.apply(this, arguments) } if (process.env['NODE_ENV'] === 'production') { async.waterfall( [ function (callback) { fs.readFile(app_package_json_path, 'utf8', function (err, packageJSON) { if (err && err.code === 'ENOENT') callback(null, true); else if (err) callback(err); else { packageInfo = JSON.parse(packageJSON); fs.readFile(client_build_json_path, 'utf8', function (err, buildJSON) { var buildInfo; if (err && err.code === 'ENOENT') callback(null, true); else if (err) callback(err); else { buildInfo = JSON.parse(buildJSON); if (buildInfo.version === packageInfo.version) callback(null, false); else callback(null, true); } }) } }) } , function (_buildClient, callback) { buildClient = _buildClient; if (buildClient) { console.log('--- Building optimized client bundle. This may take several minutes.'); rimraf(client_build_dir_path, function (err) { if (err && err.code === 'ENOENT') callback(null); else callback(err); }); } else callback(null); } , function (callback) { if (buildClient) async.forEachSeries( [ client_build_dir_path , client_build_js_dir_path , client_build_css_dir_path ] , function (dirPath, callback) { fs.mkdir(dirPath, callback); } , callback ); else callback(null); } , function (callback) { async.parallel( [ function (callback) { async.waterfall( [ function (callback) { if (buildClient) watchAndOptimize(js_dir_path, { path : client_build_js_dir_path }, optimize_js_tmpdir, callback); else callback(null); } , function (callback) { if (packageInfo) fs.writeFile(client_build_json_path, JSON.stringify(packageInfo), 'utf8', function (err) { callback(err); }); else callback(null); } , function (callback) { if (buildClient) wrench.copyDirRecursive( path.join(__dirname, 'rocket-js') , path.join(client_build_js_dir_path, 'rocket') , callback); else callback(null); } , function(callback) { //sets up a static middleware to serve the webapp optimized tmp js folder //as `/js` app.use('/js', express.static(client_build_js_dir_path)); if (buildClient) { console.log('\t--- JS build done.'); } callback(null); } ] , callback ); } , function (callback) { async.waterfall( [ function (callback) { if (buildClient) watchAndOptimize(css_dir_path, { path : client_build_css_dir_path }, optimize_css_tmpdir, callback); else callback(null); } , function(callback) { if (buildClient) { console.log('\t--- CSS build done.'); } //sets up a static middleware to serve the webapp optimized tmp css folder //as `/css` app.use('/css', express.static(client_build_css_dir_path)); callback(null); } ] , callback ); } , async.apply(watchAndExportConfig, require_js_config_path) ] , function (err) { if (buildClient) { if (err) console.log('xxx ERROR Building client:', err); else console.log('--- Client Bundle Successfully Built'); } callback(err); } ); } ] , doneCallback); } else { /*** * sets up a static middleware to serve the rocket `rocket-js` folder * as `/rocket-js` */ app.use('/js/rocket', express.static(path.join(__dirname, 'rocket-js'))); //sets up a static middleware to serve the webapp `client/js` folder //as `/js` app.use('/js', express.static(js_dir_path)); app.use('/css', express.static(css_dir_path)); app.use('/js', express.static(client_views_tmpdir.path)); async.parallel([ async.apply(setupDevModeViews, js_dir_path, client_views_tmpdir) , async.apply(watchAndExportConfig, require_js_config_path) ], doneCallback); } }