UNPKG

14.5 kBJavaScriptView Raw
1var path = require('path')
2
3var logger = require('./logger')
4var log = logger.create('config')
5var helper = require('./helper')
6var constant = require('./constants')
7
8var _ = require('lodash')
9
10var COFFEE_SCRIPT_AVAILABLE = false
11var LIVE_SCRIPT_AVAILABLE = false
12var TYPE_SCRIPT_AVAILABLE = false
13
14// Coffee is required here to enable config files written in coffee-script.
15// It's not directly used in this file.
16try {
17 require('coffee-script').register()
18 COFFEE_SCRIPT_AVAILABLE = true
19} catch (e) {}
20
21// CoffeeScript lost the hyphen in the module name a long time ago, all new version are named this:
22try {
23 require('coffeescript').register()
24 COFFEE_SCRIPT_AVAILABLE = true
25} catch (e) {}
26
27// LiveScript is required here to enable config files written in LiveScript.
28// It's not directly used in this file.
29try {
30 require('LiveScript')
31 LIVE_SCRIPT_AVAILABLE = true
32} catch (e) {}
33
34try {
35 require('ts-node').register()
36 TYPE_SCRIPT_AVAILABLE = true
37} catch (e) {}
38
39var Pattern = function (pattern, served, included, watched, nocache, type) {
40 this.pattern = pattern
41 this.served = helper.isDefined(served) ? served : true
42 this.included = helper.isDefined(included) ? included : true
43 this.watched = helper.isDefined(watched) ? watched : true
44 this.nocache = helper.isDefined(nocache) ? nocache : false
45 this.weight = helper.mmPatternWeight(pattern)
46 this.type = type
47}
48
49Pattern.prototype.compare = function (other) {
50 return helper.mmComparePatternWeights(this.weight, other.weight)
51}
52
53var UrlPattern = function (url) {
54 Pattern.call(this, url, false, true, false, false)
55}
56
57var createPatternObject = function (pattern) {
58 if (pattern && helper.isString(pattern)) {
59 return helper.isUrlAbsolute(pattern) ? new UrlPattern(pattern) : new Pattern(pattern)
60 }
61
62 if (helper.isObject(pattern)) {
63 if (pattern.pattern && helper.isString(pattern.pattern)) {
64 return helper.isUrlAbsolute(pattern.pattern)
65 ? new UrlPattern(pattern.pattern)
66 : new Pattern(
67 pattern.pattern,
68 pattern.served,
69 pattern.included,
70 pattern.watched,
71 pattern.nocache,
72 pattern.type)
73 }
74
75 log.warn('Invalid pattern %s!\n\tObject is missing "pattern" property.', pattern)
76 return new Pattern(null, false, false, false, false)
77 }
78
79 log.warn('Invalid pattern %s!\n\tExpected string or object with "pattern" property.', pattern)
80 return new Pattern(null, false, false, false, false)
81}
82
83var normalizeUrl = function (url) {
84 if (url.charAt(0) !== '/') {
85 url = '/' + url
86 }
87
88 if (url.charAt(url.length - 1) !== '/') {
89 url = url + '/'
90 }
91
92 return url
93}
94
95var normalizeUrlRoot = function (urlRoot) {
96 var normalizedUrlRoot = normalizeUrl(urlRoot)
97
98 if (normalizedUrlRoot !== urlRoot) {
99 log.warn('urlRoot normalized to "%s"', normalizedUrlRoot)
100 }
101
102 return normalizedUrlRoot
103}
104
105var normalizeProxyPath = function (proxyPath) {
106 var normalizedProxyPath = normalizeUrl(proxyPath)
107
108 if (normalizedProxyPath !== proxyPath) {
109 log.warn('proxyPath normalized to "%s"', normalizedProxyPath)
110 }
111
112 return normalizedProxyPath
113}
114
115var normalizeConfig = function (config, configFilePath) {
116 var basePathResolve = function (relativePath) {
117 if (helper.isUrlAbsolute(relativePath)) {
118 return relativePath
119 }
120
121 if (!helper.isDefined(config.basePath) || !helper.isDefined(relativePath)) {
122 return ''
123 }
124 return path.resolve(config.basePath, relativePath)
125 }
126
127 var createPatternMapper = function (resolve) {
128 return function (objectPattern) {
129 objectPattern.pattern = resolve(objectPattern.pattern)
130
131 return objectPattern
132 }
133 }
134
135 if (helper.isString(configFilePath)) {
136 // resolve basePath
137 config.basePath = path.resolve(path.dirname(configFilePath), config.basePath)
138
139 // always ignore the config file itself
140 config.exclude.push(configFilePath)
141 } else {
142 config.basePath = path.resolve(config.basePath || '.')
143 }
144
145 config.files = config.files.map(createPatternObject).map(createPatternMapper(basePathResolve))
146 config.exclude = config.exclude.map(basePathResolve)
147 config.customContextFile = config.customContextFile && basePathResolve(config.customContextFile)
148 config.customDebugFile = config.customDebugFile && basePathResolve(config.customDebugFile)
149 config.customClientContextFile = config.customClientContextFile && basePathResolve(config.customClientContextFile)
150
151 // normalize paths on windows
152 config.basePath = helper.normalizeWinPath(config.basePath)
153 config.files = config.files.map(createPatternMapper(helper.normalizeWinPath))
154 config.exclude = config.exclude.map(helper.normalizeWinPath)
155 config.customContextFile = helper.normalizeWinPath(config.customContextFile)
156 config.customDebugFile = helper.normalizeWinPath(config.customDebugFile)
157 config.customClientContextFile = helper.normalizeWinPath(config.customClientContextFile)
158
159 // normalize urlRoot
160 config.urlRoot = normalizeUrlRoot(config.urlRoot)
161
162 // normalize and default upstream proxy settings if given
163 if (config.upstreamProxy) {
164 var proxy = config.upstreamProxy
165 proxy.path = _.isUndefined(proxy.path) ? '/' : normalizeProxyPath(proxy.path)
166 proxy.hostname = _.isUndefined(proxy.hostname) ? 'localhost' : proxy.hostname
167 proxy.port = _.isUndefined(proxy.port) ? 9875 : proxy.port
168
169 // force protocol to end with ':'
170 proxy.protocol = (proxy.protocol || 'http').split(':')[0] + ':'
171 if (proxy.protocol.match(/https?:/) === null) {
172 log.warn('"%s" is not a supported upstream proxy protocol, defaulting to "http:"',
173 proxy.protocol)
174 proxy.protocol = 'http:'
175 }
176 }
177
178 // force protocol to end with ':'
179 config.protocol = (config.protocol || 'http').split(':')[0] + ':'
180 if (config.protocol.match(/https?:/) === null) {
181 log.warn('"%s" is not a supported protocol, defaulting to "http:"',
182 config.protocol)
183 config.protocol = 'http:'
184 }
185
186 if (config.proxies && config.proxies.hasOwnProperty(config.urlRoot)) {
187 log.warn('"%s" is proxied, you should probably change urlRoot to avoid conflicts',
188 config.urlRoot)
189 }
190
191 if (config.singleRun && config.autoWatch) {
192 log.debug('autoWatch set to false, because of singleRun')
193 config.autoWatch = false
194 }
195
196 if (config.runInParent) {
197 log.debug('useIframe set to false, because using runInParent')
198 config.useIframe = false
199 }
200
201 if (!config.singleRun && !config.useIframe && config.runInParent) {
202 log.debug('singleRun set to true, because using runInParent')
203 config.singleRun = true
204 }
205
206 if (helper.isString(config.reporters)) {
207 config.reporters = config.reporters.split(',')
208 }
209
210 if (config.client && config.client.args && !Array.isArray(config.client.args)) {
211 throw new Error('Invalid configuration: client.args must be an array of strings')
212 }
213
214 if (config.browsers && Array.isArray(config.browsers) === false) {
215 throw new TypeError('Invalid configuration: browsers option must be an array')
216 }
217
218 if (config.formatError && !helper.isFunction(config.formatError)) {
219 throw new TypeError('Invalid configuration: formatError option must be a function.')
220 }
221
222 if (config.processKillTimeout && !helper.isNumber(config.processKillTimeout)) {
223 throw new TypeError('Invalid configuration: processKillTimeout option must be a number.')
224 }
225
226 var defaultClient = config.defaultClient || {}
227 Object.keys(defaultClient).forEach(function (key) {
228 var option = config.client[key]
229 config.client[key] = helper.isDefined(option) ? option : defaultClient[key]
230 })
231
232 // normalize preprocessors
233 var preprocessors = config.preprocessors || {}
234 var normalizedPreprocessors = config.preprocessors = Object.create(null)
235
236 Object.keys(preprocessors).forEach(function (pattern) {
237 var normalizedPattern = helper.normalizeWinPath(basePathResolve(pattern))
238
239 normalizedPreprocessors[normalizedPattern] = helper.isString(preprocessors[pattern])
240 ? [preprocessors[pattern]] : preprocessors[pattern]
241 })
242
243 // define custom launchers/preprocessors/reporters - create an inlined plugin
244 var module = Object.create(null)
245 var hasSomeInlinedPlugin = false
246 var types = ['launcher', 'preprocessor', 'reporter']
247
248 types.forEach(function (type) {
249 var definitions = config['custom' + helper.ucFirst(type) + 's'] || {}
250
251 Object.keys(definitions).forEach(function (name) {
252 var definition = definitions[name]
253
254 if (!helper.isObject(definition)) {
255 return log.warn('Can not define %s %s. Definition has to be an object.', type, name)
256 }
257
258 if (!helper.isString(definition.base)) {
259 return log.warn('Can not define %s %s. Missing base %s.', type, name, type)
260 }
261
262 var token = type + ':' + definition.base
263 var locals = {
264 args: ['value', definition]
265 }
266
267 module[type + ':' + name] = ['factory', function (injector) {
268 var plugin = injector.createChild([locals], [token]).get(token)
269 if (type === 'launcher' && helper.isDefined(definition.displayName)) {
270 plugin.displayName = definition.displayName
271 }
272 return plugin
273 }]
274 hasSomeInlinedPlugin = true
275 })
276 })
277
278 if (hasSomeInlinedPlugin) {
279 config.plugins.push(module)
280 }
281
282 return config
283}
284
285var Config = function () {
286 var config = this
287
288 this.LOG_DISABLE = constant.LOG_DISABLE
289 this.LOG_ERROR = constant.LOG_ERROR
290 this.LOG_WARN = constant.LOG_WARN
291 this.LOG_INFO = constant.LOG_INFO
292 this.LOG_DEBUG = constant.LOG_DEBUG
293
294 this.set = function (newConfig) {
295 _.mergeWith(config, newConfig, function (obj, src) {
296 // Overwrite arrays to keep consistent with #283
297 if (_.isArray(src)) {
298 return src
299 }
300 })
301 }
302
303 // DEFAULT CONFIG
304 this.frameworks = []
305 this.protocol = 'http:'
306 this.port = constant.DEFAULT_PORT
307 this.listenAddress = constant.DEFAULT_LISTEN_ADDR
308 this.hostname = constant.DEFAULT_HOSTNAME
309 this.httpsServerConfig = {}
310 this.basePath = ''
311 this.files = []
312 this.browserConsoleLogOptions = {
313 level: 'debug',
314 format: '%b %T: %m',
315 terminal: true
316 }
317 this.customContextFile = null
318 this.customDebugFile = null
319 this.customClientContextFile = null
320 this.exclude = []
321 this.logLevel = constant.LOG_INFO
322 this.colors = true
323 this.autoWatch = true
324 this.autoWatchBatchDelay = 250
325 this.restartOnFileChange = false
326 this.usePolling = process.platform === 'linux'
327 this.reporters = ['progress']
328 this.singleRun = false
329 this.browsers = []
330 this.captureTimeout = 60000
331 this.proxies = {}
332 this.proxyValidateSSL = true
333 this.preprocessors = {}
334 this.urlRoot = '/'
335 this.upstreamProxy = undefined
336 this.reportSlowerThan = 0
337 this.loggers = [constant.CONSOLE_APPENDER]
338 this.transports = ['polling', 'websocket']
339 this.forceJSONP = false
340 this.plugins = ['karma-*']
341 this.defaultClient = this.client = {
342 args: [],
343 useIframe: true,
344 runInParent: false,
345 captureConsole: true,
346 clearContext: true
347 }
348 this.browserDisconnectTimeout = 2000
349 this.browserDisconnectTolerance = 0
350 this.browserNoActivityTimeout = 10000
351 this.processKillTimeout = 2000
352 this.concurrency = Infinity
353 this.failOnEmptyTestSuite = true
354 this.retryLimit = 2
355 this.detached = false
356 this.crossOriginAttribute = true
357}
358
359var CONFIG_SYNTAX_HELP = ' module.exports = function(config) {\n' +
360 ' config.set({\n' +
361 ' // your config\n' +
362 ' });\n' +
363 ' };\n'
364
365var parseConfig = function (configFilePath, cliOptions) {
366 var configModule
367 if (configFilePath) {
368
369 try {
370 configModule = require(configFilePath)
371 if (typeof configModule === 'object' && typeof configModule.default !== 'undefined') {
372 configModule = configModule.default
373 }
374 } catch (e) {
375 if (e.code === 'MODULE_NOT_FOUND' && e.message.indexOf(configFilePath) !== -1) {
376 log.error('File %s does not exist!', configFilePath)
377 } else {
378 log.error('Invalid config file!\n ' + e.stack)
379
380 var extension = path.extname(configFilePath)
381 if (extension === '.coffee' && !COFFEE_SCRIPT_AVAILABLE) {
382 log.error('You need to install CoffeeScript.\n' +
383 ' npm install coffee-script --save-dev')
384 } else if (extension === '.ls' && !LIVE_SCRIPT_AVAILABLE) {
385 log.error('You need to install LiveScript.\n' +
386 ' npm install LiveScript --save-dev')
387 } else if (extension === '.ts' && !TYPE_SCRIPT_AVAILABLE) {
388 log.error('You need to install TypeScript.\n' +
389 ' npm install typescript ts-node --save-dev')
390 }
391 }
392 return process.exit(1)
393 }
394 if (!helper.isFunction(configModule)) {
395 log.error('Config file must export a function!\n' + CONFIG_SYNTAX_HELP)
396 return process.exit(1)
397 }
398 } else {
399 // if no config file path is passed, we define a dummy config module.
400 configModule = function () {}
401 }
402
403 var config = new Config()
404
405 // save and reset hostname and listenAddress so we can detect if the user
406 // changed them
407 var defaultHostname = config.hostname
408 config.hostname = null
409 var defaultListenAddress = config.listenAddress
410 config.listenAddress = null
411
412 // add the user's configuration in
413 config.set(cliOptions)
414
415 try {
416 configModule(config)
417 } catch (e) {
418 log.error('Error in config file!\n', e)
419 return process.exit(1)
420 }
421
422 // merge the config from config file and cliOptions (precedence)
423 config.set(cliOptions)
424
425 // if the user changed listenAddress, but didn't set a hostname, warn them
426 if (config.hostname === null && config.listenAddress !== null) {
427 log.warn('ListenAddress was set to %s but hostname was left as the default: ' +
428 '%s. If your browsers fail to connect, consider changing the hostname option.',
429 config.listenAddress, defaultHostname)
430 }
431 // restore values that weren't overwritten by the user
432 if (config.hostname === null) {
433 config.hostname = defaultHostname
434 }
435 if (config.listenAddress === null) {
436 config.listenAddress = defaultListenAddress
437 }
438
439 // configure the logger as soon as we can
440 logger.setup(config.logLevel, config.colors, config.loggers)
441
442 if (configFilePath) {
443 log.debug('Loading config %s', configFilePath)
444 } else {
445 log.debug('No config file specified.')
446 }
447
448 return normalizeConfig(config, configFilePath)
449}
450
451// PUBLIC API
452exports.parseConfig = parseConfig
453exports.Pattern = Pattern
454exports.createPatternObject = createPatternObject
455exports.Config = Config