cordova-config-utils
Version:
Modify your config.xml after cordova prepare
324 lines (279 loc) • 11.6 kB
JavaScript
var fs = require('fs');
var path = require('path');
var _ = require('lodash');
var et = require('elementtree');
var plist = require('plist');
var xcode = require('xcode');
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': {}
};
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;
});
},
// 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 the 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';
if (!preferenceMappingData[platform]) {
return;
}
_.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 === 'ios' && targetFileName === 'project.pbxproj') {
projectName = platformConfig.getConfigXml().findtext('name');
targetFile = path.join(platformPath, projectName + '.xcodeproj', 'project.pbxproj');
platformConfig.updateIosXcodeproj(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
if (!childEl) {
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');
},
/* Updates the *.xcodeproj/project.pbxproj file with data from config.xml using the npm 'xcode' module. */
updateIosXcodeproj: function(targetFile, configItems) {
var xcodeProject = xcode.project(targetFile);
xcodeProject.parse(function(err) {
if (err) {
throw new Error('Failed to parse ' + targetFile + ': ' + JSON.stringify(err));
}
_.each(configItems, function(item) {
if (item.data.attrib && item.data.attrib.name && item.data.attrib.value) {
xcodeProject.addBuildProperty(item.data.attrib.name, item.data.attrib.value);
}
});
fs.writeFileSync(targetFile, xcodeProject.writeSync(), 'utf-8');
});
}
};
})();
// Main
(function() {
if (rootdir) {
// go through each of the platform directories that have been prepared
var platforms = (process.env.CORDOVA_PLATFORMS ? process.env.CORDOVA_PLATFORMS.split(',') : []);
_.each(platforms, function(platform) {
try {
platform = platform.trim().toLowerCase();
platformConfig.updatePlatformConfig(platform);
} catch (e) {
process.stdout.write(e);
}
});
}
})();