text: /*
   * build.js: Titanium Mobile Web CLI build command
   *
   * Copyright (c) 2012-2015, Appcelerator, Inc.  All Rights Reserved.
   * See the LICENSE file for more information.
   */
  
  const
  	appc = require('node-appc'),
  	async = require('async'),
  	Builder = require('titanium-sdk/lib/builder'),
  	CleanCSS = require('clean-css'),
  	ejs = require('ejs'),
  	fields = require('fields'),
  	fs = require('fs'),
  	i18n = appc.i18n(__dirname),
  	jsanalyze = require('titanium-sdk/lib/jsanalyze'),
  	mobilewebPackageJson = appc.pkginfo.package(module),
  	path = require('path'),
  	ti = require('titanium-sdk'),
  	util = require('util'),
  	wrench = require('wrench'),
  	__ = i18n.__,
  	__n = i18n.__n,
  	afs = appc.fs,
  	parallel = appc.async.parallel;
  
  function escapeQuotes(s) {
  	return String(s).replace(/"/g, '\\"');
  }
  
  function MobileWebBuilder() {
  	Builder.apply(this, arguments);
  
  	this.templatesDir = path.join(this.platformPath, 'templates', 'build');
  
  	this.imageMimeTypes = {
  		'.png': 'image/png',
  		'.gif': 'image/gif',
  		'.jpg': 'image/jpg',
  		'.jpeg': 'image/jpg'
  	};
  
  	this.prefetch = [];
  
  	this.targets = ['web'];
  }
  
  util.inherits(MobileWebBuilder, Builder);
  
  MobileWebBuilder.prototype.config = function config(logger, config, cli) {
  	Builder.prototype.config.apply(this, arguments);
  
  	// we need to load all Mobile Web hooks immediately so that the hooks can
  	// modify the config. the cli will do this, but only after config() has
  	// been called.
  	cli.scanHooks(path.resolve(__dirname, '..', 'hooks'));
  
  	// make sure we have Java before we waste time validating the command line arguments
  	cli.on('cli:pre-validate', function (obj, callback) {
  		if (cli.argv.platform && cli.argv.platform != 'mobileweb') {
  			return callback();
  		}
  
  		appc.jdk.detect(config, null, function (jdkInfo) {
  			if (!jdkInfo.executables.java) {
  				logger.error(__('Unable to locate Java'));
  				logger.error(__("If you already have Java installed, make sure it's in the system PATH"));
  				logger.error(__('Java can be downloaded and installed from %s', 'http://appcelerator.com/jdk') + '\n');
  				process.exit(1);
  			}
  			callback();
  		});
  	});
  
  	var conf = {
  		options: {
  			'build-type': {
  				hidden: true
  			},
  			'deploy-type': {
  				abbr: 'D',
  				default: 'development',
  				desc: __('the type of deployment; production performs optimizations'),
  				hint: __('type'),
  				order: 100,
  				values: ['production', 'development']
  			},
  			'target': {
  				abbr: 'T',
  				default: 'web',
  				desc: __('the target to build for'),
  				order: 110,
  				values: this.targets
  			}
  		}
  	};
  
  	var configHook = cli.createHook('build.mobileweb.config', this, function (conf, callback) {
  		callback(null, conf);
  	});
  
  	return function (finished) {
  		configHook(conf, function (err, conf) {
  			finished(conf);
  		});
  	};
  };
  
  MobileWebBuilder.prototype.validate = function validate(logger, config, cli) {
  	Builder.prototype.validate.apply(this, arguments);
  
  	this.target = cli.argv.target;
  	this.deployType = cli.argv['deploy-type'];
  	this.buildType = cli.argv['build-type'] || '';
  
  	this.logger.warn(__('MobileWeb platform has been deprecated in 5.4.0 and will be removed in 7.0.0.'));
  
  	switch (this.deployType) {
  		case 'production':
  			this.minifyJS = true;
  			this.enableLogging = false;
  			break;
  
  		case 'development':
  		default:
  			this.minifyJS = false;
  			this.enableLogging = true;
  	}
  
  	if (!cli.tiapp.icon || !['Resources', 'Resources/mobileweb'].some(function (p) {
  			return fs.existsSync(cli.argv['project-dir'], p, cli.tiapp.icon);
  		})) {
  		cli.tiapp.icon = 'appicon.png';
  	}
  
  	return function (callback) {
  		this.validateTiModules('mobileweb', this.deployType, function (err, modules) {
  			this.modules = modules.found;
  
  			modules.found.forEach(function (module) {
  				// scan the module for any CLI hooks
  				cli.scanHooks(path.join(module.modulePath, 'hooks'));
  			});
  
  			callback();
  		}.bind(this));
  	}.bind(this);
  };
  
  MobileWebBuilder.prototype.run = function run(logger, config, cli, finished) {
  	Builder.prototype.run.apply(this, arguments);
  
  	appc.async.series(this, [
  		function (next) {
  			cli.emit('build.pre.construct', this, next);
  		},
  
  		'doAnalytics',
  		'initialize',
  
  		function (next) {
  			cli.emit('build.pre.compile', this, next);
  		},
  
  		'createBuildDirs',
  
  		function (next) {
  			parallel(this, [
  				'copyFiles',
  				'findProjectDependencies'
  			], function () {
  				parallel(this, [
  					'createIcons',
  					'findModulesToCache',
  					'findPrecacheModules',
  					'findPrecacheImages',
  					'findTiModules',
  					'findI18N'
  				], function () {
  					parallel(this, [
  						'findDistinctCachedModules',
  						'detectCircularDependencies'
  					], function () {
  						logger.info(
  							__n('Found %s dependency', 'Found %s dependencies', this.projectDependencies.length) + ', ' +
  							__n('%s package', '%s packages', this.packages.length) + ', ' +
  							__n('%s module', '%s modules', this.modulesToCache.length)
  						);
  						parallel(this, [
  							'assembleTitaniumJS',
  							'assembleTitaniumCSS'
  						], next);
  					});
  				});
  			});
  		},
  
  		'processJavaScript',
  		'createFilesystemRegistry',
  		'createIndexHtml',
  
  		function (next) {
  			cli.emit('build.post.compile', this, next);
  		},
  
  		function (next) {
  			cli.emit('build.finalize', this, next);
  		},
  	], finished);
  };
  
  MobileWebBuilder.prototype.doAnalytics = function doAnalytics(next) {
  	var cli = this.cli;
  	cli.addAnalyticsEvent('mobileweb.build.' + cli.argv['deploy-type'], {
  		dir: cli.argv['project-dir'],
  		name: cli.tiapp.name,
  		publisher: cli.tiapp.publisher,
  		url: cli.tiapp.url,
  		image: cli.tiapp.icon,
  		appid: cli.tiapp.id,
  		description: cli.tiapp.description,
  		type: cli.argv.type,
  		guid: cli.tiapp.guid,
  		version: cli.tiapp.version,
  		copyright: cli.tiapp.copyright,
  		date: (new Date()).toDateString()
  	});
  	next();
  };
  
  MobileWebBuilder.prototype.initialize = function initialize(next) {
  	this.logger.info(__('Compiling "%s" build', this.deployType));
  
  	this.projectResDir = path.join(this.projectDir, 'Resources');
  	this.mobilewebThemeDir = path.join(this.platformPath, 'themes');
  	this.mobilewebTitaniumDir = path.join(this.platformPath, 'titanium');
  
  	this.moduleSearchPaths = [ this.projectDir, this.globalModulesPath ];
  	if (this.config.paths && Array.isArray(this.config.paths.modules)) {
  		this.moduleSearchPaths = this.moduleSearchPaths.concat(this.config.paths.modules);
  	}
  
  	this.projectDependencies = [];
  	this.modulesToLoad = [];
  	this.tiModulesToLoad = [];
  	this.requireCache = {};
  	this.moduleMap = {};
  	this.modulesToCache = [];
  	this.precacheImages = [];
  	this.locales = [];
  	this.appNames = {};
  	this.splashHtml = '';
  
  	this.buildOnly = this.cli.argv['build-only'];
  
  	this.logger.info(__('Reading Titanium Mobile Web package.json file'));
  	var mwPackageFile = path.join(this.platformPath, 'titanium', 'package.json');
  
  	if (!fs.existsSync(mwPackageFile)) {
  	 	this.logger.error(__('Unable to find Titanium Mobile Web package.json file'));
  		this.logger.error(__("Your SDK installation may be corrupt. You can reinstall it by running '%s'.", (cli.argv.$ + ' sdk update --force --default').cyan) + '\n');
  		process.exit(1);
  	}
  
  	var pkgJson;
  	try {
  		pkgJson = JSON.parse(fs.readFileSync(mwPackageFile));
  	} catch (e) {
  		this.logger.error(__("Unable to parse Titanium Mobile Web's package.json file"));
  		this.logger.error(__("Your SDK installation may be corrupt. You can reinstall it by running '%s'.", (cli.argv.$ + ' sdk update --force --default').cyan) + '\n');
  		process.exit(1);
  	}
  
  	this.packages = [{
  		name: pkgJson.name,
  		location: './titanium',
  		main: pkgJson.main
  	}];
  
  	this.dependenciesMap = JSON.parse(fs.readFileSync(path.join(this.mobilewebTitaniumDir, 'dependencies.json')));
  
  	// read the tiapp.xml and initialize some sensible defaults
  	(function applyDefaults(dest, src) {
  		Object.keys(src).forEach(function (key) {
  			if (dest.hasOwnProperty(key)) {
  				if (Object.prototype.toString.call(dest[key]) == '[object Object]') {
  					applyDefaults(dest[key], src[key]);
  				}
  			} else {
  				if (Object.prototype.toString.call(src[key]) == '[object Object]') {
  					dest[key] = {};
  					applyDefaults(dest[key], src[key]);
  				} else {
  					dest[key] = src[key];
  				}
  			}
  		});
  	}(this.tiapp, {
  		mobileweb: {
  			analytics: {
  				'use-xhr': false
  			},
  			build: {},
  			'disable-error-screen': false,
  			filesystem: {
  				backend: '', // blank defaults to Ti/_/Filesystem/Local
  				registry: 'ondemand'
  			},
  			map: {
  				backend: '', // blank defaults to Ti/_/Map/Google
  				apikey: ''
  			},
  			splash: {
  				enabled: true,
  				'inline-css-images': true
  			},
  			theme: 'default'
  		}
  	}));
  
  	this.logger.info(__('Validating theme'));
  	this.theme = this.tiapp.mobileweb.theme || 'default';
  	if (!fs.existsSync(path.join(this.mobilewebThemeDir, this.theme))) {
  		this.logger.error(__('Unable to find the "%s" theme. Please verify the theme setting in the tiapp.xml.', this.theme) + '\n');
  		process.exit(1);
  	}
  	this.logger.debug(__('Using %s theme', this.theme.cyan));
  
  	// Note: code processor is a pre-compile hook
  	this.codeProcessor = this.cli.codeProcessor;
  
  	next();
  };
  
  MobileWebBuilder.prototype.createBuildDirs = function createBuildDirs(next) {
  	// Make sure we have an app.js. This used to be validated in validate(), but since plugins like
  	// Alloy generate an app.js, it may not have existed during validate(), but should exist now
  	// that build.pre.compile was fired.
  	ti.validateAppJsExists(this.projectDir, this.logger, 'mobileweb');
  
  	if (fs.existsSync(this.buildDir)) {
  		this.logger.debug(__('Deleting existing build directory'));
  		try {
  			wrench.rmdirSyncRecursive(this.buildDir);
  		} catch (e) {
  			this.logger.error(__('Failed to remove build directory: %s', this.buildDir));
  			if (e.message.indexOf('resource busy or locked') != -1) {
  				this.logger.error(__('Build directory is busy or locked'));
  				this.logger.error(__('Check that you don\'t have any terminal sessions or programs with open files in the build directory') + '\n');
  			} else {
  				this.logger.error(e.message + '\n');
  			}
  			process.exit(1);
  		}
  	}
  
  	try {
  		wrench.mkdirSyncRecursive(this.buildDir);
  	} catch (e) {
  		if (e.code == 'EPERM') {
  			// hmm, try again?
  			try {
  				wrench.mkdirSyncRecursive(this.buildDir);
  			} catch (e) {
  				if (e.code == 'EPERM') {
  					// hmm, try again?
  					this.logger.error(__('Unable to create build directory: %s', this.buildDir));
  					this.logger.error(__("It's possible that the build directory is busy or locked"));
  					this.logger.error(__("Check that you don't have any terminal sessions or programs with open files in the build directory") + '\n');
  					process.exit(1);
  				} else {
  					throw e;
  				}
  			}
  		} else {
  			throw e;
  		}
  	}
  
  	next();
  };
  
  MobileWebBuilder.prototype.copyFiles = function copyFiles(next) {
  	var logger = this.logger;
  
  	logger.info(__('Copying project files'));
  
  	var copyOpts = {
  		preserve: true,
  		ignoreDirs: this.ignoreDirs,
  		ignoreFiles: this.ignoreFiles,
  		callback: function (src, dest, contents) {
  			logger.debug(__('Copying %s => %s', src.cyan, dest.cyan));
  			return contents;
  		}
  	};
  
  	afs.copyDirSyncRecursive(this.mobilewebTitaniumDir, path.join(this.buildDir, 'titanium'), copyOpts);
  	afs.copyDirSyncRecursive(this.mobilewebThemeDir, path.join(this.buildDir, 'themes'), copyOpts);
  
  	copyOpts.rootIgnore = ti.availablePlatformsNames;
  	afs.copyDirSyncRecursive(this.projectResDir, this.buildDir, copyOpts);
  
  	var mobilewebDir = path.join(this.projectResDir, 'mobileweb');
  	if (fs.existsSync(mobilewebDir)) {
  		copyOpts.rootIgnore = ['apple_startup_images', 'splash'];
  		afs.copyDirSyncRecursive(mobilewebDir, this.buildDir, copyOpts);
  	}
  
  	this.cli.emit('build.mobileweb.copyFiles', this, next);
  };
  
  MobileWebBuilder.prototype.findProjectDependencies = function findProjectDependencies(next) {
  	var i, len,
  		plugins,
  		usedAPIs,
  		p;
  
  	this.logger.info(__('Finding all Titanium API dependencies'));
  
  	if (this.codeProcessor && this.codeProcessor.plugins) {
  		plugins = this.codeProcessor.plugins;
  		for (i = 0, len = plugins.length; i < len; i++) {
  			if (plugins[i].name === 'ti-api-usage-finder') {
  				usedAPIs = plugins[i].global,
  				this.projectDependencies = ['Ti'];
  				for(p in usedAPIs) {
  					p = p.replace('Titanium', 'Ti').replace(/\./g, '/');
  					if (p in this.dependenciesMap && this.projectDependencies.indexOf(p) != -1) {
  						this.logger.debug(__('Found Titanium API module: ') + p.replace('Ti', 'Titanium').replace(/\//g, '.'));
  						this.projectDependencies.push(p);
  					}
  				}
  				break;
  			}
  		}
  	} else {
  		this.projectDependencies = Object.keys(this.dependenciesMap);
  	}
  
  	next();
  };
  
  MobileWebBuilder.prototype.createIcons = function createIcons(next) {
  	this.cli.emit('build.mobileweb.createIcons', this, next);
  };
  
  MobileWebBuilder.prototype.findModulesToCache = function findModulesToCache(next) {
  	this.logger.info(__('Finding all required modules to be cached'));
  	this.projectDependencies.forEach(function (mid) {
  		this.parseModule(mid);
  	}, this);
  	this.modulesToCache = this.modulesToCache.concat(Object.keys(this.requireCache));
  	next();
  };
  
  MobileWebBuilder.prototype.findPrecacheModules = function findPrecacheModules(next) {
  	this.logger.info(__('Finding all precached modules'));
  	var mwTiapp = this.tiapp.mobileweb;
  	if (mwTiapp.precache) {
  		mwTiapp.precache.require && mwTiapp.precache.require.forEach(function (x) {
  			this.modulesToCache.push('commonjs:' + x);
  		}, this);
  		mwTiapp.precache.includes && mwTiapp.precache.includes.forEach(function (x) {
  			this.modulesToCache.push('url:' + x);
  		}, this);
  	}
  	next();
  };
  
  MobileWebBuilder.prototype.findPrecacheImages = function findPrecacheImages(next) {
  	this.logger.info(__('Finding all precached images'));
  	this.moduleMap['Ti/UI/TableViewRow'] && this.precacheImages.push('/themes/' + this.theme + '/UI/TableViewRow/child.png');
  	var images = (this.tiapp.mobileweb.precache && this.tiapp.mobileweb.precache.images) || [];
  	images && (this.precacheImages = this.precacheImages.concat(images));
  	next();
  };
  
  MobileWebBuilder.prototype.findTiModules = function findTiModules(next) {
  	this.modules.forEach(function (module) {
  		var moduleDir = module.modulePath,
  			pkgJson,
  			pkgJsonFile = path.join(moduleDir, 'package.json');
  
  		if (!fs.existsSync(pkgJsonFile)) {
  			this.logger.error(__('Invalid Titanium Mobile Module "%s": missing package.json', module.id) + '\n');
  			process.exit(1);
  		}
  
  		try {
  			pkgJson = JSON.parse(fs.readFileSync(pkgJsonFile));
  		} catch (e) {
  			this.logger.error(__('Invalid Titanium Mobile Module "%s": unable to parse package.json', module.id) + '\n');
  			process.exit(1);
  		}
  
  		var libDir = ((pkgJson.directories && pkgJson.directories.lib) || '').replace(/^\//, '');
  
  		var mainFilePath = path.join(moduleDir, libDir, (pkgJson.main || '').replace(/\.js$/, '') + '.js');
  		if (!fs.existsSync(mainFilePath)) {
  			this.logger.error(__('Invalid Titanium Mobile Module "%s": unable to find main file "%s"', module.id, pkgJson.main) + '\n');
  			process.exit(1);
  		}
  
  		this.logger.info(__('Bundling Titanium Mobile Module %s', module.id.cyan));
  
  		this.projectDependencies.push(pkgJson.main);
  
  		var moduleName = module.id != pkgJson.main ? module.id + '/' + pkgJson.main : module.id;
  
  		if (/\/commonjs/.test(moduleDir)) {
  			this.modulesToCache.push((/\/commonjs/.test(moduleDir) ? 'commonjs:' : '') + moduleName);
  		} else {
  			this.modulesToCache.push(moduleName);
  			this.tiModulesToLoad.push(module.id);
  		}
  
  		this.packages.push({
  			'name': module.id,
  			'location': './' + this.collapsePath('modules/' + module.id + (libDir ? '/' + libDir : '')),
  			'main': pkgJson.main,
  			'root': 1
  		});
  
  		// TODO: need to combine ALL Ti module .js files into the titanium.js, not just the main file
  
  		// TODO: need to combine ALL Ti module .css files into the titanium.css
  
  		var dest = path.join(this.buildDir, 'modules', module.id);
  		wrench.mkdirSyncRecursive(dest);
  		afs.copyDirSyncRecursive(moduleDir, dest, { preserve: true });
  	}, this);
  
  	next();
  };
  
  MobileWebBuilder.prototype.findI18N = function findI18N(next) {
  	var data = ti.i18n.load(this.projectDir, this.logger),
  		precacheLocales = (this.tiapp.mobileweb.precache || {}).locales || [];
  
  	Object.keys(data).forEach(function (lang) {
  		data[lang].app && data[lang].appname && (self.appNames[lang] = data[lang].appname);
  		if (data[lang].strings) {
  			var dir = path.join(this.buildDir, 'titanium', 'Ti', 'Locale', lang);
  			wrench.mkdirSyncRecursive(dir);
  			fs.writeFileSync(path.join(dir, 'i18n.js'), 'define(' + JSON.stringify(data[lang].strings, null, '\t') + ');');
  			this.locales.push(lang);
  			precacheLocales.indexOf(lang) != -1 && this.modulesToCache.push('Ti/Locale/' + lang + '/i18n');
  		};
  	}, this);
  
  	next();
  };
  
  MobileWebBuilder.prototype.findDistinctCachedModules = function findDistinctCachedModules(next) {
  	this.logger.info(__('Finding all distinct cached modules'));
  	var depMap = {};
  	this.modulesToCache.forEach(function (m) {
  		for (var i in this.moduleMap) {
  			if (this.moduleMap.hasOwnProperty(i) && this.moduleMap[i].indexOf(m) != -1) {
  				depMap[m] = 1;
  			}
  		}
  	}, this);
  	Object.keys(this.moduleMap).forEach(function (m) {
  		depMap[m] || this.modulesToLoad.push(m);
  	}, this);
  	next();
  };
  
  MobileWebBuilder.prototype.detectCircularDependencies = function detectCircularDependencies(next) {
  	this.modulesToCache.forEach(function (m) {
  		var deps = this.moduleMap[m];
  		deps && deps.forEach(function (d) {
  			if (this.moduleMap[d] && this.moduleMap[d].indexOf(m) != -1) {
  				this.logger.warn(__('Circular dependency detected: %s dependent on %s'), m, d);
  			}
  		}, this);
  	}, this);
  	next();
  };
  
  MobileWebBuilder.prototype.assembleTitaniumJS = function assembleTitaniumJS(next) {
  	this.logger.info(__('Assembling titanium.js'));
  
  	var tiapp = this.tiapp;
  
  	async.waterfall([
  		// 1) render the header
  		function (next) {
  			next(null, ejs.render(fs.readFileSync(path.join(this.templatesDir, 'header.ejs')).toString(), { escapeQuotes: escapeQuotes }) + '\n');
  		}.bind(this),
  
  		// 2) read in the config.js and fill in the template
  		function (tiJS, next) {
  			this.cli.createHook('build.mobileweb.assembleConfigTemplate', this, function (template, options, callback) {
  				callback(null, tiJS + ejs.render(template, options) + '\n\n');
  			})(
  				fs.readFileSync(path.join(this.platformPath, 'src', 'config.js')).toString(),
  				{
  					appAnalytics: tiapp.analytics,
  					appCopyright: tiapp.copyright,
  					appDescription: tiapp.description,
  					appGuid: tiapp.guid,
  					appId: tiapp.id,
  					appName: tiapp.name,
  					appNames: JSON.stringify(this.appNames),
  					appPublisher: tiapp.publisher,
  					appUrl: tiapp.url,
  					appVersion: tiapp.version,
  					buildType: this.buildType,
  					deployType: this.deployType,
  					locales: JSON.stringify(this.locales),
  					packages: JSON.stringify(this.packages),
  					projectId: tiapp.id,
  					projectName: tiapp.name,
  					target: this.target,
  					tiAnalyticsPlatformName: 'mobileweb',
  					tiFsRegistry: tiapp.mobileweb.filesystem.registry,
  					tiTheme: this.theme,
  					tiGithash: ti.manifest.githash,
  					tiOsName: 'mobileweb',
  					tiPlatformName: 'mobileweb',
  					tiTimestamp: ti.manifest.timestamp,
  					tiVersion: ti.manifest.version,
  					hasAnalyticsUseXhr: tiapp.mobileweb.analytics ? tiapp.mobileweb.analytics['use-xhr'] === true : false,
  					hasShowErrors: this.deployType != 'production' && tiapp.mobileweb['disable-error-screen'] !== true,
  					hasInstrumentation: !!tiapp.mobileweb.instrumentation,
  					hasAllowTouch: tiapp.mobileweb.hasOwnProperty('allow-touch') ? !!tiapp.mobileweb['allow-touch'] : true,
  					escapeQuotes: escapeQuotes
  				},
  				next
  			);
  		}.bind(this),
  
  		// 3) copy platform specific functionality
  		function (tiJS, next) {
  			this.cli.createHook('build.mobileweb.assemblePlatformImplementation', this, function (contents, callback) {
  				callback(null, contents);
  			})(tiJS, next);
  		}.bind(this),
  
  		// 4) copy in instrumentation if it's enabled
  		function (tiJS, next) {
  			if (tiapp.mobileweb.instrumentation) {
  				next(null, tiJS + fs.readFileSync(path.join(this.platformPath, 'src', 'instrumentation.js')).toString() + '\n');
  			} else {
  				next(null, tiJS);
  			}
  		}.bind(this),
  
  		// 5) copy in the loader
  		function (tiJS, next) {
  			next(null, tiJS + fs.readFileSync(path.join(this.platformPath, 'src', 'loader.js')).toString() + '\n\n');
  		}.bind(this),
  
  		// 6) cache the dependencies
  		function (tiJS, next) {
  			var first = true,
  				requireCacheWritten = false,
  				moduleCounter = 0,
  				tiJSFile = path.join(this.buildDir, 'titanium.js'),
  				tiDir = path.join(this.buildDir, 'titanium') + path.sep;
  
  			// uncomment next line to bypass module caching (which is ill advised):
  			// this.modulesToCache = [];
  
  			this.modulesToCache.forEach(function (moduleName) {
  				var isCommonJS = false;
  				if (/^commonjs\:/.test(moduleName)) {
  					isCommonJS = true;
  					moduleName = moduleName.substring(9);
  				}
  
  				var dep = this.resolveModuleId(moduleName);
  				if (!dep.length) return;
  
  				if (!requireCacheWritten) {
  					tiJS += 'require.cache({\n';
  					requireCacheWritten = true;
  				}
  
  				if (!first) {
  					tiJS += ',\n';
  				}
  				first = false;
  				moduleCounter++;
  
  				var file = path.join(dep[0], /\.js$/.test(dep[1]) ? dep[1] : dep[1] + '.js'),
  					r;
  
  				try {
  					r = jsanalyze.analyzeJsFile(file, { minify: /^url\:/.test(moduleName) && this.minifyJS, skipStats: file == tiJSFile || file.indexOf(tiDir) == 0 });
  				} catch (ex) {
  					ex.message.split('\n').forEach(this.logger.error);
  					this.logger.log();
  					process.exit(1);
  				}
  
  				if (/^url\:/.test(moduleName)) {
  					if (this.minifyJS) {
  						this.logger.debug(__('Minifying include %s', file.cyan));
  						try {
  							fs.writeFileSync(file, r.contents);
  						} catch (ex) {
  							this.logger.error(__('Failed to minify %s', file));
  							if (ex.line) {
  								this.logger.error(__('%s [line %s, column %s]', ex.message, ex.line, ex.col));
  							} else {
  								this.logger.error(__('%s', ex.message));
  							}
  							try {
  								var contents = fs.readFileSync(file).toString().split('\n');
  								if (ex.line && ex.line <= contents.length) {
  									this.logger.error('');
  									this.logger.error('    ' + contents[ex.line-1]);
  									if (ex.col) {
  										var i = 0,
  											len = ex.col;
  											buffer = '    ';
  										for (; i < len; i++) {
  											buffer += '-';
  										}
  										this.logger.error(buffer + '^');
  									}
  									this.logger.log();
  								}
  							} catch (ex2) {}
  							process.exit(1);
  						}
  					}
  					tiJS += '"' + moduleName + '":"' + fs.readFileSync(file).toString().trim().replace(/\\/g, '\\\\').replace(/\n/g, '\\n\\\n').replace(/"/g, '\\"') + '"';
  				} else if (isCommonJS) {
  					tiJS += '"' + moduleName + '":function(){\n/* ' + file.replace(this.buildDir, '') + ' */\ndefine(function(require,exports,module){\n' + fs.readFileSync(file).toString() + '\n});\n}';
  				} else {
  					tiJS += '"' + moduleName + '":function(){\n/* ' + file.replace(this.buildDir, '') + ' */\n\n' + fs.readFileSync(file).toString() + '\n}';
  				}
  			}, this);
  
  			this.precacheImages.forEach(function (url) {
  				url = url.replace(/\\/g, '/');
  
  				var img = path.join(this.projectResDir, /^\//.test(url) ? '.' + url : url),
  					m = img.match(/(\.[a-zA-Z]{3,4})$/),
  					type = m && this.imageMimeTypes[m[1]];
  
  				if (type && fs.existsSync(img)) {
  					if (!requireCacheWritten) {
  						tiJS += 'require.cache({\n';
  						requireCacheWritten = true;
  					}
  
  					if (!first) {
  						tiJS += ',\n';
  					}
  					first = false;
  					moduleCounter++;
  
  					tiJS += '"url:' + url + '":"data:' + type + ';base64,' + fs.readFileSync(img).toString('base64') + '"';
  				}
  			}, this);
  
  			if (requireCacheWritten) {
  				tiJS += '});\n';
  			}
  
  			next(null, tiJS);
  		}.bind(this),
  
  		// 7) write the ti.app.properties
  		function (tiJS, next) {
  			var props = this.tiapp.properties || {};
  			this.tiapp.mobileweb.filesystem.backend && (props['ti.fs.backend'] = { type: 'string', value: this.tiapp.mobileweb.filesystem.backend });
  			this.tiapp.mobileweb.map.backend && (props['ti.map.backend'] = { type: 'string', value: this.tiapp.mobileweb.map.backend });
  			this.tiapp.mobileweb.map.apikey && (props['ti.map.apikey'] = { type: 'string', value: this.tiapp.mobileweb.map.apikey });
  
  			tiJS += 'require("Ti/App/Properties", function(p) {\n';
  			Object.keys(props).forEach(function (name) {
  				var prop = props[name],
  					type = prop.type || 'string';
  				tiJS += '\tp.set' + type.charAt(0).toUpperCase() + type.substring(1).toLowerCase() + '("'
  					+ name.replace(/"/g, '\\"') + '",' + (type == 'string' ? '"' + prop.value.replace(/"/g, '\\"') + '"': prop.value) + ');\n';
  			});
  			tiJS += '});\n';
  
  			next(null, tiJS);
  		}.bind(this),
  
  		// 8) write require() to load all Ti modules
  		function (tiJS, next) {
  			this.modulesToLoad.sort();
  			this.modulesToLoad = this.modulesToLoad.concat(this.tiModulesToLoad);
  			next(null, tiJS + 'require(' + JSON.stringify(this.modulesToLoad) + ');\n');
  		}.bind(this)
  
  	], function (err, tiJS) {
  		fs.writeFile(path.join(this.buildDir, 'titanium.js'), tiJS, next);
  	}.bind(this));
  };
  
  MobileWebBuilder.prototype.assembleTitaniumCSS = function assembleTitaniumCSS(next) {
  	var tiCSS = [
  		ejs.render(fs.readFileSync(path.join(this.templatesDir, 'header.ejs')).toString(), { escapeQuotes: escapeQuotes }), '\n'
  	];
  
  	if (this.tiapp.mobileweb.splash.enabled) {
  		var splashDir = path.join(this.projectResDir, 'mobileweb', 'splash'),
  			splashHtmlFile = path.join(splashDir, 'splash.html'),
  			splashCssFile = path.join(splashDir, 'splash.css');
  
  		if (fs.existsSync(splashDir)) {
  			this.logger.info(__('Processing splash screen'));
  			fs.existsSync(splashHtmlFile) && (this.splashHtml = fs.readFileSync(splashHtmlFile));
  			if (fs.existsSync(splashCssFile)) {
  				var css = fs.readFileSync(splashCssFile).toString();
  				if (this.tiapp.mobileweb.splash['inline-css-images']) {
  					var parts = css.split('url('),
  						i = 1, p, img, imgPath, imgType,
  						len = parts.length;
  					for (; i < len; i++) {
  						p = parts[i].indexOf(')');
  						if (p != -1) {
  							img = parts[i].substring(0, p).replace(/["']/g, '').trim();
  							if (!/^data\:/.test(img)) {
  								imgPath = img.charAt(0) == '/' ? this.projectResDir + img : splashDir + '/' + img;
  								imgType = this.imageMimeTypes[imgPath.match(/(\.[a-zA-Z]{3})$/)[1]];
  								if (fs.existsSync(imgPath) && imgType) {
  									parts[i] = 'data:' + imgType + ';base64,' + fs.readFileSync(imgPath).toString('base64') + parts[i].substring(p);
  								}
  							}
  						}
  					}
  					css = parts.join('url(');
  				}
  				tiCSS.push(css);
  			}
  		}
  	}
  
  	this.logger.info(__('Assembling titanium.css'));
  
  	var commonCss = this.mobilewebThemeDir + '/common.css';
  	fs.existsSync(commonCss) && tiCSS.push(fs.readFileSync(commonCss).toString());
  
  	// TODO: need to rewrite absolute paths for urls
  
  	// TODO: code below does NOT inline imports, nor remove them... do NOT use imports until themes are fleshed out
  
  	var themePath = path.join(this.projectResDir, 'themes', this.theme);
  	fs.existsSync(themePath) || (themePath = path.join(this.projectResDir, this.theme));
  	fs.existsSync(themePath) || (themePath = path.join(this.platformPath, 'themes', this.theme));
  	if (!fs.existsSync(themePath)) {
  		this.logger.error(__('Unable to locate theme "%s"', this.theme) + '\n');
  		process.exit(1);
  	}
  
  	wrench.readdirSyncRecursive(themePath).forEach(function (file) {
  		/\.css$/.test(file) && tiCSS.push(fs.readFileSync(path.join(themePath, file)).toString() + '\n');
  	});
  
  	// detect any fonts and add font face rules to the css file
  	var fonts = {},
  		fontFormats = {
  			'ttf': 'truetype'
  		},
  		prefix = this.projectResDir.replace(/\\/g, '/') + '/';
  
  	(function walk(dir, isMobileWebDir, isRoot) {
  		fs.existsSync(dir) && fs.readdirSync(dir).forEach(function (name) {
  			var file = path.join(dir, name);
  			if (fs.statSync(file).isDirectory()) {
  				if (!isRoot || name == 'mobileweb' || ti.availablePlatformsNames.indexOf(name) == -1) {
  					walk(file, isMobileWebDir || name == 'mobileweb');
  				}
  				return;
  			}
  
  			var m = name.match(/^(.+)\.(otf|woff|ttf|svg)$/);
  			if (m) {
  				var p = file.replace(/\\/g, '/').replace(prefix, '');
  				fonts[m[1]] || (fonts[m[1]] = {});
  				fonts[m[1]][m[2]] = {
  					path: isMobileWebDir ? p.replace('mobileweb/', '') : p,
  					format: fontFormats[m[2]] || m[2]
  				};
  			}
  		});
  	}(this.projectResDir, false, true));
  
  	Object.keys(fonts).forEach(function (name) {
  		var font = fonts[name],
  			src = [];
  
  		this.logger.debug(__('Found font: %s', name.cyan));
  
  		['woff', 'otf', 'ttf', 'svg'].forEach(function (type) {
  			if (font[type]) {
  				// this.prefetch.push(font[type].path);
  				src.push('url("' + font[type].path + '") format("' + font[type].format + '")');
  			}
  		}, this);
  
  		tiCSS.push('@font-face{font-family:"' + name + '";src:' + src.join(',') + ';}\n');
  	}, this);
  
  	// write the titanium.css
  	fs.writeFileSync(path.join(this.buildDir, 'titanium.css'), this.deployType == 'production' ? new CleanCSS({ processImport: false }).minify(tiCSS.join('')).styles : tiCSS.join(''));
  
  	next();
  };
  
  MobileWebBuilder.prototype.processJavaScript = function processJavaScript(next) {
  	var self = this,
  		tiJSFile = path.join(this.buildDir, 'titanium.js');
  
  	this.logger.info(this.minifyJS ? __('Minifying JavaScript') : __('Analyzing JavaScript'));
  
  	(function walk(dir, isTitaniumFolder) {
  		fs.readdirSync(dir).sort().forEach(function (filename) {
  			var file = path.join(dir, filename),
  				stat = fs.statSync(file);
  			if (stat.isDirectory()) {
  				walk(file, isTitaniumFolder || filename == 'titanium');
  			} else if (/\.js$/.test(filename)) {
  				self.logger.debug(self.minifyJS ? __('Minifying %s', file.cyan) : __('Analyzing %s', file.cyan));
  				try {
  					var r = jsanalyze.analyzeJsFile(file, { minify: self.minifyJS, skipStats: isTitaniumFolder || file == tiJSFile });
  					self.minifyJS && fs.writeFileSync(file, r.contents);
  				} catch (ex) {
  					ex.message.split('\n').forEach(self.logger.error);
  					self.logger.log();
  					process.exit(1);
  				}
  			}
  		});
  	}(this.buildDir));
  
  	next();
  };
  
  MobileWebBuilder.prototype.createFilesystemRegistry = function createFilesystemRegistry(next) {
  	this.logger.info(__('Creating the filesystem registry'));
  
  	var registry = 'ts\t' + fs.statSync(this.buildDir).ctime.getTime() + '\n' +
  		(function walk(dir, depth) {
  			var s = '';
  			depth = depth | 0;
  			fs.readdirSync(dir).sort().forEach(function (file) {
  				// TODO: screen out specific file/folder patterns (i.e. uncompressed js files)
  				var stat = fs.statSync(path.join(dir, file));
  				if (stat.isDirectory()) {
  					s += (depth ? (new Array(depth + 1)).join('\t') : '') + file + '\n' + walk(path.join(dir, file), depth + 1);
  				} else {
  					s += (depth ? (new Array(depth + 1)).join('\t') : '') + file + '\t' + stat.size + '\n';
  				}
  			});
  			return s;
  		}(this.buildDir)).trim();
  
  	fs.writeFileSync(path.join(this.buildDir, 'titanium', 'filesystem.registry'), registry);
  
  	if (this.tiapp.mobileweb.filesystem.registry == 'preload') {
  		fs.appendFileSync(path.join(this.buildDir, 'titanium.js'), 'require.cache({"url:/titanium/filesystem.registry":"' + registry.replace(/\n/g, '|') + '"});');
  	}
  
  	next();
  };
  
  MobileWebBuilder.prototype.createIndexHtml = function createIndexHtml(next) {
  	this.logger.info(__('Creating the index.html'));
  
  	// get status bar style
  	var statusBarStyle = this.tiapp['statusbar-style'];
  	if (statusBarStyle && /^(?:black\-translucent|translucent_black|transparent|translucent)$/.test(statusBarStyle)) {
  		statusBarStyle = 'black-translucent';
  	} else {
  		statusBarStyle = 'black';
  	}
  
  	// write the index.html
  	this.cli.createHook('build.mobileweb.createIndexHtml', this, function (template, options, callback) {
  		fs.writeFile(path.join(this.buildDir, 'index.html'), ejs.render(template, options), callback);
  	})(
  		fs.readFileSync(path.join(this.platformPath, 'src', 'index.html')).toString(),
  		{
  			target: this.target,
  			tiHeader: ejs.render(fs.readFileSync(path.join(this.templatesDir, 'header.html.ejs')).toString()),
  			projectName: this.tiapp.name || '',
  			appDescription: this.tiapp.description || '',
  			appPublisher: this.tiapp.publisher || '',
  			tiGenerator: 'Appcelerator Titanium Mobile ' + ti.manifest.version,
  			tiStatusbarStyle: statusBarStyle,
  			tiCss: fs.readFileSync(path.join(this.buildDir, 'titanium.css')).toString(),
  			splashScreen: this.splashHtml,
  			tiJs: fs.readFileSync(path.join(this.buildDir, 'titanium.js')).toString(),
  			prefetch: this.prefetch,
  			escapeQuotes: escapeQuotes
  		},
  		next
  	);
  };
  
  MobileWebBuilder.prototype.collapsePath = function collapsePath(p) {
  	var result = [], segment, lastSegment;
  	p = p.replace(/\\/g, '/').split('/');
  	while (p.length) {
  		segment = p.shift();
  		if (segment == '..' && result.length && lastSegment != '..') {
  			result.pop();
  			lastSegment = result[result.length - 1];
  		} else if (segment != '.') {
  			result.push(lastSegment = segment);
  		}
  	}
  	return result.join('/');
  };
  
  MobileWebBuilder.prototype.resolveModuleId = function resolveModuleId(mid, ref) {
  	var parts = mid.split('!');
  	mid = parts[parts.length-1];
  
  	if (/^url\:/.test(mid)) {
  		mid = mid.substring(4);
  		if (/^\//.test(mid)) {
  			mid = '.' + mid;
  		}
  		parts = mid.split('/');
  		for (var i = 0, l = this.packages.length; i < l; i++) {
  			if (this.packages[i].name == parts[0]) {
  				return [this.collapsePath(this.buildDir + '/' + this.packages[i].location), mid];
  			}
  		}
  		return [this.buildDir, mid];
  	}
  
  	if (mid.indexOf(':') != -1) return [];
  	if (/^\//.test(mid) || (parts.length == 1 && /\.js$/.test(mid))) return [this.buildDir, mid];
  	/^\./.test(mid) && ref && (mid = this.collapsePath(ref + mid));
  	parts = mid.split('/');
  
  	for (var i = 0, l = this.packages.length; i < l; i++) {
  		if (this.packages[i].name == parts[0]) {
  			this.packages[i].name != 'Ti' && (mid = mid.replace(this.packages[i].name + '/', ''));
  			return [ this.collapsePath(path.join(this.buildDir, this.packages[i].location)), mid ];
  		}
  	}
  
  	return [ this.buildDir, mid ];
  };
  
  MobileWebBuilder.prototype.parseModule = function parseModule(mid, ref) {
  	if (this.requireCache[mid] || mid == 'require') {
  		return;
  	}
  
  	var parts = mid.split('!');
  
  	if (parts.length == 1) {
  		if (mid.charAt(0) == '.' && ref) {
  			mid = this.collapsePath(ref + mid);
  		}
  		this.requireCache[mid] = 1;
  	}
  
  	var dep = this.resolveModuleId(mid, ref);
  	if (!dep.length) {
  		return;
  	}
  
  	parts.length > 1 && (this.requireCache['url:' + parts[1]] = 1);
  
  	var deps = this.dependenciesMap[parts.length > 1 ? mid : dep[1]];
  	for (var i = 0, l = deps.length; i < l; i++) {
  		dep = deps[i];
  		ref = mid.split('/');
  		ref.pop();
  		ref = ref.join('/') + '/';
  		this.parseModule(dep, ref);
  	}
  	this.moduleMap[mid] = deps;
  };
  
  // create the builder instance and expose the public api
  (function (mobileWebBuilder) {
  	exports.config   = mobileWebBuilder.config.bind(mobileWebBuilder);
  	exports.validate = mobileWebBuilder.validate.bind(mobileWebBuilder);
  	exports.run      = mobileWebBuilder.run.bind(mobileWebBuilder);
  }(new MobileWebBuilder(module)));
  