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.

312 lines
8.5 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
  1. #!/usr/bin/env node
  2. 'use strict'
  3. const path = require('path')
  4. const flatten = require('reduce-flatten')
  5. const arrayify = require('array-back')
  6. const ansi = require('ansi-escape-sequences')
  7. /**
  8. * @module local-web-server
  9. */
  10. /**
  11. * @alias module:local-web-server
  12. * @extends module:middleware-stack
  13. */
  14. class LocalWebServer {
  15. /**
  16. * @param [options] {object} - Server options
  17. * @param [options.port} {number} - Port
  18. * @param [options.stack} {string[]|Features[]} - Port
  19. */
  20. constructor (initOptions) {
  21. initOptions = initOptions || {}
  22. const commandLineUsage = require('command-line-usage')
  23. const CliView = require('./cli-view')
  24. const cli = require('../lib/cli-data')
  25. /* get stored config */
  26. const loadConfig = require('config-master')
  27. const stored = loadConfig('local-web-server')
  28. /* read the config and command-line for feature paths */
  29. const featurePaths = parseFeaturePaths(initOptions.stack || stored.stack)
  30. /**
  31. * Loaded feature modules
  32. * @type {Feature[]}
  33. */
  34. this.features = this._buildFeatureStack(featurePaths)
  35. /* gather feature optionDefinitions and parse the command line */
  36. const featureOptionDefinitions = gatherOptionDefinitions(this.features)
  37. const usage = commandLineUsage(cli.usage(featureOptionDefinitions))
  38. const allOptionDefinitions = cli.optionDefinitions.concat(featureOptionDefinitions)
  39. let options = initOptions.testMode ? {} : parseCommandLineOptions(allOptionDefinitions, this.view)
  40. /* combine in stored config */
  41. options = Object.assign(
  42. { port: 8000 },
  43. initOptions,
  44. stored,
  45. options.server,
  46. options.middleware,
  47. options.misc
  48. )
  49. /**
  50. * Config
  51. * @type {object}
  52. */
  53. this.options = options
  54. /**
  55. * Current view.
  56. * @type {View}
  57. */
  58. this.view = null
  59. /* --config */
  60. if (options.config) {
  61. console.error(JSON.stringify(options, null, ' '))
  62. process.exit(0)
  63. /* --version */
  64. } else if (options.version) {
  65. const pkg = require(path.resolve(__dirname, '..', 'package.json'))
  66. console.error(pkg.version)
  67. process.exit(0)
  68. /* --help */
  69. } else if (options.help) {
  70. console.error(usage)
  71. process.exit(0)
  72. } else {
  73. /**
  74. * Node.js server
  75. * @type {Server}
  76. */
  77. this.server = this.getServer()
  78. if (options.view) {
  79. const View = loadModule(options.view)
  80. this.view = new View(this)
  81. } else {
  82. this.view = new CliView(this)
  83. }
  84. }
  85. }
  86. /**
  87. * Returns a middleware application suitable for passing to `http.createServer`. The application is a function with three args (req, res and next) which can be created by express, Koa or hand-rolled.
  88. * @returns {function}
  89. */
  90. getApplication () {
  91. const Koa = require('koa')
  92. const app = new Koa()
  93. const compose = require('koa-compose')
  94. const convert = require('koa-convert')
  95. const middlewareStack = this.features
  96. .filter(mw => mw.middleware)
  97. .map(mw => mw.middleware(this.options, this))
  98. .reduce(flatten, [])
  99. .filter(mw => mw)
  100. .map(convert)
  101. app.use(compose(middlewareStack))
  102. app.on('error', err => {
  103. console.error(ansi.format(err.stack, 'red'))
  104. })
  105. return app.callback()
  106. }
  107. /**
  108. * Returns a listening server which processes requests using the middleware supplied.
  109. * @returns {Server}
  110. */
  111. getServer () {
  112. const app = this.getApplication()
  113. const options = this.options
  114. let key = options.key
  115. let cert = options.cert
  116. if (options.https && !(key && cert)) {
  117. key = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.key')
  118. cert = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.crt')
  119. }
  120. let server = null
  121. if (key && cert) {
  122. const fs = require('fs')
  123. const serverOptions = {
  124. key: fs.readFileSync(key),
  125. cert: fs.readFileSync(cert)
  126. }
  127. const https = require('https')
  128. server = https.createServer(serverOptions, app)
  129. server.isHttps = true
  130. } else {
  131. const http = require('http')
  132. server = http.createServer(app)
  133. }
  134. server.listen(options.port)
  135. // if (onListening) server.on('listening', onListening)
  136. /* on server-up message */
  137. if (!options.testMode) {
  138. server.on('listening', () => {
  139. const ipList = getIPList()
  140. .map(iface => `[underline]{${server.isHttps ? 'https' : 'http'}://${iface.address}:${options.port}}`)
  141. .join(', ')
  142. console.error('Serving at', ansi.format(ipList))
  143. })
  144. }
  145. return server
  146. }
  147. _buildFeatureStack (featurePaths) {
  148. return featurePaths
  149. .map(featurePath => loadStack(featurePath))
  150. .map(Feature => new Feature())
  151. .map(feature => {
  152. if (feature.stack) {
  153. const featureStack = feature.stack()
  154. .map(Feature => new Feature())
  155. feature.optionDefinitions = function () {
  156. return featureStack
  157. .map(feature => feature.optionDefinitions && feature.optionDefinitions())
  158. .filter(definitions => definitions)
  159. .reduce(flatten, [])
  160. }
  161. feature.middleware = function (options, view) {
  162. return featureStack
  163. .map(feature => feature.middleware(options, view))
  164. .reduce(flatten, [])
  165. .filter(mw => mw)
  166. }
  167. }
  168. return feature
  169. })
  170. }
  171. }
  172. /**
  173. * Loads a module by either path or name.
  174. * @returns {object}
  175. */
  176. function loadStack (modulePath) {
  177. const isModule = module => module.prototype && (module.prototype.middleware || module.prototype.stack)
  178. if (isModule(modulePath)) return modulePath
  179. const module = loadModule(modulePath)
  180. if (module) {
  181. if (!isModule(module)) {
  182. const insp = require('util').inspect(module, { depth: 3, colors: true })
  183. const msg = `Not valid Middleware at: ${insp}`
  184. console.error(msg)
  185. process.exit(1)
  186. }
  187. } else {
  188. const msg = `No module found for: ${modulePath}`
  189. console.error(msg)
  190. process.exit(1)
  191. }
  192. return module
  193. }
  194. function loadModule (modulePath) {
  195. let module
  196. const tried = []
  197. if (modulePath) {
  198. try {
  199. tried.push(path.resolve(modulePath))
  200. module = require(path.resolve(modulePath))
  201. } catch (err) {
  202. const walkBack = require('walk-back')
  203. const foundPath = walkBack(process.cwd(), path.join('node_modules', 'local-web-server-' + modulePath))
  204. tried.push('local-web-server-' + modulePath)
  205. if (foundPath) {
  206. module = require(foundPath)
  207. } else {
  208. const foundPath2 = walkBack(process.cwd(), path.join('node_modules', modulePath))
  209. tried.push(modulePath)
  210. if (foundPath2) {
  211. module = require(foundPath2)
  212. }
  213. }
  214. }
  215. }
  216. return module
  217. }
  218. function getIPList () {
  219. const flatten = require('reduce-flatten')
  220. const os = require('os')
  221. let ipList = Object.keys(os.networkInterfaces())
  222. .map(key => os.networkInterfaces()[key])
  223. .reduce(flatten, [])
  224. .filter(iface => iface.family === 'IPv4')
  225. ipList.unshift({ address: os.hostname() })
  226. return ipList
  227. }
  228. /* manually scan for any --stack passed, as we may need to display stack options */
  229. function parseFeaturePaths (configStack) {
  230. const featurePaths = arrayify(configStack)
  231. const featureIndex = process.argv.indexOf('--stack')
  232. if (featureIndex > -1) {
  233. for (var i = featureIndex + 1; i < process.argv.length; i++) {
  234. const featurePath = process.argv[i]
  235. if (/^-/.test(featurePath)) {
  236. break
  237. } else {
  238. featurePaths.push(featurePath)
  239. }
  240. }
  241. }
  242. /* if the user did not supply a stack, use the default */
  243. if (!featurePaths.length) featurePaths.push(path.resolve(__dirname, '..', 'node_modules', 'local-web-server-default-stack'))
  244. return featurePaths
  245. }
  246. function gatherOptionDefinitions (features) {
  247. return features
  248. .filter(mw => mw.optionDefinitions)
  249. .map(mw => mw.optionDefinitions())
  250. .reduce(flatten, [])
  251. .filter(def => def)
  252. .map(def => {
  253. def.group = 'middleware'
  254. return def
  255. })
  256. }
  257. function parseCommandLineOptions (allOptionDefinitions) {
  258. const commandLineArgs = require('command-line-args')
  259. try {
  260. return commandLineArgs(allOptionDefinitions)
  261. } catch (err) {
  262. console.error(err)
  263. /* handle duplicate option names */
  264. if (err.name === 'DUPLICATE_NAME') {
  265. console.error('\nOption Definitions:')
  266. console.error(allOptionDefinitions.map(def => {
  267. return `name: ${def.name}${def.alias ? ', alias: ' + def.alias : ''}`
  268. }).join('\n'))
  269. }
  270. console.error(usage)
  271. process.exit(1)
  272. }
  273. }
  274. module.exports = LocalWebServer