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.

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