#!/usr/bin/env node 'use strict' const ansi = require('ansi-escape-sequences') const path = require('path') const CommandLineTool = require('command-line-tool') const flatten = require('reduce-flatten') const arrayify = require('array-back') /** * @module local-web-server */ const tool = new CommandLineTool() /** * @alias module:local-web-server * @extends module:middleware-stack */ class LocalWebServer { constructor (initOptions) { initOptions = initOptions || {} const commandLineArgs = require('command-line-args') const commandLineUsage = require('command-line-usage') const cli = require('../lib/cli-data') const loadConfig = require('config-master') const stored = loadConfig('local-web-server') /* manually scan for any --stack passed, as we may need to display stack options */ const stackPaths = arrayify(initOptions.stack || stored.stack) || [] const stackIndex = process.argv.indexOf('--stack') if (stackIndex > -1) { for (var i = stackIndex + 1; i < process.argv.length; i++) { const stackPath = process.argv[i] if (/^-/.test(stackPath)) { break } else { stackPaths.push(stackPath) } } } /* if the user did not supply a stack, use the default */ if (!stackPaths.length) stackPaths.push(path.resolve(__dirname, '..', 'node_modules', 'local-web-server-default-stack')) /* load the stack */ const stackModules = stackPaths .map(stackPath => loadStack(stackPath)) .map(Middleware => new Middleware()) .map(module => { if (module.stack) { const featureStack = module.stack() module.optionDefinitions = function () { return featureStack .map(Feature => new Feature()) .map(feature => feature.optionDefinitions && feature.optionDefinitions()) .filter(definitions => definitions) .reduce(flatten, []) } module.middleware = function (options) { return featureStack .map(Feature => new Feature()) .map(feature => feature.middleware(options)) .reduce(flatten, []) .filter(mw => mw) } } return module }) /* gather stack option definitions and parse the command line */ const middlewareOptionDefinitions = stackModules .filter(mw => mw.optionDefinitions) .map(mw => mw.optionDefinitions()) .reduce(flatten, []) .filter(def => def) .map(def => { def.group = 'middleware' return def }) const usage = commandLineUsage(cli.usage(middlewareOptionDefinitions)) let options = {} const allOptionDefinitions = cli.optionDefinitions.concat(middlewareOptionDefinitions) try { options = commandLineArgs(allOptionDefinitions) } catch (err) { tool.printError(err) tool.printError(allOptionDefinitions.map(def => { return `name: ${def.name}${def.alias ? ', alias: ' + def.alias : ''}` }).join('\n')) console.error(usage) tool.halt() } /* combine in stored config */ options = Object.assign({ port: 8000 }, initOptions, stored || {}, options.server, options.middleware, options.misc) this.options = options // console.log(initOptions, stored, options) if (options.verbose) { // debug.setLevel(1) } /* --config */ if (options.config) { tool.stop(JSON.stringify(options, null, ' '), 0) /* --version */ } else if (options.version) { const pkg = require(path.resolve(__dirname, '..', 'package.json')) tool.stop(pkg.version) /* --help */ } else if (options.help) { tool.stop(usage) } else { this.stack = stackModules } } getApplication () { const Koa = require('koa') const app = new Koa() const compose = require('koa-compose') const convert = require('koa-convert') const middlewareStack = this.stack .filter(mw => mw.middleware) .map(mw => mw.middleware(this.options)) .reduce(flatten, []) .filter(mw => mw) .map(convert) app.use(compose(middlewareStack)) app.on('error', err => { const defaultLogInUse = this.stack.some(mw => mw.constructor.name === 'Log') if (defaultLogInUse) { if (this.options['log.format']) console.error(ansi.format(err.stack, 'red')) } else { console.error(ansi.format(err.stack, 'red')) } }) return app } getServer () { const app = this.getApplication() const options = this.options let key = options.key let cert = options.cert if (options.https && !(key && cert)) { key = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.key') cert = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.crt') } let server = null if (key && cert) { const fs = require('fs') const serverOptions = { key: fs.readFileSync(key), cert: fs.readFileSync(cert) } const https = require('https') server = https.createServer(serverOptions, app.callback()) server.isHttps = true } else { const http = require('http') server = http.createServer(app.callback()) } return server } listen () { const options = this.options const server = this._server = this.getServer() // console.log(options) return new Promise((resolve, reject) => { server.listen(options.port, () => { onServerUp(options.port, options['static.root'], server.isHttps) resolve(server) }) }) } close () { this._server.close() } } function onServerUp (port, directory, isHttps) { const ipList = getIPList() .map(iface => `[underline]{${isHttps ? 'https' : 'http'}://${iface.address}:${port}}`) .join(', ') console.error(ansi.format( path.resolve(directory || '') === process.cwd() ? `serving at ${ipList}` : `serving [underline]{${directory}} at ${ipList}` )) } function getIPList () { const flatten = require('reduce-flatten') const os = require('os') let ipList = Object.keys(os.networkInterfaces()) .map(key => os.networkInterfaces()[key]) .reduce(flatten, []) .filter(iface => iface.family === 'IPv4') ipList.unshift({ address: os.hostname() }) return ipList } /** * Loads a module by either path or name. * @returns {object} */ function loadStack (modulePath) { let module if (isModule(modulePath)) return modulePath const tried = [] if (modulePath) { try { tried.push(path.resolve(modulePath)) module = require(path.resolve(modulePath)) } catch (err) { const walkBack = require('walk-back') const foundPath = walkBack(process.cwd(), path.join('node_modules', 'local-web-server-' + modulePath)) tried.push('local-web-server-' + modulePath) if (foundPath) { module = require(foundPath) } else { const foundPath2 = walkBack(process.cwd(), path.join('node_modules', modulePath)) tried.push(modulePath) if (foundPath2) { module = require(foundPath2) } } } } if (module) { if (!isModule(module)) { const insp = require('util').inspect(module, { depth: 3, colors: true }) const msg = `Not valid Middleware at: ${insp}` tool.halt(new Error(msg)) } } else { const msg = `No module found at: \n${tried.join('\n')}` tool.halt(new Error(msg)) } return module } function isModule (module) { return module.prototype && (module.prototype.middleware || module.prototype.stack) } module.exports = LocalWebServer