diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..9cf9495 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/README.md b/README.md index 9dc0247..4942f22 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ How to use ```bash npm install [-g] spserver -spserver -f ./myfile.html -s ./public -p 3000 +spserver --file ./myfile.html --serve ./public -p 3000 ``` Options @@ -42,23 +42,23 @@ Options By default, spserver will use the settings located in `config.json`. You can also override them or run it directly using only the commands below. -`--config, -c` Location of the config file for the server [default: config.json] +`--config` Location of the config file for the server [default: config.json] -`--port, -p` The port server should bind to [default: 3001 or 80 in production mode] +`--port` The port server should bind to [default: 3001 or 80 in production mode] -`--file, -f` Single static file the server should serve on all unknown requests +`--file` Single static file the server should serve on all unknown requests -`--bunyan, -b` Use bunyan instead of console to log to [default: true in production mode] +`--bunyan` Use bunyan instead of console to log to [default: true in production mode] -`--template, -t` Parse the static file as lodash template with all options/settings being passed to it +`--template` Parse the static file as lodash template with all options/settings being passed to it -`--name, -n` The name for this server for logging [default: spserver] +`--name` The name for this server for logging [default: spserver] -`--serve, -s` Folder path to serve static files from [default: public] +`--serve` Folder path to serve static files from [default: public] -`--prod, -P` Force run the server in production mode +`--prod` Force run the server in production mode -`--debug, -d` Force run the server in development mode +`--debug` Force run the server in development mode Config ====== @@ -91,7 +91,7 @@ Any of the settings in the `config.json` file can be overridden using the CLI op Template ======== -spserver can also help provide any additional info to your single file thanks to lodash.template. If template mode is specified, it will parse the single file first through lodash.template with the whole config file. This can allow you to specify configuration in your config file and expose them in your single file. +spserver can also help provide any additional info to your single file thanks to lodash: `template`. If template mode is specified, it will parse the single file first through `lodash.template` with the whole config file. This can allow you to specify configuration in your config file and expose them in your single file. Example: @@ -115,4 +115,4 @@ Example: ``` Then you can run it like this: -`spserver -c ./config.json -t -f ./base.html -s ./public` +`spserver --config ./config.json --template --file ./base.html --serve ./public` diff --git a/bin.js b/bin.js deleted file mode 100755 index 58a7268..0000000 --- a/bin.js +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -var config = require('./lib/config'); -var server = require('./lib/spserver'); - -var env = config.get('NODE_ENV'); - -//Check if we any -var displayHelp = config.get('help'); -if (!config.get('file') && !config.get(env + ':file') && - !config.get('serve') && !config.get(env + ':serve')) { - displayHelp = true; -} - -if (displayHelp) { - console.log('Run static server for static files, simple servers or pure MVVM projects.'); - console.log('Specifying either file or folder serving is required.'); - console.log(''); - console.log('Usage:'); - console.log(' spserver [options]'); - console.log(''); - console.log(config.stores.argv.help()); - console.log('Examples:'); - console.log(' spserver -p 2000 -f base.html -s ./dist'); - console.log(''); - console.log(' Will run the server on port 2000 serving static files from the ./dist folder'); - console.log(' with any unknown file being served the contents of base.html.'); - console.log(''); - console.log(' spserver -f base.html -t --custom test'); - console.log(''); - console.log(' Will run the server with the base.html as a template as well as'); - console.log(' passing the contents of "test" argument into the template.'); - - process.exit(0); -} - -//Run our server \o/ -server(); diff --git a/bin.mjs b/bin.mjs new file mode 100644 index 0000000..0a1248b --- /dev/null +++ b/bin.mjs @@ -0,0 +1,56 @@ +#!/usr/bin/env node + +import config from './lib/config.mjs' +import SPServer from './lib/spserver.mjs' + +var env = config.get('NODE_ENV'); + +//Check if we any +var displayHelp = config.get('help'); +if (!config.get('file') && !config.get(env + ':file') && + !config.get('serve') && !config.get(env + ':serve')) { + displayHelp = true; +} + +if (displayHelp) { + console.log(`Run static server for static files, simple servers or pure MVVM projects. +Specifying either file or folder serving is required. + +Usage: + spserver [options] + +Options: + --config Location of the config file for the server [default: + config.json] + --port The port server should bind to [default: 3001 or 80 in + production mode] + --file Single static file the server should serve on all unknown + requests + --bunyan Use bunyan instead of console to log to [default: true in + production mode] + --template Parse the static file as lodash template with all + options/settings being passed to it + --name The name for this server for logging [default: spserver] + --serve Folder path to serve static files from [default: public] + --prod Force run the server in production mode + --debug Force run the server in development mode + --ip IP server runs on [default: 0.0.0.0] + --rooturlpath Root URL path server is deployed on; will be removed from + URL when resolving to files [default: /] + +Examples: + spserver -p 2000 -f base.html -s ./dist + + Will run the server on port 2000 serving static files from the ./dist folder + with any unknown file being served the contents of base.html. + + spserver -f base.html -t --custom test + + Will run the server with the base.html as a template as well as + passing the contents of "test" argument into the template.`); + process.exit(0); +} + +//Run our server \o/ + +let server = new SPServer() diff --git a/lib/arguments.js b/lib/arguments.js deleted file mode 100644 index 685b625..0000000 --- a/lib/arguments.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -module.exports = { - config: { - alias: 'c', - describe: 'Location of the config file for the server [default: config.json]' - }, - port: { - alias: 'p', - describe: 'The port server should bind to [default: 3001 or 80 in production mode]' - }, - file: { - alias: 'f', - describe: 'Single static file the server should serve on all unknown requests' - }, - bunyan: { - alias: 'b', - describe: 'Use bunyan instead of console to log to [default: true in production mode]' - }, - template: { - alias: 't', - describe: 'Parse the static file as lodash template with all options/settings being passed to it' - }, - name: { - alias: 'n', - describe: 'The name for this server for logging [default: spserver]' - }, - serve: { - alias: 's', - describe: 'Folder path to serve static files from [default: public]' - }, - prod: { - alias: 'P', - describe: 'Force run the server in production mode' - }, - debug: { - alias: 'd', - describe: 'Force run the server in development mode' - }, - ip: { - alias: 'i', - describe: 'IP server runs on [default: 0.0.0.0]' - }, - rooturlpath: { - alias: 'r', - describe: 'Root URL path server is deployed on; will be removed from URL when resolving to files [default: /]' - }, -}; diff --git a/lib/config.js b/lib/config.mjs similarity index 67% rename from lib/config.js rename to lib/config.mjs index 587b108..a056ae0 100644 --- a/lib/config.js +++ b/lib/config.mjs @@ -1,9 +1,10 @@ -'use strict'; +import fs from 'fs' +import Nconf from 'nconf-lite' -var nconf = require('nconf'); +const nconf = new Nconf() // Load arguments as highest priority -nconf.argv(require('./arguments')); +nconf.argv({ lowerCase: true, separator: '__', parseValues: true, useEqualsign: true }); // Overrides var overrides = {}; @@ -20,8 +21,11 @@ nconf.overrides(overrides); // Load enviroment variables as third priority nconf.env(); -// Load the config if it exists. -nconf.file(nconf.get('config') || './config.json'); +var filename = nconf.get('config') || '../config.json' +if (fs.existsSync(filename)) { + // Load the config if it exists. + nconf.file('main', nconf.get('config') || '../config.json'); +} // Default variables nconf.defaults({ @@ -46,4 +50,4 @@ nconf.defaults({ }, }); -module.exports = nconf; +export default nconf \ No newline at end of file diff --git a/lib/logger.js b/lib/logger.mjs similarity index 82% rename from lib/logger.js rename to lib/logger.mjs index 839b7d6..dbdff01 100644 --- a/lib/logger.js +++ b/lib/logger.mjs @@ -1,10 +1,6 @@ -'use strict'; - -var _ = require('lodash'); -var bunyan = require('bunyan-lite'); - -//Get the config -var config = require('./config'); +import _ from 'lodash' +import bunyan from 'bunyan-lite' +import config from './config.mjs' //Create our variables var env = config.get('NODE_ENV'); @@ -14,7 +10,7 @@ if (config.get('bunyan') || config.get(env + ':use_bunyan')) { var settings = _.cloneDeep(config.get(env + ':bunyan')); // Stream can be specified either in settings.streams[ix] or globally in settings.stream, but not - // both. Since the defaults specify stetings.stream, if the user specifies anything of vaulue + // both. Since the defaults specify settings.stream, if the user specifies anything of vaulue // in settings.streams, we should delete the global defaults, because bunyan gets angry if there // are multiple keys if (_.has(settings, 'streams')) { @@ -43,4 +39,4 @@ if (config.get('bunyan') || config.get(env + ':use_bunyan')) { output.debug = console.log.bind(console); } -module.exports = output; +export default output; diff --git a/lib/spserver.js b/lib/spserver.js deleted file mode 100644 index f1537b7..0000000 --- a/lib/spserver.js +++ /dev/null @@ -1,137 +0,0 @@ -'use strict'; - -var fs = require('fs'); -var http = require('http'); -var path = require('path'); -var url = require('url'); - -var _ = require('lodash'); -var nStatic = require('node-static'); - -var config = require('./config'); -var logger = require('./logger'); - - -// The different config sources sometimes manipulate different setting names. -// E.g. command line flags maniuplate root settings, but config files can -// manipulate settings at the prod/debug level. Resolve all of these into a -// final object of settings. -function _resolveFinalSettings(settings) { - var finalSettings = {}; - var env = config.get('NODE_ENV'); - - if (!settings) { - settings = config.get(); - } - if (!settings[env]) { - settings[env] = {}; - } - - // For 'name', 'file', 'serve', 'ip', and 'port', default to the global setting rather than an - // individual environment's setting, because it might have been set via command-line flags - _(['name', 'file', 'serve', 'ip', 'port']).forEach(function (field) { - finalSettings[field] = settings[field] || settings[env][field]; - }); - - finalSettings.rooturlpath = config.get('rooturlpath') || config.get('ROOT_URL_PATH') || '/'; - - // For 'staticOptions', there are no command-line flags, so individual configuration options - // override global defaults where set - finalSettings.staticOptions = _.defaultsDeep(settings.staticOptions, settings[env].staticOptions); - - // Make a template function so we can just pass that in downstream - finalSettings.template = (settings.template || settings[env].template) ? - function (contents) { - // Note: template is run with _original_, non-resolved settings - return _.template(contents)(settings); - } : null; - - return finalSettings; -} - -function generateBase(file, finalSettings) { - if (!file) { - return null; - } - - if (_.endsWith(file, 'js')) { - return require(file); - } - - var contents = fs.readFileSync(file); - if (finalSettings.template) { - contents = finalSettings.template(contents); - } - - return function(req, res) { - res.writeHead(200, {'Content-Type': 'text/html'}); - res.end(contents); - }; -} - -function _rerouteRootUrl(reqUrl, rootUrl) { - var parsedUrl = url.parse(reqUrl); - - parsedUrl.pathname = path.normalize( - parsedUrl.pathname.replace(rootUrl, '/') || '/' - ); - - return url.format(parsedUrl); -} - -var spserver = function (settings) { - var finalSettings = _resolveFinalSettings(settings); - - var fileServer = new nStatic.Server( - path.resolve(finalSettings.serve), - finalSettings.staticOptions - ); - - var base = generateBase(path.resolve(finalSettings.file), finalSettings); - - var server = http.createServer(function (req, res) { - logger.debug('[REQ]', req.method + ':', req.url); - var startTime = new Date().getTime(); - - var done = function () { - var requestTime = new Date().getTime() - startTime; - logger.debug('[RES]', req.method + ':', req.url, - '(' + res.statusCode + ')', 'took', requestTime, 'ms'); - }; - - req.url = _rerouteRootUrl(req.url, finalSettings.rooturlpath); - - res.addListener('finish', done); - res.addListener('close', done); - - req.addListener('end', function () { - fileServer.serve(req, res, function (err) { - if (err) { - if (err.status === 404 && base) { - return base(req, res); - } else { - logger.error(err); - - res.writeHead(err.status, err.headers); - res.end(err.message); - } - } - }); - }).resume(); - }); - - server.listen(finalSettings.port, finalSettings.ip); - - logger.info( - 'Started single-page server: ' + finalSettings.name + - ', base file: ' + finalSettings.file + - ', static folder: ' + finalSettings.serve + - ', port: ' + finalSettings.port - ); - - return server; -}; - -spserver.generateBase = generateBase; - -module.exports = spserver; diff --git a/lib/spserver.mjs b/lib/spserver.mjs new file mode 100644 index 0000000..2549636 --- /dev/null +++ b/lib/spserver.mjs @@ -0,0 +1,144 @@ +import fs from 'fs' +import http from 'http' +import path from 'path' +import url from 'url' +import _ from 'lodash' +import nStatic from 'node-static' + +import config from './config.mjs' +import logger from './logger.mjs' + + +// The different config sources sometimes manipulate different setting names. +// E.g. command line flags maniuplate root settings, but config files can +// manipulate settings at the prod/debug level. Resolve all of these into a +// final object of settings. +function _resolveFinalSettings(settings) { + var finalSettings = {}; + var env = config.get('NODE_ENV'); + + if (!settings) { + settings = config.get(); + } + if (!settings[env]) { + settings[env] = {}; + } + + // For 'name', 'file', 'serve', 'ip', and 'port', default to the global setting rather than an + // individual environment's setting, because it might have been set via command-line flags + _(['name', 'file', 'serve', 'ip', 'port']).forEach(function (field) { + finalSettings[field] = settings[field] || settings[env][field]; + }); + + finalSettings.rooturlpath = config.get('rooturlpath') || config.get('ROOT_URL_PATH') || '/'; + + // For 'staticOptions', there are no command-line flags, so individual configuration options + // override global defaults where set + finalSettings.staticOptions = _.defaultsDeep(settings.staticOptions, settings[env].staticOptions); + + // Make a template function so we can just pass that in downstream + finalSettings.template = (settings.template || settings[env].template) ? + function (contents) { + // Note: template is run with _original_, non-resolved settings + return _.template(contents)(settings); + } : null; + + return finalSettings; +} + +function _rerouteRootUrl(reqUrl, rootUrl) { + var parsedUrl = url.parse(reqUrl); + + parsedUrl.pathname = path.normalize( + parsedUrl.pathname.replace(rootUrl, '/') || '/' + ); + + return url.format(parsedUrl); +} + +class SPServer { + constructor(settings, opts = {}) { + Object.assign(this, { + fs: opts.fs || fs, + }) + + var finalSettings = _resolveFinalSettings(settings); + + var fileServer = new nStatic.Server( + path.resolve(finalSettings.serve), + finalSettings.staticOptions + ); + + var base = SPServer.generateBase(finalSettings.file ? path.resolve(finalSettings.file) : null, finalSettings); + + var server = http.createServer(function (req, res) { + var startTime = new Date().getTime(); + + var isFinished = false + var done = function () { + if (isFinished) return + isFinished = true + var requestTime = new Date().getTime() - startTime; + logger.debug('[RES]', req.method + ':', '(' + res.statusCode + ')', req.url, + 'took', requestTime, 'ms'); + }; + + req.url = _rerouteRootUrl(req.url, finalSettings.rooturlpath); + + res.addListener('finish', done); + res.addListener('close', done); + + req.addListener('end', function () { + fileServer.serve(req, res, function (err) { + if (err) { + if (err.status === 404 && base) { + return base(req, res); + } else if (err.status === 404) { + res.writeHead(err.status, err.headers); + res.end(err.message); + } else { + logger.debug('[REQ]', req.method + ':', req.url); + logger.error(err); + + res.writeHead(err.status, err.headers); + res.end(err.message); + } + } + }); + }).resume(); + }); + + server.listen(finalSettings.port, finalSettings.ip); + + logger.info( + 'Started single-page server: ' + finalSettings.name + + ', base file: ' + (finalSettings.file || '') + + ', static folder: ' + finalSettings.serve + + ', port: ' + finalSettings.port + ); + + this.server = server + } + + static generateBase(file, finalSettings, useFs = fs) { + if (!file) { + return null; + } + + if (_.endsWith(file, 'js')) { + throw new Error('javascript file has been deprecated') + } + + var contents = useFs.readFileSync(file, { encoding: 'utf-8' }); + if (finalSettings.template) { + contents = finalSettings.template(contents) + } + + return function(req, res) { + res.writeHead(200, {'Content-Type': 'text/html'}); + res.end(contents); + }; + } +}; + +export default SPServer; diff --git a/package.json b/package.json index 555c0d8..4ef7c58 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,10 @@ "name": "spserver", "version": "0.3.0", "description": "Node static page server for running quick MVVM file server", - "main": "lib/spserver.js", + "main": "lib/spserver.mjs", "scripts": { - "test": "mocha --reporter spec test/base.test.js" + "test": "set NODE_ENV=test&& eltro test/**/*.test.mjs -r dot", + "test:linux": "NODE_ENV=test eltro 'test/**/*.test.mjs' -r dot" }, "repository": { "type": "git", @@ -21,14 +22,13 @@ }, "homepage": "https://github.com/TheThing/spserver", "dependencies": { - "bunyan-lite": "^1.0.0", + "bunyan-lite": "^1.1.1", "lodash": "^4.17.2", - "nconf": "^0.8.4", + "nconf-lite": "^2.0.0", "node-static": "^0.7.6" }, - "bin": "./bin.js", + "bin": "./bin.mjs", "devDependencies": { - "mocha": "^5.2.0", - "sinon": "^1.12.2" + "eltro": "^1.2.3" } } diff --git a/test/base.test.js b/test/base.test.js deleted file mode 100644 index f4f7ad2..0000000 --- a/test/base.test.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -var fs = require('fs'); -var assert = require('assert'); -var sinon = require('sinon'); - -describe('spserver', function () { - - var spserver = require('../lib/spserver'); - - describe('#generateBase()', function () { - it('should return null when file is empty', function () { - assert.strictEqual(null, spserver.generateBase()); - assert.strictEqual(null, spserver.generateBase(null, {})); - assert.strictEqual(null, spserver.generateBase('')); - assert.strictEqual(null, spserver.generateBase('', {})); - }); - - it('should read file contents if string', function () { - var stub = sinon.stub(fs, 'readFileSync').returns('bla'); - - spserver.generateBase('asdf', {}); - assert(stub.called); - stub.restore(); - }); - - it('should return function if file is javascript', function () { - var path = require('path'); - var nothing = require('./nothing'); - var test = spserver.generateBase(path.resolve('test/nothing.js'), {}); - - assert.strictEqual(nothing.toString(), test.toString()); - }); - }); -}); diff --git a/test/base.test.mjs b/test/base.test.mjs new file mode 100644 index 0000000..a5bed2e --- /dev/null +++ b/test/base.test.mjs @@ -0,0 +1,31 @@ +import { Eltro as t, assert, stub} from 'eltro' + +import path from 'path' +import SPServer from '../lib/spserver.mjs' + + +t.describe('spserver', function () { + const fakeFs = { readFileSync: stub() } + + t.describe('#generateBase()', function () { + t.test('should return null when file is empty', function () { + assert.strictEqual(null, SPServer.generateBase(undefined, undefined, fakeFs)); + assert.strictEqual(null, SPServer.generateBase(null, {}, fakeFs)); + assert.strictEqual(null, SPServer.generateBase('', undefined, fakeFs)); + assert.strictEqual(null, SPServer.generateBase('', {}, fakeFs)); + }); + + t.test('should read file contents if string', function () { + fakeFs.readFileSync.reset() + assert.notOk(fakeFs.readFileSync.called) + SPServer.generateBase('asdf', {}, fakeFs); + assert.ok(fakeFs.readFileSync.called) + }); + + t.test('should throw if file is javascript', async function () { + assert.throws(function() { + SPServer.generateBase(path.resolve('test/nothing.js'), {}); + }) + }); + }); +}); diff --git a/test/nothing.js b/test/nothing.js index d5c6bc1..82be37d 100644 --- a/test/nothing.js +++ b/test/nothing.js @@ -1,3 +1,3 @@ -'use strict'; - -module.exports = function() {}; +export default function(req, res) { + res.end('nothing') +}