UNPKG

ionic

Version:

A tool for creating and developing Ionic Framework mobile apps.

858 lines (702 loc) • 27.4 kB
var fs = require('fs'); var path = require('path'); var Q = require('q'); var shelljs = require('shelljs'); var Utils = require('./utils'); var _ = require('underscore'); var log = require('./logging').logger; var State = module.exports; shelljs.config.silent = true; State.readInPackageJson = function readInPackageJson(jsonPath) { return require(jsonPath); }; State.getPackageJson = function getPackageJson(appDirectory) { var packageJsonPath = path.join(appDirectory, 'package.json'); var packageJson = null; try { // packageJson = require(packageJsonPath); packageJson = State.readInPackageJson(packageJsonPath); if (!packageJson.cordovaPlugins) { packageJson.cordovaPlugins = []; } if (!packageJson.cordovaPlatforms) { packageJson.cordovaPlatforms = []; } } catch (ex) { log.error('There was an error opening your package.json file.'); log.error(ex); Utils.fail(ex); } return packageJson; }; State.getPackageJsonReadStream = function(appDirectory) { var packageJsonPath = path.join(appDirectory, 'package.json'); return fs.createReadStream(packageJsonPath); }; State.addOrUpdatePluginToPackageJson = function addOrUpdatePluginToPackageJson(packageJson, pluginId, pluginInfo) { var existingPlugin; if (typeof pluginInfo === 'undefined') { pluginInfo = pluginId; } // We need to check cordovaPlugins // perhaps the ID already exists, or the 'id' in the object with locator exists. for (var i = 0, j = packageJson.cordovaPlugins.length; i < j; i += 1) { if (typeof packageJson.cordovaPlugins[i] == 'string' && packageJson.cordovaPlugins[i] === pluginId) { existingPlugin = packageJson.cordovaPlugins[i]; } else if (packageJson.cordovaPlugins[i].id === pluginId) { existingPlugin = packageJson.cordovaPlugins[i]; } } if (!existingPlugin) { packageJson.cordovaPlugins.push(pluginInfo); } }; State.saveState = function saveState(appDirectory) { log.info('Saving your Ionic app state of platforms and plugins'); var packageJson = State.getPackageJson(appDirectory); try { State.saveExistingPlatforms(appDirectory, packageJson); log.info('Saved platform'); State.saveExistingPlugins(appDirectory, packageJson); log.info('Saved plugins'); State.savePackageJson(appDirectory, packageJson); log.info('Saved package.json'); } catch (ex) { log.error('There was an error saving your current Ionic setup'); log.error(ex); } }; State.platformExists = function platformExists(appDirectory, platform) { var platformExists = false; var platformPath; var platformStats; try { platformPath = path.join(appDirectory, 'platforms', platform); platformStats = fs.statSync(platformPath); if (platformStats.isDirectory()) { platformExists = true; } else { platformExists = false; } } catch (ex) {} // eslint-disable-line no-empty return platformExists; }; State.saveExistingPlatforms = function saveExistingPlatforms(appDirectory, packageJson) { var pp = path.join(appDirectory, 'platforms'), platforms = [], platformPath, platformStats; try { platforms = fs.readdirSync(pp); } catch (ex) { return; } // log.info('h') platforms.forEach(function(platform) { // Ignore .git directory if it exists. if (platform.indexOf('.git') !== -1) { return; } platformPath = path.join(appDirectory, 'platforms', platform); platformStats = fs.statSync(platformPath); if (!platformStats.isDirectory()) { return; } try { var versionPath = path.join(appDirectory, platform, 'cordova', 'version'); var version = State.getPlatformVersion(versionPath); var locator = platform; // Check to see if its crosswalk if (platform === 'android' && version.indexOf('-dev') !== -1) { // Look up path for engine/cordova-android path var engineFiles = fs.readdirSync(path.join(appDirectory, 'engine')); var enginePath = null; engineFiles.forEach(function(engineDir) { if (engineDir.indexOf('android') !== -1) { enginePath = engineDir; } }); locator = path.join(appDirectory, 'engine', enginePath); } var platformExists = _.findWhere(packageJson.cordovaPlatforms, { platform: platform }); if (!platformExists) { packageJson.cordovaPlatforms.push({ platform: platform, version: version, locator: locator }); } } catch (ex) { log.info('There was an error trying to save your existing state', ex); } }); }; State.saveExistingPlugins = function saveExistingPlugins(appDirectory, packageJson) { // Lets try just relying on the fetch.json file // this file lists all plugins with where they come from, etc var cordovaPlugins = []; var fetchJson; var hasVariables = false; var locator; var pluginId; var plugin; var pluginPath; var pluginPathStats; var pluginXmlPath; var pluginXml; var preferences; var variableList = []; var keyValueList = {}; try { fetchJson = require(path.join(appDirectory, 'plugins', 'fetch.json')); } catch (ex) {} // eslint-disable-line no-empty if (fetchJson) { // log.info('fetchJson', fetchJson) // This will break with what we had before for (pluginId in fetchJson) { if (fetchJson.hasOwnProperty(pluginId)) { // cordovaPlugins.push(); plugin = fetchJson[pluginId]; try { pluginPath = path.join(appDirectory, 'plugins', pluginId); pluginPathStats = fs.statSync(pluginPath); } catch (ex) { log.info('Plugin ' + pluginId + ' does not exist in the plugins directory. Skipping'); continue; } if (!pluginPathStats.isDirectory()) { log.info('Plugin ' + pluginId + ' does not exist in the plugins directory. Skipping'); continue; } // log.info('plugin.source.type', plugin.source.type); if (plugin.source.type === 'registry') { locator = pluginId; } else if (plugin.source.type === 'local') { locator = plugin.source.path; } else { // assume its git locator = plugin.source.url; } pluginXmlPath = path.join(appDirectory, 'plugins', pluginId, 'plugin.xml'); pluginXml = State.getXmlData(pluginXmlPath); preferences = pluginXml.plugin.preference; // reset variables on each plugin before checking to see if we have preferences/variables hasVariables = false; variableList = []; if (preferences && preferences.length > 0) { hasVariables = true; variableList = preferences.map(function(preference) { return preference.$.name; }); } keyValueList = {}; if (hasVariables) { // log.info('we have avariables to look at:', variableList) // var features = configXmlData.widget.feature; // events.emit('log', 'features', features) var pluginPreferences = State.getPluginPreferences(fetchJson, pluginId); // why we are searching for plugin params from the config xml? // featureParams = State.getPluginParameters(configXmlData, pluginName); // features.forEach(function(potFeature) { // if(potFeature.$.name == pluginName) { // feature = potFeature // } // }) // log.info('feature found:', feature); // var featureParams = feature.param; variableList.forEach(function(variable) { // events.emit('log', 'Looking up variable:', variable) // in package json we are saving only the variables not some features /* for (var i = 0, j = featureParams.length; i < j; i++) { if (variable == featureParams[i].$.name) { keyValueList[variable] = featureParams[i].$.value; } } */ if (pluginPreferences[variable]) { keyValueList[variable] = pluginPreferences[variable]; } }); // log.info('We have the plugin parameters with values:', keyValueList) var pluginObj = { id: pluginId, locator: locator, variables: keyValueList }; cordovaPlugins.push(pluginObj); } else if (plugin.source.type === 'git' || plugin.source.type === 'local') { cordovaPlugins.push({ locator: locator, id: pluginId }); } else { cordovaPlugins.push(pluginId); } } } // closes for loop for fetch.json packageJson.cordovaPlugins = cordovaPlugins; } else { log.error('There was no fetch.json file available to restore plugin information from.'); log.error('Restoring plugins from scanning the plugins folder is still being worked on.'); } }; // Closes saveExistingPlugins State.getXmlData = function getXmlData(xmlPath) { var xml2js = require('xml2js'); var xmlString = fs.readFileSync(xmlPath, { encoding: 'utf8' }); var xmlData; var parseString = xml2js.parseString; parseString(xmlString, function(err, jsonConfig) { if (err) { return Utils.fail('Error parsing xml file: ' + err); } try { xmlData = jsonConfig; } catch (e) { return Utils.fail('Error parsing ' + xmlPath + ': ' + e); } }); return xmlData; }; State.getPlatformVersion = function getPlatformVersion(path) { var result = shelljs.exec(['node', path].join(' '), { silent: true }); if (result.code !== 0) { return ''; } var version = result.output; // log.info('Version for path:', path, version) return version.replace(/\n/g, ''); }; State.addOrUpdatePlatformToPackageJson = function addOrUpdatePlatformToPackageJson(packageJson, platformId, platformInfo) { // var platformExists = _.findWhere(packageJson.cordovaPlatforms, {platform: platformId}); var existingPlatform; if (typeof platformInfo === 'undefined') { platformInfo = platformId; } var existingPlatformIndex; for (var i = 0, j = packageJson.cordovaPlatforms.length; i < j; i += 1) { if (typeof packageJson.cordovaPlatforms[i] == 'string' && packageJson.cordovaPlatforms[i] === platformId) { existingPlatform = packageJson.cordovaPlatforms[i]; existingPlatformIndex = i; break; } else if (packageJson.cordovaPlatforms[i].platform === platformId) { existingPlatform = packageJson.cordovaPlatforms[i]; existingPlatform.locator = platformInfo.locator; existingPlatformIndex = i; break; } } if (!existingPlatform) { packageJson.cordovaPlatforms.push(platformInfo); } else if (platformInfo) { log.debug('A platform already exists - now updating the entry:', existingPlatform, platformInfo); packageJson.cordovaPlatforms[existingPlatformIndex] = platformInfo; } // log.info('platformExists:', platformExists) // if (!platformExists && requiresLocator) { // packageJson.cordovaPlatforms.push({platform: platform, locator: locator}); // // packageJson.cordovaPlatforms.push({platform: platform, version: version, locator: locator}); // } else if (!platformExists && !requiresLocator) { // packageJson.cordovaPlatforms.push(platform); // } else { // platformExists.platform = platform // platformExists.locator = locator // } }; State.savePlatform = function savePlatform(appDirectory, locator) { log.debug('State.savePlatform', appDirectory, locator); // Locator may be: // Name: ios, android // Name with version: ios@3.8.0, android@4.0.0 // Local path: ./engine/cordova-android-c0.6.1 // Http url: https://github.com/apache/cordova-android.git // log.info('platform args:', platformArgs); // var locator = platformArgs._[2]; var platform = 'ios'; var version; var packageJson = State.getPackageJson(appDirectory); // Test to see if its just ios or android if (locator === 'ios' || locator === 'android') { platform = locator; State.addOrUpdatePlatformToPackageJson(packageJson, platform); return State.savePackageJson(appDirectory, packageJson); } if (locator.indexOf('@') !== -1) { // platform add ios@3.8.0 var locatorSplits = locator.split('@'); platform = locatorSplits[0]; version = locatorSplits[1]; } else { // platform add ./some/path/ios platform = locator.indexOf('ios') !== -1 ? 'ios' : 'android'; } var platformInfo = { platform: platform, version: version, locator: locator }; State.addOrUpdatePlatformToPackageJson(packageJson, platform, platformInfo); State.savePackageJson(appDirectory, packageJson); }; State.savePackageJson = function savePackageJson(appDirectory, packageJsonData) { log.debug('State.savePackageJson', appDirectory, packageJsonData); try { var packageJsonPath = path.join(appDirectory, 'package.json'); fs.writeFileSync(packageJsonPath, JSON.stringify(packageJsonData, null, 2) + '\n'); } catch (e) { log.error(('Error saving ' + packageJsonPath + ': %s').bold, e, {}); } }; State.restoreState = function restoreState(appDirectory, options) { log.debug('State.restoreState', appDirectory, options); if (!options || !appDirectory) { throw 'You must pass an application directory and options to restore state.'; } if (!options.platforms && !options.plugins) { log.error('You must specify either platforms or plugins to restore the state of your Ionic application.'); return; } log.info('Attempting to restore your Ionic application from package.json\n'); // var packageJsonPath = path.join(appDirectory, 'package.json'); // var packageJson = require(packageJsonPath); var packageJson = State.getPackageJson(appDirectory); var restorePromise; if (options.platforms) { log.info('Restoring Platforms\n'); restorePromise = State.restorePlatforms(appDirectory, packageJson) .then(function() { log.info('\nRestore platforms is complete\n'); }); } else { restorePromise = Q(); } return restorePromise .then(function() { if (options.plugins) { log.info('\rRestoring Plugins\n'); return State.restorePlugins(appDirectory, packageJson) .then(function() { log.info('Restore plugins is complete\n'); }); } else { return Q(); } }) .then(function() { log.info('Ionic state restore completed\n'); }) .catch(function(ex) { log.error('Ionic state restore failed\n'); log.error(ex); }); }; State.restorePlugins = function restorePlugins(appDirectory, packageJson) { log.debug('State.restorePlugins', appDirectory, packageJson); var q = Q.defer(); State.processPlugin(appDirectory, 0, packageJson, q); return q.promise; }; State.restorePlatforms = function restorePlatforms(appDirectory, packageJson) { log.debug('State.restorePlatforms', appDirectory, packageJson); var q = Q.defer(); State.processPlatform(appDirectory, 0, packageJson, q); return q.promise; }; State.processPlatform = function processPlatform(appDirectory, index, packageJson, promise) { if (index >= packageJson.cordovaPlatforms.length) { promise.resolve(); return; } try { // log.info('processing platform', index, packageJson) var platform = packageJson.cordovaPlatforms[index]; var platformCommand; if (typeof platform == 'string') { platformCommand = ['cordova platform add ', platform].join(''); } else { // Here, they have either a special version, or locator for // local install or github install platformCommand = 'cordova platform add ' + platform.locator; } // var platformCommand = State.createAddRemoveStatement(platform); log.info(platformCommand); shelljs.exec(platformCommand, function() { State.processPlatform(appDirectory, index + 1, packageJson, promise); }); } catch (ex) { log.error('An error happened processing the previous cordova plugins'); log.error(ex); promise.reject(ex); } }; State.createAddRemoveStatement = function createAddRemoveStatement(plugin) { // log.info('Creating add/remove statement', plugin) try { var pluginCmd = 'cordova plugin add '; if (typeof plugin === 'string') { pluginCmd += plugin; } else { pluginCmd += plugin.locator + ' '; if (plugin.variables) { Object.keys(plugin.variables).forEach(function(variable) { pluginCmd += '--variable ' + variable + '="' + plugin.variables[variable] + '" '; }); } } } catch (ex) { log.error('Failed to create add plugin statement: %s', ex, {}); } // log.info('plugin cmd', pluginCmd) return pluginCmd; }; State.processPlugin = function processPlugin(appDirectory, index, packageJson, promise) { if (index >= packageJson.cordovaPlugins.length) { promise.resolve(); return; } try { // log.info('processing plugin', index, packageJson) var plugin = packageJson.cordovaPlugins[index]; var pluginCommand = State.createAddRemoveStatement(plugin); log.info(pluginCommand); shelljs.exec(pluginCommand, { async: true }, function(code, output) { if (code !== 0) { throw 'Error executing "' + pluginCommand + '":\n' + output; } State.processPlugin(appDirectory, index + 1, packageJson, promise); }); } catch (ex) { log.error('An error happened processing the previous cordova plugins'); log.error(ex); promise.reject(ex); } }; State.removePlatform = function removePlatform(appDirectory, platform) { // log.info('Args:', platformArgs); // Expecting - ionic platform remove [ios|android] // TODO - check if they pass multiple platforms // ionic platform remove ios android var packageJson = State.getPackageJson(appDirectory); var platformEntry; for (var i = 0, j = packageJson.cordovaPlatforms.length; i < j; i += 1) { platformEntry = packageJson.cordovaPlatforms[i]; if (typeof platformEntry === 'string' && platformEntry === platform) { packageJson.cordovaPlatforms.splice(i, 1); break; } else if (platformEntry.platform === platform) { // Its object {platform: 'android', locator: './engine/cordova-android'} packageJson.cordovaPlatforms.splice(i, 1); break; } } State.savePackageJson(appDirectory, packageJson); }; State.getPluginParameters = function getPluginParameters(configXmlData, pluginName) { if (!configXmlData || !configXmlData.widget || !configXmlData.widget.feature) { throw 'Invalid Config XML Data'; } var features = configXmlData.widget.feature; var feature; features.forEach(function(potFeature) { if (potFeature.$.name === pluginName) { feature = potFeature; } }); if (feature && feature.param) { return feature.param; } return null; }; State.getPluginPreferences = function getPluginPreferences(fetchJson, pluginName) { if (!fetchJson || !fetchJson[pluginName]) { throw 'Invalid fetch.json Data'; } var preferences = fetchJson[pluginName].variables; if (Object.keys(preferences).length !== 0) { return preferences; } return null; }; // /** * Used after `ionic plugin add <id|locator>` to save the plugin * to package.json and config.xml. * @pluginArgs {Object} contains the command line args passed * from the ionic command. * { _: [ 'plugin', 'add', '../phonegap-facebook-plugin' ], variable: [ 'APP_ID=123456789', 'APP_NAME=myApplication' ], '$0': '/usr/local/bin/ionic' } */ State.savePlugin = function savePlugin(appDirectory, pluginId, variables) { log.debug('State.savePlugin - appDirectory:', appDirectory, 'pluginId', pluginId, 'variables', variables); // Expects - either simple ID for plugin registry // or a local path, with or without variables // ionic plugin add org.apache.cordova.splashscreen // ionic plugin add ../phonegap-facebook-plugin --variable APP_ID="123456789" --variable APP_NAME="myApplication" // With a local file locator, we can look at the plugin.xml as it exists // If its a git resource locator, we'll have to look at fetch.json // Idea: this is run after platform add/rm is done var packageJson = State.getPackageJson(appDirectory); var pluginInfo = {}; // This is necessary to ensure variables is actually an array if (typeof variables == 'string') { variables = [variables]; } // Check and save for variables if (variables) { pluginInfo.variables = {}; for (var i = 0, j = variables.length; i < j; i += 1) { // variable: [ 'APP_ID=123456789', 'APP_NAME=myApplication' ] var splits = variables[i].split('='); pluginInfo.variables[splits[0]] = splits[1]; } } // pluginId could be org.ionic.keyboard or ./engine/facebook-plugin if (pluginId.indexOf('/') === -1) { // its just an id // Check and save for variables if (!variables) { State.addOrUpdatePluginToPackageJson(packageJson, pluginId); } else { pluginInfo.locator = pluginId; pluginInfo.id = pluginId = State.getPluginFromFetchJsonByLocator(appDirectory, pluginInfo.locator); State.addOrUpdatePluginToPackageJson(packageJson, pluginId, pluginInfo); } return State.savePackageJson(appDirectory, packageJson); } // By here, we know its not a registry plugin (git/local) // Its a locator, local file path or https://github.com repo pluginInfo.locator = pluginId; pluginInfo.id = pluginId = State.getPluginFromFetchJsonByLocator(appDirectory, pluginInfo.locator); // If there are no variables, we just add to package, then save if (!variables) { State.addOrUpdatePluginToPackageJson(packageJson, pluginId, pluginInfo); return State.savePackageJson(appDirectory, packageJson); } State.addOrUpdatePluginToPackageJson(packageJson, pluginId, pluginInfo); log.info('Save plugin to package.json completed'); // By now we assume pluginId is set, and locator might be set. return State.savePackageJson(appDirectory, packageJson); }; State.removePlugin = function removePlugin(appDirectory, pluginId) { log.debug('State.removePlugin - ', appDirectory, pluginId); var packageJson = State.getPackageJson(appDirectory); for (var i = 0, j = packageJson.cordovaPlugins.length; i < j; i += 1) { if (typeof packageJson.cordovaPlugins[i] == 'string' && packageJson.cordovaPlugins[i] === pluginId) { packageJson.cordovaPlugins.splice(i, 1); break; } else if (packageJson.cordovaPlugins[i].id === pluginId) { packageJson.cordovaPlugins.splice(i, 1); break; } } State.savePackageJson(appDirectory, packageJson); }; State.saveXmlFile = function saveXmlFile(xmlData, xmlPath) { try { var xml2js = require('xml2js'); var xmlBuilder = new xml2js.Builder(); var configString = xmlBuilder.buildObject(xmlData); fs.writeFileSync(xmlPath, configString); } catch (ex) { log.error('Could not save your xml file to path:', xmlPath); } }; State.getPluginFromFetchJsonByLocator = function getPluginFromFetchJsonByLocator(appDirectory, pluginLocator) { var fetchJson = require(path.join(appDirectory, 'plugins', 'fetch.json')); var lookupId; var lookupPlugin; var isNotRegistyPlugin; var hasUrlOrPathOfLocator; var pluginId; if (pluginLocator.indexOf('#') !== -1) { // We have https://github.com/apache/cordova-plugin-whitelist.git#r1.0.0 var pluginSplits = pluginLocator.split('#'); pluginLocator = pluginSplits[0]; } lookupPlugin = fetchJson[pluginLocator]; if (lookupPlugin && lookupPlugin.source && lookupPlugin.source.id === pluginLocator) { return pluginLocator; } for (lookupId in fetchJson) { if (fetchJson.hasOwnProperty(lookupId)) { lookupPlugin = fetchJson[lookupId]; isNotRegistyPlugin = lookupPlugin.source.type && lookupPlugin.source.type !== 'registry'; hasUrlOrPathOfLocator = lookupPlugin.source.path === pluginLocator || lookupPlugin.source.url === pluginLocator; var hasPathWithLeadingDot = lookupPlugin.source && typeof lookupPlugin.source === 'string' && lookupPlugin.source.replace('./', '') === pluginLocator; var hasVariables = Object.keys(lookupPlugin.variables).length !== 0; if ((isNotRegistyPlugin && hasUrlOrPathOfLocator) || (isNotRegistyPlugin && hasPathWithLeadingDot) || (!isNotRegistyPlugin && !hasUrlOrPathOfLocator && !hasPathWithLeadingDot && hasVariables)) { pluginId = lookupId; break; } } } return pluginId; }; State.resetState = function resetState(appDirectory, options) { var platformPath = path.join(appDirectory, 'platforms'); var pluginPath = path.join(appDirectory, 'plugins'); shelljs.rm('-rf', [platformPath, pluginPath]); log.info('Removed platforms and plugins'); State.restoreState(appDirectory, options) .then(function() { log.info('Ionic reset state complete'); }); }; State.clearState = function clearState(appDirectory) { log.info('Clearing out your Ionic app of platforms, plugins, and package.json entries'); var platformPath = path.join(appDirectory, 'platforms'); var pluginPath = path.join(appDirectory, 'plugins'); shelljs.rm('-rf', [platformPath, pluginPath]); var packageJson = State.getPackageJson(appDirectory); packageJson.cordovaPlatforms = packageJson.cordovaPlugins = []; State.savePackageJson(appDirectory, packageJson); log.info('Ionic app state cleared'); }; State.getPluginsFromFetchJson = function getPluginsFromFetchJson(appDirectory) { var fetchJson; var pluginId; var installedPlugins = {}; var pluginPath; var pluginPathStats; var pluginXmlPath; var pluginXml; var pluginDescription; var pluginName; try { fetchJson = require(path.join(appDirectory, 'plugins', 'fetch.json')); } catch (ex) { // Error - no fetch.json exists. return []; } try { for (pluginId in fetchJson) { if (fetchJson.hasOwnProperty(pluginId)) { try { pluginPath = path.join(appDirectory, 'plugins', pluginId); pluginPathStats = fs.statSync(pluginPath); } catch (ex) { log.info('Plugin ' + pluginId + ' does not exist in the plugins directory. Skipping'); continue; } if (pluginPathStats && !pluginPathStats.isDirectory()) { log.info('Plugin ' + pluginId + ' does not exist in the plugins directory. Skipping'); continue; } pluginXmlPath = path.join(appDirectory, 'plugins', pluginId, 'plugin.xml'); pluginXml = State.getXmlData(pluginXmlPath); pluginDescription = pluginXml.plugin.description[0]; pluginName = pluginXml.plugin.name[0]; var displayText = pluginDescription.length >= 40 ? pluginName : pluginDescription; installedPlugins[pluginId] = { id: pluginId, name: pluginName, description: pluginDescription, displayText: displayText }; } } // closes for loop for fetch.json } catch (ex) { Utils.fail('Error occurred retrieving installed plugins', ex); } return installedPlugins; };