function parseConfig (configFilePath, cliOptions, parseOptions) {
  const promiseConfig = parseOptions && parseOptions.promiseConfig === true
  const throwErrors = parseOptions && parseOptions.throwErrors === true
  const shouldSetupLoggerEarly = promiseConfig
  if (shouldSetupLoggerEarly) {
    // `setupFromConfig` provides defaults for `colors` and `logLevel`.
    // `setup` provides defaults for `appenders`
    // The first argument MUST BE an object
    logger.setupFromConfig({})
  }
  function fail () {
    log.error(...arguments)
    if (throwErrors) {
      const errorMessage = Array.from(arguments).join(' ')
      const err = new Error(errorMessage)
      if (promiseConfig) {
        return Promise.reject(err)
      }
      throw err
    } else {
      const warningMessage =
        'The `parseConfig()` function historically called `process.exit(1)`' +
        ' when it failed. This behavior is now deprecated and function will' +
        ' throw an error in the next major release. To suppress this warning' +
        ' pass `throwErrors: true` as a third argument to opt-in into the new' +
        ' behavior and adjust your code to respond to the exception' +
        ' accordingly.' +
        ' Example: `parseConfig(path, cliOptions, { throwErrors: true })`'
      log.warn(warningMessage)
      process.exit(1)
    }
  }

  let configModule
  if (configFilePath) {
    try {
      if (path.extname(configFilePath) === '.ts' && TYPE_SCRIPT_AVAILABLE) {
        require('ts-node').register()
      }
      configModule = require(configFilePath)
      if (typeof configModule === 'object' && typeof configModule.default !== 'undefined') {
        configModule = configModule.default
      }
    } catch (e) {
      const extension = path.extname(configFilePath)
      if (extension === '.coffee' && !COFFEE_SCRIPT_AVAILABLE) {
        log.error('You need to install CoffeeScript.\n  npm install coffeescript --save-dev')
      } else if (extension === '.ls' && !LIVE_SCRIPT_AVAILABLE) {
        log.error('You need to install LiveScript.\n  npm install LiveScript --save-dev')
      } else if (extension === '.ts' && !TYPE_SCRIPT_AVAILABLE) {
        log.error('You need to install TypeScript.\n  npm install typescript ts-node --save-dev')
      }
      return fail('Error in config file!\n  ' + e.stack || e)
    }
    if (!helper.isFunction(configModule)) {
      return fail('Config file must export a function!\n' + CONFIG_SYNTAX_HELP)
    }
  } else {
    configModule = () => {} // if no config file path is passed, we define a dummy config module.
  }

  const config = new Config()

  // save and reset hostname and listenAddress so we can detect if the user
  // changed them
  const defaultHostname = config.hostname
  config.hostname = null
  const defaultListenAddress = config.listenAddress
  config.listenAddress = null

  // add the user's configuration in
  config.set(cliOptions)

  let configModuleReturn
  try {
    configModuleReturn = configModule(config)
  } catch (e) {
    return fail('Error in config file!\n', e)
  }
  function finalizeConfig (config) {
    // merge the config from config file and cliOptions (precedence)
    config.set(cliOptions)

    // if the user changed listenAddress, but didn't set a hostname, warn them
    if (config.hostname === null && config.listenAddress !== null) {
      log.warn(`ListenAddress was set to ${config.listenAddress} but hostname was left as the default: ` +
      `${defaultHostname}. If your browsers fail to connect, consider changing the hostname option.`)
    }
    // restore values that weren't overwritten by the user
    if (config.hostname === null) {
      config.hostname = defaultHostname
    }
    if (config.listenAddress === null) {
      config.listenAddress = defaultListenAddress
    }

    // configure the logger as soon as we can
    logger.setup(config.logLevel, config.colors, config.loggers)

    log.debug(configFilePath ? `Loading config ${configFilePath}` : 'No config file specified.')

    return normalizeConfig(config, configFilePath)
  }

  /**
   * Return value is a function or (non-null) object that has a `then` method.
   *
   * @type {boolean}
   * @see {@link https://promisesaplus.com/}
   */
  const returnIsThenable = (
    (
      (configModuleReturn != null && typeof configModuleReturn === 'object') ||
      typeof configModuleReturn === 'function'
    ) && typeof configModuleReturn.then === 'function'
  )
  if (returnIsThenable) {
    if (promiseConfig !== true) {
      const errorMessage =
        'The `parseOptions.promiseConfig` option must be set to `true` to ' +
        'enable promise return values from configuration files. ' +
        'Example: `parseConfig(path, cliOptions, { promiseConfig: true })`'
      return fail(errorMessage)
    }
    return configModuleReturn.then(
      function onKarmaConfigModuleFulfilled (/* ignoredResolutionValue */) {
        return finalizeConfig(config)
      },
      function onKarmaConfigModuleRejected (reason) {
        return fail('Error in config file!\n', reason)
      }
    )
  } else {
    if (promiseConfig) {
      try {
        return Promise.resolve(finalizeConfig(config))
      } catch (exception) {
        return Promise.reject(exception)
      }
    } else {
      return finalizeConfig(config)
    }
  }
}
Retrieve a parsed and finalized Karma `Config` instance. This `karmaConfig`
object may be used to configure public API methods such a `Server`,
`runner.run`, and `stopper.stop`.

@param {?string} [configFilePath=null]
    A string representing a file system path pointing to the config file
    whose default export is a function that will be used to set Karma
    configuration options. This function will be passed an instance of the
    `Config` class as its first argument. If this option is not provided,
    then only the options provided by the `cliOptions` argument will be
    set.
@param {Object} cliOptions
    An object whose values will take priority over options set in the
    config file. The config object passed to function exported by the
    config file will already have these options applied. Any changes the
    config file makes to these options will effectively be ignored in the
    final configuration.

    `cliOptions` all the same options as the config file and is applied
    using the same `config.set()` method.
@param {Object} parseOptions
@param {boolean} [parseOptions.promiseConfig=false]
    When `true`, a promise that resolves to a `Config` object will be
    returned. This also allows the function exported by config files (if
    provided) to be asynchronous by returning a promise. Resolving this
    promise indicates that all async activity has completed. The resolution
    value itself is ignored, all configuration must be done with
    `config.set`.
@param {boolean} [parseOptions.throwErrors=false]
    When `true`, process exiting on critical failures will be disabled. In
    The error will be thrown as an exception. If
    `parseOptions.promiseConfig` is also `true`, then the error will
    instead be used as the promise's reject reason.
@returns {Config|Promise<Config>}