-
-
Save marcocarnazzo/6f01a57d390e8fe3071f to your computer and use it in GitHub Desktop.
| #!/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 | |
| <platform name="android"> | |
| //These preferences are actually available in Cordova by default although not currently documented | |
| <preference name="android-minSdkVersion" value="8" /> | |
| <preference name="android-maxSdkVersion" value="19" /> | |
| <preference name="android-targetSdkVersion" value="19" /> | |
| //custom preferences examples | |
| <preference name="android-windowSoftInputMode" value="stateVisible" /> | |
| <preference name="android-installLocation" value="auto" /> | |
| <preference name="android-launchMode" value="singleTop" /> | |
| <preference name="android-activity-hardwareAccelerated" value="false" /> | |
| <preference name="android-manifest-hardwareAccelerated" value="false" /> | |
| <preference name="android-configChanges" value="orientation" /> | |
| <preference name="android-theme" value="@android:style/Theme.Black.NoTitleBar" /> | |
| <config-file target="AndroidManifest.xml" parent="/*> | |
| <supports-screens | |
| android:xlargeScreens="false" | |
| android:largeScreens="false" | |
| android:smallScreens="false" /> | |
| <uses-permission android:name="android.permission.READ_CONTACTS" android:maxSdkVersion="15" /> | |
| <uses-permission android:name="android.permission.WRITE_CONTACTS" /> | |
| </config-file> | |
| </platform> | |
| *-Info.plist | |
| <platform name="ios"> | |
| <config-file platform="ios" target="*-Info.plist" parent="UISupportedInterfaceOrientations"> | |
| <array> | |
| <string>UIInterfaceOrientationLandscapeOmg</string> | |
| </array> | |
| </config-file> | |
| <config-file platform="ios" target="*-Info.plist" parent="SomeOtherPlistKey"> | |
| <string>someValue</string> | |
| </config-file> | |
| </platform> | |
| 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. | |
| == ABOUT THIS CODE == | |
| Original code was written by Devin Jett ( https://github.com/djett41 ) | |
| and then modified by Marco Carnazzo ( https://github.com/marcocarnazzo ). | |
| This hook is in public domain. | |
| */ | |
| // 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 += '</' + tag + '>'; | |
| 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 <preferences ..> 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 = '<plist><dict><key>' + key + '</key>'; | |
| plistXml += platformConfig.eltreeToXmlString(item.data) + '</dict></plist>'; | |
| var configPlistObj = plist.parse(plistXml); | |
| infoPlist[key] = configPlistObj[key]; | |
| }); | |
| tempInfoPlist = plist.build(infoPlist); | |
| tempInfoPlist = tempInfoPlist.replace(/<string>[\s\r\n]*<\/string>/g,'<string></string>'); | |
| 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); | |
| } | |
| }); | |
| } | |
| })(); |
With the last node version (5.9.1) i get this error:
`net.js:625
throw new TypeError('invalid data');
^
TypeError: invalid data
at Socket.write (net.js:625:11)
at /Applications/MAMP/htdocs/Didiha/hooks/after_platform_add/030_update_platform_config.js:366:32
at arrayEach (/Applications/MAMP/htdocs/Didiha/node_modules/lodash/index.js:1289:13)
at Function. (/Applications/MAMP/htdocs/Didiha/node_modules/lodash/index.js:3345:13)
at /Applications/MAMP/htdocs/Didiha/hooks/after_platform_add/030_update_platform_config.js:361:11
at Object. (/Applications/MAMP/htdocs/Didiha/hooks/after_platform_add/030_update_platform_config.js:370:3)
at Module._compile (module.js:413:34)
at Object.Module._extensions..js (module.js:422:10)
at Module.load (module.js:357:32)
at Function.Module._load (module.js:314:12)
Error: Hook failed with error code 1: /Applications/MAMP/htdocs/Didiha/hooks/after_platform_add/030_update_platform_config.js
`
This doesn't happens if i remove these lines from my config file:
<config-file platform="ios" target="*-Info.plist" parent="NSAppTransportSecurity"> <dict> <key>NSAllowsArbitraryLoads</key> <true /> </dict> </config-file> <config-file platform="ios" target="*-Info.plist" parent="CFBundleDevelopmentRegion"> <string>Italy</string> </config-file>
This is awesome - thank you! @mtshare, you need to make the change mentioned by @ceoaliongroo and @pranit21 to make this work.
Can we put this under source control again? It would be helpful to be able to submit pull requests.
_.indexBy has been replaced by _.keyBy in lodash by the way
This is not working in Xcode8 + iOS10.
I am unable to find the exact bug, but the app doesn't start if this script is running and changing the plist file.
If I remove this script the app runs fine.
Also, the custom config that I am trying to add with this script, if I add the exact same value manually in the plist file via editor - the app works just fine.
I suspect something is wrong with respect to the Xcode8 !
@sur I think I may have the same issue. However it's not xCode, but how the script transforms the -info.plist file. For me it seems to be converting
<key>NSMainNibFile</key> <string/> <key>NSMainNibFile~ipad</key> <string/>
to
<key>NSMainNibFile</key> <string>NSMainNibFile~ipad</string>
Which crashed the app when you run it on the device. The issue is with [email protected].* (see issue: TooTallNate/plist.js#79). Workaround: Just roll back to use [email protected].
@halyburton Thanks for the hint! There seems to be something wrong with parsing ~ characters, but I got no idea where that comes from. In the end I just wrote a script to add my plist stuff to the platforms/ios/<project>/<project>-Info.plist file.
This hook helped me a lot though!
@marcocarnazzo this hook was very helpful, thank you! As others have noted I needed to change _.indexBy to _.keyBy in the getConfigFilesByTargetAndParent method. After that my plist is perfect, no more needing to edit manually!
+1 that this should be in repo so it can be edited and updated
I have changed the indexBy, added the element to the widget tag and removed entries in config.xml as suggested, but is still getting
net.js:655
throw new TypeError(
^
TypeError: Invalid data, chunk must be a string or buffer, not object
at Socket.write (net.js:655:11)
at D:\Dokumenter\GitHub\Geme\hooks\after_prepare\030_update_platform_config.js:366:32
at arrayEach (D:\Dokumenter\GitHub\Geme\node_modules\lodash\lodash.js:537:11)
at Function.forEach (D:\Dokumenter\GitHub\Geme\node_modules\lodash\lodash.js:9359:14)
at D:\Dokumenter\GitHub\Geme\hooks\after_prepare\030_update_platform_config.js:361:11
at Object. (D:\Dokumenter\GitHub\Geme\hooks\after_prepare\030_update_platform_config.js:370:3)
at Module._compile (module.js:570:32)
at Object.Module._extensions..js (module.js:579:10)
at Module.load (module.js:487:32)
at tryModuleLoad (module.js:446:12)
Error: Hook failed with error code 1: D:\Dokumenter\GitHub\Geme\hooks\after_prepare\030_update_platform_config.js
Any other ideas
@ThorvaldAagaard your error is related to @AgDude's comment about error reporting. You're not seeing the original error. Change the code as per @AgDude's instructions and you will see the actual error.
My AndroidManifest xml not being modified 😢
This works for me but only after I make these changes, and noting some things that are somehow implicitly assumed
- needed to change _.indexBy => _.keyBy
- looks like this script is expected to be placed in the same location as config.xml. if you place this script in a folder that is not the same as the config.xml file you need to update the rootdir location
- @AgDude's suggestion is required, otherwise it won't work at all, stdout.write(e.toString() + '\n');
I only do ios build for now so can't verify for Android yet
I'm so sorry!
GitHub does not notify by about your comments.
I just edited this hook as you suggest (thank you!).
I also write something about re-use of this code (public domain: use it as you like!).
Just a note: I'm not using Ionic anymore so this code is unmantained.
Sorry again.
👍 @ceoallongroo, It does not work for me without your proposed change:
_.indexBy => _.keyBy