alloy
Version:
TiDev Titanium MVC Framework
1,204 lines (1,056 loc) • 42.4 kB
JavaScript
var ejs = require('ejs'),
path = require('path'),
fs = require('fs-extra'),
walkSync = require('walk-sync'),
vm = require('vm'),
babel = require('@babel/core'),
async = require('async'),
// alloy requires
_ = require('lodash'),
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,
restrictionPath,
// NOTE: the following line creates the empty Resources/app.js file
paths = U.getAndValidateProjectPaths(
program.outputPath || args[0] || process.cwd()
);
// 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('=');
if (alloyConfig[parts[0]]) {
alloyConfig[parts[0]] = [].concat(alloyConfig[parts[0]], parts[1]);
} else {
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);
// check the platform and i18n to see if it was generated by us last time
var destI18NDir = path.join(paths.project, 'i18n');
var destPlatformDir = path.join(paths.project, 'platform', buildPlatform === 'iphone' ? 'ios' : buildPlatform);
if (fs.existsSync(destI18NDir) && !fs.existsSync(path.join(destI18NDir, 'alloy_generated'))) {
U.die([
'Detected legacy "/i18n" directory in project directory.',
'Please move the "/i18n" directory to "/app/i18n" for Alloy 1.8.0 or later.'
]);
}
if (fs.existsSync(destPlatformDir) && !fs.existsSync(path.join(destPlatformDir, 'alloy_generated'))) {
U.die([
'Detected legacy "/platform" directory in project directory.',
'Please move the "/platform" directory to "/app/platform" for Alloy 1.8.0 or later.'
]);
}
// check that the .gitignore is looking good
var gitignoreFile = path.join(paths.project, '.gitignore');
if (fs.existsSync(gitignoreFile)) {
var folders = {
'/i18n': false,
'/platform': false,
'/Resources': false
};
fs.readFileSync(gitignoreFile).toString().split('\n').forEach(function (line) {
line = line.trim();
if (/^\/?i18n(\/.*)?$/.test(line)) {
folders['/i18n'] = true;
} else if (/^\/?platform(\/.*)?$/.test(line)) {
folders['/platform'] = true;
} else if (/^\/?Resources(\/.*)?$/.test(line)) {
folders['/Resources'] = true;
}
});
var warned = false;
Object.keys(folders).some(function (dir) {
if (!folders[dir]) {
logger.warn('Generated "' + dir + '" directory is not ignored by Git, please add it to your .gitignore');
warned = true;
}
});
warned && logger.debug();
}
// allow to filter the file to compile
if (!alloyConfig.file) {
restrictionPath = null;
} else {
restrictionPath = _.map([].concat(alloyConfig.file), function (file) {
return path.join(paths.project, file);
});
}
// create compile config from paths and various alloy config files
logger.debug('----- CONFIG.JSON -----');
// NOTE: the following line creates the Resources/alloy/CFG.js and Resources/<platform-name>/alloy/CFG.js
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 (fs.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',
(_.includes(CONST.SUPPORTED_BACKBONE_VERSIONS, compileConfig.backbone))
? compileConfig.backbone
: CONST.DEFAULT_BACKBONE_VERSION,
'backbone.js'
),
path.join(paths.resources, titaniumFolder, 'alloy', 'backbone.js')
);
if (restrictionPath === null) {
// Generate alloy.js from template
var libAlloyJsDest = path.join(paths.resources, titaniumFolder, 'alloy.js');
var pkginfo = require('pkginfo')(module, 'version');
logger.trace('Generating ' + path.relative(titaniumFolder, libAlloyJsDest).yellow);
fs.writeFileSync(
libAlloyJsDest,
ejs.render(
fs.readFileSync(path.join(alloyRoot, 'template', 'lib', 'alloy.js'), 'utf8'),
{ version: module.exports.version }
)
);
}
// NOTE: copies `common/constants.js` from Alloy into `<project-dir>/Resources/<platform>/alloy`
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]);
fs.mkdirpSync(p);
});
// 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 }
);
}
var defaultIcons = ['DefaultIcon.png', 'DefaultIcon-' + buildPlatform + '.png'];
// check theme for assets
if (theme) {
_.each(['ASSETS', 'LIB', 'VENDOR'], function(type) {
var themeAssetsPath = path.join(paths.app, 'themes', theme, CONST.DIR[type]);
if (fs.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
}
);
}
});
// This is not ideal, but until the build system allows icons to be picked up in another location,
// we need to do this for now.
defaultIcons.forEach(function (file) {
var themeIconFile = path.join(paths.app, 'themes', theme, file);
var projectIconFile = path.join(paths.project, file);
var appcDirIconFile = path.join(paths.app, file);
if (fs.existsSync(themeIconFile)) {
if (fs.existsSync(projectIconFile) && !fs.existsSync(appcDirIconFile)) {
// 1st time theming defaulticon, copy icon file from root to app dir
logger.debug('Use themed DefaultIcon, make a copy of the DefaultIcon file in app folder.');
logger.debug('Moving ' + projectIconFile.yellow + ' --> ' + appcDirIconFile.yellow);
U.copyFileSync(projectIconFile, appcDirIconFile);
}
logger.debug('Use themed DefaultIcon.');
logger.debug('Copying ' + themeIconFile.yellow + ' --> ' + projectIconFile.yellow);
fs.copySync(themeIconFile, projectIconFile, { preserveTimestamps: true });
}
});
} else {
defaultIcons.forEach(function (file) {
var src = path.join(paths.app, file);
var dest = path.join(paths.project, file);
if (fs.existsSync(src)) {
logger.debug('Copying ' + src.yellow + ' --> ' + dest.yellow);
fs.copySync(src, dest, { preserveTimestamps: true });
}
});
}
function generateMessage(dir) {
return 'This directory is generated from the "/app/' + dir + '" and "/app/theme/<name>/' + dir + '" directories.\n\n' +
'Do not modify any files in this directory. Your changes will be lost on next build.\n\n' +
'Please make sure "/' + dir + '" is added to your version control\'s ignore list (i.e. .gitignore).';
}
// copy the platform and theme platform directories
var sourcePlatformDirs;
if (buildPlatform === 'ios' || buildPlatform === 'iphone') {
sourcePlatformDirs = [ 'platform/iphone', 'platform/ios' ];
var iPhonePlatformDir = path.join(paths.project, 'platform', 'iphone');
if (fs.existsSync(iPhonePlatformDir)) {
logger.trace('Deleting ' + iPhonePlatformDir.yellow);
fs.removeSync(iPhonePlatformDir);
}
} else {
sourcePlatformDirs = [ 'platform/' + buildPlatform ];
}
if (fs.existsSync(destPlatformDir)) {
logger.debug('Resetting ' + destPlatformDir.yellow);
fs.removeSync(destPlatformDir);
}
fs.mkdirpSync(destPlatformDir);
fs.writeFileSync(path.join(destPlatformDir, 'alloy_generated'), generateMessage('platform'));
sourcePlatformDirs.forEach(function (dir) {
var dirs = [ dir ];
theme && dirs.push('themes/' + theme + '/' + dir);
dirs.forEach(function (dir) {
dir = path.join(paths.app, dir);
if (fs.existsSync(dir)) {
logger.debug('Copying ' + dir.yellow + ' --> ' + destPlatformDir.yellow);
fs.copySync(dir, destPlatformDir, { preserveTimestamps: true });
}
});
});
logger.debug('');
// copy the i18n and i18n platform directories
var sourceI18NPaths = [ path.join(paths.app, 'i18n') ];
if (theme) {
sourceI18NPaths.push(path.join(paths.app, 'themes', theme, 'i18n'));
}
if (fs.existsSync(destI18NDir)) {
logger.debug('Resetting ' + destI18NDir.yellow);
fs.removeSync(destI18NDir);
}
fs.mkdirpSync(destI18NDir);
fs.writeFileSync(path.join(destI18NDir, 'alloy_generated'), generateMessage('i18n'));
sourceI18NPaths.forEach(function (dir) {
if (fs.existsSync(dir)) {
CU.mergeI18N(dir, destI18NDir, { override: true });
}
});
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);
widgetDirs.push({ dir: path.join(paths.project, CONST.ALLOY_DIR) });
// Process all models
var models = processModels(widgetDirs);
_.each(models, function(m) {
CU.models.push(m);
});
// 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(widgetDirs, function(collection) {
// generate runtime controllers from views
var theViewDir = path.join(collection.dir, CONST.DIR.VIEW);
if (fs.existsSync(theViewDir)) {
_.each(walkSync(theViewDir), function(view) {
view = path.normalize(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(walkSync(theControllerDir), function(controller) {
controller = path.normalize(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('');
generateAppJs(paths, compileConfig, restrictionPath, compilerMakeFile);
U.copyFileSync(path.join(alloyRoot, 'template', 'alloy.bootstrap.js'), path.join(paths.resources, titaniumFolder, 'alloy.bootstrap.js'));
// 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, compilerMakeFile) {
var alloyJs = path.join(paths.app, 'alloy.js');
if (restrictionPath !== null && !_.includes(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: path.join('Resources', titaniumFolder, '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 compile?
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...');
// trigger our custom compiler makefile
if (compilerMakeFile.isActive) {
compilerMakeFile.trigger('compile:app.js', _.clone(compileConfig));
}
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 |= _.includes(fileRestriction, file);
} 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__: '',
ES6Mod: ''
},
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 (fs.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 (!fs.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 (fs.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 (fs.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 for the view
if (docRoot.hasAttribute(CONST.DOCROOT_MODULE_PROPERTY)) {
CU[CONST.DOCROOT_MODULE_PROPERTY] = docRoot.getAttribute(CONST.DOCROOT_MODULE_PROPERTY);
} else {
CU[CONST.DOCROOT_MODULE_PROPERTY] = null;
}
// see if baseController attribute has been set on the docRoot (<Alloy>) tag for the view
if (docRoot.hasAttribute(CONST.DOCROOT_BASECONTROLLER_PROPERTY)) {
CU[CONST.DOCROOT_BASECONTROLLER_PROPERTY] = '"' + docRoot.getAttribute(CONST.DOCROOT_BASECONTROLLER_PROPERTY) + '"';
} else {
CU[CONST.DOCROOT_BASECONTROLLER_PROPERTY] = null;
}
// 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.iOS.SplitWindow',
'Ti.UI.TabGroup',
'Ti.UI.iOS.NavigationWindow',
'Ti.UI.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 (!_.includes(valid, inspect.names[j])) {
found = false;
break;
}
}
} else {
found = _.includes(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 = _.includes(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
_.each(rootChildren, function(node, i) {
// should we use the default id?
var defaultId = CU.isNodeForCurrentPlatform(node) ? viewName : undefined;
// 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 (fs.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 : CU[CONST.DOCROOT_BASECONTROLLER_PROPERTY] || "'BaseController'";
template.__MAPMARKER_CONTROLLER_CODE__ += cCode.controller;
template.preCode += cCode.pre;
template.ES6Mod += cCode.es6mods;
// 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() {';
_.each(mapping.models, function(modelVar) {
template.viewCode += modelVar + '.__transform = _.isFunction(' + modelVar + '.transform) ? ' + modelVar + '.transform() : ' + modelVar + '.toJSON();';
});
CU.destroyCode += modelVar + ' && ' + ((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.bindings, function(b) {return b.condition;}), function(bindings, condition) {
var bCode = '';
// for each binding belonging to this model/conditional pair...
_.each(bindings, function(binding) {
bCode += '$.' + binding.id + '.' + binding.prop + ' = ' + binding.val + ';';
});
// 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) {
fs.mkdirpSync(
path.join(compileConfig.dir.resources, titaniumFolder, 'alloy', CONST.DIR.WIDGET,
manifest.id, widgetDir)
);
fs.mkdirpSync(
path.join(compileConfig.dir.resources, titaniumFolder, 'alloy', CONST.DIR.WIDGET,
manifest.id, widgetStyleDir)
);
// [ALOY-967] merge "i18n" dir in widget folder
CU.mergeI18N(path.join(dir, 'i18n'), path.join(compileConfig.dir.project, 'i18n'), { 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 &&
!_.includes(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 });
}
fs.mkdirpSync(path.dirname(runtimeStylePath));
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');
}
fs.mkdirpSync(modelRuntimeDir);
fs.writeFileSync(path.join(modelRuntimeDir, casedBasename + '.js'), code);
models.push(basename);
});
});
return models;
}
function updateFilesWithBuildLog(src, dst, opts) {
// filter on retrictionPath
if (opts.restrictionPath === null || _.find(opts.restrictionPath, function(f) {return f.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 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'
].concat(compileConfig.optimizingExceptions || []);
// widget controllers are already optimized. It should be listed in exceptions.
_.each(compileConfig.dependencies, function (version, widgetName) {
exceptions.push('alloy/widgets/' + widgetName + '/controllers/');
});
_.each(exceptions.slice(0), function(ex) {
exceptions.push(`${titaniumFolder}/${ex}`);
});
var excludePatterns = otherPlatforms.concat(['.+node_modules']);
var rx = new RegExp('^(?!' + excludePatterns.join('|') + ').+\\.js$');
return _.filter(walkSync(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) {
var options = _.extend(_.clone(sourceMapper.OPTIONS_OUTPUT), {
plugins: [
[require('./ast/builtins-plugin'), compileConfig],
[require('./ast/optimizer-plugin'), compileConfig.alloyConfig],
]
}),
fullpath = path.join(compileConfig.dir.resources, file);
logger.info('- ' + file);
try {
var result = babel.transformFileSync(fullpath, options);
fs.writeFileSync(fullpath, result.code);
} catch (e) {
U.die('Error transforming JS file', e);
}
});
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');
}
}