config.js 14 KB

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