[6a3a178] | 1 | 'use strict'
|
---|
| 2 |
|
---|
| 3 | const path = require('path')
|
---|
| 4 | const assert = require('assert')
|
---|
| 5 |
|
---|
| 6 | const logger = require('./logger')
|
---|
| 7 | const log = logger.create('config')
|
---|
| 8 | const helper = require('./helper')
|
---|
| 9 | const constant = require('./constants')
|
---|
| 10 |
|
---|
| 11 | const _ = require('lodash')
|
---|
| 12 |
|
---|
| 13 | let COFFEE_SCRIPT_AVAILABLE = false
|
---|
| 14 | let LIVE_SCRIPT_AVAILABLE = false
|
---|
| 15 | let TYPE_SCRIPT_AVAILABLE = false
|
---|
| 16 |
|
---|
| 17 | try {
|
---|
| 18 | require('coffeescript').register()
|
---|
| 19 | COFFEE_SCRIPT_AVAILABLE = true
|
---|
| 20 | } catch (e) {}
|
---|
| 21 |
|
---|
| 22 | // LiveScript is required here to enable config files written in LiveScript.
|
---|
| 23 | // It's not directly used in this file.
|
---|
| 24 | try {
|
---|
| 25 | require('LiveScript')
|
---|
| 26 | LIVE_SCRIPT_AVAILABLE = true
|
---|
| 27 | } catch (e) {}
|
---|
| 28 |
|
---|
| 29 | try {
|
---|
| 30 | require('ts-node')
|
---|
| 31 | TYPE_SCRIPT_AVAILABLE = true
|
---|
| 32 | } catch (e) {}
|
---|
| 33 |
|
---|
| 34 | class Pattern {
|
---|
| 35 | constructor (pattern, served, included, watched, nocache, type, isBinary) {
|
---|
| 36 | this.pattern = pattern
|
---|
| 37 | this.served = helper.isDefined(served) ? served : true
|
---|
| 38 | this.included = helper.isDefined(included) ? included : true
|
---|
| 39 | this.watched = helper.isDefined(watched) ? watched : true
|
---|
| 40 | this.nocache = helper.isDefined(nocache) ? nocache : false
|
---|
| 41 | this.weight = helper.mmPatternWeight(pattern)
|
---|
| 42 | this.type = type
|
---|
| 43 | this.isBinary = isBinary
|
---|
| 44 | }
|
---|
| 45 |
|
---|
| 46 | compare (other) {
|
---|
| 47 | return helper.mmComparePatternWeights(this.weight, other.weight)
|
---|
| 48 | }
|
---|
| 49 | }
|
---|
| 50 |
|
---|
| 51 | class UrlPattern extends Pattern {
|
---|
| 52 | constructor (url, type) {
|
---|
| 53 | super(url, false, true, false, false, type)
|
---|
| 54 | }
|
---|
| 55 | }
|
---|
| 56 |
|
---|
| 57 | function createPatternObject (pattern) {
|
---|
| 58 | if (pattern && helper.isString(pattern)) {
|
---|
| 59 | return helper.isUrlAbsolute(pattern)
|
---|
| 60 | ? new UrlPattern(pattern)
|
---|
| 61 | : new Pattern(pattern)
|
---|
| 62 | } else if (helper.isObject(pattern) && pattern.pattern && helper.isString(pattern.pattern)) {
|
---|
| 63 | return helper.isUrlAbsolute(pattern.pattern)
|
---|
| 64 | ? new UrlPattern(pattern.pattern, pattern.type)
|
---|
| 65 | : new Pattern(pattern.pattern, pattern.served, pattern.included, pattern.watched, pattern.nocache, pattern.type)
|
---|
| 66 | } else {
|
---|
| 67 | log.warn(`Invalid pattern ${pattern}!\n\tExpected string or object with "pattern" property.`)
|
---|
| 68 | return new Pattern(null, false, false, false, false)
|
---|
| 69 | }
|
---|
| 70 | }
|
---|
| 71 |
|
---|
| 72 | function normalizeUrl (url) {
|
---|
| 73 | if (!url.startsWith('/')) {
|
---|
| 74 | url = `/${url}`
|
---|
| 75 | }
|
---|
| 76 |
|
---|
| 77 | if (!url.endsWith('/')) {
|
---|
| 78 | url = url + '/'
|
---|
| 79 | }
|
---|
| 80 |
|
---|
| 81 | return url
|
---|
| 82 | }
|
---|
| 83 |
|
---|
| 84 | function normalizeUrlRoot (urlRoot) {
|
---|
| 85 | const normalizedUrlRoot = normalizeUrl(urlRoot)
|
---|
| 86 |
|
---|
| 87 | if (normalizedUrlRoot !== urlRoot) {
|
---|
| 88 | log.warn(`urlRoot normalized to "${normalizedUrlRoot}"`)
|
---|
| 89 | }
|
---|
| 90 |
|
---|
| 91 | return normalizedUrlRoot
|
---|
| 92 | }
|
---|
| 93 |
|
---|
| 94 | function normalizeProxyPath (proxyPath) {
|
---|
| 95 | const normalizedProxyPath = normalizeUrl(proxyPath)
|
---|
| 96 |
|
---|
| 97 | if (normalizedProxyPath !== proxyPath) {
|
---|
| 98 | log.warn(`proxyPath normalized to "${normalizedProxyPath}"`)
|
---|
| 99 | }
|
---|
| 100 |
|
---|
| 101 | return normalizedProxyPath
|
---|
| 102 | }
|
---|
| 103 |
|
---|
| 104 | function normalizeConfig (config, configFilePath) {
|
---|
| 105 | function basePathResolve (relativePath) {
|
---|
| 106 | if (helper.isUrlAbsolute(relativePath)) {
|
---|
| 107 | return relativePath
|
---|
| 108 | } else if (helper.isDefined(config.basePath) && helper.isDefined(relativePath)) {
|
---|
| 109 | return path.resolve(config.basePath, relativePath)
|
---|
| 110 | } else {
|
---|
| 111 | return ''
|
---|
| 112 | }
|
---|
| 113 | }
|
---|
| 114 |
|
---|
| 115 | function createPatternMapper (resolve) {
|
---|
| 116 | return (objectPattern) => Object.assign(objectPattern, { pattern: resolve(objectPattern.pattern) })
|
---|
| 117 | }
|
---|
| 118 |
|
---|
| 119 | if (helper.isString(configFilePath)) {
|
---|
| 120 | config.basePath = path.resolve(path.dirname(configFilePath), config.basePath) // resolve basePath
|
---|
| 121 | config.exclude.push(configFilePath) // always ignore the config file itself
|
---|
| 122 | } else {
|
---|
| 123 | config.basePath = path.resolve(config.basePath || '.')
|
---|
| 124 | }
|
---|
| 125 |
|
---|
| 126 | config.files = config.files.map(createPatternObject).map(createPatternMapper(basePathResolve))
|
---|
| 127 | config.exclude = config.exclude.map(basePathResolve)
|
---|
| 128 | config.customContextFile = config.customContextFile && basePathResolve(config.customContextFile)
|
---|
| 129 | config.customDebugFile = config.customDebugFile && basePathResolve(config.customDebugFile)
|
---|
| 130 | config.customClientContextFile = config.customClientContextFile && basePathResolve(config.customClientContextFile)
|
---|
| 131 |
|
---|
| 132 | // normalize paths on windows
|
---|
| 133 | config.basePath = helper.normalizeWinPath(config.basePath)
|
---|
| 134 | config.files = config.files.map(createPatternMapper(helper.normalizeWinPath))
|
---|
| 135 | config.exclude = config.exclude.map(helper.normalizeWinPath)
|
---|
| 136 | config.customContextFile = helper.normalizeWinPath(config.customContextFile)
|
---|
| 137 | config.customDebugFile = helper.normalizeWinPath(config.customDebugFile)
|
---|
| 138 | config.customClientContextFile = helper.normalizeWinPath(config.customClientContextFile)
|
---|
| 139 |
|
---|
| 140 | // normalize urlRoot
|
---|
| 141 | config.urlRoot = normalizeUrlRoot(config.urlRoot)
|
---|
| 142 |
|
---|
| 143 | // normalize and default upstream proxy settings if given
|
---|
| 144 | if (config.upstreamProxy) {
|
---|
| 145 | const proxy = config.upstreamProxy
|
---|
| 146 | proxy.path = helper.isDefined(proxy.path) ? normalizeProxyPath(proxy.path) : '/'
|
---|
| 147 | proxy.hostname = helper.isDefined(proxy.hostname) ? proxy.hostname : 'localhost'
|
---|
| 148 | proxy.port = helper.isDefined(proxy.port) ? proxy.port : 9875
|
---|
| 149 |
|
---|
| 150 | // force protocol to end with ':'
|
---|
| 151 | proxy.protocol = (proxy.protocol || 'http').split(':')[0] + ':'
|
---|
| 152 | if (proxy.protocol.match(/https?:/) === null) {
|
---|
| 153 | log.warn(`"${proxy.protocol}" is not a supported upstream proxy protocol, defaulting to "http:"`)
|
---|
| 154 | proxy.protocol = 'http:'
|
---|
| 155 | }
|
---|
| 156 | }
|
---|
| 157 |
|
---|
| 158 | // force protocol to end with ':'
|
---|
| 159 | config.protocol = (config.protocol || 'http').split(':')[0] + ':'
|
---|
| 160 | if (config.protocol.match(/https?:/) === null) {
|
---|
| 161 | log.warn(`"${config.protocol}" is not a supported protocol, defaulting to "http:"`)
|
---|
| 162 | config.protocol = 'http:'
|
---|
| 163 | }
|
---|
| 164 |
|
---|
| 165 | if (config.proxies && Object.prototype.hasOwnProperty.call(config.proxies, config.urlRoot)) {
|
---|
| 166 | log.warn(`"${config.urlRoot}" is proxied, you should probably change urlRoot to avoid conflicts`)
|
---|
| 167 | }
|
---|
| 168 |
|
---|
| 169 | if (config.singleRun && config.autoWatch) {
|
---|
| 170 | log.debug('autoWatch set to false, because of singleRun')
|
---|
| 171 | config.autoWatch = false
|
---|
| 172 | }
|
---|
| 173 |
|
---|
| 174 | if (config.runInParent) {
|
---|
| 175 | log.debug('useIframe set to false, because using runInParent')
|
---|
| 176 | config.useIframe = false
|
---|
| 177 | }
|
---|
| 178 |
|
---|
| 179 | if (!config.singleRun && !config.useIframe && config.runInParent) {
|
---|
| 180 | log.debug('singleRun set to true, because using runInParent')
|
---|
| 181 | config.singleRun = true
|
---|
| 182 | }
|
---|
| 183 |
|
---|
| 184 | if (helper.isString(config.reporters)) {
|
---|
| 185 | config.reporters = config.reporters.split(',')
|
---|
| 186 | }
|
---|
| 187 |
|
---|
| 188 | if (config.client && config.client.args) {
|
---|
| 189 | assert(Array.isArray(config.client.args), 'Invalid configuration: client.args must be an array of strings')
|
---|
| 190 | }
|
---|
| 191 |
|
---|
| 192 | if (config.browsers) {
|
---|
| 193 | assert(Array.isArray(config.browsers), 'Invalid configuration: browsers option must be an array')
|
---|
| 194 | }
|
---|
| 195 |
|
---|
| 196 | if (config.formatError) {
|
---|
| 197 | assert(helper.isFunction(config.formatError), 'Invalid configuration: formatError option must be a function.')
|
---|
| 198 | }
|
---|
| 199 |
|
---|
| 200 | if (config.processKillTimeout) {
|
---|
| 201 | assert(helper.isNumber(config.processKillTimeout), 'Invalid configuration: processKillTimeout option must be a number.')
|
---|
| 202 | }
|
---|
| 203 |
|
---|
| 204 | if (config.browserSocketTimeout) {
|
---|
| 205 | assert(helper.isNumber(config.browserSocketTimeout), 'Invalid configuration: browserSocketTimeout option must be a number.')
|
---|
| 206 | }
|
---|
| 207 |
|
---|
| 208 | if (config.pingTimeout) {
|
---|
| 209 | assert(helper.isNumber(config.pingTimeout), 'Invalid configuration: pingTimeout option must be a number.')
|
---|
| 210 | }
|
---|
| 211 |
|
---|
| 212 | const defaultClient = config.defaultClient || {}
|
---|
| 213 | Object.keys(defaultClient).forEach(function (key) {
|
---|
| 214 | const option = config.client[key]
|
---|
| 215 | config.client[key] = helper.isDefined(option) ? option : defaultClient[key]
|
---|
| 216 | })
|
---|
| 217 |
|
---|
| 218 | // normalize preprocessors
|
---|
| 219 | const preprocessors = config.preprocessors || {}
|
---|
| 220 | const normalizedPreprocessors = config.preprocessors = Object.create(null)
|
---|
| 221 |
|
---|
| 222 | Object.keys(preprocessors).forEach(function (pattern) {
|
---|
| 223 | const normalizedPattern = helper.normalizeWinPath(basePathResolve(pattern))
|
---|
| 224 |
|
---|
| 225 | normalizedPreprocessors[normalizedPattern] = helper.isString(preprocessors[pattern])
|
---|
| 226 | ? [preprocessors[pattern]] : preprocessors[pattern]
|
---|
| 227 | })
|
---|
| 228 |
|
---|
| 229 | // define custom launchers/preprocessors/reporters - create a new plugin
|
---|
| 230 | const module = Object.create(null)
|
---|
| 231 | let hasSomeInlinedPlugin = false
|
---|
| 232 | const types = ['launcher', 'preprocessor', 'reporter']
|
---|
| 233 |
|
---|
| 234 | types.forEach(function (type) {
|
---|
| 235 | const definitions = config[`custom${helper.ucFirst(type)}s`] || {}
|
---|
| 236 |
|
---|
| 237 | Object.keys(definitions).forEach(function (name) {
|
---|
| 238 | const definition = definitions[name]
|
---|
| 239 |
|
---|
| 240 | if (!helper.isObject(definition)) {
|
---|
| 241 | return log.warn(`Can not define ${type} ${name}. Definition has to be an object.`)
|
---|
| 242 | }
|
---|
| 243 |
|
---|
| 244 | if (!helper.isString(definition.base)) {
|
---|
| 245 | return log.warn(`Can not define ${type} ${name}. Missing base ${type}.`)
|
---|
| 246 | }
|
---|
| 247 |
|
---|
| 248 | const token = type + ':' + definition.base
|
---|
| 249 | const locals = {
|
---|
| 250 | args: ['value', definition]
|
---|
| 251 | }
|
---|
| 252 |
|
---|
| 253 | module[type + ':' + name] = ['factory', function (injector) {
|
---|
| 254 | const plugin = injector.createChild([locals], [token]).get(token)
|
---|
| 255 | if (type === 'launcher' && helper.isDefined(definition.displayName)) {
|
---|
| 256 | plugin.displayName = definition.displayName
|
---|
| 257 | }
|
---|
| 258 | return plugin
|
---|
| 259 | }]
|
---|
| 260 | hasSomeInlinedPlugin = true
|
---|
| 261 | })
|
---|
| 262 | })
|
---|
| 263 |
|
---|
| 264 | if (hasSomeInlinedPlugin) {
|
---|
| 265 | config.plugins.push(module)
|
---|
| 266 | }
|
---|
| 267 |
|
---|
| 268 | return config
|
---|
| 269 | }
|
---|
| 270 |
|
---|
| 271 | /**
|
---|
| 272 | * @class
|
---|
| 273 | */
|
---|
| 274 | class Config {
|
---|
| 275 | constructor () {
|
---|
| 276 | this.LOG_DISABLE = constant.LOG_DISABLE
|
---|
| 277 | this.LOG_ERROR = constant.LOG_ERROR
|
---|
| 278 | this.LOG_WARN = constant.LOG_WARN
|
---|
| 279 | this.LOG_INFO = constant.LOG_INFO
|
---|
| 280 | this.LOG_DEBUG = constant.LOG_DEBUG
|
---|
| 281 |
|
---|
| 282 | // DEFAULT CONFIG
|
---|
| 283 | this.frameworks = []
|
---|
| 284 | this.protocol = 'http:'
|
---|
| 285 | this.port = constant.DEFAULT_PORT
|
---|
| 286 | this.listenAddress = constant.DEFAULT_LISTEN_ADDR
|
---|
| 287 | this.hostname = constant.DEFAULT_HOSTNAME
|
---|
| 288 | this.httpsServerConfig = {}
|
---|
| 289 | this.basePath = ''
|
---|
| 290 | this.files = []
|
---|
| 291 | this.browserConsoleLogOptions = {
|
---|
| 292 | level: 'debug',
|
---|
| 293 | format: '%b %T: %m',
|
---|
| 294 | terminal: true
|
---|
| 295 | }
|
---|
| 296 | this.customContextFile = null
|
---|
| 297 | this.customDebugFile = null
|
---|
| 298 | this.customClientContextFile = null
|
---|
| 299 | this.exclude = []
|
---|
| 300 | this.logLevel = constant.LOG_INFO
|
---|
| 301 | this.colors = true
|
---|
| 302 | this.autoWatch = true
|
---|
| 303 | this.autoWatchBatchDelay = 250
|
---|
| 304 | this.restartOnFileChange = false
|
---|
| 305 | this.usePolling = process.platform === 'linux'
|
---|
| 306 | this.reporters = ['progress']
|
---|
| 307 | this.singleRun = false
|
---|
| 308 | this.browsers = []
|
---|
| 309 | this.captureTimeout = 60000
|
---|
| 310 | this.pingTimeout = 5000
|
---|
| 311 | this.proxies = {}
|
---|
| 312 | this.proxyValidateSSL = true
|
---|
| 313 | this.preprocessors = {}
|
---|
| 314 | this.preprocessor_priority = {}
|
---|
| 315 | this.urlRoot = '/'
|
---|
| 316 | this.upstreamProxy = undefined
|
---|
| 317 | this.reportSlowerThan = 0
|
---|
| 318 | this.loggers = [constant.CONSOLE_APPENDER]
|
---|
| 319 | this.transports = ['polling', 'websocket']
|
---|
| 320 | this.forceJSONP = false
|
---|
| 321 | this.plugins = ['karma-*']
|
---|
| 322 | this.defaultClient = this.client = {
|
---|
| 323 | args: [],
|
---|
| 324 | useIframe: true,
|
---|
| 325 | runInParent: false,
|
---|
| 326 | captureConsole: true,
|
---|
| 327 | clearContext: true
|
---|
| 328 | }
|
---|
| 329 | this.browserDisconnectTimeout = 2000
|
---|
| 330 | this.browserDisconnectTolerance = 0
|
---|
| 331 | this.browserNoActivityTimeout = 30000
|
---|
| 332 | this.processKillTimeout = 2000
|
---|
| 333 | this.concurrency = Infinity
|
---|
| 334 | this.failOnEmptyTestSuite = true
|
---|
| 335 | this.retryLimit = 2
|
---|
| 336 | this.detached = false
|
---|
| 337 | this.crossOriginAttribute = true
|
---|
| 338 | this.browserSocketTimeout = 20000
|
---|
| 339 | }
|
---|
| 340 |
|
---|
| 341 | set (newConfig) {
|
---|
| 342 | _.mergeWith(this, newConfig, (obj, src) => {
|
---|
| 343 | // Overwrite arrays to keep consistent with #283
|
---|
| 344 | if (Array.isArray(src)) {
|
---|
| 345 | return src
|
---|
| 346 | }
|
---|
| 347 | })
|
---|
| 348 | }
|
---|
| 349 | }
|
---|
| 350 |
|
---|
| 351 | const CONFIG_SYNTAX_HELP = ' module.exports = function(config) {\n' +
|
---|
| 352 | ' config.set({\n' +
|
---|
| 353 | ' // your config\n' +
|
---|
| 354 | ' });\n' +
|
---|
| 355 | ' };\n'
|
---|
| 356 |
|
---|
| 357 | /**
|
---|
| 358 | * Retrieve a parsed and finalized Karma `Config` instance. This `karmaConfig`
|
---|
| 359 | * object may be used to configure public API methods such a `Server`,
|
---|
| 360 | * `runner.run`, and `stopper.stop`.
|
---|
| 361 | *
|
---|
| 362 | * @param {?string} [configFilePath=null]
|
---|
| 363 | * A string representing a file system path pointing to the config file
|
---|
| 364 | * whose default export is a function that will be used to set Karma
|
---|
| 365 | * configuration options. This function will be passed an instance of the
|
---|
| 366 | * `Config` class as its first argument. If this option is not provided,
|
---|
| 367 | * then only the options provided by the `cliOptions` argument will be
|
---|
| 368 | * set.
|
---|
| 369 | * @param {Object} cliOptions
|
---|
| 370 | * An object whose values will take priority over options set in the
|
---|
| 371 | * config file. The config object passed to function exported by the
|
---|
| 372 | * config file will already have these options applied. Any changes the
|
---|
| 373 | * config file makes to these options will effectively be ignored in the
|
---|
| 374 | * final configuration.
|
---|
| 375 | *
|
---|
| 376 | * `cliOptions` all the same options as the config file and is applied
|
---|
| 377 | * using the same `config.set()` method.
|
---|
| 378 | * @param {Object} parseOptions
|
---|
| 379 | * @param {boolean} [parseOptions.promiseConfig=false]
|
---|
| 380 | * When `true`, a promise that resolves to a `Config` object will be
|
---|
| 381 | * returned. This also allows the function exported by config files (if
|
---|
| 382 | * provided) to be asynchronous by returning a promise. Resolving this
|
---|
| 383 | * promise indicates that all async activity has completed. The resolution
|
---|
| 384 | * value itself is ignored, all configuration must be done with
|
---|
| 385 | * `config.set`.
|
---|
| 386 | * @param {boolean} [parseOptions.throwErrors=false]
|
---|
| 387 | * When `true`, process exiting on critical failures will be disabled. In
|
---|
| 388 | * The error will be thrown as an exception. If
|
---|
| 389 | * `parseOptions.promiseConfig` is also `true`, then the error will
|
---|
| 390 | * instead be used as the promise's reject reason.
|
---|
| 391 | * @returns {Config|Promise<Config>}
|
---|
| 392 | */
|
---|
| 393 | function parseConfig (configFilePath, cliOptions, parseOptions) {
|
---|
| 394 | const promiseConfig = parseOptions && parseOptions.promiseConfig === true
|
---|
| 395 | const throwErrors = parseOptions && parseOptions.throwErrors === true
|
---|
| 396 | const shouldSetupLoggerEarly = promiseConfig
|
---|
| 397 | if (shouldSetupLoggerEarly) {
|
---|
| 398 | // `setupFromConfig` provides defaults for `colors` and `logLevel`.
|
---|
| 399 | // `setup` provides defaults for `appenders`
|
---|
| 400 | // The first argument MUST BE an object
|
---|
| 401 | logger.setupFromConfig({})
|
---|
| 402 | }
|
---|
| 403 | function fail () {
|
---|
| 404 | log.error(...arguments)
|
---|
| 405 | if (throwErrors) {
|
---|
| 406 | const errorMessage = Array.from(arguments).join(' ')
|
---|
| 407 | const err = new Error(errorMessage)
|
---|
| 408 | if (promiseConfig) {
|
---|
| 409 | return Promise.reject(err)
|
---|
| 410 | }
|
---|
| 411 | throw err
|
---|
| 412 | } else {
|
---|
| 413 | const warningMessage =
|
---|
| 414 | 'The `parseConfig()` function historically called `process.exit(1)`' +
|
---|
| 415 | ' when it failed. This behavior is now deprecated and function will' +
|
---|
| 416 | ' throw an error in the next major release. To suppress this warning' +
|
---|
| 417 | ' pass `throwErrors: true` as a third argument to opt-in into the new' +
|
---|
| 418 | ' behavior and adjust your code to respond to the exception' +
|
---|
| 419 | ' accordingly.' +
|
---|
| 420 | ' Example: `parseConfig(path, cliOptions, { throwErrors: true })`'
|
---|
| 421 | log.warn(warningMessage)
|
---|
| 422 | process.exit(1)
|
---|
| 423 | }
|
---|
| 424 | }
|
---|
| 425 |
|
---|
| 426 | let configModule
|
---|
| 427 | if (configFilePath) {
|
---|
| 428 | try {
|
---|
| 429 | if (path.extname(configFilePath) === '.ts' && TYPE_SCRIPT_AVAILABLE) {
|
---|
| 430 | require('ts-node').register()
|
---|
| 431 | }
|
---|
| 432 | configModule = require(configFilePath)
|
---|
| 433 | if (typeof configModule === 'object' && typeof configModule.default !== 'undefined') {
|
---|
| 434 | configModule = configModule.default
|
---|
| 435 | }
|
---|
| 436 | } catch (e) {
|
---|
| 437 | const extension = path.extname(configFilePath)
|
---|
| 438 | if (extension === '.coffee' && !COFFEE_SCRIPT_AVAILABLE) {
|
---|
| 439 | log.error('You need to install CoffeeScript.\n npm install coffeescript --save-dev')
|
---|
| 440 | } else if (extension === '.ls' && !LIVE_SCRIPT_AVAILABLE) {
|
---|
| 441 | log.error('You need to install LiveScript.\n npm install LiveScript --save-dev')
|
---|
| 442 | } else if (extension === '.ts' && !TYPE_SCRIPT_AVAILABLE) {
|
---|
| 443 | log.error('You need to install TypeScript.\n npm install typescript ts-node --save-dev')
|
---|
| 444 | }
|
---|
| 445 | return fail('Error in config file!\n ' + e.stack || e)
|
---|
| 446 | }
|
---|
| 447 | if (!helper.isFunction(configModule)) {
|
---|
| 448 | return fail('Config file must export a function!\n' + CONFIG_SYNTAX_HELP)
|
---|
| 449 | }
|
---|
| 450 | } else {
|
---|
| 451 | configModule = () => {} // if no config file path is passed, we define a dummy config module.
|
---|
| 452 | }
|
---|
| 453 |
|
---|
| 454 | const config = new Config()
|
---|
| 455 |
|
---|
| 456 | // save and reset hostname and listenAddress so we can detect if the user
|
---|
| 457 | // changed them
|
---|
| 458 | const defaultHostname = config.hostname
|
---|
| 459 | config.hostname = null
|
---|
| 460 | const defaultListenAddress = config.listenAddress
|
---|
| 461 | config.listenAddress = null
|
---|
| 462 |
|
---|
| 463 | // add the user's configuration in
|
---|
| 464 | config.set(cliOptions)
|
---|
| 465 |
|
---|
| 466 | let configModuleReturn
|
---|
| 467 | try {
|
---|
| 468 | configModuleReturn = configModule(config)
|
---|
| 469 | } catch (e) {
|
---|
| 470 | return fail('Error in config file!\n', e)
|
---|
| 471 | }
|
---|
| 472 | function finalizeConfig (config) {
|
---|
| 473 | // merge the config from config file and cliOptions (precedence)
|
---|
| 474 | config.set(cliOptions)
|
---|
| 475 |
|
---|
| 476 | // if the user changed listenAddress, but didn't set a hostname, warn them
|
---|
| 477 | if (config.hostname === null && config.listenAddress !== null) {
|
---|
| 478 | log.warn(`ListenAddress was set to ${config.listenAddress} but hostname was left as the default: ` +
|
---|
| 479 | `${defaultHostname}. If your browsers fail to connect, consider changing the hostname option.`)
|
---|
| 480 | }
|
---|
| 481 | // restore values that weren't overwritten by the user
|
---|
| 482 | if (config.hostname === null) {
|
---|
| 483 | config.hostname = defaultHostname
|
---|
| 484 | }
|
---|
| 485 | if (config.listenAddress === null) {
|
---|
| 486 | config.listenAddress = defaultListenAddress
|
---|
| 487 | }
|
---|
| 488 |
|
---|
| 489 | // configure the logger as soon as we can
|
---|
| 490 | logger.setup(config.logLevel, config.colors, config.loggers)
|
---|
| 491 |
|
---|
| 492 | log.debug(configFilePath ? `Loading config ${configFilePath}` : 'No config file specified.')
|
---|
| 493 |
|
---|
| 494 | return normalizeConfig(config, configFilePath)
|
---|
| 495 | }
|
---|
| 496 |
|
---|
| 497 | /**
|
---|
| 498 | * Return value is a function or (non-null) object that has a `then` method.
|
---|
| 499 | *
|
---|
| 500 | * @type {boolean}
|
---|
| 501 | * @see {@link https://promisesaplus.com/}
|
---|
| 502 | */
|
---|
| 503 | const returnIsThenable = (
|
---|
| 504 | (
|
---|
| 505 | (configModuleReturn != null && typeof configModuleReturn === 'object') ||
|
---|
| 506 | typeof configModuleReturn === 'function'
|
---|
| 507 | ) && typeof configModuleReturn.then === 'function'
|
---|
| 508 | )
|
---|
| 509 | if (returnIsThenable) {
|
---|
| 510 | if (promiseConfig !== true) {
|
---|
| 511 | const errorMessage =
|
---|
| 512 | 'The `parseOptions.promiseConfig` option must be set to `true` to ' +
|
---|
| 513 | 'enable promise return values from configuration files. ' +
|
---|
| 514 | 'Example: `parseConfig(path, cliOptions, { promiseConfig: true })`'
|
---|
| 515 | return fail(errorMessage)
|
---|
| 516 | }
|
---|
| 517 | return configModuleReturn.then(
|
---|
| 518 | function onKarmaConfigModuleFulfilled (/* ignoredResolutionValue */) {
|
---|
| 519 | return finalizeConfig(config)
|
---|
| 520 | },
|
---|
| 521 | function onKarmaConfigModuleRejected (reason) {
|
---|
| 522 | return fail('Error in config file!\n', reason)
|
---|
| 523 | }
|
---|
| 524 | )
|
---|
| 525 | } else {
|
---|
| 526 | if (promiseConfig) {
|
---|
| 527 | try {
|
---|
| 528 | return Promise.resolve(finalizeConfig(config))
|
---|
| 529 | } catch (exception) {
|
---|
| 530 | return Promise.reject(exception)
|
---|
| 531 | }
|
---|
| 532 | } else {
|
---|
| 533 | return finalizeConfig(config)
|
---|
| 534 | }
|
---|
| 535 | }
|
---|
| 536 | }
|
---|
| 537 |
|
---|
| 538 | // PUBLIC API
|
---|
| 539 | exports.parseConfig = parseConfig
|
---|
| 540 | exports.Pattern = Pattern
|
---|
| 541 | exports.createPatternObject = createPatternObject
|
---|
| 542 | exports.Config = Config
|
---|