PageRenderTime 50ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/mobileweb/cli/commands/_build.js

http://github.com/appcelerator/titanium_mobile
JavaScript | 1089 lines | 1064 code | 17 blank | 8 comment | 22 complexity | 6da70e8c6b3fa4c42dccd7a666952bec MD5 | raw file
Possible License(s): MIT, JSON, Apache-2.0, 0BSD, CC-BY-SA-3.0, BSD-2-Clause, MPL-2.0-no-copyleft-exception, BSD-3-Clause, CC-BY-3.0, Unlicense
  1. /*
  2. * build.js: Titanium Mobile Web CLI build command
  3. *
  4. * Copyright (c) 2012-2015, Appcelerator, Inc. All Rights Reserved.
  5. * See the LICENSE file for more information.
  6. */
  7. const
  8. appc = require('node-appc'),
  9. async = require('async'),
  10. Builder = require('titanium-sdk/lib/builder'),
  11. CleanCSS = require('clean-css'),
  12. ejs = require('ejs'),
  13. fields = require('fields'),
  14. fs = require('fs'),
  15. i18n = appc.i18n(__dirname),
  16. jsanalyze = require('titanium-sdk/lib/jsanalyze'),
  17. mobilewebPackageJson = appc.pkginfo.package(module),
  18. path = require('path'),
  19. ti = require('titanium-sdk'),
  20. util = require('util'),
  21. wrench = require('wrench'),
  22. __ = i18n.__,
  23. __n = i18n.__n,
  24. afs = appc.fs,
  25. parallel = appc.async.parallel;
  26. function escapeQuotes(s) {
  27. return String(s).replace(/"/g, '\\"');
  28. }
  29. function MobileWebBuilder() {
  30. Builder.apply(this, arguments);
  31. this.templatesDir = path.join(this.platformPath, 'templates', 'build');
  32. this.imageMimeTypes = {
  33. '.png': 'image/png',
  34. '.gif': 'image/gif',
  35. '.jpg': 'image/jpg',
  36. '.jpeg': 'image/jpg'
  37. };
  38. this.prefetch = [];
  39. this.targets = ['web'];
  40. }
  41. util.inherits(MobileWebBuilder, Builder);
  42. MobileWebBuilder.prototype.config = function config(logger, config, cli) {
  43. Builder.prototype.config.apply(this, arguments);
  44. // we need to load all Mobile Web hooks immediately so that the hooks can
  45. // modify the config. the cli will do this, but only after config() has
  46. // been called.
  47. cli.scanHooks(path.resolve(__dirname, '..', 'hooks'));
  48. // make sure we have Java before we waste time validating the command line arguments
  49. cli.on('cli:pre-validate', function (obj, callback) {
  50. if (cli.argv.platform && cli.argv.platform != 'mobileweb') {
  51. return callback();
  52. }
  53. appc.jdk.detect(config, null, function (jdkInfo) {
  54. if (!jdkInfo.executables.java) {
  55. logger.error(__('Unable to locate Java'));
  56. logger.error(__("If you already have Java installed, make sure it's in the system PATH"));
  57. logger.error(__('Java can be downloaded and installed from %s', 'http://appcelerator.com/jdk') + '\n');
  58. process.exit(1);
  59. }
  60. callback();
  61. });
  62. });
  63. var conf = {
  64. options: {
  65. 'build-type': {
  66. hidden: true
  67. },
  68. 'deploy-type': {
  69. abbr: 'D',
  70. default: 'development',
  71. desc: __('the type of deployment; production performs optimizations'),
  72. hint: __('type'),
  73. order: 100,
  74. values: ['production', 'development']
  75. },
  76. 'target': {
  77. abbr: 'T',
  78. default: 'web',
  79. desc: __('the target to build for'),
  80. order: 110,
  81. values: this.targets
  82. }
  83. }
  84. };
  85. var configHook = cli.createHook('build.mobileweb.config', this, function (conf, callback) {
  86. callback(null, conf);
  87. });
  88. return function (finished) {
  89. configHook(conf, function (err, conf) {
  90. finished(conf);
  91. });
  92. };
  93. };
  94. MobileWebBuilder.prototype.validate = function validate(logger, config, cli) {
  95. Builder.prototype.validate.apply(this, arguments);
  96. this.target = cli.argv.target;
  97. this.deployType = cli.argv['deploy-type'];
  98. this.buildType = cli.argv['build-type'] || '';
  99. this.logger.warn(__('MobileWeb platform has been deprecated in 5.4.0 and will be removed in 7.0.0.'));
  100. switch (this.deployType) {
  101. case 'production':
  102. this.minifyJS = true;
  103. this.enableLogging = false;
  104. break;
  105. case 'development':
  106. default:
  107. this.minifyJS = false;
  108. this.enableLogging = true;
  109. }
  110. if (!cli.tiapp.icon || !['Resources', 'Resources/mobileweb'].some(function (p) {
  111. return fs.existsSync(cli.argv['project-dir'], p, cli.tiapp.icon);
  112. })) {
  113. cli.tiapp.icon = 'appicon.png';
  114. }
  115. return function (callback) {
  116. this.validateTiModules('mobileweb', this.deployType, function (err, modules) {
  117. this.modules = modules.found;
  118. modules.found.forEach(function (module) {
  119. // scan the module for any CLI hooks
  120. cli.scanHooks(path.join(module.modulePath, 'hooks'));
  121. });
  122. callback();
  123. }.bind(this));
  124. }.bind(this);
  125. };
  126. MobileWebBuilder.prototype.run = function run(logger, config, cli, finished) {
  127. Builder.prototype.run.apply(this, arguments);
  128. appc.async.series(this, [
  129. function (next) {
  130. cli.emit('build.pre.construct', this, next);
  131. },
  132. 'doAnalytics',
  133. 'initialize',
  134. function (next) {
  135. cli.emit('build.pre.compile', this, next);
  136. },
  137. 'createBuildDirs',
  138. function (next) {
  139. parallel(this, [
  140. 'copyFiles',
  141. 'findProjectDependencies'
  142. ], function () {
  143. parallel(this, [
  144. 'createIcons',
  145. 'findModulesToCache',
  146. 'findPrecacheModules',
  147. 'findPrecacheImages',
  148. 'findTiModules',
  149. 'findI18N'
  150. ], function () {
  151. parallel(this, [
  152. 'findDistinctCachedModules',
  153. 'detectCircularDependencies'
  154. ], function () {
  155. logger.info(
  156. __n('Found %s dependency', 'Found %s dependencies', this.projectDependencies.length) + ', ' +
  157. __n('%s package', '%s packages', this.packages.length) + ', ' +
  158. __n('%s module', '%s modules', this.modulesToCache.length)
  159. );
  160. parallel(this, [
  161. 'assembleTitaniumJS',
  162. 'assembleTitaniumCSS'
  163. ], next);
  164. });
  165. });
  166. });
  167. },
  168. 'processJavaScript',
  169. 'createFilesystemRegistry',
  170. 'createIndexHtml',
  171. function (next) {
  172. cli.emit('build.post.compile', this, next);
  173. },
  174. function (next) {
  175. cli.emit('build.finalize', this, next);
  176. },
  177. ], finished);
  178. };
  179. MobileWebBuilder.prototype.doAnalytics = function doAnalytics(next) {
  180. var cli = this.cli;
  181. cli.addAnalyticsEvent('mobileweb.build.' + cli.argv['deploy-type'], {
  182. dir: cli.argv['project-dir'],
  183. name: cli.tiapp.name,
  184. publisher: cli.tiapp.publisher,
  185. url: cli.tiapp.url,
  186. image: cli.tiapp.icon,
  187. appid: cli.tiapp.id,
  188. description: cli.tiapp.description,
  189. type: cli.argv.type,
  190. guid: cli.tiapp.guid,
  191. version: cli.tiapp.version,
  192. copyright: cli.tiapp.copyright,
  193. date: (new Date()).toDateString()
  194. });
  195. next();
  196. };
  197. MobileWebBuilder.prototype.initialize = function initialize(next) {
  198. this.logger.info(__('Compiling "%s" build', this.deployType));
  199. this.projectResDir = path.join(this.projectDir, 'Resources');
  200. this.mobilewebThemeDir = path.join(this.platformPath, 'themes');
  201. this.mobilewebTitaniumDir = path.join(this.platformPath, 'titanium');
  202. this.moduleSearchPaths = [ this.projectDir, this.globalModulesPath ];
  203. if (this.config.paths && Array.isArray(this.config.paths.modules)) {
  204. this.moduleSearchPaths = this.moduleSearchPaths.concat(this.config.paths.modules);
  205. }
  206. this.projectDependencies = [];
  207. this.modulesToLoad = [];
  208. this.tiModulesToLoad = [];
  209. this.requireCache = {};
  210. this.moduleMap = {};
  211. this.modulesToCache = [];
  212. this.precacheImages = [];
  213. this.locales = [];
  214. this.appNames = {};
  215. this.splashHtml = '';
  216. this.buildOnly = this.cli.argv['build-only'];
  217. this.logger.info(__('Reading Titanium Mobile Web package.json file'));
  218. var mwPackageFile = path.join(this.platformPath, 'titanium', 'package.json');
  219. if (!fs.existsSync(mwPackageFile)) {
  220. this.logger.error(__('Unable to find Titanium Mobile Web package.json file'));
  221. this.logger.error(__("Your SDK installation may be corrupt. You can reinstall it by running '%s'.", (cli.argv.$ + ' sdk update --force --default').cyan) + '\n');
  222. process.exit(1);
  223. }
  224. var pkgJson;
  225. try {
  226. pkgJson = JSON.parse(fs.readFileSync(mwPackageFile));
  227. } catch (e) {
  228. this.logger.error(__("Unable to parse Titanium Mobile Web's package.json file"));
  229. this.logger.error(__("Your SDK installation may be corrupt. You can reinstall it by running '%s'.", (cli.argv.$ + ' sdk update --force --default').cyan) + '\n');
  230. process.exit(1);
  231. }
  232. this.packages = [{
  233. name: pkgJson.name,
  234. location: './titanium',
  235. main: pkgJson.main
  236. }];
  237. this.dependenciesMap = JSON.parse(fs.readFileSync(path.join(this.mobilewebTitaniumDir, 'dependencies.json')));
  238. // read the tiapp.xml and initialize some sensible defaults
  239. (function applyDefaults(dest, src) {
  240. Object.keys(src).forEach(function (key) {
  241. if (dest.hasOwnProperty(key)) {
  242. if (Object.prototype.toString.call(dest[key]) == '[object Object]') {
  243. applyDefaults(dest[key], src[key]);
  244. }
  245. } else {
  246. if (Object.prototype.toString.call(src[key]) == '[object Object]') {
  247. dest[key] = {};
  248. applyDefaults(dest[key], src[key]);
  249. } else {
  250. dest[key] = src[key];
  251. }
  252. }
  253. });
  254. }(this.tiapp, {
  255. mobileweb: {
  256. analytics: {
  257. 'use-xhr': false
  258. },
  259. build: {},
  260. 'disable-error-screen': false,
  261. filesystem: {
  262. backend: '', // blank defaults to Ti/_/Filesystem/Local
  263. registry: 'ondemand'
  264. },
  265. map: {
  266. backend: '', // blank defaults to Ti/_/Map/Google
  267. apikey: ''
  268. },
  269. splash: {
  270. enabled: true,
  271. 'inline-css-images': true
  272. },
  273. theme: 'default'
  274. }
  275. }));
  276. this.logger.info(__('Validating theme'));
  277. this.theme = this.tiapp.mobileweb.theme || 'default';
  278. if (!fs.existsSync(path.join(this.mobilewebThemeDir, this.theme))) {
  279. this.logger.error(__('Unable to find the "%s" theme. Please verify the theme setting in the tiapp.xml.', this.theme) + '\n');
  280. process.exit(1);
  281. }
  282. this.logger.debug(__('Using %s theme', this.theme.cyan));
  283. // Note: code processor is a pre-compile hook
  284. this.codeProcessor = this.cli.codeProcessor;
  285. next();
  286. };
  287. MobileWebBuilder.prototype.createBuildDirs = function createBuildDirs(next) {
  288. // Make sure we have an app.js. This used to be validated in validate(), but since plugins like
  289. // Alloy generate an app.js, it may not have existed during validate(), but should exist now
  290. // that build.pre.compile was fired.
  291. ti.validateAppJsExists(this.projectDir, this.logger, 'mobileweb');
  292. if (fs.existsSync(this.buildDir)) {
  293. this.logger.debug(__('Deleting existing build directory'));
  294. try {
  295. wrench.rmdirSyncRecursive(this.buildDir);
  296. } catch (e) {
  297. this.logger.error(__('Failed to remove build directory: %s', this.buildDir));
  298. if (e.message.indexOf('resource busy or locked') != -1) {
  299. this.logger.error(__('Build directory is busy or locked'));
  300. this.logger.error(__('Check that you don\'t have any terminal sessions or programs with open files in the build directory') + '\n');
  301. } else {
  302. this.logger.error(e.message + '\n');
  303. }
  304. process.exit(1);
  305. }
  306. }
  307. try {
  308. wrench.mkdirSyncRecursive(this.buildDir);
  309. } catch (e) {
  310. if (e.code == 'EPERM') {
  311. // hmm, try again?
  312. try {
  313. wrench.mkdirSyncRecursive(this.buildDir);
  314. } catch (e) {
  315. if (e.code == 'EPERM') {
  316. // hmm, try again?
  317. this.logger.error(__('Unable to create build directory: %s', this.buildDir));
  318. this.logger.error(__("It's possible that the build directory is busy or locked"));
  319. this.logger.error(__("Check that you don't have any terminal sessions or programs with open files in the build directory") + '\n');
  320. process.exit(1);
  321. } else {
  322. throw e;
  323. }
  324. }
  325. } else {
  326. throw e;
  327. }
  328. }
  329. next();
  330. };
  331. MobileWebBuilder.prototype.copyFiles = function copyFiles(next) {
  332. var logger = this.logger;
  333. logger.info(__('Copying project files'));
  334. var copyOpts = {
  335. preserve: true,
  336. ignoreDirs: this.ignoreDirs,
  337. ignoreFiles: this.ignoreFiles,
  338. callback: function (src, dest, contents) {
  339. logger.debug(__('Copying %s => %s', src.cyan, dest.cyan));
  340. return contents;
  341. }
  342. };
  343. afs.copyDirSyncRecursive(this.mobilewebTitaniumDir, path.join(this.buildDir, 'titanium'), copyOpts);
  344. afs.copyDirSyncRecursive(this.mobilewebThemeDir, path.join(this.buildDir, 'themes'), copyOpts);
  345. copyOpts.rootIgnore = ti.availablePlatformsNames;
  346. afs.copyDirSyncRecursive(this.projectResDir, this.buildDir, copyOpts);
  347. var mobilewebDir = path.join(this.projectResDir, 'mobileweb');
  348. if (fs.existsSync(mobilewebDir)) {
  349. copyOpts.rootIgnore = ['apple_startup_images', 'splash'];
  350. afs.copyDirSyncRecursive(mobilewebDir, this.buildDir, copyOpts);
  351. }
  352. this.cli.emit('build.mobileweb.copyFiles', this, next);
  353. };
  354. MobileWebBuilder.prototype.findProjectDependencies = function findProjectDependencies(next) {
  355. var i, len,
  356. plugins,
  357. usedAPIs,
  358. p;
  359. this.logger.info(__('Finding all Titanium API dependencies'));
  360. if (this.codeProcessor && this.codeProcessor.plugins) {
  361. plugins = this.codeProcessor.plugins;
  362. for (i = 0, len = plugins.length; i < len; i++) {
  363. if (plugins[i].name === 'ti-api-usage-finder') {
  364. usedAPIs = plugins[i].global,
  365. this.projectDependencies = ['Ti'];
  366. for(p in usedAPIs) {
  367. p = p.replace('Titanium', 'Ti').replace(/\./g, '/');
  368. if (p in this.dependenciesMap && this.projectDependencies.indexOf(p) != -1) {
  369. this.logger.debug(__('Found Titanium API module: ') + p.replace('Ti', 'Titanium').replace(/\//g, '.'));
  370. this.projectDependencies.push(p);
  371. }
  372. }
  373. break;
  374. }
  375. }
  376. } else {
  377. this.projectDependencies = Object.keys(this.dependenciesMap);
  378. }
  379. next();
  380. };
  381. MobileWebBuilder.prototype.createIcons = function createIcons(next) {
  382. this.cli.emit('build.mobileweb.createIcons', this, next);
  383. };
  384. MobileWebBuilder.prototype.findModulesToCache = function findModulesToCache(next) {
  385. this.logger.info(__('Finding all required modules to be cached'));
  386. this.projectDependencies.forEach(function (mid) {
  387. this.parseModule(mid);
  388. }, this);
  389. this.modulesToCache = this.modulesToCache.concat(Object.keys(this.requireCache));
  390. next();
  391. };
  392. MobileWebBuilder.prototype.findPrecacheModules = function findPrecacheModules(next) {
  393. this.logger.info(__('Finding all precached modules'));
  394. var mwTiapp = this.tiapp.mobileweb;
  395. if (mwTiapp.precache) {
  396. mwTiapp.precache.require && mwTiapp.precache.require.forEach(function (x) {
  397. this.modulesToCache.push('commonjs:' + x);
  398. }, this);
  399. mwTiapp.precache.includes && mwTiapp.precache.includes.forEach(function (x) {
  400. this.modulesToCache.push('url:' + x);
  401. }, this);
  402. }
  403. next();
  404. };
  405. MobileWebBuilder.prototype.findPrecacheImages = function findPrecacheImages(next) {
  406. this.logger.info(__('Finding all precached images'));
  407. this.moduleMap['Ti/UI/TableViewRow'] && this.precacheImages.push('/themes/' + this.theme + '/UI/TableViewRow/child.png');
  408. var images = (this.tiapp.mobileweb.precache && this.tiapp.mobileweb.precache.images) || [];
  409. images && (this.precacheImages = this.precacheImages.concat(images));
  410. next();
  411. };
  412. MobileWebBuilder.prototype.findTiModules = function findTiModules(next) {
  413. this.modules.forEach(function (module) {
  414. var moduleDir = module.modulePath,
  415. pkgJson,
  416. pkgJsonFile = path.join(moduleDir, 'package.json');
  417. if (!fs.existsSync(pkgJsonFile)) {
  418. this.logger.error(__('Invalid Titanium Mobile Module "%s": missing package.json', module.id) + '\n');
  419. process.exit(1);
  420. }
  421. try {
  422. pkgJson = JSON.parse(fs.readFileSync(pkgJsonFile));
  423. } catch (e) {
  424. this.logger.error(__('Invalid Titanium Mobile Module "%s": unable to parse package.json', module.id) + '\n');
  425. process.exit(1);
  426. }
  427. var libDir = ((pkgJson.directories && pkgJson.directories.lib) || '').replace(/^\//, '');
  428. var mainFilePath = path.join(moduleDir, libDir, (pkgJson.main || '').replace(/\.js$/, '') + '.js');
  429. if (!fs.existsSync(mainFilePath)) {
  430. this.logger.error(__('Invalid Titanium Mobile Module "%s": unable to find main file "%s"', module.id, pkgJson.main) + '\n');
  431. process.exit(1);
  432. }
  433. this.logger.info(__('Bundling Titanium Mobile Module %s', module.id.cyan));
  434. this.projectDependencies.push(pkgJson.main);
  435. var moduleName = module.id != pkgJson.main ? module.id + '/' + pkgJson.main : module.id;
  436. if (/\/commonjs/.test(moduleDir)) {
  437. this.modulesToCache.push((/\/commonjs/.test(moduleDir) ? 'commonjs:' : '') + moduleName);
  438. } else {
  439. this.modulesToCache.push(moduleName);
  440. this.tiModulesToLoad.push(module.id);
  441. }
  442. this.packages.push({
  443. 'name': module.id,
  444. 'location': './' + this.collapsePath('modules/' + module.id + (libDir ? '/' + libDir : '')),
  445. 'main': pkgJson.main,
  446. 'root': 1
  447. });
  448. // TODO: need to combine ALL Ti module .js files into the titanium.js, not just the main file
  449. // TODO: need to combine ALL Ti module .css files into the titanium.css
  450. var dest = path.join(this.buildDir, 'modules', module.id);
  451. wrench.mkdirSyncRecursive(dest);
  452. afs.copyDirSyncRecursive(moduleDir, dest, { preserve: true });
  453. }, this);
  454. next();
  455. };
  456. MobileWebBuilder.prototype.findI18N = function findI18N(next) {
  457. var data = ti.i18n.load(this.projectDir, this.logger),
  458. precacheLocales = (this.tiapp.mobileweb.precache || {}).locales || [];
  459. Object.keys(data).forEach(function (lang) {
  460. data[lang].app && data[lang].appname && (self.appNames[lang] = data[lang].appname);
  461. if (data[lang].strings) {
  462. var dir = path.join(this.buildDir, 'titanium', 'Ti', 'Locale', lang);
  463. wrench.mkdirSyncRecursive(dir);
  464. fs.writeFileSync(path.join(dir, 'i18n.js'), 'define(' + JSON.stringify(data[lang].strings, null, '\t') + ');');
  465. this.locales.push(lang);
  466. precacheLocales.indexOf(lang) != -1 && this.modulesToCache.push('Ti/Locale/' + lang + '/i18n');
  467. };
  468. }, this);
  469. next();
  470. };
  471. MobileWebBuilder.prototype.findDistinctCachedModules = function findDistinctCachedModules(next) {
  472. this.logger.info(__('Finding all distinct cached modules'));
  473. var depMap = {};
  474. this.modulesToCache.forEach(function (m) {
  475. for (var i in this.moduleMap) {
  476. if (this.moduleMap.hasOwnProperty(i) && this.moduleMap[i].indexOf(m) != -1) {
  477. depMap[m] = 1;
  478. }
  479. }
  480. }, this);
  481. Object.keys(this.moduleMap).forEach(function (m) {
  482. depMap[m] || this.modulesToLoad.push(m);
  483. }, this);
  484. next();
  485. };
  486. MobileWebBuilder.prototype.detectCircularDependencies = function detectCircularDependencies(next) {
  487. this.modulesToCache.forEach(function (m) {
  488. var deps = this.moduleMap[m];
  489. deps && deps.forEach(function (d) {
  490. if (this.moduleMap[d] && this.moduleMap[d].indexOf(m) != -1) {
  491. this.logger.warn(__('Circular dependency detected: %s dependent on %s'), m, d);
  492. }
  493. }, this);
  494. }, this);
  495. next();
  496. };
  497. MobileWebBuilder.prototype.assembleTitaniumJS = function assembleTitaniumJS(next) {
  498. this.logger.info(__('Assembling titanium.js'));
  499. var tiapp = this.tiapp;
  500. async.waterfall([
  501. // 1) render the header
  502. function (next) {
  503. next(null, ejs.render(fs.readFileSync(path.join(this.templatesDir, 'header.ejs')).toString(), { escapeQuotes: escapeQuotes }) + '\n');
  504. }.bind(this),
  505. // 2) read in the config.js and fill in the template
  506. function (tiJS, next) {
  507. this.cli.createHook('build.mobileweb.assembleConfigTemplate', this, function (template, options, callback) {
  508. callback(null, tiJS + ejs.render(template, options) + '\n\n');
  509. })(
  510. fs.readFileSync(path.join(this.platformPath, 'src', 'config.js')).toString(),
  511. {
  512. appAnalytics: tiapp.analytics,
  513. appCopyright: tiapp.copyright,
  514. appDescription: tiapp.description,
  515. appGuid: tiapp.guid,
  516. appId: tiapp.id,
  517. appName: tiapp.name,
  518. appNames: JSON.stringify(this.appNames),
  519. appPublisher: tiapp.publisher,
  520. appUrl: tiapp.url,
  521. appVersion: tiapp.version,
  522. buildType: this.buildType,
  523. deployType: this.deployType,
  524. locales: JSON.stringify(this.locales),
  525. packages: JSON.stringify(this.packages),
  526. projectId: tiapp.id,
  527. projectName: tiapp.name,
  528. target: this.target,
  529. tiAnalyticsPlatformName: 'mobileweb',
  530. tiFsRegistry: tiapp.mobileweb.filesystem.registry,
  531. tiTheme: this.theme,
  532. tiGithash: ti.manifest.githash,
  533. tiOsName: 'mobileweb',
  534. tiPlatformName: 'mobileweb',
  535. tiTimestamp: ti.manifest.timestamp,
  536. tiVersion: ti.manifest.version,
  537. hasAnalyticsUseXhr: tiapp.mobileweb.analytics ? tiapp.mobileweb.analytics['use-xhr'] === true : false,
  538. hasShowErrors: this.deployType != 'production' && tiapp.mobileweb['disable-error-screen'] !== true,
  539. hasInstrumentation: !!tiapp.mobileweb.instrumentation,
  540. hasAllowTouch: tiapp.mobileweb.hasOwnProperty('allow-touch') ? !!tiapp.mobileweb['allow-touch'] : true,
  541. escapeQuotes: escapeQuotes
  542. },
  543. next
  544. );
  545. }.bind(this),
  546. // 3) copy platform specific functionality
  547. function (tiJS, next) {
  548. this.cli.createHook('build.mobileweb.assemblePlatformImplementation', this, function (contents, callback) {
  549. callback(null, contents);
  550. })(tiJS, next);
  551. }.bind(this),
  552. // 4) copy in instrumentation if it's enabled
  553. function (tiJS, next) {
  554. if (tiapp.mobileweb.instrumentation) {
  555. next(null, tiJS + fs.readFileSync(path.join(this.platformPath, 'src', 'instrumentation.js')).toString() + '\n');
  556. } else {
  557. next(null, tiJS);
  558. }
  559. }.bind(this),
  560. // 5) copy in the loader
  561. function (tiJS, next) {
  562. next(null, tiJS + fs.readFileSync(path.join(this.platformPath, 'src', 'loader.js')).toString() + '\n\n');
  563. }.bind(this),
  564. // 6) cache the dependencies
  565. function (tiJS, next) {
  566. var first = true,
  567. requireCacheWritten = false,
  568. moduleCounter = 0,
  569. tiJSFile = path.join(this.buildDir, 'titanium.js'),
  570. tiDir = path.join(this.buildDir, 'titanium') + path.sep;
  571. // uncomment next line to bypass module caching (which is ill advised):
  572. // this.modulesToCache = [];
  573. this.modulesToCache.forEach(function (moduleName) {
  574. var isCommonJS = false;
  575. if (/^commonjs\:/.test(moduleName)) {
  576. isCommonJS = true;
  577. moduleName = moduleName.substring(9);
  578. }
  579. var dep = this.resolveModuleId(moduleName);
  580. if (!dep.length) return;
  581. if (!requireCacheWritten) {
  582. tiJS += 'require.cache({\n';
  583. requireCacheWritten = true;
  584. }
  585. if (!first) {
  586. tiJS += ',\n';
  587. }
  588. first = false;
  589. moduleCounter++;
  590. var file = path.join(dep[0], /\.js$/.test(dep[1]) ? dep[1] : dep[1] + '.js'),
  591. r;
  592. try {
  593. r = jsanalyze.analyzeJsFile(file, { minify: /^url\:/.test(moduleName) && this.minifyJS, skipStats: file == tiJSFile || file.indexOf(tiDir) == 0 });
  594. } catch (ex) {
  595. ex.message.split('\n').forEach(this.logger.error);
  596. this.logger.log();
  597. process.exit(1);
  598. }
  599. if (/^url\:/.test(moduleName)) {
  600. if (this.minifyJS) {
  601. this.logger.debug(__('Minifying include %s', file.cyan));
  602. try {
  603. fs.writeFileSync(file, r.contents);
  604. } catch (ex) {
  605. this.logger.error(__('Failed to minify %s', file));
  606. if (ex.line) {
  607. this.logger.error(__('%s [line %s, column %s]', ex.message, ex.line, ex.col));
  608. } else {
  609. this.logger.error(__('%s', ex.message));
  610. }
  611. try {
  612. var contents = fs.readFileSync(file).toString().split('\n');
  613. if (ex.line && ex.line <= contents.length) {
  614. this.logger.error('');
  615. this.logger.error(' ' + contents[ex.line-1]);
  616. if (ex.col) {
  617. var i = 0,
  618. len = ex.col;
  619. buffer = ' ';
  620. for (; i < len; i++) {
  621. buffer += '-';
  622. }
  623. this.logger.error(buffer + '^');
  624. }
  625. this.logger.log();
  626. }
  627. } catch (ex2) {}
  628. process.exit(1);
  629. }
  630. }
  631. tiJS += '"' + moduleName + '":"' + fs.readFileSync(file).toString().trim().replace(/\\/g, '\\\\').replace(/\n/g, '\\n\\\n').replace(/"/g, '\\"') + '"';
  632. } else if (isCommonJS) {
  633. tiJS += '"' + moduleName + '":function(){\n/* ' + file.replace(this.buildDir, '') + ' */\ndefine(function(require,exports,module){\n' + fs.readFileSync(file).toString() + '\n});\n}';
  634. } else {
  635. tiJS += '"' + moduleName + '":function(){\n/* ' + file.replace(this.buildDir, '') + ' */\n\n' + fs.readFileSync(file).toString() + '\n}';
  636. }
  637. }, this);
  638. this.precacheImages.forEach(function (url) {
  639. url = url.replace(/\\/g, '/');
  640. var img = path.join(this.projectResDir, /^\//.test(url) ? '.' + url : url),
  641. m = img.match(/(\.[a-zA-Z]{3,4})$/),
  642. type = m && this.imageMimeTypes[m[1]];
  643. if (type && fs.existsSync(img)) {
  644. if (!requireCacheWritten) {
  645. tiJS += 'require.cache({\n';
  646. requireCacheWritten = true;
  647. }
  648. if (!first) {
  649. tiJS += ',\n';
  650. }
  651. first = false;
  652. moduleCounter++;
  653. tiJS += '"url:' + url + '":"data:' + type + ';base64,' + fs.readFileSync(img).toString('base64') + '"';
  654. }
  655. }, this);
  656. if (requireCacheWritten) {
  657. tiJS += '});\n';
  658. }
  659. next(null, tiJS);
  660. }.bind(this),
  661. // 7) write the ti.app.properties
  662. function (tiJS, next) {
  663. var props = this.tiapp.properties || {};
  664. this.tiapp.mobileweb.filesystem.backend && (props['ti.fs.backend'] = { type: 'string', value: this.tiapp.mobileweb.filesystem.backend });
  665. this.tiapp.mobileweb.map.backend && (props['ti.map.backend'] = { type: 'string', value: this.tiapp.mobileweb.map.backend });
  666. this.tiapp.mobileweb.map.apikey && (props['ti.map.apikey'] = { type: 'string', value: this.tiapp.mobileweb.map.apikey });
  667. tiJS += 'require("Ti/App/Properties", function(p) {\n';
  668. Object.keys(props).forEach(function (name) {
  669. var prop = props[name],
  670. type = prop.type || 'string';
  671. tiJS += '\tp.set' + type.charAt(0).toUpperCase() + type.substring(1).toLowerCase() + '("'
  672. + name.replace(/"/g, '\\"') + '",' + (type == 'string' ? '"' + prop.value.replace(/"/g, '\\"') + '"': prop.value) + ');\n';
  673. });
  674. tiJS += '});\n';
  675. next(null, tiJS);
  676. }.bind(this),
  677. // 8) write require() to load all Ti modules
  678. function (tiJS, next) {
  679. this.modulesToLoad.sort();
  680. this.modulesToLoad = this.modulesToLoad.concat(this.tiModulesToLoad);
  681. next(null, tiJS + 'require(' + JSON.stringify(this.modulesToLoad) + ');\n');
  682. }.bind(this)
  683. ], function (err, tiJS) {
  684. fs.writeFile(path.join(this.buildDir, 'titanium.js'), tiJS, next);
  685. }.bind(this));
  686. };
  687. MobileWebBuilder.prototype.assembleTitaniumCSS = function assembleTitaniumCSS(next) {
  688. var tiCSS = [
  689. ejs.render(fs.readFileSync(path.join(this.templatesDir, 'header.ejs')).toString(), { escapeQuotes: escapeQuotes }), '\n'
  690. ];
  691. if (this.tiapp.mobileweb.splash.enabled) {
  692. var splashDir = path.join(this.projectResDir, 'mobileweb', 'splash'),
  693. splashHtmlFile = path.join(splashDir, 'splash.html'),
  694. splashCssFile = path.join(splashDir, 'splash.css');
  695. if (fs.existsSync(splashDir)) {
  696. this.logger.info(__('Processing splash screen'));
  697. fs.existsSync(splashHtmlFile) && (this.splashHtml = fs.readFileSync(splashHtmlFile));
  698. if (fs.existsSync(splashCssFile)) {
  699. var css = fs.readFileSync(splashCssFile).toString();
  700. if (this.tiapp.mobileweb.splash['inline-css-images']) {
  701. var parts = css.split('url('),
  702. i = 1, p, img, imgPath, imgType,
  703. len = parts.length;
  704. for (; i < len; i++) {
  705. p = parts[i].indexOf(')');
  706. if (p != -1) {
  707. img = parts[i].substring(0, p).replace(/["']/g, '').trim();
  708. if (!/^data\:/.test(img)) {
  709. imgPath = img.charAt(0) == '/' ? this.projectResDir + img : splashDir + '/' + img;
  710. imgType = this.imageMimeTypes[imgPath.match(/(\.[a-zA-Z]{3})$/)[1]];
  711. if (fs.existsSync(imgPath) && imgType) {
  712. parts[i] = 'data:' + imgType + ';base64,' + fs.readFileSync(imgPath).toString('base64') + parts[i].substring(p);
  713. }
  714. }
  715. }
  716. }
  717. css = parts.join('url(');
  718. }
  719. tiCSS.push(css);
  720. }
  721. }
  722. }
  723. this.logger.info(__('Assembling titanium.css'));
  724. var commonCss = this.mobilewebThemeDir + '/common.css';
  725. fs.existsSync(commonCss) && tiCSS.push(fs.readFileSync(commonCss).toString());
  726. // TODO: need to rewrite absolute paths for urls
  727. // TODO: code below does NOT inline imports, nor remove them... do NOT use imports until themes are fleshed out
  728. var themePath = path.join(this.projectResDir, 'themes', this.theme);
  729. fs.existsSync(themePath) || (themePath = path.join(this.projectResDir, this.theme));
  730. fs.existsSync(themePath) || (themePath = path.join(this.platformPath, 'themes', this.theme));
  731. if (!fs.existsSync(themePath)) {
  732. this.logger.error(__('Unable to locate theme "%s"', this.theme) + '\n');
  733. process.exit(1);
  734. }
  735. wrench.readdirSyncRecursive(themePath).forEach(function (file) {
  736. /\.css$/.test(file) && tiCSS.push(fs.readFileSync(path.join(themePath, file)).toString() + '\n');
  737. });
  738. // detect any fonts and add font face rules to the css file
  739. var fonts = {},
  740. fontFormats = {
  741. 'ttf': 'truetype'
  742. },
  743. prefix = this.projectResDir.replace(/\\/g, '/') + '/';
  744. (function walk(dir, isMobileWebDir, isRoot) {
  745. fs.existsSync(dir) && fs.readdirSync(dir).forEach(function (name) {
  746. var file = path.join(dir, name);
  747. if (fs.statSync(file).isDirectory()) {
  748. if (!isRoot || name == 'mobileweb' || ti.availablePlatformsNames.indexOf(name) == -1) {
  749. walk(file, isMobileWebDir || name == 'mobileweb');
  750. }
  751. return;
  752. }
  753. var m = name.match(/^(.+)\.(otf|woff|ttf|svg)$/);
  754. if (m) {
  755. var p = file.replace(/\\/g, '/').replace(prefix, '');
  756. fonts[m[1]] || (fonts[m[1]] = {});
  757. fonts[m[1]][m[2]] = {
  758. path: isMobileWebDir ? p.replace('mobileweb/', '') : p,
  759. format: fontFormats[m[2]] || m[2]
  760. };
  761. }
  762. });
  763. }(this.projectResDir, false, true));
  764. Object.keys(fonts).forEach(function (name) {
  765. var font = fonts[name],
  766. src = [];
  767. this.logger.debug(__('Found font: %s', name.cyan));
  768. ['woff', 'otf', 'ttf', 'svg'].forEach(function (type) {
  769. if (font[type]) {
  770. // this.prefetch.push(font[type].path);
  771. src.push('url("' + font[type].path + '") format("' + font[type].format + '")');
  772. }
  773. }, this);
  774. tiCSS.push('@font-face{font-family:"' + name + '";src:' + src.join(',') + ';}\n');
  775. }, this);
  776. // write the titanium.css
  777. fs.writeFileSync(path.join(this.buildDir, 'titanium.css'), this.deployType == 'production' ? new CleanCSS({ processImport: false }).minify(tiCSS.join('')).styles : tiCSS.join(''));
  778. next();
  779. };
  780. MobileWebBuilder.prototype.processJavaScript = function processJavaScript(next) {
  781. var self = this,
  782. tiJSFile = path.join(this.buildDir, 'titanium.js');
  783. this.logger.info(this.minifyJS ? __('Minifying JavaScript') : __('Analyzing JavaScript'));
  784. (function walk(dir, isTitaniumFolder) {
  785. fs.readdirSync(dir).sort().forEach(function (filename) {
  786. var file = path.join(dir, filename),
  787. stat = fs.statSync(file);
  788. if (stat.isDirectory()) {
  789. walk(file, isTitaniumFolder || filename == 'titanium');
  790. } else if (/\.js$/.test(filename)) {
  791. self.logger.debug(self.minifyJS ? __('Minifying %s', file.cyan) : __('Analyzing %s', file.cyan));
  792. try {
  793. var r = jsanalyze.analyzeJsFile(file, { minify: self.minifyJS, skipStats: isTitaniumFolder || file == tiJSFile });
  794. self.minifyJS && fs.writeFileSync(file, r.contents);
  795. } catch (ex) {
  796. ex.message.split('\n').forEach(self.logger.error);
  797. self.logger.log();
  798. process.exit(1);
  799. }
  800. }
  801. });
  802. }(this.buildDir));
  803. next();
  804. };
  805. MobileWebBuilder.prototype.createFilesystemRegistry = function createFilesystemRegistry(next) {
  806. this.logger.info(__('Creating the filesystem registry'));
  807. var registry = 'ts\t' + fs.statSync(this.buildDir).ctime.getTime() + '\n' +
  808. (function walk(dir, depth) {
  809. var s = '';
  810. depth = depth | 0;
  811. fs.readdirSync(dir).sort().forEach(function (file) {
  812. // TODO: screen out specific file/folder patterns (i.e. uncompressed js files)
  813. var stat = fs.statSync(path.join(dir, file));
  814. if (stat.isDirectory()) {
  815. s += (depth ? (new Array(depth + 1)).join('\t') : '') + file + '\n' + walk(path.join(dir, file), depth + 1);
  816. } else {
  817. s += (depth ? (new Array(depth + 1)).join('\t') : '') + file + '\t' + stat.size + '\n';
  818. }
  819. });
  820. return s;
  821. }(this.buildDir)).trim();
  822. fs.writeFileSync(path.join(this.buildDir, 'titanium', 'filesystem.registry'), registry);
  823. if (this.tiapp.mobileweb.filesystem.registry == 'preload') {
  824. fs.appendFileSync(path.join(this.buildDir, 'titanium.js'), 'require.cache({"url:/titanium/filesystem.registry":"' + registry.replace(/\n/g, '|') + '"});');
  825. }
  826. next();
  827. };
  828. MobileWebBuilder.prototype.createIndexHtml = function createIndexHtml(next) {
  829. this.logger.info(__('Creating the index.html'));
  830. // get status bar style
  831. var statusBarStyle = this.tiapp['statusbar-style'];
  832. if (statusBarStyle && /^(?:black\-translucent|translucent_black|transparent|translucent)$/.test(statusBarStyle)) {
  833. statusBarStyle = 'black-translucent';
  834. } else {
  835. statusBarStyle = 'black';
  836. }
  837. // write the index.html
  838. this.cli.createHook('build.mobileweb.createIndexHtml', this, function (template, options, callback) {
  839. fs.writeFile(path.join(this.buildDir, 'index.html'), ejs.render(template, options), callback);
  840. })(
  841. fs.readFileSync(path.join(this.platformPath, 'src', 'index.html')).toString(),
  842. {
  843. target: this.target,
  844. tiHeader: ejs.render(fs.readFileSync(path.join(this.templatesDir, 'header.html.ejs')).toString()),
  845. projectName: this.tiapp.name || '',
  846. appDescription: this.tiapp.description || '',
  847. appPublisher: this.tiapp.publisher || '',
  848. tiGenerator: 'Appcelerator Titanium Mobile ' + ti.manifest.version,
  849. tiStatusbarStyle: statusBarStyle,
  850. tiCss: fs.readFileSync(path.join(this.buildDir, 'titanium.css')).toString(),
  851. splashScreen: this.splashHtml,
  852. tiJs: fs.readFileSync(path.join(this.buildDir, 'titanium.js')).toString(),
  853. prefetch: this.prefetch,
  854. escapeQuotes: escapeQuotes
  855. },
  856. next
  857. );
  858. };
  859. MobileWebBuilder.prototype.collapsePath = function collapsePath(p) {
  860. var result = [], segment, lastSegment;
  861. p = p.replace(/\\/g, '/').split('/');
  862. while (p.length) {
  863. segment = p.shift();
  864. if (segment == '..' && result.length && lastSegment != '..') {
  865. result.pop();
  866. lastSegment = result[result.length - 1];
  867. } else if (segment != '.') {
  868. result.push(lastSegment = segment);
  869. }
  870. }
  871. return result.join('/');
  872. };
  873. MobileWebBuilder.prototype.resolveModuleId = function resolveModuleId(mid, ref) {
  874. var parts = mid.split('!');
  875. mid = parts[parts.length-1];
  876. if (/^url\:/.test(mid)) {
  877. mid = mid.substring(4);
  878. if (/^\//.test(mid)) {
  879. mid = '.' + mid;
  880. }
  881. parts = mid.split('/');
  882. for (var i = 0, l = this.packages.length; i < l; i++) {
  883. if (this.packages[i].name == parts[0]) {
  884. return [this.collapsePath(this.buildDir + '/' + this.packages[i].location), mid];
  885. }
  886. }
  887. return [this.buildDir, mid];
  888. }
  889. if (mid.indexOf(':') != -1) return [];
  890. if (/^\//.test(mid) || (parts.length == 1 && /\.js$/.test(mid))) return [this.buildDir, mid];
  891. /^\./.test(mid) && ref && (mid = this.collapsePath(ref + mid));
  892. parts = mid.split('/');
  893. for (var i = 0, l = this.packages.length; i < l; i++) {
  894. if (this.packages[i].name == parts[0]) {
  895. this.packages[i].name != 'Ti' && (mid = mid.replace(this.packages[i].name + '/', ''));
  896. return [ this.collapsePath(path.join(this.buildDir, this.packages[i].location)), mid ];
  897. }
  898. }
  899. return [ this.buildDir, mid ];
  900. };
  901. MobileWebBuilder.prototype.parseModule = function parseModule(mid, ref) {
  902. if (this.requireCache[mid] || mid == 'require') {
  903. return;
  904. }
  905. var parts = mid.split('!');
  906. if (parts.length == 1) {
  907. if (mid.charAt(0) == '.' && ref) {
  908. mid = this.collapsePath(ref + mid);
  909. }
  910. this.requireCache[mid] = 1;
  911. }
  912. var dep = this.resolveModuleId(mid, ref);
  913. if (!dep.length) {
  914. return;
  915. }
  916. parts.length > 1 && (this.requireCache['url:' + parts[1]] = 1);
  917. var deps = this.dependenciesMap[parts.length > 1 ? mid : dep[1]];
  918. for (var i = 0, l = deps.length; i < l; i++) {
  919. dep = deps[i];
  920. ref = mid.split('/');
  921. ref.pop();
  922. ref = ref.join('/') + '/';
  923. this.parseModule(dep, ref);
  924. }
  925. this.moduleMap[mid] = deps;
  926. };
  927. // create the builder instance and expose the public api
  928. (function (mobileWebBuilder) {
  929. exports.config = mobileWebBuilder.config.bind(mobileWebBuilder);
  930. exports.validate = mobileWebBuilder.validate.bind(mobileWebBuilder);
  931. exports.run = mobileWebBuilder.run.bind(mobileWebBuilder);
  932. }(new MobileWebBuilder(module)));