alloy
Version:
Appcelerator Titanium MVC Framework
1,083 lines (948 loc) • 36.8 kB
JavaScript
var path = require('path'),
fs = require('fs'),
wrench = require('wrench'),
vm = require('vm'),
uglifyjs = require('uglify-js'),
// alloy requires
_ = require('../../lib/alloy/underscore'),
logger = require('../../logger'),
U = require('../../utils'),
tiapp = require('../../tiapp'),
CONST = require('../../common/constants'),
platforms = require('../../../platforms/index'),
// alloy compiler requires
CU = require('./compilerUtils'),
styler = require('./styler'),
sourceMapper = require('./sourceMapper'),
CompilerMakeFile = require('./CompilerMakeFile'),
BuildLog = require('./BuildLog'),
Orphanage = require('./Orphanage');
var alloyRoot = path.join(__dirname,'..','..'),
viewRegex = new RegExp('\\.' + CONST.FILE_EXT.VIEW + '$'),
controllerRegex = new RegExp('\\.' + CONST.FILE_EXT.CONTROLLER + '$'),
modelRegex = new RegExp('\\.' + CONST.FILE_EXT.MODEL + '$'),
compileConfig = {},
otherPlatforms,
buildPlatform,
titaniumFolder,
buildLog,
theme,
platformTheme,
widgetIds = [];
var times = {
first: null,
last: null,
msgs: []
};
var fileRestrictionUpdatedFiles = [],
restrictionSkipOptimize = false;
//////////////////////////////////////
////////// command function //////////
//////////////////////////////////////
module.exports = function(args, program) {
BENCHMARK();
var alloyConfig = {},
compilerMakeFile,
paths = U.getAndValidateProjectPaths(
program.outputPath || args[0] || process.cwd()
),
restrictionPath;
// Initialize modules used throughout the compile process
buildLog = new BuildLog(paths.project);
tiapp.init(path.join(paths.project, 'tiapp.xml'));
// validate the current Titanium SDK version, exit on failure
tiapp.validateSdkVersion();
// construct compiler config from command line config parameters
// and print the configuration data
logger.debug('----- CONFIGURATION -----');
if (program.config && _.isString(program.config)) {
logger.debug('raw config = "' + program.config + '"');
_.each(program.config.split(','), function(v) {
var parts = v.split('=');
alloyConfig[parts[0]] = parts[1];
logger.debug(parts[0] + ' = ' + parts[1]);
});
}
if (program.platform) {
logger.debug('platform = ' + program.platform);
alloyConfig.platform = program.platform;
}
if (!alloyConfig.deploytype) {
alloyConfig.deploytype = 'development';
logger.debug('deploytype = ' + alloyConfig.deploytype);
}
logger.debug('project path = ' + paths.project);
logger.debug('app path = ' + paths.app);
logger.debug('');
// make sure a platform was specified
buildPlatform = alloyConfig.platform;
if (!buildPlatform) {
U.die([
'You must define a target platform for the alloy compile command',
' Ex. "alloy compile --config platform=ios"'
]);
}
titaniumFolder = platforms[buildPlatform].titaniumFolder;
otherPlatforms = _.without(CONST.PLATFORM_FOLDERS, titaniumFolder);
// allow to filter the file to compile
if (!alloyConfig.file) {
restrictionPath = null;
} else {
restrictionPath = path.join(paths.project, alloyConfig.file);
}
// create compile config from paths and various alloy config files
logger.debug('----- CONFIG.JSON -----');
compileConfig = CU.createCompileConfig(paths.app, paths.project, alloyConfig, buildLog);
theme = compileConfig.theme;
platformTheme = buildLog.data[buildPlatform] ? buildLog.data[buildPlatform]['theme'] : "";
buildLog.data.themeChanged = theme !== platformTheme;
buildLog.data.theme = theme;
// track whether deploy type has changed since previous build
buildLog.data.deploytypeChanged = buildLog.data.deploytype !== alloyConfig.deploytype;
buildLog.data.deploytype = alloyConfig.deploytype;
logger.debug('');
// wipe the controllers, models, and widgets
logger.debug('----- CLEANING RESOURCES -----');
var orphanage = new Orphanage(paths.project, buildPlatform, {
theme: theme,
adapters: compileConfig.adapters
});
orphanage.clean();
logger.debug('');
// process project makefiles
compilerMakeFile = new CompilerMakeFile();
var alloyJMK = path.resolve(path.join(paths.app, 'alloy.jmk'));
if (path.existsSync(alloyJMK)) {
logger.debug('Loading "alloy.jmk" compiler hooks...');
var script = vm.createScript(fs.readFileSync(alloyJMK), 'alloy.jmk');
// process alloy.jmk compile file
try {
script.runInNewContext(compilerMakeFile);
compilerMakeFile.isActive = true;
} catch(e) {
logger.error(e.stack);
U.die('Project build at "' + alloyJMK + '" generated an error during load.');
}
compilerMakeFile.trigger('pre:load', _.clone(compileConfig));
logger.debug('');
}
// create generated controllers folder in resources
logger.debug('----- BASE RUNTIME FILES -----');
U.installPlugin(path.join(alloyRoot,'..'), paths.project);
// copy in all lib resources from alloy module, exclude backbone dir
updateFilesWithBuildLog(
path.join(alloyRoot, 'lib'),
path.join(paths.resources, titaniumFolder),
{
rootDir: paths.project,
filter: new RegExp('^alloy[\\/\\\\]backbone([\\/\\\\]|$)'),
exceptions: _.map(_.difference(CONST.ADAPTERS, compileConfig.adapters), function(a) {
return path.join('alloy', 'sync', a + '.js');
}),
restrictionPath: restrictionPath
}
);
// Copy the version of backbone that is specified in config.json
U.copyFileSync(
path.join(
alloyRoot, "lib", "alloy", "backbone",
(_.contains(CONST.SUPPORTED_BACKBONE_VERSIONS, compileConfig.backbone))
? compileConfig.backbone
: CONST.DEFAULT_BACKBONE_VERSION,
"backbone.js"
),
path.join(paths.resources, titaniumFolder, "alloy", "backbone.js")
);
updateFilesWithBuildLog(
path.join(alloyRoot, 'common'),
path.join(paths.resources, titaniumFolder, 'alloy'),
{ rootDir: paths.project, restrictionPath: restrictionPath }
);
// create runtime folder structure for alloy
_.each(['COMPONENT','WIDGET','RUNTIME_STYLE'], function(type) {
var p = path.join(paths.resources, titaniumFolder, 'alloy', CONST.DIR[type]);
wrench.mkdirSyncRecursive(p, 0755);
});
// Copy in all developer assets, libs, and additional resources
_.each(['ASSETS','LIB','VENDOR'], function(type) {
updateFilesWithBuildLog(
path.join(paths.app, CONST.DIR[type]),
path.join(paths.resources, titaniumFolder),
{
rootDir: paths.project,
themeChanged: buildLog.data.themeChanged,
filter: new RegExp('^(?:' + otherPlatforms.join('|') + ')[\\/\\\\]'),
exceptions: otherPlatforms,
createSourceMap: (type==='ASSETS') ? false : compileConfig.sourcemap,
compileConfig: compileConfig,
titaniumFolder: titaniumFolder,
type: type,
restrictionPath: restrictionPath
}
);
});
// copy in test specs if not in production
if (alloyConfig.deploytype !== 'production') {
updateFilesWithBuildLog(
path.join(paths.app,'specs'),
path.join(paths.resources, titaniumFolder, 'specs'),
{ rootDir: paths.project, restrictionPath: restrictionPath }
);
}
// check theme for assets
if (theme) {
_.each(['ASSETS','LIB','VENDOR'], function(type) {
var themeAssetsPath = path.join(paths.app,'themes',theme, CONST.DIR[type]);
if (path.existsSync(themeAssetsPath)) {
updateFilesWithBuildLog(
themeAssetsPath,
path.join(paths.resources, titaniumFolder),
{
rootDir: paths.project,
themeChanged: buildLog.data.themeChanged,
filter: new RegExp('^(?:' + otherPlatforms.join('|') + ')[\\/\\\\]'),
exceptions: otherPlatforms,
titaniumFolder: titaniumFolder,
restrictionPath: restrictionPath
}
);
}
});
}
logger.debug('');
// trigger our custom compiler makefile
if (compilerMakeFile.isActive) {
compilerMakeFile.trigger('pre:compile', _.clone(compileConfig));
}
logger.info('----- MVC GENERATION -----');
// create the global style, if it exists
styler.setPlatform(buildPlatform);
styler.loadGlobalStyles(paths.app, theme ? {theme:theme} : {});
// Create collection of all widget and app paths
var widgetDirs = U.getWidgetDirectories(paths.app);
var viewCollection = widgetDirs;
viewCollection.push({ dir: path.join(paths.project,CONST.ALLOY_DIR) });
// Process all models
var models = processModels(viewCollection);
_.each(models, function(m) {
CU.models.push(m.charAt(0).toLowerCase() + m.slice(1));
});
// Create a regex for determining which platform-specific
// folders should be used in the compile process
var filteredPlatforms = _.reject(CONST.PLATFORM_FOLDERS_ALLOY, function(p) {
return p === buildPlatform;
});
filteredPlatforms = _.map(filteredPlatforms, function(p) { return p + '[\\\\\\/]'; });
var filterRegex = new RegExp('^(?:(?!' + filteredPlatforms.join('|') + '))');
// don't process XML/controller files inside .svn folders (ALOY-839)
var excludeRegex = new RegExp('(?:^|[\\/\\\\])(?:' + CONST.EXCLUDED_FILES.join('|') + ')(?:$|[\\/\\\\])');
// Process all views/controllers and generate their runtime
// commonjs modules and source maps.
var tracker = {};
_.each(viewCollection, function(collection) {
// generate runtime controllers from views
var theViewDir = path.join(collection.dir,CONST.DIR.VIEW);
if (fs.existsSync(theViewDir)) {
_.each(wrench.readdirSyncRecursive(theViewDir), function(view) {
if (viewRegex.test(view) && filterRegex.test(view) && !excludeRegex.test(view)) {
// make sure this controller is only generated once
var theFile = view.substring(0, view.lastIndexOf('.'));
var theKey = theFile.replace(new RegExp('^' + buildPlatform + '[\\/\\\\]'), '');
var fp = path.join(collection.dir, theKey);
if (tracker[fp]) { return; }
// generate runtime controller
logger.info('[' + view + '] ' + (collection.manifest ? collection.manifest.id +
' ' : '') + 'view processing...');
parseAlloyComponent(view, collection.dir, collection.manifest, null, restrictionPath);
tracker[fp] = true;
}
});
}
// generate runtime controllers from any controller code that has no
// corresponding view markup
var theControllerDir = path.join(collection.dir,CONST.DIR.CONTROLLER);
if (fs.existsSync(theControllerDir)) {
_.each(wrench.readdirSyncRecursive(theControllerDir), function(controller) {
if (controllerRegex.test(controller) && filterRegex.test(controller) && !excludeRegex.test(controller)) {
// make sure this controller is only generated once
var theFile = controller.substring(0,controller.lastIndexOf('.'));
var theKey = theFile.replace(new RegExp('^' + buildPlatform + '[\\/\\\\]'), '');
var fp = path.join(collection.dir, theKey);
if (tracker[fp]) { return; }
// generate runtime controller
logger.info('[' + controller + '] ' + (collection.manifest ?
collection.manifest.id + ' ' : '') + 'controller processing...');
parseAlloyComponent(controller, collection.dir, collection.manifest, true, restrictionPath);
tracker[fp] = true;
}
});
}
});
logger.info('');
// [ALOY-858] handle theme "i18n" and "platform" folders
if (theme) {
var themeI18nPath = path.join(paths.app, CONST.DIR.THEME, theme, CONST.DIR.I18N),
themePlatformPath = path.join(paths.app, CONST.DIR.THEME, theme, CONST.DIR.PLATFORM);
if (path.existsSync(themeI18nPath)) {
CU.mergeI18n(themeI18nPath, compileConfig.dir, { override: true });
}
if (path.existsSync(themePlatformPath)) {
logger.info(' themes platform: "' + themePlatformPath + '"');
var tempDir = path.join(paths.project, CONST.DIR.BUILD_PLATFORM),
appPlatformDir = path.join(paths.project, CONST.DIR.PLATFORM);
if (!fs.existsSync(tempDir)) {
wrench.mkdirSyncRecursive(tempDir, 0755);
if (fs.existsSync(appPlatformDir)) {
wrench.copyDirSyncRecursive(appPlatformDir, tempDir, {preserve: true});
}
}
wrench.copyDirSyncRecursive(themePlatformPath, tempDir, {preserve: true});
}
}
logger.info('');
generateAppJs(paths, compileConfig, restrictionPath);
// ALOY-905: workaround TiSDK < 3.2.0 iOS device build bug where it can't reference app.js
// in platform-specific folders, so we just copy the platform-specific one to
// the Resources folder.
if (buildPlatform === 'ios' && tiapp.version.lt('3.2.0')) {
U.copyFileSync(path.join(paths.resources, titaniumFolder, 'app.js'), path.join(paths.resources, 'app.js'));
}
// optimize code
logger.info('----- OPTIMIZING -----');
if (restrictionSkipOptimize) {
logger.info('Skipping optimize due to file restriction.');
} else {
optimizeCompiledCode(alloyConfig, paths);
}
// trigger our custom compiler makefile
if (compilerMakeFile.isActive) {
compilerMakeFile.trigger('post:compile', _.clone(compileConfig));
}
// write out the log for this build
buildLog.write();
BENCHMARK('TOTAL', true);
};
///////////////////////////////////////
////////// private functions //////////
///////////////////////////////////////
function generateAppJs(paths, compileConfig, restrictionPath) {
var alloyJs = path.join(paths.app, 'alloy.js');
if (restrictionPath !== null && restrictionPath !== path.join(paths.app, 'alloy.js')) {
// skip alloy.js processing when filtering on another file
return;
}
// info needed to generate app.js
var target = {
filename: 'Resources' + path.sep + titaniumFolder + path.sep + 'app.js',
filepath: path.join(paths.resources, titaniumFolder, 'app.js'),
template: path.join(alloyRoot, 'template', 'app.js')
},
// additional data used for source mapping
data = {
'__MAPMARKER_ALLOY_JS__': {
filename: 'app' + path.sep + 'alloy.js',
filepath: alloyJs
}
},
// hash used to determine if we need to rebuild
hash = U.createHash(alloyJs);
// is it already generated from a prior copile?
buildLog.data[buildPlatform] || (buildLog.data[buildPlatform] = {});
if (!compileConfig.buildLog.data.deploytypeChanged && fs.existsSync(target.filepath) && buildLog.data[buildPlatform][alloyJs] === hash) {
logger.info('[app.js] using cached app.js...');
restrictionSkipOptimize = (restrictionPath !== null);
// if not, generate the platform-specific app.js and save its hash
} else {
logger.info('[app.js] Titanium entry point processing...');
sourceMapper.generateCodeAndSourceMap({
target: target,
data: data,
}, compileConfig);
fileRestrictionUpdatedFiles.push(path.relative('Resources', target.filename));
buildLog.data[buildPlatform][alloyJs] = hash;
}
buildLog.data[buildPlatform]['theme'] = theme;
logger.info('');
}
function matchesRestriction(files, fileRestriction) {
var matches = false;
_.each(files, function(file) {
if (typeof file === 'string') {
matches |= (file === fileRestriction);
} else if (typeof file === 'object') {
// platform-specific TSS files result in an object
// with a property of platform === true which needs
// to be removed to prevent a compile error
delete file.platform;
matches |= matchesRestriction(file, fileRestriction);
} else {
throw 'Unsupported file type: ' + typeof file;
}
});
return matches;
}
function parseAlloyComponent(view, dir, manifest, noView, fileRestriction) {
var parseType = noView ? 'controller' : 'view';
fileRestriction = fileRestriction || null;
// validate parameters
if (!view) { U.die('Undefined ' + parseType + ' passed to parseAlloyComponent()'); }
if (!dir) { U.die('Failed to parse ' + parseType + ' "' + view + '", no directory given'); }
var dirRegex = new RegExp('^(?:' + CONST.PLATFORM_FOLDERS_ALLOY.join('|') + ')[\\\\\\/]*');
var basename = path.basename(view, '.' + CONST.FILE_EXT[parseType.toUpperCase()]),
dirname = path.dirname(view).replace(dirRegex,''),
viewName = basename,
template = {
viewCode: '',
modelVariable: CONST.BIND_MODEL_VAR,
parentVariable: CONST.PARENT_SYMBOL_VAR,
itemTemplateVariable: CONST.ITEM_TEMPLATE_VAR,
controllerPath: (dirname ? path.join(dirname,viewName) : viewName).replace(/\\/g, '/'),
preCode: '',
postCode: '',
Widget: !manifest ? '' : 'var ' + CONST.WIDGET_OBJECT +
" = new (require('alloy/widget'))('" + manifest.id + "');this.__widgetId='" +
manifest.id + "';",
WPATH: !manifest ? '' : _.template(
fs.readFileSync(path.join(alloyRoot,'template','wpath.js'),'utf8'),
{ WIDGETID: manifest.id }
),
__MAPMARKER_CONTROLLER_CODE__: ''
},
widgetDir = dirname ? path.join(CONST.DIR.COMPONENT,dirname) : CONST.DIR.COMPONENT,
widgetStyleDir = dirname ? path.join(CONST.DIR.RUNTIME_STYLE,dirname) :
CONST.DIR.RUNTIME_STYLE,
state = { parent: {}, styles: [] },
files = {};
// reset the bindings map
styler.bindingsMap = {};
CU.destroyCode = '';
CU.postCode = '';
CU[CONST.AUTOSTYLE_PROPERTY] = compileConfig[CONST.AUTOSTYLE_PROPERTY];
CU.currentManifest = manifest;
CU.currentDefaultId = viewName;
// create a list of file paths
var searchPaths = noView ? ['CONTROLLER'] : ['VIEW','STYLE','CONTROLLER'];
_.each(searchPaths, function(fileType) {
// get the path values for the file
var fileTypeRoot = path.join(dir, CONST.DIR[fileType]);
var filename = viewName + '.' + CONST.FILE_EXT[fileType];
var filepath = dirname ? path.join(dirname, filename) : filename;
// check for platform-specific versions of the file
var baseFile = path.join(fileTypeRoot,filepath);
if (buildPlatform) {
var platformSpecificFile = path.join(fileTypeRoot,buildPlatform,filepath);
if (path.existsSync(platformSpecificFile)) {
if (fileType === 'STYLE') {
files[fileType] = [
{ file:baseFile },
{ file:platformSpecificFile, platform:true }
];
} else {
files[fileType] = platformSpecificFile;
}
return;
}
}
files[fileType] = baseFile;
});
if (fileRestriction !== null && !matchesRestriction(files, fileRestriction)) {
logger.info(' Not matching the file restriction, skipping');
return;
}
_.each(['COMPONENT','RUNTIME_STYLE'], function(fileType) {
files[fileType] = path.join(compileConfig.dir.resources, 'alloy', CONST.DIR[fileType]);
if (dirname) { files[fileType] = path.join(files[fileType], dirname); }
files[fileType] = path.join(files[fileType], viewName+'.js');
});
// we are processing a view, not just a controller
if (!noView) {
// validate view
if (!path.existsSync(files.VIEW)) {
logger.warn('No ' + CONST.FILE_EXT.VIEW + ' view file found for view ' + files.VIEW);
return;
}
// load global style, if present
state.styles = styler.globalStyle || [];
// Load the style and update the state
if (files.STYLE) {
var theStyles = _.isArray(files.STYLE) ? files.STYLE : [{file:files.STYLE}];
_.each(theStyles, function(style) {
if (fs.existsSync(style.file)) {
logger.info(' style: "' +
path.relative(path.join(dir,CONST.DIR.STYLE),style.file) + '"');
state.styles = styler.loadAndSortStyle(style.file, {
existingStyle: state.styles,
platform: style.platform
});
}
});
}
if (theme) {
// if a theme is applied, override TSS definitions with those defined in the theme
var themeStylesDir, theStyle, themeStylesFile, psThemeStylesFile;
if(!manifest) {
// theming a "normal" controller
themeStylesDir = path.join(compileConfig.dir.themes,theme,'styles');
theStyle = dirname ? path.join(dirname,viewName+'.tss') : viewName+'.tss';
themeStylesFile = path.join(themeStylesDir,theStyle);
psThemeStylesFile = path.join(themeStylesDir,buildPlatform,theStyle);
} else {
// theming a widget
themeStylesDir = path.join(compileConfig.dir.themes,theme,'widgets',manifest.id,'styles');
theStyle = dirname ? path.join(dirname,viewName+'.tss') : viewName+'.tss';
themeStylesFile = path.join(themeStylesDir,theStyle);
psThemeStylesFile = path.join(themeStylesDir,buildPlatform,theStyle);
}
if (path.existsSync(themeStylesFile)) {
// load theme-specific styles, overriding default definitions
logger.info(' theme: "' + path.join(theme.toUpperCase(),theStyle) + '"');
state.styles = styler.loadAndSortStyle(themeStylesFile, {
existingStyle: state.styles,
theme: true
});
}
if (path.existsSync(psThemeStylesFile)) {
// load theme- and platform-specific styles, overriding default definitions
logger.info(' theme: "' +
path.join(theme.toUpperCase(), buildPlatform, theStyle) + '"');
state.styles = styler.loadAndSortStyle(psThemeStylesFile, {
existingStyle: state.styles,
platform: true,
theme: true
});
}
}
// Load view from file into an XML document root node
var docRoot;
try {
logger.info(' view: "' +
path.relative(path.join(dir, CONST.DIR.VIEW), files.VIEW) + '"');
docRoot = U.XML.getAlloyFromFile(files.VIEW);
} catch (e) {
U.die([
e.stack,
'Error parsing XML for view "' + view + '"'
]);
}
// see if autoStyle is enabled for the view
if (docRoot.hasAttribute(CONST.AUTOSTYLE_PROPERTY)) {
CU[CONST.AUTOSTYLE_PROPERTY] =
docRoot.getAttribute(CONST.AUTOSTYLE_PROPERTY) === 'true';
}
// see if module attribute has been set on the docRoot (<Alloy>) tag
if(docRoot.hasAttribute(CONST.DOCROOT_MODULE_PROPERTY)) {
CU[CONST.DOCROOT_MODULE_PROPERTY] = docRoot.getAttribute(CONST.DOCROOT_MODULE_PROPERTY);
}
// make sure we have a Window, TabGroup, or SplitWindow
var rootChildren = U.XML.getElementsFromNodes(docRoot.childNodes);
if (viewName === 'index' && !dirname) {
var valid = [
'Ti.UI.Window',
'Ti.UI.iPad.SplitWindow',
'Ti.UI.TabGroup',
'Ti.UI.iOS.NavigationWindow'
].concat(CONST.MODEL_ELEMENTS);
_.each(rootChildren, function(node) {
var found = true;
var args = CU.getParserArgs(node, {}, { doSetId: false });
if (args.fullname === 'Alloy.Require') {
var inspect = CU.inspectRequireNode(node);
for (var j = 0; j < inspect.names.length; j++) {
if (!_.contains(valid, inspect.names[j])) {
found = false;
break;
}
}
} else {
found = _.contains(valid, args.fullname);
}
if (!found) {
U.die([
'Compile failed. index.xml must have a top-level container element.',
'Valid elements: [' + valid.join(',') + ']'
]);
}
});
}
// process any model/collection nodes
_.each(rootChildren, function(node, i) {
var fullname = CU.getNodeFullname(node);
var isModelElement = _.contains(CONST.MODEL_ELEMENTS,fullname);
if (isModelElement) {
var vCode = CU.generateNode(node, state, undefined, false, true);
template.viewCode += vCode.content;
template.preCode += vCode.pre;
// remove the model/collection nodes when done
docRoot.removeChild(node);
}
});
// rebuild the children list since model elements have been removed
rootChildren = U.XML.getElementsFromNodes(docRoot.childNodes);
// process the UI nodes
var hasUsedDefaultId = false;
_.each(rootChildren, function(node, i) {
// should we use the default id?
var defaultId;
if (!hasUsedDefaultId && CU.isNodeForCurrentPlatform(node)) {
hasUsedDefaultId = true;
defaultId = viewName;
}
// generate the code for this node
var fullname = CU.getNodeFullname(node);
template.viewCode += CU.generateNode(node, {
parent:{},
styles:state.styles,
widgetId: manifest ? manifest.id : undefined,
parentFormFactor: node.hasAttribute('formFactor') ? node.getAttribute('formFactor') : undefined
}, defaultId, true);
});
}
// process the controller code
if (path.existsSync(files.CONTROLLER)) {
logger.info(' controller: "' +
path.relative(path.join(dir, CONST.DIR.CONTROLLER), files.CONTROLLER) + '"');
}
var cCode = CU.loadController(files.CONTROLLER);
template.parentController = (cCode.parentControllerName !== '') ?
cCode.parentControllerName : "'BaseController'";
template.__MAPMARKER_CONTROLLER_CODE__ += cCode.controller;
template.preCode += cCode.pre;
// process the bindingsMap, if it contains any data bindings
var bTemplate = "$.<%= id %>.<%= prop %>=_.isFunction(<%= model %>.transform)?";
bTemplate += "<%= model %>.transform()['<%= attr %>']: _.template('<%= tplVal %>', {<%= mname %>: <%= model %>.toJSON()});";
// for each model variable in the bindings map...
_.each(styler.bindingsMap, function(mapping,modelVar) {
// open the model binding handler
var handlerVar = CU.generateUniqueId();
template.viewCode += 'var ' + handlerVar + '=function() {';
CU.destroyCode += ((state.parentFormFactor) ? 'is' + U.ucfirst(state.parentFormFactor) : '' ) +
modelVar + ".off('" + CONST.MODEL_BINDING_EVENTS + "'," + handlerVar + ");";
// for each specific conditional within the bindings map....
_.each(_.groupBy(mapping, function(b){return b.condition;}), function(bindings,condition) {
var bCode = '';
// for each binding belonging to this model/conditional pair...
_.each(bindings, function(binding) {
bCode += _.template(bTemplate, {
id: binding.id,
prop: binding.prop,
model: modelVar,
attr: binding.attr,
mname: binding.mname,
tplVal: binding.tplVal
});
});
// if this is a legit conditional, wrap the binding code in it
if (typeof condition !== 'undefined' && condition !== 'undefined') {
bCode = 'if(' + condition + '){' + bCode + '}';
}
template.viewCode += bCode;
});
template.viewCode += "};";
template.viewCode += modelVar + ".on('" + CONST.MODEL_BINDING_EVENTS + "'," +
handlerVar + ");";
});
// add destroy() function to view for cleaning up bindings
template.viewCode += 'exports.destroy=function(){' + CU.destroyCode + '};';
// add dataFunction of original name (if data-binding with form factor has been used)
if(!_.isEmpty(CU.dataFunctionNames)) {
_.each(Object.keys(CU.dataFunctionNames), function(funcName) {
template.viewCode += 'function ' + funcName + '() { ';
_.each(CU.dataFunctionNames[funcName], function(formFactor){
template.viewCode += ' if(Alloy.is' + U.ucfirst(formFactor) + ') { ' + funcName + U.ucfirst(formFactor) + '(); } '
});
template.viewCode += '}';
});
}
// add any postCode after the controller code
template.postCode += CU.postCode;
// create generated controller module code for this view/controller or widget
var controllerCode = template.__MAPMARKER_CONTROLLER_CODE__;
delete template.__MAPMARKER_CONTROLLER_CODE__;
var code = _.template(fs.readFileSync(
path.join(compileConfig.dir.template, 'component.js'), 'utf8'), template);
// prep the controller paths based on whether it's an app
// controller or widget controller
var targetFilepath = path.join(compileConfig.dir.resources, titaniumFolder,
path.relative(compileConfig.dir.resources, files.COMPONENT));
var runtimeStylePath = path.join(compileConfig.dir.resources, titaniumFolder,
path.relative(compileConfig.dir.resources, files.RUNTIME_STYLE));
if (manifest) {
wrench.mkdirSyncRecursive(
path.join(compileConfig.dir.resources, titaniumFolder, 'alloy', CONST.DIR.WIDGET,
manifest.id, widgetDir),
0755
);
wrench.mkdirSyncRecursive(
path.join(compileConfig.dir.resources, titaniumFolder, 'alloy', CONST.DIR.WIDGET,
manifest.id, widgetStyleDir),
0755
);
// [ALOY-967] merge "i18n" dir in widget folder
if (fs.existsSync(path.join(dir,CONST.DIR.I18N))) {
CU.mergeI18n(path.join(dir,CONST.DIR.I18N), compileConfig.dir, { override: false });
}
widgetIds.push(manifest.id);
CU.copyWidgetResources(
[path.join(dir,CONST.DIR.ASSETS), path.join(dir,CONST.DIR.LIB)],
path.join(compileConfig.dir.resources, titaniumFolder),
manifest.id,
{
filter: new RegExp('^(?:' + otherPlatforms.join('|') + ')[\\/\\\\]'),
exceptions: otherPlatforms,
titaniumFolder: titaniumFolder,
theme: theme
}
);
targetFilepath = path.join(
compileConfig.dir.resources, titaniumFolder, 'alloy', CONST.DIR.WIDGET, manifest.id,
widgetDir, viewName + '.js'
);
runtimeStylePath = path.join(
compileConfig.dir.resources, titaniumFolder, 'alloy', CONST.DIR.WIDGET, manifest.id,
widgetStyleDir, viewName + '.js'
);
}
// generate the code and source map for the current controller
sourceMapper.generateCodeAndSourceMap({
target: {
filename: path.relative(compileConfig.dir.project,files.COMPONENT),
filepath: targetFilepath,
templateContent: code
},
data: {
__MAPMARKER_CONTROLLER_CODE__: {
filename: path.relative(compileConfig.dir.project,files.CONTROLLER),
fileContent: controllerCode
}
}
}, compileConfig);
// initiate runtime style module creation
var relativeStylePath = path.relative(compileConfig.dir.project, runtimeStylePath);
logger.info(' created: "' + relativeStylePath + '"');
// skip optimize process, as the file is an alloy component
restrictionSkipOptimize = (fileRestriction !== null);
// pre-process runtime controllers to save runtime performance
var STYLE_PLACEHOLDER = '__STYLE_PLACEHOLDER__';
var STYLE_REGEX = new RegExp('[\'"]' + STYLE_PLACEHOLDER + '[\'"]');
var processedStyles = [];
_.each(state.styles, function(s) {
var o = {};
// make sure this style entry applies to the current platform
if (s && s.queries && s.queries.platform &&
!_.contains(s.queries.platform, buildPlatform)) {
return;
}
// get the runtime processed version of the JSON-safe style
var processed = '{' + styler.processStyle(s.style, state) + '}';
// create a temporary style object, sans style key
_.each(s, function(v,k) {
if (k === 'queries') {
var queriesObj = {};
// optimize style conditionals for runtime
_.each(s[k], function(query, queryKey) {
if (queryKey === 'platform') {
// do nothing, we don't need the platform key anymore
} else if (queryKey === 'formFactor') {
queriesObj[queryKey] = 'is' + U.ucfirst(query);
} else if (queryKey === 'if') {
queriesObj[queryKey] = query;
} else {
logger.warn('Unknown device query "' + queryKey + '"');
}
});
// add the queries object, if not empty
if (!_.isEmpty(queriesObj)) {
o[k] = queriesObj;
}
} else if (k !== 'style') {
o[k] = v;
}
});
// Create a full processed style string by inserting the processed style
// into the JSON stringifed temporary style object
o.style = STYLE_PLACEHOLDER;
processedStyles.push(JSON.stringify(o).replace(STYLE_REGEX, processed));
});
// write out the pre-processed styles to runtime module files
var styleCode = 'module.exports = [' + processedStyles.join(',') + '];';
if (manifest) {
styleCode += _.template(
fs.readFileSync(path.join(alloyRoot,'template','wpath.js'), 'utf8'),
{ WIDGETID: manifest.id }
);
}
wrench.mkdirSyncRecursive(path.dirname(runtimeStylePath), 0755);
fs.writeFileSync(runtimeStylePath, styleCode);
}
function findModelMigrations(name, inDir) {
try {
var migrationsDir = inDir || compileConfig.dir.migrations;
var files = fs.readdirSync(migrationsDir);
var part = '_'+name+'.'+CONST.FILE_EXT.MIGRATION;
// look for our model
files = _.reject(files, function(f) { return f.indexOf(part) === -1; });
// sort them in the oldest order first
files = files.sort(function(a,b){
var x = a.substring(0,a.length - part.length -1);
var y = b.substring(0,b.length - part.length -1);
if (x<y) { return -1; }
if (x>y) { return 1; }
return 0;
});
var codes = [];
_.each(files,function(f) {
var mf = path.join(migrationsDir,f);
var m = fs.readFileSync(mf,'utf8');
var code = "(function(migration){\n " +
"migration.name = '" + name + "';\n" +
"migration.id = '" + f.substring(0,f.length-part.length).replace(/_/g,'') + "';\n" +
m +
"})";
codes.push(code);
});
logger.info("Found " + codes.length + " migrations for model: " + name);
return codes;
} catch(E) {
return [];
}
}
function processModels(dirs) {
var models = [];
var modelTemplateFile = path.join(alloyRoot,'template','model.js');
_.each(dirs, function(dirObj) {
var modelDir = path.join(dirObj.dir,CONST.DIR.MODEL);
if (!fs.existsSync(modelDir)) {
return;
}
var migrationDir = path.join(dirObj.dir,CONST.DIR.MIGRATION);
var manifest = dirObj.manifest;
var isWidget = typeof manifest !== 'undefined' && manifest !== null;
var pathPrefix = isWidget ? 'widgets/' + manifest.id + '/': '';
_.each(fs.readdirSync(modelDir), function(file) {
if (!modelRegex.test(file)) {
logger.warn('Non-model file "' + file + '" in ' + pathPrefix + 'models directory');
return;
}
logger.info('[' + pathPrefix + 'models/' + file + '] model processing...');
var fullpath = path.join(modelDir,file);
var basename = path.basename(fullpath, '.'+CONST.FILE_EXT.MODEL);
// generate model code based on model.js template and migrations
var code = _.template(fs.readFileSync(modelTemplateFile,'utf8'), {
basename: basename,
modelJs: fs.readFileSync(fullpath,'utf8'),
migrations: findModelMigrations(basename, migrationDir)
});
// write the model to the runtime file
var casedBasename = U.properCase(basename);
var modelRuntimeDir = path.join(compileConfig.dir.resources,
titaniumFolder, 'alloy', 'models');
if (isWidget) {
modelRuntimeDir = path.join(compileConfig.dir.resources,
titaniumFolder, 'alloy', 'widgets', manifest.id, 'models');
}
wrench.mkdirSyncRecursive(modelRuntimeDir, 0755);
fs.writeFileSync(path.join(modelRuntimeDir,casedBasename+'.js'), code);
models.push(casedBasename);
});
});
return models;
}
function updateFilesWithBuildLog(src, dst, opts) {
// filter on retrictionPath
if (opts.restrictionPath === null || opts.restrictionPath.indexOf(src) === 0) {
var updatedFiles = U.updateFiles(src, dst, _.extend({ isNew: buildLog.isNew }, opts));
if (typeof updatedFiles == 'object' && updatedFiles.length > 0 && opts.restrictionPath !== null) {
fileRestrictionUpdatedFiles = _.union(fileRestrictionUpdatedFiles, updatedFiles);
}
}
}
function optimizeCompiledCode(alloyConfig, paths) {
var mods = [
'builtins',
'optimizer',
'compress'
],
modLocation = './ast/',
lastFiles = [],
files;
// Get the list of JS files from the Resources directory
// and exclude files that don't need to be optimized, or
// have already been optimized.
function getJsFiles() {
if (alloyConfig.file && (fileRestrictionUpdatedFiles.length > 0)) {
logger.info('Restricting optimize on file(s) : ' + fileRestrictionUpdatedFiles.join(', '));
return fileRestrictionUpdatedFiles;
}
var exceptions = [
'app.js',
'alloy/CFG.js',
'alloy/controllers/',
'alloy/styles/',
'alloy/backbone.js',
'alloy/constants.js',
'alloy/underscore.js',
'alloy/widget.js'
];
_.each(exceptions.slice(0), function(ex) {
exceptions.push(path.join(titaniumFolder, ex));
});
var rx = new RegExp('^(?!' + otherPlatforms.join('|') + ').+\\.js$');
return _.filter(wrench.readdirSyncRecursive(compileConfig.dir.resources), function(f) {
return rx.test(f) && !_.find(exceptions, function(e) {
return f.indexOf(e) === 0;
}) && !fs.statSync(path.join(compileConfig.dir.resources, f)).isDirectory();
});
}
while((files = _.difference(getJsFiles(),lastFiles)).length > 0) {
_.each(files, function(file) {
// generate AST from file
var fullpath = path.join(compileConfig.dir.resources,file);
var ast;
logger.info('- ' + file);
try {
ast = uglifyjs.parse(fs.readFileSync(fullpath,'utf8'), {
filename: file
});
} catch (e) {
U.die('Error generating AST for "' + fullpath + '"', e);
}
// process all AST operations
_.each(mods, function(mod) {
logger.trace(' processing "' + mod + '" module...');
ast.figure_out_scope();
ast = require(modLocation+mod).process(ast, compileConfig) || ast;
});
// Write out the optimized file
var stream = uglifyjs.OutputStream(sourceMapper.OPTIONS_OUTPUT);
ast.print(stream);
fs.writeFileSync(fullpath, stream.toString());
});
// Combine lastFiles and files, so on the next iteration we can make sure that the
// list of files to be processed has not grown, like in the case of builtins.
lastFiles = _.union(lastFiles, files);
}
}
function BENCHMARK(desc, isFinished) {
var places = Math.pow(10,5);
desc = desc || '<no description>';
if (times.first === null) {
times.first = process.hrtime();
return;
}
function hrtimeInSeconds(t) {
return t[0] + (t[1] / 1000000000);
}
var total = process.hrtime(times.first);
var current = hrtimeInSeconds(total) - (times.last ? hrtimeInSeconds(times.last) : 0);
times.last = total;
var thisTime = Math.round((isFinished ? hrtimeInSeconds(total) : current)*places)/places;
times.msgs.push('[' + thisTime + 's] ' + desc);
if (isFinished) {
logger.trace(' ');
logger.trace('Benchmarking');
logger.trace('------------');
logger.trace(times.msgs);
logger.info('');
logger.info('Alloy compiled in ' + thisTime + 's');
}
}