This repository was archived by the owner on Jul 20, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 36
Expand file tree
/
Copy pathupdatePlatformConfig.js
More file actions
385 lines (328 loc) · 18.7 KB
/
updatePlatformConfig.js
File metadata and controls
385 lines (328 loc) · 18.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
#!/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.
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-permssion 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.
*/
// global vars
var fs = require("fs"),
path = require("path");
module.exports = function(context) {
var cordova_util = context.requireCordovaModule("../cordova/util"),
platforms = context.requireCordovaModule("../platforms/platforms"),
rootdir = cordova_util.isCordova(),
et = context.requireCordovaModule('elementtree'),
plist = context.requireCordovaModule('plist');
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: "__cordovaMainActivity__", destination: 'android:configChanges'},
'android-launchMode': {target: 'AndroidManifest.xml', parent: "__cordovaMainActivity__", destination: 'android:launchMode'},
'android-theme': {target: 'AndroidManifest.xml', parent: "__cordovaMainActivity__", destination: 'android:theme'},
'android-windowSoftInputMode': {target: 'AndroidManifest.xml', parent: "__cordovaMainActivity__", destination: 'android:windowSoftInputMode'},
'android-applicationName': {target: 'AndroidManifest.xml', parent: 'application', destination: 'android:name'},
'android-allowBackup': {target: 'AndroidManifest.xml', parent: 'application', destination: 'android:allowBackup'}
},
'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 {
data.getchildren().forEach(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');
var result = {};
configFileData.forEach(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 = './';
}
var key = item.attrib.target + '|' + parent;
result[key] = item;
});
return result;
},
// 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';
preferences.forEach( function (preference) {
// check if there are specific configuration to map to config for this platform
if (!preferenceMappingData[platform]) {
return;
}
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';
for (var key in configFiles) {
if (configFiles.hasOwnProperty(key)) {
var configFile = configFiles[key];
var keyParts = key.split('|');
var target = keyParts[0];
var parent = keyParts[1];
var items = configData[target] || [];
configFile.getchildren().forEach( 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);
for (var targetFileName in configData) {
if (configData.hasOwnProperty(targetFileName)) {
var configItems = configData[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);
}
}
}
},
getMainAndroidActivityNode: function(rootManifest) {
var cordovaApp = "application/activity/intent-filter/action[@android:name='android.intent.action.MAIN']/../..";
var tempNode = rootManifest.find(cordovaApp);
return tempNode;
},
// Updates the AndroidManifest.xml target file with data from config.xml
updateAndroidManifest: function (targetFile, configItems) {
var tempManifest = platformConfig.parseElementtreeSync(targetFile),
root = tempManifest.getroot();
var mainActivity = platformConfig.getMainAndroidActivityNode(root);
configItems.forEach( function (item) {
var parentEl;
if (item.parent === "__cordovaMainActivity__") {
parentEl = mainActivity;
} else {
// if parent is not found on the root, child/grandchild nodes are searched
parentEl = root.find(item.parent) || root.find('*/' + item.parent);
}
var 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);
}
if (typeof data === "object") {
// copy all config.xml data except for the generated _id property
for (var key in data) {
// skip loop if the property is from prototype
if (!data.hasOwnProperty(key)) continue;
if(key !== '_id') {
childEl[key] = data[key];
}
}
}
}
});
fs.writeFileSync(targetFile, tempManifest.write({indent: 4}), 'utf-8');
console.log("Wrote AndroidManifest.xml: "+targetFile);
},
/* 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;
configItems.forEach( 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');
console.log("Wrote iOS Plist: "+ targetFile);
}
};
})();
// Main
(function () {
if (rootdir) {
// go through each of the platform directories that have been prepared
var platforms = [];
fs.readdirSync('platforms').forEach( function (file) {
if (fs.statSync(path.resolve('platforms', file)).isDirectory()) {
platforms.push(file);
}
});
platforms.forEach( function (platform) {
try {
platform = platform.trim().toLowerCase();
console.log("Processing settings for platform: "+ platform);
platformConfig.updatePlatformConfig(platform);
} catch (e) {
process.stdout.write(e);
}
});
}
})();
}