function _mergeConfigFragment(base, extension, overwriteArrays = false) {
  // If the default value doesn't exist or is explicitly null, defer to the extending value
  if (typeof base === 'undefined' || base === null) {
    return extension;
  } else if (typeof extension === 'undefined') {
    return base;
  } else if (Array.isArray(extension)) {
    if (overwriteArrays) return extension;
    if (!Array.isArray(base)) throw new TypeError(`Expected array but got ${typeof base}`);
    const merged = base.slice();
    extension.forEach(item => {
      if (!merged.some(candidate => isEqual(candidate, item))) merged.push(item);
    });

    return merged;
  } else if (typeof extension === 'object') {
    if (typeof base !== 'object') throw new TypeError(`Expected object but got ${typeof base}`);
    if (Array.isArray(base)) throw new TypeError('Expected object but got Array');
    Object.keys(extension).forEach(key => {
      const localOverwriteArrays = overwriteArrays ||
        (key === 'settings' && typeof base[key] === 'object');
      base[key] = _mergeConfigFragment(base[key], extension[key], localOverwriteArrays);
    });
    return base;
  }

  return extension;
}
Recursively merges config fragment objects in a somewhat Lighthouse-specific way.

   - `null` is treated similarly to `undefined` for whether a value should be overridden.
   - `overwriteArrays` controls array extension behavior:
       - true: Arrays are overwritten without any merging or concatenation.
       - false: Arrays are concatenated and de-duped by isEqual.
   - Objects are recursively merged.
   - If the `settings` key is encountered while traversing an object, its arrays are *always*
     overridden, not concatenated. (`overwriteArrays` is flipped to `true`)

More widely typed than exposed merge() function, below.
@param {Object<string, any>|Array<any>|undefined|null} base
@param {Object<string, any>|Array<any>} extension
@param {boolean=} overwriteArrays