From a8ab4e561820087bd28b8c638fcceb97721bb418 Mon Sep 17 00:00:00 2001 From: Nick Date: Wed, 22 Mar 2017 16:40:29 -0400 Subject: [PATCH] Created update_platform_config.js --- update_platform_config.js | 372 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 372 insertions(+) create mode 100644 update_platform_config.js diff --git a/update_platform_config.js b/update_platform_config.js new file mode 100644 index 0000000..c40f7aa --- /dev/null +++ b/update_platform_config.js @@ -0,0 +1,372 @@ +#!/usr/bin/env node + +/** This hook updates platform configuration files based on preferences and config-file data defined in config.xml. + Currently only the AndroidManifest.xml and IOS *-Info.plist file are supported. + + See http://stackoverflow.com/questions/28198983/ionic-cordova-add-intent-filter-using-config-xml + + Preferences: + 1. Preferences defined outside of the platform element will apply to all platforms + 2. Preferences defined inside a platform element will apply only to the specified platform + 3. Platform preferences take precedence over common preferences + 4. The preferenceMappingData object contains all of the possible custom preferences to date including the + target file they belong to, parent element, and destination element or attribute + + Config Files + 1. config-file elements MUST be defined inside a platform element, otherwise they will be ignored. + 2. config-file target attributes specify the target file to update. (AndroidManifest.xml or *-Info.plist) + 3. config-file parent attributes specify the parent element (AndroidManifest.xml) or parent key (*-Info.plist) + that the child data will replace or be appended to. + 4. config-file elements are uniquely indexed by target AND parent for each platform. + 5. If there are multiple config-file's defined with the same target AND parent, the last config-file will be used + 6. Elements defined WITHIN a config-file will replace or be appended to the same elements relative to the parent element + 7. If a unique config-file contains multiples of the same elements (other than uses-permission elements which are + selected by by the uses-permission name attribute), the last defined element will be retrieved. + + Examples: + + AndroidManifest.xml + NOTE: For possible manifest values see http://developer.android.com/guide/topics/manifest/manifest-intro.html + + + //These preferences are actually available in Cordova by default although not currently documented + + + + + //custom preferences examples + + + + + + + + + + + + + + + + *-Info.plist + + + + + UIInterfaceOrientationLandscapeOmg + + + + + someValue + + + + NOTE: Currently, items aren't removed from the platform config files if you remove them from config.xml. + For example, if you add a custom permission, build the remove it, it will still be in the manifest. + If you make a mistake, for example adding an element to the wrong parent, you may need to remove and add your platform, + or revert to your previous manifest/plist file. + + TODO: We may need to capture all default manifest/plist elements/keys created by Cordova along with any plugin elements/keys to compare against custom elements to remove. + */ + +// global vars +var fs = require('fs'); +var path = require('path'); +var _ = require('lodash'); +var et = require('elementtree'); +var plist = require('plist'); + +var rootdir = path.resolve(__dirname, '../../'); + +var platformConfig = (function(){ + /* Global object that defines the available custom preferences for each platform. + Maps a config.xml preference to a specific target file, parent element, and destination attribute or element + */ + var preferenceMappingData = { + 'android': { + 'android-manifest-hardwareAccelerated': {target: 'AndroidManifest.xml', parent: './', destination: 'android:hardwareAccelerated'}, + 'android-installLocation': {target: 'AndroidManifest.xml', parent: './', destination: 'android:installLocation'}, + 'android-activity-hardwareAccelerated': {target: 'AndroidManifest.xml', parent: 'application', destination: 'android:hardwareAccelerated'}, + 'android-configChanges': {target: 'AndroidManifest.xml', parent: 'application/activity[@android:name=\'CordovaApp\']', destination: 'android:configChanges'}, + 'android-launchMode': {target: 'AndroidManifest.xml', parent: 'application/activity[@android:name=\'CordovaApp\']', destination: 'android:launchMode'}, + 'android-theme': {target: 'AndroidManifest.xml', parent: 'application/activity[@android:name=\'CordovaApp\']', destination: 'android:theme'}, + 'android-windowSoftInputMode': {target: 'AndroidManifest.xml', parent: 'application/activity[@android:name=\'CordovaApp\']', destination: 'android:windowSoftInputMode'} + }, + 'ios': {} + }; + + /* Global object that defines tags that should be added and not replaced + */ + var multipleTags = { + 'android': ['intent-filter'], + 'ios': [] + }; + + var configXmlData, preferencesData; + + return { + // Parses a given file into an elementtree object + parseElementtreeSync: function (filename) { + var contents = fs.readFileSync(filename, 'utf-8'); + if(contents) { + //Windows is the BOM. Skip the Byte Order Mark. + contents = contents.substring(contents.indexOf('<')); + } + return new et.ElementTree(et.XML(contents)); + }, + + // Converts an elementtree object to an xml string. Since this is used for plist values, we don't care about attributes + eltreeToXmlString: function (data) { + var tag = data.tag; + var el = '<' + tag + '>'; + + if(data.text && data.text.trim()) { + el += data.text.trim(); + } else { + _.each(data.getchildren(), function (child) { + el += platformConfig.eltreeToXmlString(child); + }); + } + + el += ''; + return el; + }, + + // Parses the config.xml into an elementtree object and stores in the config object + getConfigXml: function () { + if(!configXmlData) { + configXmlData = this.parseElementtreeSync(path.join(rootdir, 'config.xml')); + } + + return configXmlData; + }, + + /* Retrieves all from config.xml and returns a map of preferences with platform as the key. + If a platform is supplied, common prefs + platform prefs will be returned, otherwise just common prefs are returned. + */ + getPreferences: function (platform) { + var configXml = this.getConfigXml(); + + //init common config.xml prefs if we haven't already + if(!preferencesData) { + preferencesData = { + common: configXml.findall('preference') + }; + } + + var prefs = preferencesData.common || []; + if(platform) { + if(!preferencesData[platform]) { + preferencesData[platform] = configXml.findall('platform[@name=\'' + platform + '\']/preference'); + } + prefs = prefs.concat(preferencesData[platform]); + } + + return prefs; + }, + + /* Retrieves all configured xml for a specific platform/target/parent element nested inside a platforms config-file + element within the config.xml. The config-file elements are then indexed by target|parent so if there are + any config-file elements per platform that have the same target and parent, the last config-file element is used. + */ + getConfigFilesByTargetAndParent: function (platform) { + var configFileData = this.getConfigXml().findall('platform[@name=\'' + platform + '\']/config-file'); + return _.keyBy(configFileData, function(item) { + + var parent = item.attrib.parent; + //if parent attribute is undefined /* or */, set parent to top level elementree selector + if(!parent || parent === '/*' || parent === '*/') { + parent = './'; + } + return item.attrib.target + '|' + parent; + }); + }, + + /** + * Check if a tag can be used multiple times in config + */ + isMultipleTag: function(platform, tag) { + var platformMultipleTags = multipleTags[platform]; + if (platformMultipleTags) { + var isInArray = (platformMultipleTags.indexOf(tag) >= 0); + return isInArray; + } else { + return false; + } + }, + + // Parses the config.xml's preferences and config-file elements for a given platform + parseConfigXml: function (platform) { + var configData = {}; + this.parsePreferences(configData, platform); + this.parseConfigFiles(configData, platform); + + return configData; + }, + + // Retrieves th e config.xml's pereferences for a given platform and parses them into JSON data + parsePreferences: function (configData, platform) { + var preferences = this.getPreferences(platform), + type = 'preference'; + + _.each(preferences, function (preference) { + var prefMappingData = preferenceMappingData[platform][preference.attrib.name], + target, + prefData; + + if (prefMappingData) { + prefData = { + parent: prefMappingData.parent, + type: type, + destination: prefMappingData.destination, + data: preference + }; + + target = prefMappingData.target; + if(!configData[target]) { + configData[target] = []; + } + configData[target].push(prefData); + } + }); + }, + + // Retrieves the config.xml's config-file elements for a given platform and parses them into JSON data + parseConfigFiles: function (configData, platform) { + + var configFiles = this.getConfigFilesByTargetAndParent(platform), + type = 'configFile'; + + _.each(configFiles, function (configFile, key) { + var keyParts = key.split('|'); + var target = keyParts[0]; + var parent = keyParts[1]; + var items = configData[target] || []; + + _.each(configFile.getchildren(), function (element) { + items.push({ + parent: parent, + type: type, + destination: element.tag, + data: element + }); + }); + + configData[target] = items; + }); + }, + + // Parses config.xml data, and update each target file for a specified platform + updatePlatformConfig: function (platform) { + var configData = this.parseConfigXml(platform), + platformPath = path.join(rootdir, 'platforms', platform); + _.each(configData, function (configItems, targetFileName) { + var projectName, targetFile; + + if (platform === 'ios' && targetFileName.indexOf("Info.plist") > -1) { + projectName = platformConfig.getConfigXml().findtext('name'); + targetFile = path.join(platformPath, projectName, projectName + '-Info.plist'); + platformConfig.updateIosPlist(targetFile, configItems); + } else if (platform === 'android' && targetFileName === 'AndroidManifest.xml') { + targetFile = path.join(platformPath, targetFileName); + platformConfig.updateAndroidManifest(targetFile, configItems); + } + }); + }, + + // Updates the AndroidManifest.xml target file with data from config.xml + updateAndroidManifest: function (targetFile, configItems) { + var tempManifest = platformConfig.parseElementtreeSync(targetFile), + root = tempManifest.getroot(); + + _.each(configItems, function (item) { + // if parent is not found on the root, child/grandchild nodes are searched + var parentEl = root.find(item.parent) || root.find('*/' + item.parent), + data = item.data, + childSelector = item.destination, + childEl; + + if(!parentEl) { + return; + } + + if(item.type === 'preference') { + parentEl.attrib[childSelector] = data.attrib['value']; + } else { + // since there can be multiple uses-permission elements, we need to select them by unique name + if(childSelector === 'uses-permission') { + childSelector += '[@android:name=\'' + data.attrib['android:name'] + '\']'; + } + + childEl = parentEl.find(childSelector); + + // if child element doesnt exist, create new element + + var isMultipleTag = platformConfig.isMultipleTag('android', childSelector); + if(!childEl || isMultipleTag) { + childEl = new et.Element(item.destination); + parentEl.append(childEl); + } + + // copy all config.xml data except for the generated _id property + _.each(data, function (prop, propName) { + if(propName !== '_id') { + childEl[propName] = prop; + } + }); + } + }); + + fs.writeFileSync(targetFile, tempManifest.write({indent: 4}), 'utf-8'); + }, + + /* Updates the *-Info.plist file with data from config.xml by parsing to an xml string, then using the plist + module to convert the data to a map. The config.xml data is then replaced or appended to the original plist file + */ + updateIosPlist: function (targetFile, configItems) { + var infoPlist = plist.parse(fs.readFileSync(targetFile, 'utf-8')), + tempInfoPlist; + + _.each(configItems, function (item) { + var key = item.parent; + var plistXml = '' + key + ''; + plistXml += platformConfig.eltreeToXmlString(item.data) + ''; + + var configPlistObj = plist.parse(plistXml); + infoPlist[key] = configPlistObj[key]; + }); + + tempInfoPlist = plist.build(infoPlist); + tempInfoPlist = tempInfoPlist.replace(/[\s\r\n]*<\/string>/g,''); + fs.writeFileSync(targetFile, tempInfoPlist, 'utf-8'); + } + }; +})(); + +// Main +(function () { + if (rootdir) { + // go through each of the platform directories that have been prepared + var platforms = _.filter(fs.readdirSync('platforms'), function (file) { + return fs.statSync(path.resolve('platforms', file)).isDirectory(); + }); + + _.each(platforms, function (platform) { + try { + platform = platform.trim().toLowerCase(); + + platformConfig.updatePlatformConfig(platform); + + } catch (e) { + process.stdout.write(e); + } + }); + } +})();