config.js 14 KB

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