You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

282 lines
7.7 KiB

9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
  1. #!/usr/bin/env node
  2. 'use strict'
  3. const path = require('path')
  4. const CommandLineTool = require('command-line-tool')
  5. const flatten = require('reduce-flatten')
  6. const arrayify = require('array-back')
  7. const ansi = require('ansi-escape-sequences')
  8. /**
  9. * @module local-web-server
  10. */
  11. const tool = new CommandLineTool()
  12. /**
  13. * @alias module:local-web-server
  14. * @extends module:middleware-stack
  15. */
  16. class LocalWebServer {
  17. constructor (initOptions) {
  18. initOptions = initOptions || {}
  19. const commandLineArgs = require('command-line-args')
  20. const commandLineUsage = require('command-line-usage')
  21. const cli = require('../lib/cli-data')
  22. const loadConfig = require('config-master')
  23. const stored = loadConfig('local-web-server')
  24. /* manually scan for any --stack passed, as we may need to display stack options */
  25. const stackPaths = arrayify(initOptions.stack || stored.stack) || []
  26. const stackIndex = process.argv.indexOf('--stack')
  27. if (stackIndex > -1) {
  28. for (var i = stackIndex + 1; i < process.argv.length; i++) {
  29. const stackPath = process.argv[i]
  30. if (/^-/.test(stackPath)) {
  31. break
  32. } else {
  33. stackPaths.push(stackPath)
  34. }
  35. }
  36. }
  37. /* if the user did not supply a stack, use the default */
  38. if (!stackPaths.length) stackPaths.push(path.resolve(__dirname, '..', 'node_modules', 'local-web-server-default-stack'))
  39. /* build the stack */
  40. const stackModules = buildStack(stackPaths, this.onVerbose.bind(this), this.onDebug.bind(this))
  41. /* gather stack option definitions and parse the command line */
  42. const middlewareOptionDefinitions = stackModules
  43. .filter(mw => mw.optionDefinitions)
  44. .map(mw => mw.optionDefinitions())
  45. .reduce(flatten, [])
  46. .filter(def => def)
  47. .map(def => {
  48. def.group = 'middleware'
  49. return def
  50. })
  51. const usage = commandLineUsage(cli.usage(middlewareOptionDefinitions))
  52. let options = {}
  53. const allOptionDefinitions = cli.optionDefinitions.concat(middlewareOptionDefinitions)
  54. if (!initOptions.testMode) {
  55. try {
  56. options = commandLineArgs(allOptionDefinitions)
  57. } catch (err) {
  58. tool.printError(err)
  59. tool.printError(allOptionDefinitions.map(def => {
  60. return `name: ${def.name}${def.alias ? ', alias: ' + def.alias : ''}`
  61. }).join('\n'))
  62. console.error(usage)
  63. tool.halt()
  64. }
  65. }
  66. /* combine in stored config */
  67. options = Object.assign(
  68. { port: 8000 },
  69. initOptions,
  70. stored,
  71. options.server,
  72. options.middleware,
  73. options.misc
  74. )
  75. /**
  76. * Config
  77. * @type {object}
  78. */
  79. this.options = options
  80. stackModules
  81. .filter(mw => mw.on)
  82. .forEach(mw => {
  83. mw.on('verbose', this.onVerbose.bind(this))
  84. mw.on('debug', this.onDebug.bind(this))
  85. })
  86. /* --config */
  87. if (options.config) {
  88. tool.stop(JSON.stringify(options, null, ' '), 0)
  89. /* --version */
  90. } else if (options.version) {
  91. const pkg = require(path.resolve(__dirname, '..', 'package.json'))
  92. tool.stop(pkg.version)
  93. /* --help */
  94. } else if (options.help) {
  95. tool.stop(usage)
  96. } else {
  97. this.stack = stackModules
  98. }
  99. }
  100. getApplication () {
  101. const Koa = require('koa')
  102. const app = new Koa()
  103. const compose = require('koa-compose')
  104. const convert = require('koa-convert')
  105. const middlewareStack = this.stack
  106. .filter(mw => mw.middleware)
  107. .map(mw => mw.middleware(this.options))
  108. .reduce(flatten, [])
  109. .filter(mw => mw)
  110. .map(convert)
  111. app.use(compose(middlewareStack))
  112. app.on('error', err => {
  113. console.error(ansi.format(err.stack, 'red'))
  114. })
  115. return app
  116. }
  117. getServer (onListening) {
  118. const app = this.getApplication()
  119. const options = this.options
  120. let key = options.key
  121. let cert = options.cert
  122. if (options.https && !(key && cert)) {
  123. key = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.key')
  124. cert = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.crt')
  125. }
  126. let server = null
  127. if (key && cert) {
  128. const fs = require('fs')
  129. const serverOptions = {
  130. key: fs.readFileSync(key),
  131. cert: fs.readFileSync(cert)
  132. }
  133. const https = require('https')
  134. server = https.createServer(serverOptions, app.callback())
  135. server.isHttps = true
  136. } else {
  137. const http = require('http')
  138. server = http.createServer(app.callback())
  139. }
  140. const tableLayout = require('table-layout')
  141. server.listen(options.port)
  142. if (onListening) server.on('listening', onListening)
  143. if (!options.testMode) {
  144. server.on('listening', function () {
  145. const ipList = getIPList()
  146. .map(iface => `[underline]{${server.isHttps ? 'https' : 'http'}://${iface.address}:${options.port}}`)
  147. .join(', ')
  148. console.error(ansi.format('Serving at', 'bold'), ansi.format(ipList))
  149. })
  150. }
  151. return server
  152. }
  153. onVerbose (title, msg) {
  154. if (this.options.verbose) {
  155. console.error(ansi.format(title, 'bold'), msg)
  156. }
  157. }
  158. onDebug (title, msg) {
  159. if (this.options.debug) {
  160. console.error(ansi.format(title, 'bold'), msg)
  161. }
  162. }
  163. }
  164. /**
  165. * Loads a module by either path or name.
  166. * @returns {object}
  167. */
  168. function loadStack (modulePath) {
  169. let module
  170. if (isModule(modulePath)) return modulePath
  171. const tried = []
  172. if (modulePath) {
  173. try {
  174. tried.push(path.resolve(modulePath))
  175. module = require(path.resolve(modulePath))
  176. } catch (err) {
  177. const walkBack = require('walk-back')
  178. const foundPath = walkBack(process.cwd(), path.join('node_modules', 'local-web-server-' + modulePath))
  179. tried.push('local-web-server-' + modulePath)
  180. if (foundPath) {
  181. module = require(foundPath)
  182. } else {
  183. const foundPath2 = walkBack(process.cwd(), path.join('node_modules', modulePath))
  184. tried.push(modulePath)
  185. if (foundPath2) {
  186. module = require(foundPath2)
  187. }
  188. }
  189. }
  190. }
  191. if (module) {
  192. if (!isModule(module)) {
  193. const insp = require('util').inspect(module, { depth: 3, colors: true })
  194. const msg = `Not valid Middleware at: ${insp}`
  195. tool.halt(new Error(msg))
  196. }
  197. } else {
  198. const msg = `No module found at: \n${tried.join('\n')}`
  199. tool.halt(new Error(msg))
  200. }
  201. return module
  202. }
  203. function isModule (module) {
  204. return module.prototype && (module.prototype.middleware || module.prototype.stack)
  205. }
  206. function getIPList () {
  207. const flatten = require('reduce-flatten')
  208. const os = require('os')
  209. let ipList = Object.keys(os.networkInterfaces())
  210. .map(key => os.networkInterfaces()[key])
  211. .reduce(flatten, [])
  212. .filter(iface => iface.family === 'IPv4')
  213. ipList.unshift({ address: os.hostname() })
  214. return ipList
  215. }
  216. function buildStack (stackPaths, onVerbose, onDebug) {
  217. return stackPaths
  218. .map(stackPath => loadStack(stackPath))
  219. .map(Middleware => new Middleware())
  220. .map(module => {
  221. if (module.stack) {
  222. const featureStack = module.stack()
  223. .map(Feature => new Feature())
  224. .map(feature => {
  225. if (feature.on) {
  226. feature.on('verbose', onVerbose)
  227. feature.on('debug', onDebug)
  228. }
  229. return feature
  230. })
  231. module.optionDefinitions = function () {
  232. return featureStack
  233. .map(feature => feature.optionDefinitions && feature.optionDefinitions())
  234. .filter(definitions => definitions)
  235. .reduce(flatten, [])
  236. }
  237. module.middleware = function (options) {
  238. return featureStack
  239. .map(feature => feature.middleware(options))
  240. .reduce(flatten, [])
  241. .filter(mw => mw)
  242. }
  243. }
  244. return module
  245. })
  246. }
  247. module.exports = LocalWebServer