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.

341 lines
9.5 KiB

9 years ago
8 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 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. for (const feature of this.features) {
  85. if (feature.ready) {
  86. feature.ready(this)
  87. }
  88. }
  89. }
  90. }
  91. /**
  92. * 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.
  93. * @returns {function}
  94. */
  95. getApplication () {
  96. const Koa = require('koa')
  97. const app = new Koa()
  98. const compose = require('koa-compose')
  99. const convert = require('koa-convert')
  100. const middlewareStack = this.features
  101. .filter(mw => mw.middleware)
  102. .map(mw => mw.middleware(this.options, this))
  103. .reduce(flatten, [])
  104. .filter(mw => mw)
  105. .map(convert)
  106. app.use(compose(middlewareStack))
  107. app.on('error', err => {
  108. console.error(ansi.format(err.stack, 'red'))
  109. })
  110. return app.callback()
  111. }
  112. /**
  113. * Returns a listening server which processes requests using the middleware supplied.
  114. * @returns {Server}
  115. */
  116. getServer () {
  117. const app = this.getApplication()
  118. const options = this.options
  119. let key = options.key
  120. let cert = options.cert
  121. if (options.https && !(key && cert)) {
  122. key = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.key')
  123. cert = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.crt')
  124. }
  125. let server = null
  126. if (key && cert) {
  127. const fs = require('fs')
  128. const serverOptions = {
  129. key: fs.readFileSync(key),
  130. cert: fs.readFileSync(cert)
  131. }
  132. const https = require('https')
  133. server = https.createServer(serverOptions, app)
  134. server.isHttps = true
  135. } else {
  136. const http = require('http')
  137. server = http.createServer(app)
  138. }
  139. server.listen(options.port)
  140. // if (onListening) server.on('listening', onListening)
  141. /* on server-up message */
  142. if (!options.testMode) {
  143. server.on('listening', () => {
  144. const ipList = getIPList()
  145. .map(iface => `[underline]{${server.isHttps ? 'https' : 'http'}://${iface.address}:${options.port}}`)
  146. .join(', ')
  147. console.error('Serving at', ansi.format(ipList))
  148. })
  149. }
  150. return server
  151. }
  152. /**
  153. * Returns an array of Feature instances, given their module paths/names.
  154. * @return {feature[]}
  155. */
  156. _buildFeatureStack (featurePaths) {
  157. const FeatureBase = require('./feature')
  158. return featurePaths
  159. .map(featurePath => loadFeature(featurePath))
  160. .map(Feature => new Feature(this))
  161. .map(feature => FeatureBase.prototype.expandStack.call(feature))
  162. }
  163. }
  164. /**
  165. * Load a module and verify it's of the correct type
  166. * @returns {Feature}
  167. */
  168. function loadFeature (modulePath) {
  169. const isModule = module => module.prototype && (module.prototype.middleware || module.prototype.stack || module.prototype.ready)
  170. if (isModule(modulePath)) return modulePath
  171. const module = loadModule(modulePath)
  172. if (module) {
  173. if (!isModule(module)) {
  174. const insp = require('util').inspect(module, { depth: 3, colors: true })
  175. const msg = `Not valid Middleware at: ${insp}`
  176. console.error(msg)
  177. process.exit(1)
  178. }
  179. } else {
  180. const msg = `No module found for: ${modulePath}`
  181. console.error(msg)
  182. process.exit(1)
  183. }
  184. return module
  185. }
  186. /**
  187. * Returns a module, loaded by the first to succeed from
  188. * - direct path
  189. * - 'node_modules/local-web-server-' + path, from current folder upward
  190. * - 'node_modules/' + path, from current folder upward
  191. * - also search local-web-server project node_modules? (e.g. to search for a feature module without need installing it locally)
  192. * @returns {object}
  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. if (!(err && err.code === 'MODULE_NOT_FOUND')) {
  203. throw err
  204. }
  205. const walkBack = require('walk-back')
  206. const foundPath = walkBack(process.cwd(), path.join('node_modules', 'local-web-server-' + modulePath))
  207. tried.push('local-web-server-' + modulePath)
  208. if (foundPath) {
  209. module = require(foundPath)
  210. } else {
  211. const foundPath2 = walkBack(process.cwd(), path.join('node_modules', modulePath))
  212. tried.push(modulePath)
  213. if (foundPath2) {
  214. module = require(foundPath2)
  215. } else {
  216. const foundPath3 = walkBack(path.resolve(__filename, '..'), path.join('node_modules', 'local-web-server-' + modulePath))
  217. if (foundPath3) {
  218. return require(foundPath3)
  219. } else {
  220. const foundPath4 = walkBack(path.resolve(__filename, '..'), path.join('node_modules', modulePath))
  221. if (foundPath4) {
  222. return require(foundPath4)
  223. }
  224. }
  225. }
  226. }
  227. }
  228. }
  229. return module
  230. }
  231. /**
  232. * Returns an array of available IPv4 network interfaces
  233. * @example
  234. * [ { address: 'mbp.local' },
  235. * { address: '127.0.0.1',
  236. * netmask: '255.0.0.0',
  237. * family: 'IPv4',
  238. * mac: '00:00:00:00:00:00',
  239. * internal: true },
  240. * { address: '192.168.1.86',
  241. * netmask: '255.255.255.0',
  242. * family: 'IPv4',
  243. * mac: 'd0:a6:37:e9:86:49',
  244. * internal: false } ]
  245. */
  246. function getIPList () {
  247. const flatten = require('reduce-flatten')
  248. const os = require('os')
  249. let ipList = Object.keys(os.networkInterfaces())
  250. .map(key => os.networkInterfaces()[key])
  251. .reduce(flatten, [])
  252. .filter(iface => iface.family === 'IPv4')
  253. ipList.unshift({ address: os.hostname() })
  254. return ipList
  255. }
  256. /* manually scan for any --stack passed, as we may need to display stack options */
  257. function parseFeaturePaths (configStack) {
  258. const featurePaths = arrayify(configStack)
  259. const featureIndex = process.argv.indexOf('--stack')
  260. if (featureIndex > -1) {
  261. for (var i = featureIndex + 1; i < process.argv.length; i++) {
  262. const featurePath = process.argv[i]
  263. if (/^-/.test(featurePath)) {
  264. break
  265. } else {
  266. featurePaths.push(featurePath)
  267. }
  268. }
  269. }
  270. /* if the user did not supply a stack, use the default */
  271. if (!featurePaths.length) featurePaths.push(path.resolve(__dirname, '..', 'node_modules', 'local-web-server-default-stack'))
  272. return featurePaths
  273. }
  274. function gatherOptionDefinitions (features) {
  275. return features
  276. .filter(mw => mw.optionDefinitions)
  277. .map(mw => mw.optionDefinitions())
  278. .reduce(flatten, [])
  279. .filter(def => def)
  280. .map(def => {
  281. def.group = 'middleware'
  282. return def
  283. })
  284. }
  285. function parseCommandLineOptions (allOptionDefinitions) {
  286. const commandLineArgs = require('command-line-args')
  287. try {
  288. return commandLineArgs(allOptionDefinitions)
  289. } catch (err) {
  290. console.error(err)
  291. /* handle duplicate option names */
  292. if (err.name === 'DUPLICATE_NAME') {
  293. console.error('\nOption Definitions:')
  294. console.error(allOptionDefinitions.map(def => {
  295. return `name: ${def.name}${def.alias ? ', alias: ' + def.alias : ''}`
  296. }).join('\n'))
  297. }
  298. console.error(usage)
  299. process.exit(1)
  300. }
  301. }
  302. module.exports = LocalWebServer