|
|
@ -1,7 +1,6 @@ |
|
|
|
#!/usr/bin/env node
|
|
|
|
'use strict' |
|
|
|
const path = require('path') |
|
|
|
const CommandLineTool = require('command-line-tool') |
|
|
|
const flatten = require('reduce-flatten') |
|
|
|
const arrayify = require('array-back') |
|
|
|
const ansi = require('ansi-escape-sequences') |
|
|
@ -10,44 +9,68 @@ const ansi = require('ansi-escape-sequences') |
|
|
|
* @module local-web-server |
|
|
|
*/ |
|
|
|
|
|
|
|
const tool = new CommandLineTool() |
|
|
|
|
|
|
|
class CliView { |
|
|
|
constructor (options) { |
|
|
|
this.options = options || {} |
|
|
|
} |
|
|
|
show (key, value) { |
|
|
|
if (key && value) { |
|
|
|
const ansi = require('ansi-escape-sequences') |
|
|
|
const tableLayout = require('table-layout') |
|
|
|
const output = tableLayout({ key: ansi.format(key, 'bold'), value: value}, { |
|
|
|
padding: { left: '', right: ' ' }, |
|
|
|
columns: [ |
|
|
|
{ name: 'key', width: 18 }, |
|
|
|
{ name: 'value', nowrap: true } |
|
|
|
] |
|
|
|
}) |
|
|
|
process.stderr.write(output) |
|
|
|
} else { |
|
|
|
console.error(key) |
|
|
|
} |
|
|
|
} |
|
|
|
verbose (key, value) { |
|
|
|
if (this.options.verbose) { |
|
|
|
this.show(key, value) |
|
|
|
} |
|
|
|
} |
|
|
|
error (msg) { |
|
|
|
console.error(ansi.format(msg, 'red')) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* @alias module:local-web-server |
|
|
|
* @extends module:middleware-stack |
|
|
|
*/ |
|
|
|
class LocalWebServer { |
|
|
|
|
|
|
|
/** |
|
|
|
* @param [options] {object} - Server options |
|
|
|
* @param [options.port} {number} - Port |
|
|
|
* @param [options.stack} {string[]|Features[]} - Port |
|
|
|
*/ |
|
|
|
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') |
|
|
|
this.view = new CliView() |
|
|
|
|
|
|
|
/* 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) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
/* get stored config */ |
|
|
|
const loadConfig = require('config-master') |
|
|
|
const stored = loadConfig('local-web-server') |
|
|
|
|
|
|
|
/* 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')) |
|
|
|
/* read the config and command-line for feature paths */ |
|
|
|
const featurePaths = parseFeaturePaths(initOptions.stack || stored.stack) |
|
|
|
|
|
|
|
/* build the stack */ |
|
|
|
const stackModules = buildStack(stackPaths, this.onVerbose.bind(this), this.onDebug.bind(this)) |
|
|
|
/* load features and build the middleware stack */ |
|
|
|
const features = this._buildFeatureStack(featurePaths) |
|
|
|
|
|
|
|
/* gather stack option definitions and parse the command line */ |
|
|
|
const middlewareOptionDefinitions = stackModules |
|
|
|
/* gather feature optionDefinitions and parse the command line */ |
|
|
|
const featureOptionDefinitions = features |
|
|
|
.filter(mw => mw.optionDefinitions) |
|
|
|
.map(mw => mw.optionDefinitions()) |
|
|
|
.reduce(flatten, []) |
|
|
@ -57,20 +80,23 @@ class LocalWebServer { |
|
|
|
return def |
|
|
|
}) |
|
|
|
|
|
|
|
const usage = commandLineUsage(cli.usage(middlewareOptionDefinitions)) |
|
|
|
const usage = commandLineUsage(cli.usage(featureOptionDefinitions)) |
|
|
|
|
|
|
|
let options = {} |
|
|
|
const allOptionDefinitions = cli.optionDefinitions.concat(middlewareOptionDefinitions) |
|
|
|
const allOptionDefinitions = cli.optionDefinitions.concat(featureOptionDefinitions) |
|
|
|
if (!initOptions.testMode) { |
|
|
|
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() |
|
|
|
this.view.error(err.toString()) |
|
|
|
if (err.name === 'DUPLICATE_NAME') { |
|
|
|
this.view.error('\nOption Definitions:') |
|
|
|
this.view.error(allOptionDefinitions.map(def => { |
|
|
|
return `name: ${def.name}${def.alias ? ', alias: ' + def.alias : ''}` |
|
|
|
}).join('\n')) |
|
|
|
} |
|
|
|
this.view.show(usage) |
|
|
|
process.exit(1) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
@ -84,33 +110,38 @@ class LocalWebServer { |
|
|
|
options.misc |
|
|
|
) |
|
|
|
|
|
|
|
this.view.options.verbose = options.verbose |
|
|
|
|
|
|
|
/** |
|
|
|
* Config |
|
|
|
* @type {object} |
|
|
|
*/ |
|
|
|
this.options = options |
|
|
|
|
|
|
|
stackModules |
|
|
|
this.features = features |
|
|
|
|
|
|
|
features |
|
|
|
.filter(mw => mw.on) |
|
|
|
.forEach(mw => { |
|
|
|
mw.on('verbose', this.onVerbose.bind(this)) |
|
|
|
mw.on('debug', this.onDebug.bind(this)) |
|
|
|
mw.on('verbose', this.view.verbose.bind(this.view)) |
|
|
|
mw.on('debug', this.view.verbose.bind(this.view)) |
|
|
|
}) |
|
|
|
|
|
|
|
/* --config */ |
|
|
|
if (options.config) { |
|
|
|
tool.stop(JSON.stringify(options, null, ' '), 0) |
|
|
|
this.view.show(JSON.stringify(options, null, ' ')) |
|
|
|
process.exit(0) |
|
|
|
|
|
|
|
/* --version */ |
|
|
|
} else if (options.version) { |
|
|
|
const pkg = require(path.resolve(__dirname, '..', 'package.json')) |
|
|
|
tool.stop(pkg.version) |
|
|
|
this.view.show(pkg.version) |
|
|
|
process.exit(0) |
|
|
|
|
|
|
|
/* --help */ |
|
|
|
} else if (options.help) { |
|
|
|
tool.stop(usage) |
|
|
|
} else { |
|
|
|
this.stack = stackModules |
|
|
|
this.view.show(usage) |
|
|
|
process.exit(0) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
@ -120,7 +151,7 @@ class LocalWebServer { |
|
|
|
const compose = require('koa-compose') |
|
|
|
const convert = require('koa-convert') |
|
|
|
|
|
|
|
const middlewareStack = this.stack |
|
|
|
const middlewareStack = this.features |
|
|
|
.filter(mw => mw.middleware) |
|
|
|
.map(mw => mw.middleware(this.options)) |
|
|
|
.reduce(flatten, []) |
|
|
@ -162,31 +193,51 @@ class LocalWebServer { |
|
|
|
server = http.createServer(app.callback()) |
|
|
|
} |
|
|
|
|
|
|
|
const tableLayout = require('table-layout') |
|
|
|
|
|
|
|
server.listen(options.port) |
|
|
|
if (onListening) server.on('listening', onListening) |
|
|
|
if (!options.testMode) { |
|
|
|
server.on('listening', function () { |
|
|
|
server.on('listening', () => { |
|
|
|
const ipList = getIPList() |
|
|
|
.map(iface => `[underline]{${server.isHttps ? 'https' : 'http'}://${iface.address}:${options.port}}`) |
|
|
|
.join(', ') |
|
|
|
console.error(ansi.format('Serving at', 'bold'), ansi.format(ipList)) |
|
|
|
this.view.show('Serving at', ansi.format(ipList)) |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
return server |
|
|
|
} |
|
|
|
|
|
|
|
onVerbose (title, msg) { |
|
|
|
if (this.options.verbose) { |
|
|
|
console.error(ansi.format(title, 'bold'), msg) |
|
|
|
} |
|
|
|
} |
|
|
|
onDebug (title, msg) { |
|
|
|
if (this.options.debug) { |
|
|
|
console.error(ansi.format(title, 'bold'), msg) |
|
|
|
} |
|
|
|
_buildFeatureStack (featurePaths) { |
|
|
|
return featurePaths |
|
|
|
.map(featurePath => loadStack(featurePath)) |
|
|
|
.map(Feature => new Feature()) |
|
|
|
.map(module => { |
|
|
|
if (module.stack) { |
|
|
|
const featureStack = module.stack() |
|
|
|
.map(Feature => new Feature()) |
|
|
|
.map(feature => { |
|
|
|
if (feature.on) { |
|
|
|
feature.on('verbose', this.view.verbose.bind(this.view)) |
|
|
|
feature.on('debug', this.view.verbose.bind(this.view)) |
|
|
|
} |
|
|
|
return feature |
|
|
|
}) |
|
|
|
|
|
|
|
module.optionDefinitions = function () { |
|
|
|
return featureStack |
|
|
|
.map(feature => feature.optionDefinitions && feature.optionDefinitions()) |
|
|
|
.filter(definitions => definitions) |
|
|
|
.reduce(flatten, []) |
|
|
|
} |
|
|
|
module.middleware = function (options) { |
|
|
|
return featureStack |
|
|
|
.map(feature => feature.middleware(options)) |
|
|
|
.reduce(flatten, []) |
|
|
|
.filter(mw => mw) |
|
|
|
} |
|
|
|
} |
|
|
|
return module |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
@ -246,37 +297,24 @@ function getIPList () { |
|
|
|
return ipList |
|
|
|
} |
|
|
|
|
|
|
|
function buildStack (stackPaths, onVerbose, onDebug) { |
|
|
|
return stackPaths |
|
|
|
.map(stackPath => loadStack(stackPath)) |
|
|
|
.map(Middleware => new Middleware()) |
|
|
|
.map(module => { |
|
|
|
if (module.stack) { |
|
|
|
const featureStack = module.stack() |
|
|
|
.map(Feature => new Feature()) |
|
|
|
.map(feature => { |
|
|
|
if (feature.on) { |
|
|
|
feature.on('verbose', onVerbose) |
|
|
|
feature.on('debug', onDebug) |
|
|
|
} |
|
|
|
return feature |
|
|
|
}) |
|
|
|
|
|
|
|
module.optionDefinitions = function () { |
|
|
|
return featureStack |
|
|
|
.map(feature => feature.optionDefinitions && feature.optionDefinitions()) |
|
|
|
.filter(definitions => definitions) |
|
|
|
.reduce(flatten, []) |
|
|
|
} |
|
|
|
module.middleware = function (options) { |
|
|
|
return featureStack |
|
|
|
.map(feature => feature.middleware(options)) |
|
|
|
.reduce(flatten, []) |
|
|
|
.filter(mw => mw) |
|
|
|
} |
|
|
|
/* manually scan for any --stack passed, as we may need to display stack options */ |
|
|
|
function parseFeaturePaths (configStack) { |
|
|
|
const featurePaths = arrayify(configStack) |
|
|
|
const featureIndex = process.argv.indexOf('--stack') |
|
|
|
if (featureIndex > -1) { |
|
|
|
for (var i = featureIndex + 1; i < process.argv.length; i++) { |
|
|
|
const featurePath = process.argv[i] |
|
|
|
if (/^-/.test(featurePath)) { |
|
|
|
break |
|
|
|
} else { |
|
|
|
featurePaths.push(featurePath) |
|
|
|
} |
|
|
|
return module |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/* if the user did not supply a stack, use the default */ |
|
|
|
if (!featurePaths.length) featurePaths.push(path.resolve(__dirname, '..', 'node_modules', 'local-web-server-default-stack')) |
|
|
|
return featurePaths |
|
|
|
} |
|
|
|
|
|
|
|
module.exports = LocalWebServer |