| /* |
| * |
| * Copyright 2013 Anis Kadri |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * |
| */ |
| |
| /* |
| * This module deals with shared configuration / dependency "stuff". That is: |
| * - XML configuration files such as config.xml, AndroidManifest.xml or WMAppManifest.xml. |
| * - plist files in iOS |
| * - pbxproj files in iOS |
| * Essentially, any type of shared resources that we need to handle with awareness |
| * of how potentially multiple plugins depend on a single shared resource, should be |
| * handled in this module. |
| * |
| * The implementation uses an object as a hash table, with "leaves" of the table tracking |
| * reference counts. |
| */ |
| |
| /* jshint node:true, sub:true, unused:true, indent:4 */ |
| |
| var fs = require('fs'), |
| path = require('path'), |
| glob = require('glob'), |
| plist = require('plist-with-patches'), |
| bplist = require('bplist-parser'), |
| xcode = require('xcode'), |
| et = require('elementtree'), |
| _ = require('underscore'), |
| xml_helpers = require('./../util/xml-helpers'), |
| platforms = require('./../platforms'), |
| events = require('./../events'), |
| plist_helpers = require('./../util/plist-helpers'); |
| |
| |
| // These frameworks are required by cordova-ios by default. We should never add/remove them. |
| var keep_these_frameworks = [ |
| 'MobileCoreServices.framework', |
| 'CoreGraphics.framework', |
| 'CoreLocation.framework', |
| 'AssetsLibrary.framework' |
| ]; |
| |
| |
| exports.PlatformMunger = PlatformMunger; |
| |
| /****************************************************************************** |
| Adapters to keep the current refactoring effort to within this file |
| ******************************************************************************/ |
| exports.add_plugin_changes = function(platform, project_dir, plugins_dir, plugin_id, plugin_vars, is_top_level, should_increment, cache) { |
| var munger = new PlatformMunger(platform, project_dir, plugins_dir); |
| munger.add_plugin_changes(plugin_id, plugin_vars, is_top_level, should_increment, cache); |
| munger.save_all(); |
| }; |
| |
| exports.remove_plugin_changes = function(platform, project_dir, plugins_dir, plugin_name, plugin_id, is_top_level, should_decrement) { |
| // TODO: should_decrement parameter is never used, remove it here and wherever called |
| var munger = new PlatformMunger(platform, project_dir, plugins_dir); |
| munger.remove_plugin_changes(plugin_name, plugin_id, is_top_level); |
| munger.save_all(); |
| }; |
| |
| exports.process = function(plugins_dir, project_dir, platform) { |
| var munger = new PlatformMunger(platform, project_dir, plugins_dir); |
| munger.process(); |
| munger.save_all(); |
| }; |
| |
| exports.get_munge_change = function(munge, keys) { |
| return deep_find.apply(null, arguments); |
| } |
| |
| /******************************************************************************/ |
| |
| |
| exports.add_installed_plugin_to_prepare_queue = add_installed_plugin_to_prepare_queue; |
| function add_installed_plugin_to_prepare_queue(plugins_dir, plugin, platform, vars, is_top_level) { |
| checkPlatform(platform); |
| var config = exports.get_platform_json(plugins_dir, platform); |
| config.prepare_queue.installed.push({'plugin':plugin, 'vars':vars, 'topLevel':is_top_level}); |
| exports.save_platform_json(config, plugins_dir, platform); |
| } |
| |
| exports.add_uninstalled_plugin_to_prepare_queue = add_uninstalled_plugin_to_prepare_queue; |
| function add_uninstalled_plugin_to_prepare_queue(plugins_dir, plugin, platform, is_top_level) { |
| checkPlatform(platform); |
| |
| var plugin_xml = xml_helpers.parseElementtreeSync(path.join(plugins_dir, plugin, 'plugin.xml')); |
| var config = exports.get_platform_json(plugins_dir, platform); |
| config.prepare_queue.uninstalled.push({'plugin':plugin, 'id':plugin_xml.getroot().attrib['id'], 'topLevel':is_top_level}); |
| exports.save_platform_json(config, plugins_dir, platform); |
| } |
| |
| |
| /****************************************************************************** |
| * PlatformMunger class |
| * |
| * Can deal with config file of a single project. |
| * Parsed config files are cached in a ConfigKeeper object. |
| ******************************************************************************/ |
| function PlatformMunger(platform, project_dir, plugins_dir) { |
| checkPlatform(platform); |
| this.platform = platform; |
| this.project_dir = project_dir; |
| this.plugins_dir = plugins_dir; |
| this.platform_handler = platforms[platform]; |
| this.config_keeper = new ConfigKeeper(); |
| } |
| |
| // Write out all unsaved files. |
| PlatformMunger.prototype.save_all = PlatformMunger_save_all; |
| function PlatformMunger_save_all() { |
| this.config_keeper.save_all(); |
| } |
| |
| // Apply a munge object to a single config file. |
| // The remove parameter tells whether to add the change or remove it. |
| PlatformMunger.prototype.apply_file_munge = PlatformMunger_apply_file_munge; |
| function PlatformMunger_apply_file_munge(file, munge, remove) { |
| var self = this; |
| var xml_child; |
| |
| if ( file === 'framework' && self.platform === 'ios' ) { |
| // ios pbxproj file |
| var pbxproj = self.config_keeper.get(self.project_dir, self.platform, 'framework'); |
| for (var src in munge.parents) { |
| for (xml_child in munge.parents[src]) { |
| var xml = munge.parents[src][xml_child].xml; |
| // Only add the framework if it's not a cordova-ios core framework |
| if (keep_these_frameworks.indexOf(src) == -1) { |
| // xml_child in this case is whether the framework should use weak or not |
| if (remove) { |
| pbxproj.data.removeFramework(src); |
| } else { |
| pbxproj.data.addFramework(src, {weak: (xml === 'true')}); |
| } |
| pbxproj.is_changed = true; |
| } |
| } |
| } |
| } else { |
| // all other types of files |
| for (var selector in munge.parents) { |
| for (xml_child in munge.parents[selector]) { |
| // this xml child is new, graft it (only if config file exists) |
| var config_file = self.config_keeper.get(self.project_dir, self.platform, file); |
| if (config_file.exists) { |
| if (remove) config_file.prune_child(selector, munge.parents[selector][xml_child]); |
| else config_file.graft_child(selector, munge.parents[selector][xml_child]); |
| } |
| } |
| } |
| } |
| } |
| |
| |
| PlatformMunger.prototype.remove_plugin_changes = remove_plugin_changes; |
| function remove_plugin_changes(plugin_name, plugin_id, is_top_level) { |
| var self = this; |
| var platform_config = exports.get_platform_json(self.plugins_dir, self.platform); |
| var plugin_dir = path.join(self.plugins_dir, plugin_name); |
| var plugin_vars = (is_top_level ? platform_config.installed_plugins[plugin_id] : platform_config.dependent_plugins[plugin_id]); |
| |
| // get config munge, aka how did this plugin change various config files |
| var config_munge = self.generate_plugin_config_munge(plugin_dir, plugin_vars); |
| // global munge looks at all plugins' changes to config files |
| var global_munge = platform_config.config_munge; |
| var munge = decrement_munge(global_munge, config_munge); |
| |
| for (var file in munge.files) { |
| if (file == 'plugins-plist' && self.platform == 'ios') { |
| // TODO: remove this check and <plugins-plist> sections in spec/plugins/../plugin.xml files. |
| events.emit( |
| 'warn', |
| 'WARNING: Plugin "' + plugin_id + '" uses <plugins-plist> element(s), ' + |
| 'which are no longer supported. Support has been removed as of Cordova 3.4.' |
| ); |
| continue; |
| } |
| self.apply_file_munge(file, munge.files[file], /* remove = */ true); |
| } |
| |
| // Remove from installed_plugins |
| if (is_top_level) { |
| delete platform_config.installed_plugins[plugin_id]; |
| } else { |
| delete platform_config.dependent_plugins[plugin_id]; |
| } |
| |
| // save |
| exports.save_platform_json(platform_config, self.plugins_dir, self.platform); |
| } |
| |
| |
| PlatformMunger.prototype.add_plugin_changes = add_plugin_changes; |
| function add_plugin_changes(plugin_id, plugin_vars, is_top_level, should_increment) { |
| var self = this; |
| var platform_config = exports.get_platform_json(self.plugins_dir, self.platform); |
| var plugin_dir = path.join(self.plugins_dir, plugin_id); |
| |
| var plugin_config = self.config_keeper.get(plugin_dir, '', 'plugin.xml'); |
| plugin_id = plugin_config.data.getroot().attrib.id; |
| |
| // get config munge, aka how should this plugin change various config files |
| var config_munge = self.generate_plugin_config_munge(plugin_dir, plugin_vars); |
| // global munge looks at all plugins' changes to config files |
| |
| // TODO: The should_increment param is only used by cordova-cli and is going away soon. |
| // If should_increment is set to false, avoid modifying the global_munge (use clone) |
| // and apply the entire config_munge because it's already a proper subset of the global_munge. |
| var munge, global_munge; |
| if (should_increment) { |
| global_munge = platform_config.config_munge; |
| munge = increment_munge(global_munge, config_munge); |
| } else { |
| global_munge = clone_munge(platform_config.config_munge); |
| munge = config_munge; |
| } |
| |
| for (var file in munge.files) { |
| // TODO: remove this warning some time after 3.4 is out. |
| if (file == 'plugins-plist' && self.platform == 'ios') { |
| events.emit( |
| 'warn', |
| 'WARNING: Plugin "' + plugin_id + '" uses <plugins-plist> element(s), ' + |
| 'which are no longer supported. Support has been removed as of Cordova 3.4.' |
| ); |
| continue; |
| } |
| self.apply_file_munge(file, munge.files[file]); |
| } |
| |
| // Move to installed_plugins if it is a top-level plugin |
| if (is_top_level) { |
| platform_config.installed_plugins[plugin_id] = plugin_vars || {}; |
| } else { |
| platform_config.dependent_plugins[plugin_id] = plugin_vars || {}; |
| } |
| |
| // save |
| exports.save_platform_json(platform_config, self.plugins_dir, self.platform); |
| } |
| |
| |
| // Load the global munge from platform json and apply all of it. |
| // Used by cordova prepare to re-generate some config file from platform |
| // defaults and the global munge. |
| PlatformMunger.prototype.reapply_global_munge = reapply_global_munge ; |
| function reapply_global_munge () { |
| var self = this; |
| |
| var platform_config = exports.get_platform_json(self.plugins_dir, self.platform); |
| var global_munge = platform_config.config_munge; |
| for (var file in global_munge.files) { |
| // TODO: remove this warning some time after 3.4 is out. |
| if (file == 'plugins-plist' && self.platform == 'ios') { |
| events.emit( |
| 'warn', |
| 'WARNING: One of your plugins uses <plugins-plist> element(s), ' + |
| 'which are no longer supported. Support has been removed as of Cordova 3.4.' |
| ); |
| continue; |
| } |
| |
| self.apply_file_munge(file, global_munge.files[file]); |
| } |
| } |
| |
| |
| // generate_plugin_config_munge |
| // Generate the munge object from plugin.xml + vars |
| PlatformMunger.prototype.generate_plugin_config_munge = generate_plugin_config_munge; |
| function generate_plugin_config_munge(plugin_dir, vars) { |
| var self = this; |
| |
| vars = vars || {}; |
| // Add PACKAGE_NAME variable into vars |
| if (!vars['PACKAGE_NAME']) { |
| vars['PACKAGE_NAME'] = self.platform_handler.package_name(self.project_dir); |
| } |
| |
| var munge = { files: {} }; |
| var plugin_config = self.config_keeper.get(plugin_dir, '', 'plugin.xml'); |
| var plugin_xml = plugin_config.data; |
| |
| var platformTag = plugin_xml.find('platform[@name="' + self.platform + '"]'); |
| var changes = []; |
| // add platform-agnostic config changes |
| changes = changes.concat(plugin_xml.findall('config-file')); |
| if (platformTag) { |
| // add platform-specific config changes if they exist |
| changes = changes.concat(platformTag.findall('config-file')); |
| |
| // note down pbxproj framework munges in special section of munge obj |
| // CB-5238 this is only for systems frameworks |
| var frameworks = platformTag.findall('framework'); |
| frameworks.forEach(function(f) { |
| var custom = f.attrib['custom']; |
| if(!custom) { |
| var file = f.attrib['src']; |
| var weak = ('true' == f.attrib['weak']).toString(); |
| |
| deep_add(munge, 'framework', file, { xml: weak, count: 1 }); |
| } |
| }); |
| } |
| |
| changes.forEach(function(change) { |
| var target = change.attrib['target']; |
| var parent = change.attrib['parent']; |
| var after = change.attrib['after']; |
| var xmls = change.getchildren(); |
| xmls.forEach(function(xml) { |
| // 1. stringify each xml |
| var stringified = (new et.ElementTree(xml)).write({xml_declaration:false}); |
| // interp vars |
| if (vars) { |
| Object.keys(vars).forEach(function(key) { |
| var regExp = new RegExp("\\$" + key, "g"); |
| stringified = stringified.replace(regExp, vars[key]); |
| }); |
| } |
| // 2. add into munge |
| deep_add(munge, target, parent, { xml: stringified, count: 1, after: after }); |
| }); |
| }); |
| return munge; |
| } |
| |
| // Go over the prepare queue an apply the config munges for each plugin |
| // that has been (un)installed. |
| PlatformMunger.prototype.process = PlatformMunger_process; |
| function PlatformMunger_process() { |
| var self = this; |
| |
| var platform_config = exports.get_platform_json(self.plugins_dir, self.platform); |
| |
| // Uninstallation first |
| platform_config.prepare_queue.uninstalled.forEach(function(u) { |
| self.remove_plugin_changes(u.plugin, u.id, u.topLevel); |
| }); |
| |
| // Now handle installation |
| platform_config.prepare_queue.installed.forEach(function(u) { |
| self.add_plugin_changes(u.plugin, u.vars, u.topLevel, true); |
| }); |
| |
| platform_config = exports.get_platform_json(self.plugins_dir, self.platform); |
| |
| // Empty out installed/ uninstalled queues. |
| platform_config.prepare_queue.uninstalled = []; |
| platform_config.prepare_queue.installed = []; |
| // save platform json |
| exports.save_platform_json(platform_config, self.plugins_dir, self.platform); |
| } |
| /**** END of PlatformMunger ****/ |
| |
| |
| /****************************************************************************** |
| * ConfigKeeper class |
| * |
| * Used to load and store config files to avoid re-parsing and writing them out |
| * multiple times. |
| * |
| * The config files are referred to by a fake path constructed as |
| * project_dir/platform/file |
| * where file is the name used for the file in config munges. |
| ******************************************************************************/ |
| function ConfigKeeper() { |
| this._cached = {}; |
| } |
| |
| ConfigKeeper.prototype.get = ConfigKeeper_get; |
| function ConfigKeeper_get(project_dir, platform, file) { |
| var self = this; |
| |
| //This fixes a bug with older plugins - when specifying config xml instead of res/xml/config.xml |
| //https://issues.apache.org/jira/browse/CB-6414 |
| if(file == 'config.xml' && platform == 'android'){ |
| file = 'res/xml/config.xml'; |
| } |
| var fake_path = path.join(project_dir, platform, file); |
| |
| if (self._cached[fake_path]) { |
| return self._cached[fake_path]; |
| } |
| // File was not cached, need to load. |
| var config_file = new ConfigFile(project_dir, platform, file); |
| self._cached[fake_path] = config_file; |
| return config_file; |
| } |
| |
| |
| ConfigKeeper.prototype.save_all = ConfigKeeper_save_all; |
| function ConfigKeeper_save_all() { |
| var self = this; |
| Object.keys(self._cached).forEach(function (fake_path) { |
| var config_file = self._cached[fake_path]; |
| if (config_file.is_changed) config_file.save(); |
| }); |
| } |
| /**** END of ConfigKeeper ****/ |
| |
| // TODO: move save/get_platform_json to be part of ConfigKeeper or ConfigFile |
| // For now they are used in many places in plugman and cordova-cli and can |
| // save the file bypassing the ConfigKeeper's cache. |
| exports.get_platform_json = get_platform_json; |
| function get_platform_json(plugins_dir, platform) { |
| checkPlatform(platform); |
| |
| var filepath = path.join(plugins_dir, platform + '.json'); |
| if (fs.existsSync(filepath)) { |
| return fix_munge(JSON.parse(fs.readFileSync(filepath, 'utf-8'))); |
| } else { |
| var config = { |
| prepare_queue:{installed:[], uninstalled:[]}, |
| config_munge:{}, |
| installed_plugins:{}, |
| dependent_plugins:{} |
| }; |
| return config; |
| } |
| } |
| |
| exports.save_platform_json = save_platform_json; |
| function save_platform_json(config, plugins_dir, platform) { |
| checkPlatform(platform); |
| var filepath = path.join(plugins_dir, platform + '.json'); |
| fs.writeFileSync(filepath, JSON.stringify(config, null, 4), 'utf-8'); |
| } |
| |
| |
| // convert a munge from the old format ([file][parent][xml] = count) to the current one |
| function fix_munge(platform_config) { |
| var munge = platform_config.config_munge; |
| if (!munge.files) { |
| var new_munge = { files: {} }; |
| for (var file in munge) { |
| for (var selector in munge[file]) { |
| for (var xml_child in munge[file][selector]) { |
| var val = parseInt(munge[file][selector][xml_child]); |
| for (var i = 0; i < val; i++) { |
| deep_add(new_munge, [file, selector, { xml: xml_child, count: val }]); |
| } |
| } |
| } |
| } |
| platform_config.config_munge = new_munge; |
| } |
| |
| return platform_config; |
| } |
| |
| /**** END of ConfigKeeper ****/ |
| |
| |
| /****************************************************************************** |
| * ConfigFile class |
| * |
| * Can load and keep various types of config files. Provides some functionality |
| * specific to some file types such as grafting XML children. In most cases it |
| * should be instantiated by ConfigKeeper. |
| * |
| * For plugin.xml files use as: |
| * plugin_config = self.config_keeper.get(plugin_dir, '', 'plugin.xml'); |
| * |
| * TODO: Consider moving it out to a separate file and maybe partially with |
| * overrides in platform handlers. |
| ******************************************************************************/ |
| function ConfigFile(project_dir, platform, file_tag) { |
| this.project_dir = project_dir; |
| this.platform = platform; |
| this.file_tag = file_tag; |
| this.is_changed = false; |
| |
| this.load(); |
| } |
| |
| // ConfigFile.load() |
| ConfigFile.prototype.load = ConfigFile_load; |
| function ConfigFile_load() { |
| var self = this; |
| |
| // config file may be in a place not exactly specified in the target |
| var filepath = self.filepath = resolveConfigFilePath(self.project_dir, self.platform, self.file_tag); |
| |
| if ( !filepath || !fs.existsSync(filepath) ) { |
| self.exists = false; |
| return; |
| } |
| self.exists = true; |
| var ext = path.extname(filepath); |
| // Windows8 uses an appxmanifest, and wp8 will likely use |
| // the same in a future release |
| if (ext == '.xml' || ext == '.appxmanifest') { |
| self.type = 'xml'; |
| self.data = xml_helpers.parseElementtreeSync(filepath); |
| } else if (ext == '.pbxproj') { |
| self.type = 'pbxproj'; |
| self.data = xcode.project(filepath); |
| self.data.parseSync(); |
| } else { |
| // plist file |
| self.type = 'plist'; |
| // TODO: isBinaryPlist() reads the file and then parse re-reads it again. |
| // We always write out text plist, not binary. |
| // Do we still need to support binary plist? |
| // If yes, use plist.parseStringSync() and read the file once. |
| self.plist_module = (isBinaryPlist(filepath) ? bplist : plist); |
| self.data = self.plist_module.parseFileSync(filepath); |
| } |
| } |
| |
| // ConfigFile.save() |
| ConfigFile.prototype.save = ConfigFile_save; |
| function ConfigFile_save() { |
| var self = this; |
| if (self.type === 'xml') { |
| fs.writeFileSync(self.filepath, self.data.write({indent: 4}), 'utf-8'); |
| } else if (self.type === 'pbxproj') { |
| fs.writeFileSync(self.filepath, self.data.writeSync()); |
| } else { |
| // plist |
| var regExp = new RegExp("<string>[ \t\r\n]+?</string>", "g"); |
| fs.writeFileSync(self.filepath, plist.build(self.data).replace(regExp, "<string></string>")); |
| } |
| self.is_changed = false; |
| } |
| |
| // ConfigFile.graft_child() |
| ConfigFile.prototype.graft_child = ConfigFile_graft_child; |
| function ConfigFile_graft_child(selector, xml_child) { |
| var self = this; |
| var filepath = self.filepath; |
| var result; |
| if (self.type === 'xml') { |
| var xml_to_graft = [et.XML(xml_child.xml)]; |
| result = xml_helpers.graftXML(self.data, xml_to_graft, selector, xml_child.after); |
| if ( !result) { |
| throw new Error('grafting xml at selector "' + selector + '" from "' + filepath + '" during config install went bad :('); |
| } |
| } else { |
| // plist file |
| result = plist_helpers.graftPLIST(self.data, xml_child.xml, selector); |
| if ( !result ) { |
| throw new Error('grafting to plist "' + filepath + '" during config install went bad :('); |
| } |
| } |
| self.is_changed = true; |
| } |
| |
| // ConfigFile.prune_child() |
| ConfigFile.prototype.prune_child = ConfigFile_prune_child; |
| function ConfigFile_prune_child(selector, xml_child) { |
| var self = this; |
| var filepath = self.filepath; |
| var result; |
| if (self.type === 'xml') { |
| var xml_to_graft = [et.XML(xml_child.xml)]; |
| result = xml_helpers.pruneXML(self.data, xml_to_graft, selector); |
| } else { |
| // plist file |
| result = plist_helpers.prunePLIST(self.data, xml_child.xml, selector); |
| } |
| if ( !result) { |
| var err_msg = 'Pruning at selector "' + selector + '" from "' + filepath + '" went bad.'; |
| throw new Error(err_msg); |
| } |
| self.is_changed = true; |
| } |
| /**** END of ConfigFile ****/ |
| |
| |
| /****************************************************************************** |
| * Utility functions |
| ******************************************************************************/ |
| |
| // Check if we know such platform |
| function checkPlatform(platform) { |
| if (!(platform in platforms)) throw new Error('platform "' + platform + '" not recognized.'); |
| } |
| |
| // determine if a plist file is binary |
| function isBinaryPlist(filename) { |
| // I wish there was a synchronous way to read only the first 6 bytes of a |
| // file. This is wasteful :/ |
| var buf = '' + fs.readFileSync(filename, 'utf8'); |
| // binary plists start with a magic header, "bplist" |
| return buf.substring(0, 6) === 'bplist'; |
| } |
| |
| // Find out the real name of an iOS project |
| // TODO: glob is slow, need a better way or caching, or avoid using more than once. |
| function getIOSProjectname(project_dir) { |
| var matches = glob.sync(path.join(project_dir, '*.xcodeproj')); |
| var iospath; |
| if (matches.length === 1) { |
| iospath = path.basename(matches[0],'.xcodeproj'); |
| } else { |
| var msg; |
| if (matches.length === 0) { |
| msg = 'Does not appear to be an xcode project, no xcode project file in ' + project_dir; |
| } |
| else { |
| msg = 'There are multiple *.xcodeproj dirs in ' + project_dir; |
| } |
| throw new Error(msg); |
| } |
| return iospath; |
| } |
| |
| // Some config-file target attributes are not qualified with a full leading directory, or contain wildcards. |
| // Resolve to a real path in this function. |
| // TODO: getIOSProjectname is slow because of glob, try to avoid calling it several times per project. |
| function resolveConfigFilePath(project_dir, platform, file) { |
| var filepath = path.join(project_dir, file); |
| var matches; |
| |
| // .pbxproj file |
| if (file === 'framework') { |
| var proj_name = getIOSProjectname(project_dir); |
| filepath = path.join(project_dir, proj_name + '.xcodeproj', 'project.pbxproj'); |
| return filepath; |
| } |
| |
| if (file.indexOf('*') > -1) { |
| // handle wildcards in targets using glob. |
| matches = glob.sync(path.join(project_dir, '**', file)); |
| if (matches.length) filepath = matches[0]; |
| return filepath; |
| } |
| |
| // special-case config.xml target that is just "config.xml". This should be resolved to the real location of the file. |
| // TODO: move the logic that contains the locations of config.xml from cordova CLI into plugman. |
| if (file == 'config.xml') { |
| if (platform == 'ubuntu') { |
| filepath = path.join(project_dir, 'config.xml'); |
| } else if (platform == 'ios') { |
| var iospath = getIOSProjectname(project_dir); |
| filepath = path.join(project_dir,iospath, 'config.xml'); |
| } else if (platform == 'android') { |
| filepath = path.join(project_dir, 'res', 'xml', 'config.xml'); |
| } else { |
| matches = glob.sync(path.join(project_dir, '**', 'config.xml')); |
| if (matches.length) filepath = matches[0]; |
| } |
| return filepath; |
| } |
| |
| // None of the special cases matched, returning project_dir/file. |
| return filepath; |
| } |
| |
| |
| /****************************************************************************** |
| * Munge object manipulations functions |
| ******************************************************************************/ |
| |
| // add the count of [key1][key2]...[keyN] to obj |
| // return true if it didn't exist before |
| function deep_add(obj, keys /* or key1, key2 .... */ ) { |
| if ( !Array.isArray(keys) ) { |
| keys = Array.prototype.slice.call(arguments, 1); |
| } |
| |
| return process_munge(obj, true/*createParents*/, function (parentArray, k) { |
| var found = _.find(parentArray, function(element) { |
| return element.xml == k.xml; |
| }); |
| if (found) { |
| found.after = found.after || k.after; |
| found.count += k.count; |
| } else { |
| parentArray.push(k); |
| } |
| return !found; |
| }, keys); |
| } |
| |
| // decrement the count of [key1][key2]...[keyN] from obj and remove if it reaches 0 |
| // return true if it was removed or not found |
| function deep_remove(obj, keys /* or key1, key2 .... */ ) { |
| if ( !Array.isArray(keys) ) { |
| keys = Array.prototype.slice.call(arguments, 1); |
| } |
| |
| var result = process_munge(obj, false/*createParents*/, function (parentArray, k) { |
| var index = -1; |
| var found = _.find(parentArray, function (element) { |
| index++; |
| return element.xml == k.xml; |
| }); |
| if (found) { |
| found.count -= k.count; |
| if (found.count > 0) { |
| return false; |
| } |
| else { |
| parentArray.splice(index, 1); |
| } |
| } |
| return undefined; |
| }, keys); |
| |
| return typeof result === "undefined" ? true : result; |
| } |
| |
| // search for [key1][key2]...[keyN] |
| // return the object or undefined if not found |
| function deep_find(obj, keys /* or key1, key2 .... */ ) { |
| if ( !Array.isArray(keys) ) { |
| keys = Array.prototype.slice.call(arguments, 1); |
| } |
| |
| return process_munge(obj, false/*createParents?*/, function (parentArray, k) { |
| return _.find(parentArray, function (element) { |
| return element.xml == (k.xml || k); |
| }); |
| }, keys); |
| } |
| |
| // Execute func passing it the parent array and the xmlChild key. |
| // When createParents is true, add the file and parent items they are missing |
| // When createParents is false, stop and return undefined if the file and/or parent items are missing |
| |
| function process_munge(obj, createParents, func, keys /* or key1, key2 .... */ ) { |
| if ( !Array.isArray(keys) ) { |
| keys = Array.prototype.slice.call(arguments, 1); |
| } |
| var k = keys[0]; |
| if (keys.length == 1) { |
| return func(obj, k); |
| } else if (keys.length == 2) { |
| if (!obj.parents[k] && !createParents) { |
| return undefined; |
| } |
| obj.parents[k] = obj.parents[k] || []; |
| return process_munge(obj.parents[k], createParents, func, keys.slice(1)); |
| } else if (keys.length == 3){ |
| if (!obj.files[k] && !createParents) { |
| return undefined; |
| } |
| obj.files[k] = obj.files[k] || { parents: {} }; |
| return process_munge(obj.files[k], createParents, func, keys.slice(1)); |
| } else { |
| throw new Error("Invalid key format. Must contain at most 3 elements (file, parent, xmlChild)."); |
| } |
| } |
| |
| // All values from munge are added to base as |
| // base[file][selector][child] += base[file][selector][child] |
| // Returns a munge object containing values that exist in munge |
| // but not in base. |
| function increment_munge(base, munge) { |
| var diff = { files: {} }; |
| |
| for (var file in munge.files) { |
| for (var selector in munge.files[file].parents) { |
| for (var xml_child in munge.files[file].parents[selector]) { |
| var val = munge.files[file].parents[selector][xml_child]; |
| // if node not in base, add it to diff and base |
| // else increment it's value in base without adding to diff |
| var newlyAdded = deep_add(base, [file, selector, val]); |
| if (newlyAdded) { |
| deep_add(diff, file, selector, val); |
| } |
| } |
| } |
| } |
| return diff; |
| } |
| |
| // Update the base munge object as |
| // base[file][selector][child] -= base[file][selector][child] |
| // nodes that reached zero value are removed from base and added to the returned munge |
| // object. |
| function decrement_munge(base, munge) { |
| var zeroed = { files: {} }; |
| |
| for (var file in munge.files) { |
| for (var selector in munge.files[file].parents) { |
| for (var xml_child in munge.files[file].parents[selector]) { |
| var val = munge.files[file].parents[selector][xml_child]; |
| // if node not in base, add it to diff and base |
| // else increment it's value in base without adding to diff |
| var removed = deep_remove(base, [file, selector, val]); |
| if (removed) { |
| deep_add(zeroed, file, selector, val); |
| } |
| } |
| } |
| } |
| return zeroed; |
| } |
| |
| // For better readability where used |
| function clone_munge(munge) { |
| return increment_munge({}, munge); |
| } |