apache / cordova-common

Compare 3c9b9ff ... +12 ... ef0923b


@@ -17,6 +17,8 @@
Loading
17 17
    under the License.
18 18
*/
19 19
20 +
// @ts-check
21 +
20 22
/**
21 23
 * contains XML utility functions, some of which are specific to elementtree
22 24
 */
@@ -27,20 +29,40 @@
Loading
27 29
const et = require('elementtree');
28 30
const stripBom = require('strip-bom');
29 31
32 +
/**
33 +
 * The part of the <edit-config> interface that is used here
34 +
 * @typedef {{oldAttrib?: et.Attributes}} EditConfigMunge
35 +
 */
36 +
30 37
module.exports = {
31 -
    // compare two et.XML nodes, see if they match
32 -
    // compares tagName, text, attributes and children (recursively)
33 -
    equalNodes: function (one, two) {
38 +
    /**
39 +
     * Compares two et.XML nodes, see if they match.
40 +
     *
41 +
     * Compares tagName, text, attributes and children (recursively)
42 +
     *
43 +
     * @param {et.Element} one
44 +
     * @param {et.Element} two
45 +
     * @return {boolean} true iff one and two are equal
46 +
     */
47 +
    equalNodes (one, two) {
34 48
        return one.tag === two.tag &&
35 49
            one.len() === two.len() &&
36 -
            one.text.trim() === two.text.trim() &&
50 +
            String(one.text).trim() === String(two.text).trim() &&
37 51
            attribMatch(one, two) &&
38 52
            _.zip(one.getchildren(), two.getchildren())
39 53
                .every(([c1, c2]) => module.exports.equalNodes(c1, c2));
40 54
    },
41 55
42 -
    // adds node to doc at selector, creating parent if it doesn't exist
43 -
    graftXML: function (doc, nodes, selector, after) {
56 +
    /**
57 +
     * Adds node to doc at selector, creating parent if it doesn't exist
58 +
     *
59 +
     * @param {et.ElementTree} doc
60 +
     * @param {et.Element[]} nodes
61 +
     * @param {string} selector
62 +
     * @param {string | undefined} [after]
63 +
     * @return {boolean}
64 +
     */
65 +
    graftXML (doc, nodes, selector, after) {
44 66
        let parent = module.exports.resolveParent(doc, selector);
45 67
46 68
        if (!parent) {
@@ -73,20 +95,45 @@
Loading
73 95
        return true;
74 96
    },
75 97
76 -
    // adds new attributes to doc at selector
77 -
    // Will only merge if attribute has not been modified already or --force is used
78 -
    graftXMLMerge: function (doc, nodes, selector, xml) {
98 +
    /**
99 +
     * Adds new attributes to doc at selector.
100 +
     *
101 +
     * Will only merge if attribute has not been modified already or --force is used
102 +
     *
103 +
     * @param {et.ElementTree} doc
104 +
     * @param {et.Element[]} nodes
105 +
     * @param {string} selector
106 +
     * @param {EditConfigMunge} xml
107 +
     * @return {boolean}
108 +
     */
109 +
    graftXMLMerge (doc, nodes, selector, xml) {
79 110
        return graftXMLAttrs(doc, nodes, selector, xml);
80 111
    },
81 112
82 -
    // overwrite all attributes to doc at selector with new attributes
83 -
    // Will only overwrite if attribute has not been modified already or --force is used
84 -
    graftXMLOverwrite: function (doc, nodes, selector, xml) {
113 +
    /**
114 +
     * Overwrites all attributes to doc at selector with new attributes.
115 +
     *
116 +
     * Will only overwrite if attribute has not been modified already or --force is used
117 +
     *
118 +
     * @param {et.ElementTree} doc
119 +
     * @param {et.Element[]} nodes
120 +
     * @param {string} selector
121 +
     * @param {EditConfigMunge} xml
122 +
     * @return {boolean}
123 +
     */
124 +
    graftXMLOverwrite (doc, nodes, selector, xml) {
85 125
        return graftXMLAttrs(doc, nodes, selector, xml, { overwrite: true });
86 126
    },
87 127
88 -
    // removes node from doc at selector
89 -
    pruneXML: function (doc, nodes, selector) {
128 +
    /**
129 +
     * Removes node from doc at selector.
130 +
     *
131 +
     * @param {et.ElementTree} doc
132 +
     * @param {et.Element[]} nodes
133 +
     * @param {string} selector
134 +
     * @return {boolean}
135 +
     */
136 +
    pruneXML (doc, nodes, selector) {
90 137
        const parent = module.exports.resolveParent(doc, selector);
91 138
        if (!parent) return false;
92 139
@@ -98,8 +145,15 @@
Loading
98 145
        return true;
99 146
    },
100 147
101 -
    // restores attributes from doc at selector
102 -
    pruneXMLRestore: function (doc, selector, xml) {
148 +
    /**
149 +
     * Restores attributes from doc at selector.
150 +
     *
151 +
     * @param {et.ElementTree} doc
152 +
     * @param {string} selector
153 +
     * @param {EditConfigMunge} xml
154 +
     * @return {boolean}
155 +
     */
156 +
    pruneXMLRestore (doc, selector, xml) {
103 157
        const target = module.exports.resolveParent(doc, selector);
104 158
        if (!target) return false;
105 159
@@ -110,7 +164,13 @@
Loading
110 164
        return true;
111 165
    },
112 166
113 -
    pruneXMLRemove: function (doc, selector, nodes) {
167 +
    /**
168 +
     * @param {et.ElementTree} doc
169 +
     * @param {string} selector
170 +
     * @param {et.Element[]} nodes
171 +
     * @return {boolean}
172 +
     */
173 +
    pruneXMLRemove (doc, selector, nodes) {
114 174
        const target = module.exports.resolveParent(doc, selector);
115 175
        if (!target) return false;
116 176
@@ -123,11 +183,20 @@
Loading
123 183
        return true;
124 184
    },
125 185
126 -
    parseElementtreeSync: function (filename) {
186 +
    /**
187 +
     * @param {string} filename
188 +
     * @return {et.ElementTree}
189 +
     */
190 +
    parseElementtreeSync (filename) {
127 191
        return et.parse(stripBom(fs.readFileSync(filename, 'utf-8')));
128 192
    },
129 193
130 -
    resolveParent: function (doc, selector) {
194 +
    /**
195 +
     * @param {et.ElementTree} doc
196 +
     * @param {string} selector
197 +
     * @return {et.Element | null}
198 +
     */
199 +
    resolveParent (doc, selector) {
131 200
        if (!selector.startsWith('/')) return doc.find(selector);
132 201
133 202
        // elementtree does not implement absolute selectors so we build an
@@ -138,6 +207,13 @@
Loading
138 207
    }
139 208
};
140 209
210 +
/**
211 +
 * @param {et.ElementTree} doc
212 +
 * @param {et.Element[]} nodes
213 +
 * @param {string} selector
214 +
 * @param {EditConfigMunge} xml
215 +
 * @return {boolean}
216 +
 */
141 217
function graftXMLAttrs (doc, nodes, selector, xml, { overwrite = false } = {}) {
142 218
    const target = module.exports.resolveParent(doc, selector);
143 219
    if (!target) return false;
@@ -151,15 +227,25 @@
Loading
151 227
    return true;
152 228
}
153 229
230 +
/**
231 +
 * @param {et.Element} node
232 +
 * @param {et.Element | et.ElementTree} parent
233 +
 * @return {et.Element | undefined}
234 +
 */
154 235
function findChild (node, parent) {
155 -
    const matches = parent.findall(node.tag);
236 +
    const matches = parent.findall(String(node.tag));
156 237
    return matches.find(m => module.exports.equalNodes(node, m));
157 238
}
158 239
159 -
// Find the index at which to insert an entry. After is a ;-separated priority list
160 -
// of tags after which the insertion should be made. E.g. If we need to
161 -
// insert an element C, and the rule is that the order of children has to be
162 -
// As, Bs, Cs. After will be equal to "C;B;A".
240 +
/**
241 +
 * Find the index at which to insert an entry.
242 +
 *
243 +
 * @param {et.Element[]} children
244 +
 * @param {string} after a ;-separated priority list of tags after which the
245 +
 *  insertion should be made. E.g. if we need to insert an element C, and the
246 +
 *  order of children has to be As, Bs, Cs then `after` will be equal to "C;B;A".
247 +
 * @return {number}
248 +
 */
163 249
function findInsertIdx (children, after) {
164 250
    const childrenTags = children.map(child => child.tag);
165 251
    const foundIndex = after.split(';')
@@ -173,9 +259,15 @@
Loading
173 259
const BLACKLIST = ['platform', 'feature', 'plugin', 'engine'];
174 260
const SINGLETONS = ['content', 'author', 'name'];
175 261
262 +
/**
263 +
 * @param {et.Element} src
264 +
 * @param {et.Element} dest
265 +
 * @param {string} platform
266 +
 * @param {boolean} clobber
267 +
 */
176 268
function mergeXml (src, dest, platform, clobber) {
177 269
    // Do nothing for blacklisted tags.
178 -
    if (BLACKLIST.includes(src.tag)) return;
270 +
    if (BLACKLIST.includes(String(src.tag))) return;
179 271
180 272
    // Handle attributes
181 273
    const omitAttrs = new Set(clobber ? [] : dest.keys());
@@ -199,8 +291,9 @@
Loading
199 291
    // Handle duplicate preference tags (by name attribute)
200 292
    removeDuplicatePreferences(dest);
201 293
294 +
    /** @param {et.Element} srcChild */
202 295
    function mergeChild (srcChild) {
203 -
        const srcTag = srcChild.tag;
296 +
        const srcTag = String(srcChild.tag);
204 297
        const query = srcTag + '';
205 298
        let destChild;
206 299
        let shouldMerge = true;
@@ -220,12 +313,13 @@
Loading
220 313
        if (destChild) {
221 314
            dest.remove(destChild);
222 315
        } else {
223 -
            destChild = new et.Element(srcTag);
316 +
            destChild = et.Element(srcTag);
224 317
        }
225 318
        mergeXml(srcChild, destChild, platform, clobber && shouldMerge);
226 319
        dest.append(destChild);
227 320
    }
228 321
322 +
    /** @param {et.Element} xml */
229 323
    function removeDuplicatePreferences (xml) {
230 324
        const prefs = xml.findall('preference[@name][@value]');
231 325
@@ -247,13 +341,24 @@
Loading
247 341
// Expose for testing.
248 342
module.exports.mergeXml = mergeXml;
249 343
344 +
/**
345 +
 * @param {et.Element} elm1
346 +
 * @param {et.Element} elm2
347 +
 * @return {boolean}
348 +
 */
250 349
function textMatch (elm1, elm2) {
251 -
    const format = text => text ? text.replace(/\s+/, '') : '';
350 +
    /** @param {et.ElementText | null} text */
351 +
    const format = text => text ? String(text).replace(/\s+/, '') : '';
252 352
    const text1 = format(elm1.text);
253 353
    const text2 = format(elm2.text);
254 354
    return (text1 === '' || text1 === text2);
255 355
}
256 356
357 +
/**
358 +
 * @param {et.Element} a
359 +
 * @param {et.Element} b
360 +
 * @return {boolean} true iff attributes on a and b are equal
361 +
 */
257 362
function attribMatch (a, b) {
258 363
    const aKeys = a.keys();
259 364
    return aKeys.length === b.keys().length &&

@@ -86,21 +86,13 @@
Loading
86 86
        const plugin_vars = is_top_level
87 87
            ? platform_config.installed_plugins[pluginInfo.id]
88 88
            : platform_config.dependent_plugins[pluginInfo.id];
89 -
        let edit_config_changes = null;
90 89
91 -
        if (pluginInfo.getEditConfigs) {
92 -
            edit_config_changes = pluginInfo.getEditConfigs(this.platform);
93 -
        }
90 +
        const edit_config_changes = this._getChanges(pluginInfo, 'EditConfig');
94 91
95 92
        // get config munge, aka how did this plugin change various config files
96 93
        const config_munge = this.generate_plugin_config_munge(pluginInfo, plugin_vars, edit_config_changes);
97 -
        // global munge looks at all plugins' changes to config files
98 -
        const global_munge = platform_config.config_munge;
99 -
        const munge = mungeutil.decrement_munge(global_munge, config_munge);
100 94
101 -
        for (const file in munge.files) {
102 -
            this.apply_file_munge(file, munge.files[file], /* remove = */ true);
103 -
        }
95 +
        this._munge_helper(config_munge, { remove: true });
104 96
105 97
        // Remove from installed_plugins
106 98
        this.platformJson.removePlugin(pluginInfo.id, is_top_level);
@@ -109,43 +101,28 @@
Loading
109 101
    }
110 102
111 103
    add_plugin_changes (pluginInfo, plugin_vars, is_top_level, should_increment, plugin_force) {
112 -
        const platform_config = this.platformJson.root;
113 -
        let config_munge;
114 -
        let edit_config_changes = null;
104 +
        const edit_config_changes = this._getChanges(pluginInfo, 'EditConfig');
115 105
116 -
        if (pluginInfo.getEditConfigs) {
117 -
            edit_config_changes = pluginInfo.getEditConfigs(this.platform);
106 +
        const { configConflicts, pluginConflicts } = this._is_conflicting(edit_config_changes);
107 +
        if (Object.keys(configConflicts.files).length > 0) {
108 +
            // plugin.xml cannot overwrite config.xml changes even if --force is used
109 +
            throw new Error(`${pluginInfo.id} cannot be added. <edit-config> changes in this plugin conflicts with <edit-config> changes in config.xml. Conflicts must be resolved before plugin can be added.`);
118 110
        }
119 -
120 -
        if (!edit_config_changes || edit_config_changes.length === 0) {
121 -
            // get config munge, aka how should this plugin change various config files
122 -
            config_munge = this.generate_plugin_config_munge(pluginInfo, plugin_vars);
123 -
        } else {
124 -
            const isConflictingInfo = this._is_conflicting(edit_config_changes, platform_config.config_munge, plugin_force);
125 -
126 -
            if (isConflictingInfo.conflictWithConfigxml) {
127 -
                throw new Error(`${pluginInfo.id} cannot be added. <edit-config> changes in this plugin conflicts with <edit-config> changes in config.xml. Conflicts must be resolved before plugin can be added.`);
128 -
            }
129 -
            if (plugin_force) {
130 -
                events.emit('warn', '--force is used. edit-config will overwrite conflicts if any. Conflicting plugins may not work as expected.');
131 -
132 -
                // remove conflicting munges
133 -
                const conflict_munge = mungeutil.decrement_munge(platform_config.config_munge, isConflictingInfo.conflictingMunge);
134 -
                for (const conflict_file in conflict_munge.files) {
135 -
                    this.apply_file_munge(conflict_file, conflict_munge.files[conflict_file], /* remove = */ true);
136 -
                }
137 -
138 -
                // force add new munges
139 -
                config_munge = this.generate_plugin_config_munge(pluginInfo, plugin_vars, edit_config_changes);
140 -
            } else if (isConflictingInfo.conflictFound) {
141 -
                throw new Error(`There was a conflict trying to modify attributes with <edit-config> in plugin ${pluginInfo.id}. The conflicting plugin, ${isConflictingInfo.conflictingPlugin}, already modified the same attributes. The conflict must be resolved before ${pluginInfo.id} can be added. You may use --force to add the plugin and overwrite the conflicting attributes.`);
142 -
            } else {
143 -
                // no conflicts, will handle edit-config
144 -
                config_munge = this.generate_plugin_config_munge(pluginInfo, plugin_vars, edit_config_changes);
145 -
            }
111 +
        if (plugin_force) {
112 +
            events.emit('warn', '--force is used. edit-config will overwrite conflicts if any. Conflicting plugins may not work as expected.');
113 +
114 +
            // remove conflicting munges, if any
115 +
            this._munge_helper(pluginConflicts, { remove: true });
116 +
        } else if (Object.keys(pluginConflicts.files).length > 0) {
117 +
            // plugin cannot overwrite other plugin changes without --force
118 +
            const witness = Object.values(Object.values(pluginConflicts.files)[0].parents)[0][0];
119 +
            const conflictingPlugin = witness.plugin;
120 +
            throw new Error(`There was a conflict trying to modify attributes with <edit-config> in plugin ${pluginInfo.id}. The conflicting plugin, ${conflictingPlugin}, already modified the same attributes. The conflict must be resolved before ${pluginInfo.id} can be added. You may use --force to add the plugin and overwrite the conflicting attributes.`);
146 121
        }
147 122
148 -
        this._munge_helper(should_increment, platform_config, config_munge);
123 +
        // get config munge, aka how should this plugin change various config files
124 +
        const config_munge = this.generate_plugin_config_munge(pluginInfo, plugin_vars, edit_config_changes);
125 +
        this._munge_helper(config_munge, { should_increment });
149 126
150 127
        // Move to installed/dependent_plugins
151 128
        this.platformJson.addPlugin(pluginInfo.id, plugin_vars || {}, is_top_level);
@@ -155,68 +132,45 @@
Loading
155 132
156 133
    // Handle edit-config changes from config.xml
157 134
    add_config_changes (config, should_increment) {
158 -
        const platform_config = this.platformJson.root;
159 -
        let changes = [];
160 -
161 -
        if (config.getEditConfigs) {
162 -
            const edit_config_changes = config.getEditConfigs(this.platform);
163 -
            if (edit_config_changes) {
164 -
                changes = changes.concat(edit_config_changes);
165 -
            }
166 -
        }
167 -
168 -
        if (config.getConfigFiles) {
169 -
            const config_files_changes = config.getConfigFiles(this.platform);
170 -
            if (config_files_changes) {
171 -
                changes = changes.concat(config_files_changes);
172 -
            }
135 +
        const changes = [
136 +
            ...this._getChanges(config, 'EditConfig'),
137 +
            ...this._getChanges(config, 'ConfigFile')
138 +
        ];
139 +
140 +
        const { configConflicts, pluginConflicts } = this._is_conflicting(changes);
141 +
        if (Object.keys(pluginConflicts.files).length !== 0) {
142 +
            events.emit('warn', 'Conflict found, edit-config changes from config.xml will overwrite plugin.xml changes');
173 143
        }
174 -
175 -
        if (changes && changes.length > 0) {
176 -
            const isConflictingInfo = this._is_conflicting(changes, platform_config.config_munge, true /* always force overwrite other edit-config */);
177 -
            if (isConflictingInfo.conflictFound) {
178 -
                if (Object.keys(isConflictingInfo.configxmlMunge.files).length !== 0) {
179 -
                    // silently remove conflicting config.xml munges so new munges can be added
180 -
                    const conflict_munge = mungeutil.decrement_munge(platform_config.config_munge, isConflictingInfo.configxmlMunge);
181 -
                    for (const conflict_file in conflict_munge.files) {
182 -
                        this.apply_file_munge(conflict_file, conflict_munge.files[conflict_file], /* remove = */ true);
183 -
                    }
184 -
                }
185 -
186 -
                if (Object.keys(isConflictingInfo.conflictingMunge.files).length !== 0) {
187 -
                    events.emit('warn', 'Conflict found, edit-config changes from config.xml will overwrite plugin.xml changes');
188 -
189 -
                    // remove conflicting plugin.xml munges
190 -
                    const conflict_munge = mungeutil.decrement_munge(platform_config.config_munge, isConflictingInfo.conflictingMunge);
191 -
                    for (const conflict_file in conflict_munge.files) {
192 -
                        this.apply_file_munge(conflict_file, conflict_munge.files[conflict_file], /* remove = */ true);
193 -
                    }
194 -
                }
195 -
            }
144 +
        // remove conflicting config.xml & plugin.xml munges, if any
145 +
        for (const conflict_munge of [configConflicts, pluginConflicts]) {
146 +
            this._munge_helper(conflict_munge, { remove: true });
196 147
        }
197 148
198 149
        // Add config.xml edit-config and config-file munges
199 150
        const config_munge = this.generate_config_xml_munge(config, changes, 'config.xml');
200 -
        this._munge_helper(should_increment, platform_config, config_munge);
151 +
        this._munge_helper(config_munge, { should_increment });
201 152
202 153
        // Move to installed/dependent_plugins
203 154
        return this;
204 155
    }
205 156
206 157
    /** @private */
207 -
    _munge_helper (should_increment, platform_config, config_munge) {
158 +
    _munge_helper (config_munge, { should_increment = true, remove = false } = {}) {
208 159
        // global munge looks at all changes to config files
209 160
        // TODO: The should_increment param is only used by cordova-cli and is going away soon.
210 161
        // If should_increment is set to false, avoid modifying the global_munge (use clone)
211 162
        // and apply the entire config_munge because it's already a proper subset of the global_munge.
212 163
164 +
        const platform_config = this.platformJson.root;
213 165
        const global_munge = platform_config.config_munge;
166 +
167 +
        const method = remove ? 'decrement_munge' : 'increment_munge';
214 168
        const munge = should_increment
215 -
            ? mungeutil.increment_munge(global_munge, config_munge)
169 +
            ? mungeutil[method](global_munge, config_munge)
216 170
            : config_munge;
217 171
218 172
        for (const file in munge.files) {
219 -
            this.apply_file_munge(file, munge.files[file]);
173 +
            this.apply_file_munge(file, munge.files[file], remove);
220 174
        }
221 175
222 176
        return this;
@@ -235,142 +189,98 @@
Loading
235 189
        return this;
236 190
    }
237 191
238 -
    // generate_config_xml_munge
239 192
    // Generate the munge object from config.xml
240 193
    generate_config_xml_munge (config, config_changes, type) {
241 -
        const munge = { files: {} };
242 -
243 -
        if (!config_changes) return munge;
244 -
245 -
        const id = type === 'config.xml' ? type : config.id;
246 -
247 -
        config_changes.forEach(change => {
248 -
            change.xmls.forEach(xml => {
249 -
                // 1. stringify each xml
250 -
                const stringified = (new et.ElementTree(xml)).write({ xml_declaration: false });
251 -
                // 2. add into munge
252 -
                if (change.mode) {
253 -
                    mungeutil.deep_add(munge, change.file, change.target, { xml: stringified, count: 1, mode: change.mode, id });
254 -
                } else {
255 -
                    mungeutil.deep_add(munge, change.target, change.parent, { xml: stringified, count: 1, after: change.after });
256 -
                }
257 -
            });
258 -
        });
259 -
260 -
        return munge;
194 +
        const originInfo = { id: type === 'config.xml' ? type : config.id };
195 +
        return this._generateMunge(config_changes, originInfo);
261 196
    }
262 197
263 -
    // generate_plugin_config_munge
264 198
    // Generate the munge object from plugin.xml + vars
265 199
    generate_plugin_config_munge (pluginInfo, vars, edit_config_changes) {
266 -
        vars = vars || {};
267 -
        const munge = { files: {} };
268 200
        const changes = pluginInfo.getConfigFiles(this.platform);
269 -
270 201
        if (edit_config_changes) {
271 202
            Array.prototype.push.apply(changes, edit_config_changes);
272 203
        }
204 +
        const filteredChanges = changes.filter(({ mode }) => mode !== 'remove');
205 +
206 +
        const originInfo = { plugin: pluginInfo.id };
207 +
        return this._generateMunge(filteredChanges, originInfo, vars || {});
208 +
    }
209 +
210 +
    /** @private */
211 +
    _generateMunge (changes, originInfo, vars = {}) {
212 +
        const munge = { files: {} };
273 213
274 214
        changes.forEach(change => {
215 +
            const [file, selector, rest] = change.mode
216 +
                ? [change.file, change.target, { mode: change.mode, ...originInfo }]
217 +
                : [change.target, change.parent, { after: change.after }];
218 +
275 219
            change.xmls.forEach(xml => {
276 220
                // 1. stringify each xml
277 221
                let stringified = (new et.ElementTree(xml)).write({ xml_declaration: false });
278 -
                // interp vars
222 +
223 +
                // interpolate vars, if any
279 224
                Object.keys(vars).forEach(key => {
280 225
                    const regExp = new RegExp(`\\$${key}`, 'g');
281 226
                    stringified = stringified.replace(regExp, vars[key]);
282 227
                });
228 +
283 229
                // 2. add into munge
284 -
                if (change.mode) {
285 -
                    if (change.mode !== 'remove') {
286 -
                        mungeutil.deep_add(munge, change.file, change.target, { xml: stringified, count: 1, mode: change.mode, plugin: pluginInfo.id });
287 -
                    }
288 -
                } else {
289 -
                    mungeutil.deep_add(munge, change.target, change.parent, { xml: stringified, count: 1, after: change.after });
290 -
                }
230 +
                mungeutil.deep_add(munge, file, selector, { xml: stringified, count: 1, ...rest });
291 231
            });
292 232
        });
293 233
294 234
        return munge;
295 235
    }
296 236
297 237
    /** @private */
298 -
    _is_conflicting (editchanges, config_munge, force) {
299 -
        const files = config_munge.files;
300 -
        let conflictFound = false;
301 -
        let conflictWithConfigxml = false;
302 -
        const conflictingMunge = { files: {} };
303 -
        const configxmlMunge = { files: {} };
304 -
        let conflictingParent;
305 -
        let conflictingPlugin;
306 -
307 -
        editchanges.forEach(editchange => {
308 -
            if (files[editchange.file]) {
309 -
                const parents = files[editchange.file].parents;
310 -
                let target = parents[editchange.target];
311 -
312 -
                // Check if the edit target will resolve to an existing target
313 -
                if (!target || target.length === 0) {
314 -
                    const targetFile = this.config_keeper.get(this.project_dir, this.platform, editchange.file);
315 -
316 -
                    // For non-XML files (e.g. plist), the selector in editchange.target uniquely identifies its target.
317 -
                    // Thus we already know that we have no conflict if we are not dealing with an XML file here.
318 -
                    if (targetFile.type !== 'xml') return;
319 -
320 -
                    // For XML files, the selector does NOT uniquely identify its target. So we resolve editchange.target
321 -
                    // and any existing selectors to their matched elements and compare those for equality.
322 -
                    const resolveEditTarget = xml_helpers.resolveParent(targetFile.data, editchange.target);
323 -
                    if (resolveEditTarget) {
324 -
                        for (const parent in parents) {
325 -
                            const resolveTarget = xml_helpers.resolveParent(targetFile.data, parent);
326 -
                            if (resolveEditTarget === resolveTarget) {
327 -
                                conflictingParent = parent;
328 -
                                target = parents[parent];
329 -
                                break;
330 -
                            }
331 -
                        }
332 -
                    }
333 -
                } else {
334 -
                    conflictingParent = editchange.target;
335 -
                }
238 +
    _getChanges (cfg, changeType) {
239 +
        const method = `get${changeType}s`;
240 +
        return (cfg[method] && cfg[method](this.platform)) || [];
241 +
    }
336 242
337 -
                if (target && target.length !== 0) {
338 -
                    // conflict has been found
339 -
                    conflictFound = true;
340 -
341 -
                    if (editchange.id === 'config.xml') {
342 -
                        if (target[0].id === 'config.xml') {
343 -
                            // Keep track of config.xml/config.xml edit-config conflicts
344 -
                            mungeutil.deep_add(configxmlMunge, editchange.file, conflictingParent, target[0]);
345 -
                        } else {
346 -
                            // Keep track of config.xml x plugin.xml edit-config conflicts
347 -
                            mungeutil.deep_add(conflictingMunge, editchange.file, conflictingParent, target[0]);
348 -
                        }
349 -
                    } else {
350 -
                        if (target[0].id === 'config.xml') {
351 -
                            // plugin.xml cannot overwrite config.xml changes even if --force is used
352 -
                            conflictWithConfigxml = true;
353 -
                            return;
354 -
                        }
355 -
356 -
                        if (force) {
357 -
                            // Need to find all conflicts when --force is used, track conflicting munges
358 -
                            mungeutil.deep_add(conflictingMunge, editchange.file, conflictingParent, target[0]);
359 -
                        } else {
360 -
                            // plugin cannot overwrite other plugin changes without --force
361 -
                            conflictingPlugin = target[0].plugin;
362 -
                        }
363 -
                    }
364 -
                }
365 -
            }
243 +
    /** @private */
244 +
    _is_conflicting (editchanges) {
245 +
        const platform_config = this.platformJson.root;
246 +
        const { files } = platform_config.config_munge;
247 +
248 +
        const configConflicts = { files: {} }; // config.xml edit-config conflicts
249 +
        const pluginConflicts = { files: {} }; // plugin.xml edit-config conflicts
250 +
251 +
        const registerConflict = (file, selector) => {
252 +
            const witness = files[file].parents[selector][0];
253 +
            const conflictMunge = witness.id === 'config.xml' ? configConflicts : pluginConflicts;
254 +
            mungeutil.deep_add(conflictMunge, file, selector, witness);
255 +
        };
256 +
257 +
        editchanges.forEach(({ file, target }) => {
258 +
            if (!files[file]) return;
259 +
            const { parents: changesBySelector } = files[file];
260 +
261 +
            const conflicts = changesBySelector[target] || [];
262 +
            if (conflicts.length > 0) return registerConflict(file, target);
263 +
264 +
            const targetFile = this.config_keeper.get(this.project_dir, this.platform, file);
265 +
266 +
            // For non-XML files (e.g. plist), the selector in editchange.target uniquely identifies its target.
267 +
            // Thus we already know that we have no conflict if we are not dealing with an XML file here.
268 +
            if (targetFile.type !== 'xml') return;
269 +
270 +
            // For XML files, the selector does NOT uniquely identify its target. So we resolve editchange.target
271 +
            // and any existing selectors to their matched elements and compare those for equality.
272 +
            const resolveEditTarget = xml_helpers.resolveParent(targetFile.data, target);
273 +
            if (!resolveEditTarget) return;
274 +
275 +
            const selector = Object.keys(changesBySelector).find(parent =>
276 +
                resolveEditTarget === xml_helpers.resolveParent(targetFile.data, parent)
277 +
            );
278 +
            if (selector) return registerConflict(file, selector);
366 279
        });
367 280
368 281
        return {
369 -
            conflictFound,
370 -
            conflictingPlugin,
371 -
            conflictingMunge,
372 -
            configxmlMunge,
373 -
            conflictWithConfigxml
282 +
            configConflicts,
283 +
            pluginConflicts
374 284
        };
375 285
    }
376 286

Everything is accounted for!

No changes detected that need to be reviewed.
What changes does Codecov check for?
Lines, not adjusted in diff, that have changed coverage data.
Files that introduced coverage data that had none before.
Files that have missing coverage data that once were tracked.
Files Coverage
src -0.41% 89.14%
cordova-common.js 0.00%
Project Totals (20 files) 87.80%
Loading