UNPKG

14.2 kBJavaScriptView Raw
1'use strict'
2
3const path = require('path')
4const assert = require('assert')
5
6const logger = require('./logger')
7const log = logger.create('config')
8const helper = require('./helper')
9const constant = require('./constants')
10
11const _ = require('lodash')
12
13let COFFEE_SCRIPT_AVAILABLE = false
14let LIVE_SCRIPT_AVAILABLE = false
15let TYPE_SCRIPT_AVAILABLE = false
16
17try {
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.
24try {
25 require('LiveScript')
26 LIVE_SCRIPT_AVAILABLE = true
27} catch (e) {}
28
29try {
30 require('ts-node').register()
31 TYPE_SCRIPT_AVAILABLE = true
32} catch (e) {}
33
34class 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
51class UrlPattern extends Pattern {
52 constructor (url, type) {
53 super(url, false, true, false, false, type)
54 }
55}
56
57function 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
72function 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
84function 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
94function 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
104function 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 an inlined 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
271class Config {
272 constructor () {
273 this.LOG_DISABLE = constant.LOG_DISABLE
274 this.LOG_ERROR = constant.LOG_ERROR
275 this.LOG_WARN = constant.LOG_WARN
276 this.LOG_INFO = constant.LOG_INFO
277 this.LOG_DEBUG = constant.LOG_DEBUG
278
279 // DEFAULT CONFIG
280 this.frameworks = []
281 this.protocol = 'http:'
282 this.port = constant.DEFAULT_PORT
283 this.listenAddress = constant.DEFAULT_LISTEN_ADDR
284 this.hostname = constant.DEFAULT_HOSTNAME
285 this.httpsServerConfig = {}
286 this.basePath = ''
287 this.files = []
288 this.browserConsoleLogOptions = {
289 level: 'debug',
290 format: '%b %T: %m',
291 terminal: true
292 }
293 this.customContextFile = null
294 this.customDebugFile = null
295 this.customClientContextFile = null
296 this.exclude = []
297 this.logLevel = constant.LOG_INFO
298 this.colors = true
299 this.autoWatch = true
300 this.autoWatchBatchDelay = 250
301 this.restartOnFileChange = false
302 this.usePolling = process.platform === 'linux'
303 this.reporters = ['progress']
304 this.singleRun = false
305 this.browsers = []
306 this.captureTimeout = 60000
307 this.pingTimeout = 5000
308 this.proxies = {}
309 this.proxyValidateSSL = true
310 this.preprocessors = {}
311 this.preprocessor_priority = {}
312 this.urlRoot = '/'
313 this.upstreamProxy = undefined
314 this.reportSlowerThan = 0
315 this.loggers = [constant.CONSOLE_APPENDER]
316 this.transports = ['polling', 'websocket']
317 this.forceJSONP = false
318 this.plugins = ['karma-*']
319 this.defaultClient = this.client = {
320 args: [],
321 useIframe: true,
322 runInParent: false,
323 captureConsole: true,
324 clearContext: true
325 }
326 this.browserDisconnectTimeout = 2000
327 this.browserDisconnectTolerance = 0
328 this.browserNoActivityTimeout = 30000
329 this.processKillTimeout = 2000
330 this.concurrency = Infinity
331 this.failOnEmptyTestSuite = true
332 this.retryLimit = 2
333 this.detached = false
334 this.crossOriginAttribute = true
335 this.browserSocketTimeout = 20000
336 }
337
338 set (newConfig) {
339 _.mergeWith(this, newConfig, (obj, src) => {
340 // Overwrite arrays to keep consistent with #283
341 if (Array.isArray(src)) {
342 return src
343 }
344 })
345 }
346}
347
348const CONFIG_SYNTAX_HELP = ' module.exports = function(config) {\n' +
349 ' config.set({\n' +
350 ' // your config\n' +
351 ' });\n' +
352 ' };\n'
353
354function parseConfig (configFilePath, cliOptions) {
355 let configModule
356 if (configFilePath) {
357 try {
358 configModule = require(configFilePath)
359 if (typeof configModule === 'object' && typeof configModule.default !== 'undefined') {
360 configModule = configModule.default
361 }
362 } catch (e) {
363 log.error('Error in config file!\n ' + e.stack || e)
364
365 const extension = path.extname(configFilePath)
366 if (extension === '.coffee' && !COFFEE_SCRIPT_AVAILABLE) {
367 log.error('You need to install CoffeeScript.\n npm install coffeescript --save-dev')
368 } else if (extension === '.ls' && !LIVE_SCRIPT_AVAILABLE) {
369 log.error('You need to install LiveScript.\n npm install LiveScript --save-dev')
370 } else if (extension === '.ts' && !TYPE_SCRIPT_AVAILABLE) {
371 log.error('You need to install TypeScript.\n npm install typescript ts-node --save-dev')
372 }
373 return process.exit(1)
374 }
375 if (!helper.isFunction(configModule)) {
376 log.error('Config file must export a function!\n' + CONFIG_SYNTAX_HELP)
377 return process.exit(1)
378 }
379 } else {
380 configModule = () => {} // if no config file path is passed, we define a dummy config module.
381 }
382
383 const config = new Config()
384
385 // save and reset hostname and listenAddress so we can detect if the user
386 // changed them
387 const defaultHostname = config.hostname
388 config.hostname = null
389 const defaultListenAddress = config.listenAddress
390 config.listenAddress = null
391
392 // add the user's configuration in
393 config.set(cliOptions)
394
395 try {
396 configModule(config)
397 } catch (e) {
398 log.error('Error in config file!\n', e)
399 return process.exit(1)
400 }
401
402 // merge the config from config file and cliOptions (precedence)
403 config.set(cliOptions)
404
405 // if the user changed listenAddress, but didn't set a hostname, warn them
406 if (config.hostname === null && config.listenAddress !== null) {
407 log.warn(`ListenAddress was set to ${config.listenAddress} but hostname was left as the default: ` +
408 `${defaultHostname}. If your browsers fail to connect, consider changing the hostname option.`)
409 }
410 // restore values that weren't overwritten by the user
411 if (config.hostname === null) {
412 config.hostname = defaultHostname
413 }
414 if (config.listenAddress === null) {
415 config.listenAddress = defaultListenAddress
416 }
417
418 // configure the logger as soon as we can
419 logger.setup(config.logLevel, config.colors, config.loggers)
420
421 log.debug(configFilePath ? `Loading config ${configFilePath}` : 'No config file specified.')
422
423 return normalizeConfig(config, configFilePath)
424}
425
426// PUBLIC API
427exports.parseConfig = parseConfig
428exports.Pattern = Pattern
429exports.createPatternObject = createPatternObject
430exports.Config = Config