UNPKG

appc-cli-titanium

Version:
873 lines (770 loc) 28.4 kB
/** * This code is closed source and Confidential and Proprietary to * Appcelerator, Inc. All Rights Reserved. This code MUST not be * modified, copied or otherwise redistributed without express * written permission of Appcelerator. This file is licensed as * part of the Appcelerator Platform and governed under the terms * of the Appcelerator license agreement. */ var fs = require('fs-extra'), path = require('path'), exec = require('child_process').exec, // eslint-disable-line security/detect-child-process tiappxml = require('tiapp.xml'), util = require('./util'), platform = require('appc-platform-sdk'), chalk = require('chalk'), semver = require('semver'); var TYPE = require('../appc').TYPE, optsMap = require('../appc').OPTS_MAP; module.exports = { type: TYPE, order: 100, subtype: 'app', // this can be either 'app' or 'server' skipACS: true, name: 'Titanium SDK (JavaScript)', fields: function (appc, opts) { var questionList = [], projTiapp; var questions = { projectName: { type: 'input', name: 'name', message: 'What\'s the project name?', validate: function (input) { // validate this still works var rx = /^[\w\u00C0-\u017F]{2,}[\w\u00C0-\u017F.-]+$/; return rx.test(input) || 'project name is invalid'; }, flags: '-n, --name <name>', description: 'name of the project' }, projectFolder: { type: 'input', name: 'project-dir', message: 'Where\'s the project directory?', validate: function (input) { var result = isInProjectDir(input); if (result) { projTiapp = getProjectTiapp(input); questions.projectName.default = projTiapp && projTiapp.name; } return result || 'project directory does not exist'; }, flags: '-d, --project-dir <value>', description: 'the directory containing the project [default: .]' }, applicationId: appc.fields.id() }; if (opts && opts.type === 'applewatch') { if (!isInProjectDir()) { questions.projectFolder.message = 'What\'s the Titanium project directory?'; questionList.push(questions.projectFolder); } else { projTiapp = getProjectTiapp(); questions.projectName.default = projTiapp && projTiapp.name; } questions.projectName.message = 'What\'s the WatchOS project name?'; questionList.push(questions.projectName); } else { questionList.push(questions.projectName); questionList.push(questions.applicationId); } if (opts && opts.type === 'timodule') { // if a --sdk flag is passed in then use that, otherwise fallback to the activeSDK the appc cli looks up const sdkVersion = opts.sdk || opts.activeSDK; if (sdkVersion) { const coercedSDKVersion = semver.coerce(sdkVersion); if (coercedSDKVersion && semver.gte(coercedSDKVersion, '9.1.0')) { questionList.push({ type: 'list', name: 'androidCodeBase', message: 'Android code base language', choices: [ { name: 'Java', value: 'java' }, { name: 'Kotlin', value: 'kotlin' } ], flags: '--android-code-base <language>', default: 'java', description: 'Android code base language', when: () => { // Don't prompt --platforms was passed and doesn't include android const platforms = opts.platforms && opts.platforms.split(','); if (platforms && !platforms.includes('android')) { return false; } return true; } }); questionList.push({ type: 'list', name: 'iosCodeBase', message: 'iOS code base language', choices: [ { name: 'Objective-C', value: 'objc' }, { name: 'Swift', value: 'swift' } ], default: 'objc', flags: '--ios-code-base <language>', description: 'iOS code base language', when: () => { // Don't prompt --platforms was passed and doesn't include and ios related value const platforms = opts.platforms && opts.platforms.split(','); if (platforms && (!platforms.includes('ios') && !platforms.includes('iphone') && !platforms.includes('ipad'))) { return false; } return true; } }); } } } return questionList; }, command: function (program, appc) { if (program.type !== 'applewatch') { var ID = appc.fields.id(); program.option(ID.flags, ID.description); } program.option('-p, --platforms <platforms>', 'platforms for titanium project'); program.option('-u, --url <url>', 'url of the project'); program.option('--sdk <sdk>', 'Titanium SDK version to use to bootstrap project creation'); program.option('--template <template>', 'the name of the project template'); program.option('--no-services', 'skip registering appcelerator platform services for titanium project'); program.option('--classic', 'creates a titanium classic project'); program.option('--ng', 'create a titanium angular project'); program.option('--android-code-base <language>', 'the code base of the Android module project'); program.option('--ios-code-base <language>', 'the code base of the iOS module project'); }, execute: execute, provision: provision, provisioned: provisioned }; function isInProjectDir(projectDir) { var dir = projectDir || process.cwd(), file = path.join(dir, 'tiapp.xml'); return fs.existsSync(file); } function getProjectTiapp(projectDir) { var dir = projectDir || process.cwd(), file = path.join(dir, 'tiapp.xml'), tiapp; if (fs.existsSync(file)) { tiapp = tiappxml.load(file); } return tiapp; } function generateSample(appc, opts, callback) { // // generate the sample application // // right now this only generates the basic corporate directory application // need to better hook up the data bindings based on what you've choosen // for your backend // var templateDir = path.join(__dirname, '..', 'template', 'corpdir'); fs.copySync(templateDir, opts.projectDir); callback(); } function execute(appc, args, opts, callback) { var spinner = appc.spinner, tiPath = path.join(opts.projectDir ? path.dirname(opts.projectDir) : process.cwd(), opts.name), workspace = path.dirname(tiPath); opts.projectDir = opts.projectDir || tiPath; var platforms = opts.platforms || 'all'; var type = opts.type || 'app'; if (type === 'titanium') { type = 'app'; } else if (type === 'timodule') { // this needs to be 'module' since it gets passed to the titanium cli // the titanium cli syntax for defining a module is 'module' not 'timodule' type = 'module'; } // If we're not creating a classic or angular project, and a template has been provided that // starts with webpack, then we want to have the titanium CLI create the default webpack // template so that we have the initial necessary dependencies installed var templateName = opts.template || null; var classicTemplateName; if (!(opts.classic || opts.ng) && templateName && templateName.includes('webpack')) { // we're creating an alloy project that has a webpack based template, so we want to force // ti create to create the default webpack template for us we delete template from opts so // it doesn't get copied over by the argument translation further down classicTemplateName = 'webpack-default'; delete opts.template; } else if (!(opts.classic || opts.ng) && templateName) { // we're not creating a webpack based alloy project, but we still don't want the // template to be set during ti create delete opts.template; } else if (opts.classic) { // we're creating a classic app, so lets pass it through classicTemplateName = templateName; } args = [ 'create', '-t', type, '--no-banner', '--no-prompt' ]; if (type === 'applewatch') { workspace = opts['project-dir'] ? path.resolve(opts['project-dir']) : workspace; args.push('--name', opts.name, '--project-dir', workspace); } else { args.push('--platforms', platforms, '--workspace-dir', workspace); } // translate opts back to arguments since titanium can only be // invoked via CLI (there's no module interface) Object.keys(opts).forEach(function (opt) { if (opt === 'register' || opt === 'pluginPaths' || opt === 'configFn' || opt === 'session' || opts === 'colors' || optsMap[opt] === undefined) { return; } if (args.indexOf(optsMap[opt]) === -1) { args.push(optsMap[opt]); } if (opt !== 'force' || opt !== 'services') { args.push(opts[opt]); } }); if (!opts.logLevel) { args.push('--log-level', 'error'); } if (!opts.url) { args.push('--url', 'unspecified'); } if (!opts.force && fs.existsSync(path.join(opts.projectDir, 'tiapp.xml'))) { // There is no force option, so user might want to only enable services on an existing project. return callback(); } if (opts.ng) { args.push('--template', 'angular-default'); } if (opts.type === 'timodule' && opts.androidCodeBase) { args.push('--android-code-base', opts.androidCodeBase); } if (opts.type === 'timodule' && opts.iosCodeBase) { args.push('--ios-code-base', opts.iosCodeBase); } if (type === 'app' && classicTemplateName) { args.push('--template', classicTemplateName); } // make sure workspace exists workspace && fs.ensureDirSync(workspace); // launch titanium for new project spinner.start(); util.launchTi(appc, args, { stdio: [ process.stdin, 'pipe', 'pipe' ], cwd: workspace }, function (err, ti) { if (err) { return callback(err); } var tiOutput = new RegExp('\\[(INFO)\\]\\s*(.*)'); ti.stdout.on('data', function (buf) { var result = tiOutput.exec(buf.toString()); if (result && opts.logLevel === 'info') { appc.log.info(chalk.green('[' + result[1] + ']'), result[2]); } else { appc.log.info.wrap(buf); } }); ti.stderr.on('data', appc.log.error.wrap); ti.on('exit', function (code) { if (code) { spinner.stop(); return callback(new Error('titanium exited with non-zero exit code (' + code + ')')); } // get the project dir right if (tiPath !== opts.projectDir) { fs.removeSync(opts.projectDir); fs.renameSync(tiPath, opts.projectDir); } // if a classic app, angular app, applewatch app or module, skip alloy setup if (opts.classic || opts.ng || opts.type === 'timodule' || opts.type === 'applewatch') { spinner.stop(); return callback(); } // generate alloy project var alloyBin = path.resolve(path.dirname(require.resolve('alloy')) + '/../bin/alloy'), cmd = '"' + process.execPath + '" "' + alloyBin + '" new --force'; if (opts.testapp) { cmd += ' --testapp ' + opts.testapp; } if (templateName) { cmd += ' . ' + templateName; } appc.log.trace(cmd); exec(cmd, { cwd: opts.projectDir }, function (err, stdout, stderr) { appc.log.trace('alloy returned error=', err, 'stdout=', stdout, 'stderr=', stderr); if (err) { spinner.stop(); stdout && appc.log.trace(stdout); stderr && appc.log.error(stderr); return callback(err); } spinner.stop(); if (opts.sample) { return generateSample(appc, opts, callback); } else { callback(); } }); }); }); } /** * called to provision the application with the platform */ function provision(appc, opts, config, callback) { var tiapp = path.join(opts.projectDir || path.join(process.cwd(), opts.name), 'tiapp.xml'); if (fs.existsSync(tiapp)) { config.tiapp = tiapp; } return callback(); } function getEntitlements(appc, opts, callback) { var result = appc.commands.config.get('entitlements'); return callback(null, result); } function getSDKInfo(appc, opts, callback) { if (module.exports.sdkInfo) { return callback(null, module.exports.sdkInfo); } // Run ti sdk -o json command to find out the SDK location. var cmd = '"' + process.execPath + '" "' + util.getTiBinary() + '" sdk -o json'; exec(cmd, function (err, stdout) { if (err) { appc.log.error(err); return callback(err); } var result = JSON.parse(stdout.trim()); // Find the SDK to be used for creating this app. var sdk = opts.sdk || result.activeSDK; module.exports.sdkInfo = { activeSDK: sdk, sdkLocation: result.defaultInstallLocation }; return callback(null, module.exports.sdkInfo); }); } /** * called when a component has been provisioned */ function provisioned(appc, type, opts, config, callback) { appc.log.debug('titanium provisioned called', type); var cacheModDownloadInfo; var isImport = opts.import || false; if (type === 'completed' && config.tiapp) { appc.async.series([ function (cb) { var tiapp, ti; if (!opts.enableServices) { return cb(); } tiapp = config.tiapp; ti = tiappxml.load(tiapp); appc.log.trace('guid: ', ti.guid); // Check if the app exists platform.createRequest(config.session, '/api/v1/app/' + ti.guid, function (err, result) { appc.log.trace('check app exists err: ', err); if (err && err.code === 404) { // force gen new guid isImport = true; } cb(); }); }, // create the App using the tiapp guid function (cb) { var tiapp = config.tiapp, ti = tiappxml.load(tiapp); appc.log.trace('sending app post', tiapp); appc.log.debug('isImport: ', isImport); platform.App.updateTiApp(config.session, opts.session.org_id, ti.toString(), { import: isImport }, function (err, response) { if (err) { return cb(err); } appc.log.trace('app post response', response); var body = appc.lodash.isString(response) ? JSON.parse(response) : response; if (!body) { return callback(new Error('invalid response from server attempting to register the application. please re-try your request again or contact Appcelerator Support.')); } var ti = tiappxml.load(tiapp); ti.guid = body.app_guid; ti.setProperty('appc-app-id', body._id, 'string'); config.ti = ti; config.app = body; // save tiapp ti.write(); cb(); }); }, // copy over bundle alloy hook function (cb) { if (!opts.import || opts.ng) { return cb(); } var projAlloyHookFile = path.resolve(path.join(path.dirname(config.tiapp), 'plugins', 'ti.alloy', 'hooks', 'alloy.js')); var alloyHookFile = path.resolve(path.dirname(require.resolve('alloy')) + '/../hooks/alloy.js'); if (fs.existsSync(projAlloyHookFile) && fs.existsSync(alloyHookFile)) { appc.log.trace('copy over alloy.js hook file from bundled alloy'); fs.copySync(alloyHookFile, projAlloyHookFile); } cb(); }, function (cb) { getEntitlements(appc, opts, function (err, entitlements) { if (err) { appc.log.error('Failed to read entitlements', err); } opts.entitlements = entitlements; cb(); }); }, // Sync Titanium downloads function (cb) { util.syncTiDownloads(appc, function (err, data) { // if err , we should continue with the project creation. cacheModDownloadInfo = data; return cb(); }); }, // Get module/plugin versions data function (cb) { if (cacheModDownloadInfo) { return cb(); } util.listTiDownloads(appc, function (err, downloads) { if (err) { appc.log.warn('Failed to download module data: ' + err); } cacheModDownloadInfo = downloads; return cb(); }); }, // setup SDK information function (cb) { getSDKInfo(appc, opts, function (err, sdk) { if (err) { appc.log.warn('Unable to get SDK information: ' + err); } cb(); }); }, // check if we should enable hyperloop function (cb) { if (opts.services !== undefined && !opts.services) { // if they've asked for no services, dont prompt to ask if they want hyperloop opts.enableHyperloop = false; return cb(); } if (opts.promptType !== 'socket-bundle' && !opts.prompt) { opts.enableHyperloop = true; return cb(); } if (opts.template && opts.template.includes('webpack')) { opts.enableHyperloop = false; return cb(); } appc.spinner.stop(); appc.inquirer.prompt([ { type: 'confirm', name: 'enableHyperloop', message: 'Would you like to enable native API access with Hyperloop for this app?', default: true } ], promptOpts(opts), function (err, answer) { if (err) { appc.log.error(err); return cb(); } opts.enableHyperloop = answer.enableHyperloop; appc.spinner.start(); return cb(); }); }, // Enable all services for the app function (cb) { // If the command contains '--no-services', then we shouldn't register services. // TODO: remove this, make each service dependent on its own check if (opts.services !== undefined && !opts.services) { appc.log.info('Skipped registration of Appcelerator Services.'); return cb(); } appc.async.parallel([ // enable hyperloop function (cb) { var platforms = util.getNormalizedPlatforms(opts); if (platforms.length === 0 && opts.platforms !== 'all') { return cb(); } var tiapp = config.tiapp, ti = tiappxml.load(tiapp), sdk = module.exports.sdkInfo; appc.async.waterfall([ function (next) { if (!opts.enableHyperloop || !sdk) { return cb(); } var sdkLocation = sdk.sdkLocation; if (!fs.existsSync(sdkLocation)) { appc.log.error('SDKs are not available at ', sdkLocation); appc.log.error('Failed to enable Hyperloop for ', config.ti.name); return cb(); } appc.log.trace('Using SDK from location ', sdkLocation); return next(); }, function (next) { appc.log.debug('Adding Hyperloop modules for ', platforms); platforms.forEach(function (platform) { ti.setModule('hyperloop', null, platform); }); config.ti = ti; ti.write(); return next(); } ], cb); }, // enable crash if user is entitled function (cb) { if (!opts.entitlements || (opts.entitlements.partners && opts.entitlements.partners.indexOf('aca') === -1)) { appc.log.debug('No entitlement, skip Performance service.'); return cb(); } appc.log.debug('Enabling Performance service', config.ti.name, config.ti.guid); var ACA_ID = 'com.appcelerator.aca'; var platforms = util.getNormalizedPlatforms(opts); appc.async.waterfall([ function (next) { // Get the SDK information and setup getSDKInfo(appc, opts, function (err, sdk) { if (err) { appc.log.error(err); return cb(); } const sdkLocation = sdk.sdkLocation; if (!fs.existsSync(sdkLocation)) { appc.log.error('SDKs are not available at ', sdkLocation); appc.log.error('Failed to register Performance service for', config.ti.name); return cb(); } appc.log.trace('Using SDK from location ', sdkLocation); const info = { sdkLocation, activeSDK: sdk.activeSDK, }; if (cacheModDownloadInfo.modules) { const moduleInfo = cacheModDownloadInfo.modules.find(mod => mod.id === ACA_ID); info.moduleInfo = moduleInfo; } return next(null, info); }); }, function (environmentInformation, next) { // Check if we have the module installed const { sdkLocation, activeSDK, moduleInfo } = environmentInformation; const installInformation = { id: ACA_ID }; const latestCompatibleMod = util.getCompatibleModuleDownloadInfo(activeSDK, moduleInfo); let latestToDownloadVersion; if (latestCompatibleMod) { latestToDownloadVersion = latestCompatibleMod.version; installInformation.url = latestCompatibleMod.url; installInformation.version = latestCompatibleMod.version; } installInformation.installed = true; platforms.forEach(function (platform) { const modulePath = path.join(sdkLocation, 'modules', platform, ACA_ID); const version = util.detectAddonVersion(appc, modulePath, ACA_ID, platform, activeSDK, latestToDownloadVersion); if (version) { installInformation[platform] = version; } else { installInformation.installed = false; } }); return next(null, installInformation, sdkLocation); }, function (modInfo, sdkLocation, next) { // If it's not installed, install it if (modInfo.installed) { return next(null, modInfo); } appc.log.debug('Install SDK compatible performance module ' + (modInfo.version)); // download Performance module if not installed. util.installTiModule(appc, modInfo, sdkLocation, function () { return next(null, modInfo); }); }, function (modUse, next) { // Set it in the tiapp var tiapp = config.tiapp; var ti = tiappxml.load(tiapp); var modId = modUse.id; appc.log.debug('Adding Performance modules for ', platforms); platforms.forEach(function (platform) { ti.setModule(modId, null, platform); }); config.ti = ti; ti.write(); next(); } ], cb); }, // provisioning ACS // we do this here instead of the letting provisioning do it by setting // the plugin property 'skipACS' above. we do this because we want to // register the app above using the tiapp.xml and then we link the app guid // with ACS below function (cb) { appc.log.trace('calling createACSApp', config.ti.name, config.ti.guid); appc.provisioning.createACSApp(config.session, config.ti.name, config.ti.guid, function (err, apps, usernames, passwords) { if (err && !err.doNotFail) { return cb(err); } else if (err && err.doNotFail) { return cb(); } appc.log.trace('ACS apps created', apps); var ti = config.ti, meta = { acsbase: {}, keys: {}, usernames: {}, passwords: {} }, currentACSkeys = { production: config.ti.getProperty('acs-api-key-production'), development: config.ti.getProperty('acs-api-key-development') }; Object.keys(apps).forEach(function (envName) { var env = apps[envName]; var authbaseurl = env.acsAuthBaseUrl; var baseurl = env.acsBaseUrl; meta.acsbase[envName] = baseurl; // set the base urls ti.setProperty('acs-authbase-url-' + envName, authbaseurl, 'string'); ti.setProperty('acs-base-url-' + envName, baseurl, 'string'); ti.setProperty('acs-oauth-secret-' + envName, env.oauth_secret, 'string'); ti.setProperty('acs-oauth-key-' + envName, env.oauth_key, 'string'); ti.setProperty('acs-api-key-' + envName, env.key, 'string'); meta.keys[envName] = env.key; // username / passwords ti.setProperty('acs-username-' + envName, usernames[envName], 'string'); ti.setProperty('acs-password-' + envName, passwords[envName], 'string'); meta.usernames[envName] = usernames[envName]; meta.passwords[envName] = passwords[envName]; }); if (currentACSkeys.production && (currentACSkeys.production !== meta.keys.production) || currentACSkeys.development && (currentACSkeys.development !== meta.keys.development)) { appc.spinner.stop(); return appc.inquirer.prompt([ { type: 'confirm', name: 'replaceACSkeys', message: 'The app has ArrowDB (previously known as ACS) keys already defined. \n Would you like to replace them? Refer to http://appcel.us/1Gcby7G for more details.', default: false } ], promptOpts(opts), function (err, answers) { if (err) { return cb(err); } if (answers.replaceACSkeys) { appc.log.trace('acs keys exist, replace acs keys'); return doUpdateTi(opts, cb); } else { appc.log.trace('acs keys exist, skip replacing acs keys'); return cb(); } }); } else { return doUpdateTi(opts, cb); } function doUpdateTi(opts, cb) { // set the org information for this app ti.setProperty('appc-org-id', config.org_id, 'string'); ti.setProperty('appc-creator-user-id', config.session.user.guid, 'string'); config.acs = meta; // set the ti.cloud module ti.setModule('ti.cloud', null, 'commonjs'); ti.write(); if (opts.import || opts.enableServices || opts.ng) { return cb(); } // write out the ti.cloud sample code var fn; if (opts.classic) { // webpack projects reside in src and use main.js as their app.js equivalent if (opts.template && opts.template.includes('webpack')) { fn = path.join(path.dirname(config.tiapp), 'src', 'main.js'); } else { fn = path.join(path.dirname(config.tiapp), 'Resources', 'app.js'); } } else { fn = path.join(path.dirname(config.tiapp), 'app', 'alloy.js'); } fs.readFile(fn, function (err, body) { if (err) { return cb(err); } body = body.toString(); body += '\n'; body += '// added during app creation. this will automatically login to\n'; body += '// ACS for your application and then fire an event (see below)\n'; body += '// when connected or errored. if you do not use ACS in your\n'; body += '// application as a client, you should remove this block\n'; body += '(function () {\n'; body += '\tconst ACS = require(\'ti.cloud\');\n'; body += '\tconst env = Ti.App.deployType.toLowerCase() === \'production\' ? \'production\' : \'development\';\n'; // eslint-disable-next-line no-template-curly-in-string body += '\tconst username = Ti.App.Properties.getString(`acs-username-${env}`);\n'; // eslint-disable-next-line no-template-curly-in-string body += '\tconst password = Ti.App.Properties.getString(`acs-password-${env}`);\n'; body += '\n'; body += '\t// if not configured, just return\n'; body += '\tif (!env || !username || !password) {\n\t\treturn;\n\t}\n'; body += '\t/**\n'; body += '\t * Appcelerator Cloud (ACS) Admin User Login Logic\n'; body += '\t *\n'; body += '\t * fires login.success with the user as argument on success\n'; body += '\t * fires login.failed with the result as argument on error\n'; body += '\t */\n'; body += '\tACS.Users.login({\n'; body += '\t\tlogin: username,\n'; body += '\t\tpassword: password,\n'; body += '\t}, function (result) {\n'; body += '\t\tif (env === \'development\') {\n'; // eslint-disable-next-line no-template-curly-in-string body += '\t\t\tTi.API.info(`ACS Login Results for environment ${env}`);\n'; body += '\t\t\tTi.API.info(result);\n'; body += '\t\t}\n'; body += '\t\tif (result && result.success && result.users && result.users.length) {\n'; body += '\t\t\tTi.App.fireEvent(\'login.success\', result.users[0], env);\n'; body += '\t\t} else {\n'; body += '\t\t\tTi.App.fireEvent(\'login.failed\', result, env);\n'; body += '\t\t}\n'; body += '\t});\n\n'; body += '}());\n'; body += '\n'; // write the contents and then finish fs.writeFile(fn, body, cb); }); } }); } ], cb); }, function (cb) { appc.log.trace('finally update tiapp'); var tiapp = config.tiapp, ti = tiappxml.load(tiapp); platform.App.updateTiApp(config.session, opts.session.org_id, ti.toString(), function (err, response) { if (err) { appc.log.error(err); } if (response && response.message) { appc.log.warn(response.message); } cb(); }); } ], callback); } else { callback(); } } function promptOpts(opts) { return { socket: opts.promptType === 'socket' || opts.promptType === 'socket-bundle', port: opts.promptPort, bundle: opts.promptType === 'socket-bundle' }; }