VulpesOS/gaia/apps/email/build/build.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

367 lines
12 KiB
JavaScript
Executable file

'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;
});
};