mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-09-28 13:13:37 +08:00
No changes in the changelog (https://github.com/karma-runner/karma/blob/master/CHANGELOG.md) to concern ourselves about, but one useful change is they upgraded the log4js dependency which was spitting out console messages on each test run. This is fixed with this version of Karma. Bug: none Change-Id: Ic49618cbda7a079cacb9813f5129692a83508a92 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3745621 Commit-Queue: Jack Franklin <jacktfranklin@chromium.org> Auto-Submit: Jack Franklin <jacktfranklin@chromium.org> Reviewed-by: Philip Pfaffe <pfaffe@chromium.org> Commit-Queue: Philip Pfaffe <pfaffe@chromium.org>
545 lines
18 KiB
JavaScript
545 lines
18 KiB
JavaScript
'use strict'
|
|
|
|
const path = require('path')
|
|
const assert = require('assert')
|
|
|
|
const logger = require('./logger')
|
|
const log = logger.create('config')
|
|
const helper = require('./helper')
|
|
const constant = require('./constants')
|
|
|
|
const _ = require('lodash')
|
|
|
|
let COFFEE_SCRIPT_AVAILABLE = false
|
|
let LIVE_SCRIPT_AVAILABLE = false
|
|
let TYPE_SCRIPT_AVAILABLE = false
|
|
|
|
try {
|
|
require('coffeescript').register()
|
|
COFFEE_SCRIPT_AVAILABLE = true
|
|
} catch {}
|
|
|
|
// LiveScript is required here to enable config files written in LiveScript.
|
|
// It's not directly used in this file.
|
|
try {
|
|
require('LiveScript')
|
|
LIVE_SCRIPT_AVAILABLE = true
|
|
} catch {}
|
|
|
|
try {
|
|
require('ts-node')
|
|
TYPE_SCRIPT_AVAILABLE = true
|
|
} catch {}
|
|
|
|
class Pattern {
|
|
constructor (pattern, served, included, watched, nocache, type, isBinary, integrity) {
|
|
this.pattern = pattern
|
|
this.served = helper.isDefined(served) ? served : true
|
|
this.included = helper.isDefined(included) ? included : true
|
|
this.watched = helper.isDefined(watched) ? watched : true
|
|
this.nocache = helper.isDefined(nocache) ? nocache : false
|
|
this.weight = helper.mmPatternWeight(pattern)
|
|
this.type = type
|
|
this.isBinary = isBinary
|
|
this.integrity = integrity
|
|
}
|
|
|
|
compare (other) {
|
|
return helper.mmComparePatternWeights(this.weight, other.weight)
|
|
}
|
|
}
|
|
|
|
class UrlPattern extends Pattern {
|
|
constructor (url, type, integrity) {
|
|
super(url, false, true, false, false, type, undefined, integrity)
|
|
}
|
|
}
|
|
|
|
function createPatternObject (pattern) {
|
|
if (pattern && helper.isString(pattern)) {
|
|
return helper.isUrlAbsolute(pattern)
|
|
? new UrlPattern(pattern)
|
|
: new Pattern(pattern)
|
|
} else if (helper.isObject(pattern) && pattern.pattern && helper.isString(pattern.pattern)) {
|
|
return helper.isUrlAbsolute(pattern.pattern)
|
|
? new UrlPattern(pattern.pattern, pattern.type)
|
|
: new Pattern(pattern.pattern, pattern.served, pattern.included, pattern.watched, pattern.nocache, pattern.type)
|
|
} else {
|
|
log.warn(`Invalid pattern ${pattern}!\n\tExpected string or object with "pattern" property.`)
|
|
return new Pattern(null, false, false, false, false)
|
|
}
|
|
}
|
|
|
|
function normalizeUrl (url) {
|
|
if (!url.startsWith('/')) {
|
|
url = `/${url}`
|
|
}
|
|
|
|
if (!url.endsWith('/')) {
|
|
url = url + '/'
|
|
}
|
|
|
|
return url
|
|
}
|
|
|
|
function normalizeUrlRoot (urlRoot) {
|
|
const normalizedUrlRoot = normalizeUrl(urlRoot)
|
|
|
|
if (normalizedUrlRoot !== urlRoot) {
|
|
log.warn(`urlRoot normalized to "${normalizedUrlRoot}"`)
|
|
}
|
|
|
|
return normalizedUrlRoot
|
|
}
|
|
|
|
function normalizeProxyPath (proxyPath) {
|
|
const normalizedProxyPath = normalizeUrl(proxyPath)
|
|
|
|
if (normalizedProxyPath !== proxyPath) {
|
|
log.warn(`proxyPath normalized to "${normalizedProxyPath}"`)
|
|
}
|
|
|
|
return normalizedProxyPath
|
|
}
|
|
|
|
function normalizeConfig (config, configFilePath) {
|
|
function basePathResolve (relativePath) {
|
|
if (helper.isUrlAbsolute(relativePath)) {
|
|
return relativePath
|
|
} else if (helper.isDefined(config.basePath) && helper.isDefined(relativePath)) {
|
|
return path.resolve(config.basePath, relativePath)
|
|
} else {
|
|
return ''
|
|
}
|
|
}
|
|
|
|
function createPatternMapper (resolve) {
|
|
return (objectPattern) => Object.assign(objectPattern, { pattern: resolve(objectPattern.pattern) })
|
|
}
|
|
|
|
if (helper.isString(configFilePath)) {
|
|
config.basePath = path.resolve(path.dirname(configFilePath), config.basePath) // resolve basePath
|
|
config.exclude.push(configFilePath) // always ignore the config file itself
|
|
} else {
|
|
config.basePath = path.resolve(config.basePath || '.')
|
|
}
|
|
|
|
config.files = config.files.map(createPatternObject).map(createPatternMapper(basePathResolve))
|
|
config.exclude = config.exclude.map(basePathResolve)
|
|
config.customContextFile = config.customContextFile && basePathResolve(config.customContextFile)
|
|
config.customDebugFile = config.customDebugFile && basePathResolve(config.customDebugFile)
|
|
config.customClientContextFile = config.customClientContextFile && basePathResolve(config.customClientContextFile)
|
|
|
|
// normalize paths on windows
|
|
config.basePath = helper.normalizeWinPath(config.basePath)
|
|
config.files = config.files.map(createPatternMapper(helper.normalizeWinPath))
|
|
config.exclude = config.exclude.map(helper.normalizeWinPath)
|
|
config.customContextFile = helper.normalizeWinPath(config.customContextFile)
|
|
config.customDebugFile = helper.normalizeWinPath(config.customDebugFile)
|
|
config.customClientContextFile = helper.normalizeWinPath(config.customClientContextFile)
|
|
|
|
// normalize urlRoot
|
|
config.urlRoot = normalizeUrlRoot(config.urlRoot)
|
|
|
|
// normalize and default upstream proxy settings if given
|
|
if (config.upstreamProxy) {
|
|
const proxy = config.upstreamProxy
|
|
proxy.path = helper.isDefined(proxy.path) ? normalizeProxyPath(proxy.path) : '/'
|
|
proxy.hostname = helper.isDefined(proxy.hostname) ? proxy.hostname : 'localhost'
|
|
proxy.port = helper.isDefined(proxy.port) ? proxy.port : 9875
|
|
|
|
// force protocol to end with ':'
|
|
proxy.protocol = (proxy.protocol || 'http').split(':')[0] + ':'
|
|
if (proxy.protocol.match(/https?:/) === null) {
|
|
log.warn(`"${proxy.protocol}" is not a supported upstream proxy protocol, defaulting to "http:"`)
|
|
proxy.protocol = 'http:'
|
|
}
|
|
}
|
|
|
|
// force protocol to end with ':'
|
|
config.protocol = (config.protocol || 'http').split(':')[0] + ':'
|
|
if (config.protocol.match(/https?:/) === null) {
|
|
log.warn(`"${config.protocol}" is not a supported protocol, defaulting to "http:"`)
|
|
config.protocol = 'http:'
|
|
}
|
|
|
|
if (config.proxies && Object.prototype.hasOwnProperty.call(config.proxies, config.urlRoot)) {
|
|
log.warn(`"${config.urlRoot}" is proxied, you should probably change urlRoot to avoid conflicts`)
|
|
}
|
|
|
|
if (config.singleRun && config.autoWatch) {
|
|
log.debug('autoWatch set to false, because of singleRun')
|
|
config.autoWatch = false
|
|
}
|
|
|
|
if (config.runInParent) {
|
|
log.debug('useIframe set to false, because using runInParent')
|
|
config.useIframe = false
|
|
}
|
|
|
|
if (!config.singleRun && !config.useIframe && config.runInParent) {
|
|
log.debug('singleRun set to true, because using runInParent')
|
|
config.singleRun = true
|
|
}
|
|
|
|
if (helper.isString(config.reporters)) {
|
|
config.reporters = config.reporters.split(',')
|
|
}
|
|
|
|
if (config.client && config.client.args) {
|
|
assert(Array.isArray(config.client.args), 'Invalid configuration: client.args must be an array of strings')
|
|
}
|
|
|
|
if (config.browsers) {
|
|
assert(Array.isArray(config.browsers), 'Invalid configuration: browsers option must be an array')
|
|
}
|
|
|
|
if (config.formatError) {
|
|
assert(helper.isFunction(config.formatError), 'Invalid configuration: formatError option must be a function.')
|
|
}
|
|
|
|
if (config.processKillTimeout) {
|
|
assert(helper.isNumber(config.processKillTimeout), 'Invalid configuration: processKillTimeout option must be a number.')
|
|
}
|
|
|
|
if (config.browserSocketTimeout) {
|
|
assert(helper.isNumber(config.browserSocketTimeout), 'Invalid configuration: browserSocketTimeout option must be a number.')
|
|
}
|
|
|
|
if (config.pingTimeout) {
|
|
assert(helper.isNumber(config.pingTimeout), 'Invalid configuration: pingTimeout option must be a number.')
|
|
}
|
|
|
|
const defaultClient = config.defaultClient || {}
|
|
Object.keys(defaultClient).forEach(function (key) {
|
|
const option = config.client[key]
|
|
config.client[key] = helper.isDefined(option) ? option : defaultClient[key]
|
|
})
|
|
|
|
// normalize preprocessors
|
|
const preprocessors = config.preprocessors || {}
|
|
const normalizedPreprocessors = config.preprocessors = Object.create(null)
|
|
|
|
Object.keys(preprocessors).forEach(function (pattern) {
|
|
const normalizedPattern = helper.normalizeWinPath(basePathResolve(pattern))
|
|
|
|
normalizedPreprocessors[normalizedPattern] = helper.isString(preprocessors[pattern])
|
|
? [preprocessors[pattern]] : preprocessors[pattern]
|
|
})
|
|
|
|
// define custom launchers/preprocessors/reporters - create a new plugin
|
|
const module = Object.create(null)
|
|
let hasSomeInlinedPlugin = false
|
|
const types = ['launcher', 'preprocessor', 'reporter']
|
|
|
|
types.forEach(function (type) {
|
|
const definitions = config[`custom${helper.ucFirst(type)}s`] || {}
|
|
|
|
Object.keys(definitions).forEach(function (name) {
|
|
const definition = definitions[name]
|
|
|
|
if (!helper.isObject(definition)) {
|
|
return log.warn(`Can not define ${type} ${name}. Definition has to be an object.`)
|
|
}
|
|
|
|
if (!helper.isString(definition.base)) {
|
|
return log.warn(`Can not define ${type} ${name}. Missing base ${type}.`)
|
|
}
|
|
|
|
const token = type + ':' + definition.base
|
|
const locals = {
|
|
args: ['value', definition]
|
|
}
|
|
|
|
module[type + ':' + name] = ['factory', function (injector) {
|
|
const plugin = injector.createChild([locals], [token]).get(token)
|
|
if (type === 'launcher' && helper.isDefined(definition.displayName)) {
|
|
plugin.displayName = definition.displayName
|
|
}
|
|
return plugin
|
|
}]
|
|
hasSomeInlinedPlugin = true
|
|
})
|
|
})
|
|
|
|
if (hasSomeInlinedPlugin) {
|
|
config.plugins.push(module)
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
/**
|
|
* @class
|
|
*/
|
|
class Config {
|
|
constructor () {
|
|
this.LOG_DISABLE = constant.LOG_DISABLE
|
|
this.LOG_ERROR = constant.LOG_ERROR
|
|
this.LOG_WARN = constant.LOG_WARN
|
|
this.LOG_INFO = constant.LOG_INFO
|
|
this.LOG_DEBUG = constant.LOG_DEBUG
|
|
|
|
// DEFAULT CONFIG
|
|
this.frameworks = []
|
|
this.protocol = 'http:'
|
|
this.port = constant.DEFAULT_PORT
|
|
this.listenAddress = constant.DEFAULT_LISTEN_ADDR
|
|
this.hostname = constant.DEFAULT_HOSTNAME
|
|
this.httpsServerConfig = {}
|
|
this.basePath = ''
|
|
this.files = []
|
|
this.browserConsoleLogOptions = {
|
|
level: 'debug',
|
|
format: '%b %T: %m',
|
|
terminal: true
|
|
}
|
|
this.customContextFile = null
|
|
this.customDebugFile = null
|
|
this.customClientContextFile = null
|
|
this.exclude = []
|
|
this.logLevel = constant.LOG_INFO
|
|
this.colors = true
|
|
this.autoWatch = true
|
|
this.autoWatchBatchDelay = 250
|
|
this.restartOnFileChange = false
|
|
this.usePolling = process.platform === 'linux'
|
|
this.reporters = ['progress']
|
|
this.singleRun = false
|
|
this.browsers = []
|
|
this.captureTimeout = 60000
|
|
this.pingTimeout = 5000
|
|
this.proxies = {}
|
|
this.proxyValidateSSL = true
|
|
this.preprocessors = {}
|
|
this.preprocessor_priority = {}
|
|
this.urlRoot = '/'
|
|
this.upstreamProxy = undefined
|
|
this.reportSlowerThan = 0
|
|
this.loggers = [constant.CONSOLE_APPENDER]
|
|
this.transports = ['polling', 'websocket']
|
|
this.forceJSONP = false
|
|
this.plugins = ['karma-*']
|
|
this.defaultClient = this.client = {
|
|
args: [],
|
|
useIframe: true,
|
|
runInParent: false,
|
|
captureConsole: true,
|
|
clearContext: true,
|
|
allowedReturnUrlPatterns: ['^https?://']
|
|
}
|
|
this.browserDisconnectTimeout = 2000
|
|
this.browserDisconnectTolerance = 0
|
|
this.browserNoActivityTimeout = 30000
|
|
this.processKillTimeout = 2000
|
|
this.concurrency = Infinity
|
|
this.failOnEmptyTestSuite = true
|
|
this.retryLimit = 2
|
|
this.detached = false
|
|
this.crossOriginAttribute = true
|
|
this.browserSocketTimeout = 20000
|
|
}
|
|
|
|
set (newConfig) {
|
|
_.mergeWith(this, newConfig, (obj, src) => {
|
|
// Overwrite arrays to keep consistent with #283
|
|
if (Array.isArray(src)) {
|
|
return src
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
const CONFIG_SYNTAX_HELP = ' module.exports = function(config) {\n' +
|
|
' config.set({\n' +
|
|
' // your config\n' +
|
|
' });\n' +
|
|
' };\n'
|
|
|
|
/**
|
|
* 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>}
|
|
*/
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// PUBLIC API
|
|
exports.parseConfig = parseConfig
|
|
exports.Pattern = Pattern
|
|
exports.createPatternObject = createPatternObject
|
|
exports.Config = Config
|