'use strict'; /* jshint node: true, evil: true */ var utils = require('utils'); var esomin = require('esomin'); var { Cc, Ci } = require('chrome'); var converter = Cc['@mozilla.org/intl/scriptableunicodeconverter']. createInstance(Ci.nsIScriptableUnicodeConverter); converter.charset = 'UTF-8'; var secClass = Cc['@mozilla.org/security/hash;1']; var nsICryptoHash = Ci.nsICryptoHash; var digests = []; var appName; // FIXME: It will be replace with webapp-shared.js // Taken from r.js css optimizing step. var cssImportRegExp = /\@import\s+(url\()?\s*([^);]+)\s*(\))?([\w, ]*)(;)?/ig; function stripCssComments(contents) { var index, start = 0, end = contents.length, result = '', startComment = '/*', endComment = '*/'; while ((index = contents.indexOf(startComment, start)) !== -1) { result += contents.substring(start, index); start = index; index = contents.indexOf(endComment, start); if (index === -1) { start = end - 1; } else { start = index + 2; } } if (start !== end - 1) { result += contents.substring(start, end); } return result; } /** * Find javascript dependencies in shared directory for all JS files in js * directory for each app directory, and also call onFileRead for all js and * HTML files. * @param {Object} parse Parser module from r.js * @param {String} appDir App directory in gaia source tree * @param {RegExp} exclude exclude expression for js files * @param {Function} onFileRead Event handler when file contents has been * retrieved. * @return {String[]} javascript dependencies in shared directory */ function getSharedJs(parse, appDir, exclude, onFileRead) { var sharedJs = []; var jsExtRegExp = /\.js$/; var files = utils.ls(utils.getFile(appDir, 'js'), true) .filter(function(file) { return /\.(js|html)/.test(file.path) && file.isFile(); }); files.forEach(function(file) { var contents = utils.getFileContent(file); onFileRead(contents); // If JS, scan for shared resources. if (jsExtRegExp.test(file.path) && !exclude.test(file.path)) { var deps = parse.findDependencies(file.path, contents); deps.forEach(function (dep) { if (dep.indexOf('shared/') === 0) { var sharedDep = dep.replace(/shared\/js\//, '') + '.js'; // Avoid duplicate entries for cleanliness if (sharedJs.indexOf(sharedDep) === -1) { sharedJs.push(sharedDep); } } }); } }); return sharedJs; } /** * Find CSS references in shared directory for all the CSS files and call * onFileRead() in style directory for each app directory. * @param {String} appDir App directory in gaia source tree * @param {[type]} onFileRead Event handler when file contents has been * retrieved. * @return {String[][]} an array contains style and style_unstable css * files in shared directory. */ function getSharedStyles(appDir, onFileRead) { var sharedStyle = []; var sharedStyleUnstable = []; var sharedElements = []; var files = utils.ls(utils.getFile(appDir, 'style'), true) .filter(function(file) { return /\.css$/.test(file.path); }); files.forEach(function(file) { var url, match, index, contents = utils.getFileContent(file); onFileRead(contents); contents = stripCssComments(contents); cssImportRegExp.lastIndex = 0; while ((match = cssImportRegExp.exec(contents))) { // Grab the URL without quotes url = match[2].replace(/['"]/g, ''); if (url) { index = url.indexOf('/shared/'); if (index !== -1) { if (url.indexOf('shared/elements') !== -1) { sharedElements.push(url.substring(index + 1) .replace(/shared\/elements\//, '')); } else if (url.indexOf('style_unstable') === -1) { sharedStyle.push(url.substring(index + 1) .replace(/shared\/style\//, '')); } else { sharedStyleUnstable.push(url.substring(index + 1) .replace(/shared\/style_unstable\//, '')); } } } } }); return [sharedStyle, sharedStyleUnstable, sharedElements]; } // Adapted from: // https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsICryptoHash#Computing_the_Hash_of_a_String // return the two-digit hexadecimal code for a byte function toHexString(charCode) { return ('0' + charCode.toString(16)).slice(-2); } /** * Copy the services.json file to the root of the staging dir. */ function generateServicesConfig(options) { var path = options.EMAIL_SERVICES_PATH, stageJsPath = utils.joinPath(options.STAGE_APP_DIR, 'js'); // Just copy over the default file if (!path) { var sourcePath = utils.joinPath(options.APP_DIR, 'js', 'services.js'); utils.copyFileTo(sourcePath, stageJsPath, 'services.js'); return; } // read the pretty JSON var services = utils.readJSONFromPath(path); // and write out ugly JSON utils.writeContent( utils.getFile(stageJsPath, 'services.js'), 'define(' + JSON.stringify(services) + ');'); } function onFileRead(contents) { digests.push(getDigest(contents)); } function getDigest(contents) { var result = {}; // data is an array of bytes var data = converter.convertToByteArray(contents, result); var ch = secClass.createInstance(nsICryptoHash); ch.init(ch.SHA1); ch.update(data, data.length); var hash = ch.finish(false); // convert the binary hash data to a hex string. return hash.split('').map(function(char, i) { return toHexString(hash.charCodeAt(i)); }).join(''); } function writeCacheValue(options) { // Update the cache value based on digest values of all files. var finalDigest = getDigest(digests.join(',')), cacheRegExp = /HTML_CACHE_VERSION\s*=\s*["'][^"']+["']/, cacheFile = utils.getFile(options.STAGE_APP_DIR, 'js', 'html_cache_restore.js'), contents = utils.getFileContent(cacheFile); contents = contents.replace(cacheRegExp, 'HTML_CACHE_VERSION = \'' + finalDigest + '\''); utils.writeContent(cacheFile, contents); } function runOptimizer(args, requirejs) { var build = new Promise(function(resolve, reject) { requirejs.optimize(args, resolve, reject); }); return build.then(function() { utils.log(appName, 'require.js optimize done'); }) .catch(function(err) { utils.log(appName, 'require.js optimize failed'); utils.log(appName, err); }); } function optimize(options, r) { var gelamConfigFile = utils.getFile(options.APP_DIR, 'build', 'gelam_worker.build.js'); var mainFrameConfigFile = utils.getFile(options.APP_DIR, 'build', 'main-frame-setup.build.js'); var appConfigFile = utils.getFile(options.APP_DIR, 'build', 'email.build.js'); var stageShared = utils.getFile(options.STAGE_APP_DIR, 'shared'); var stageJs = utils.getFile(options.STAGE_APP_DIR, 'js'); var extPrefix = /^.*[\\\/]email[\\\/]js[\\\/]ext[\\\/]/; utils.ensureFolderExists(stageShared); utils.ensureFolderExists(stageJs); // Make sure services.js is up to date. generateServicesConfig(options); // Do gelam worker stuff first. This will copy over all of the js/ext // directory. var logLevel = 'logLevel=' + (options.VERBOSE === '1' ? '0' : '4'); return runOptimizer([gelamConfigFile.path, logLevel], r) .then(function() { // Now do main-frame-setup build for the main thread side of gelam. It is // a single file optimization, so need to manually delete files it combines // from the staging area. // Manually load and eval the options so that a function callback can be // added to the build options. var mainFrameOptions = eval('(' + utils.getFileContent(mainFrameConfigFile) + ')'); // Up the log level so we can see what was built. By default, passing object // args to r.js will run it in silent mode. mainFrameOptions.logLevel = (options.VERBOSE === '1') ? 0 : 4; // Update paths, since it is now relative to the current working directory // of this script, not the gelamConfigFile. mainFrameOptions.baseUrl = utils.getFile(options.APP_DIR, 'js').path; mainFrameOptions.out = utils.getFile(options.STAGE_APP_DIR, 'js', 'ext', 'main-frame-setup.js').path; // Minification handled later by esomin mainFrameOptions.optimize = 'none'; mainFrameOptions.onModuleBundleComplete = function(data) { // Called on layer completion. Get data.included for included files and // remove those files from build_stage. This does not happen automatically // because the gelam build is a single file optimization target from the // source area to build_stage, so it does not know of the other files that // are already in build_stage as the result of the main gaia app build. data.included.forEach(function(sourceLocation) { var destFile = utils.getFile(options.STAGE_APP_DIR, 'js', 'ext', sourceLocation.replace(extPrefix, '')); if (destFile.exists() && destFile.path.indexOf('main-frame-setup') === -1) { destFile.remove(false); } }); }; return runOptimizer(mainFrameOptions, r); }) .then(function() { // Now the rest of the gaia app optimization. This build run explicitly // ignores the ext directory. return runOptimizer([appConfigFile.path, logLevel], r); }); } function removeLoader(options) { var indexFile = utils.getFile(options.STAGE_APP_DIR, 'index.html'); var indexContent = utils.getFileContent(indexFile); utils.writeContent(indexFile, indexContent.replace(/data-loader="[^"]+"/, '')); return options; } function removeFiles(options) { var files = [ utils.getFile(options.STAGE_APP_DIR, 'js','tmpl_builder.js'), utils.getFile(options.STAGE_APP_DIR, '.jshintrc') ]; files.forEach(function(file) { if (utils.fileExists(file.path)) { file.remove(false); } }); } function getParse(r) { return new Promise(function(resolve, reject) { r.tools.useLib(function(req) { req(['parse'], function(parse) { resolve(parse); }); }); }); } exports.execute = function(options) { appName = utils.basename(options.APP_DIR); var shared = { js: [], style: [], style_unstable: [] }; var backendRegExp = /[\\\/]js[\\\/]ext[\\\/]/; var sharedJsonFile = utils.getFile(options.STAGE_APP_DIR, 'gaia_shared.json'); var stageAppDir = utils.getFile(options.STAGE_APP_DIR); utils.ensureFolderExists(stageAppDir); var rjsPath = utils.joinPath(options.GAIA_DIR, 'build', 'r.js'); var requirejs; if (utils.isNode()) { requirejs = require(rjsPath); } else { var sandbox = utils.createSandbox(); sandbox.arguments = []; sandbox.requirejsAsLib = true; sandbox.print = function() { utils.log(appName, Array.prototype.join.call(arguments, ' ')); }; utils.runScriptInSandbox(rjsPath, sandbox); requirejs = sandbox.requirejs; } var promises = [ optimize(options, requirejs), getParse(requirejs) ]; return Promise.all(promises) .then(function(result) { var [buildText, parse] = result; shared.js = getSharedJs(parse, options.APP_DIR, backendRegExp, onFileRead); [shared.style, shared.style_unstable] = getSharedStyles(options.APP_DIR, onFileRead); utils.writeContent(sharedJsonFile, JSON.stringify(shared, null, 2)); writeCacheValue(options); removeLoader(options); removeFiles(options); }) .then(function() { if (options.GAIA_OPTIMIZE === '1') { utils.log(appName, 'esomin minify done'); return esomin.minifyDir(stageAppDir); } }) .catch(function(err) { utils.log(err) utils.log(appName, 'running customize build failed'); throw err; }); };