Refactor CLI and add tests

This commit is contained in:
Typicode
2015-07-15 18:44:33 +02:00
parent e3a7db949b
commit ccf67724e3
22 changed files with 368 additions and 172 deletions

View File

@ -1,165 +1,2 @@
#!/usr/bin/env node #!/usr/bin/env node
var fs = require('fs') require('../src/cli')()
var path = require('path')
var updateNotifier = require('update-notifier')
var _db = require('underscore-db')
var yargs = require('yargs')
var chalk = require('chalk')
var got = require('got')
var pkg = require('../package.json')
var jsonServer = require('../src')
var jsMockGenerator = null
updateNotifier({packageName: pkg.name, packageVersion: pkg.version}).notify()
// Parse arguments
var argv = yargs
.usage('$0 [options] <source>')
.options({
port: {
alias: 'p',
description: 'Set port',
default: 3000
},
host: {
alias: 'H',
description: 'Set host',
default: '0.0.0.0'
},
watch: {
alias: 'w',
description: 'Reload database on JSON file change'
},
routes: {
alias: 'r',
description: 'Load routes file'
},
id: {
description: 'Set database id property (e.g. _id)',
default: 'id'
}
})
.boolean('watch')
.help('help').alias('help', 'h')
.version(pkg.version).alias('version', 'v')
.example('$0 db.json', '')
.example('$0 file.js', '')
.example('$0 http://example.com/db.json', '')
.epilog('https://github.com/typicode/json-server')
.require(1, 'Missing <source> argument')
.argv
function showResources (hostname, port, object) {
for (var prop in object) {
console.log(chalk.gray(' http://' + hostname + ':' + port + '/') + chalk.cyan(prop))
}
}
function start (object, filename) {
var port = process.env.PORT || argv.port
var hostname = argv.host === '0.0.0.0' ? 'localhost' : argv.host
console.log()
showResources(hostname, port, object)
console.log()
console.log(
'You can now go to ' + chalk.gray('http://' + hostname + ':' + port)
)
console.log()
console.log(
'Enter ' + chalk.cyan('s') + ' at any time to create a snapshot of the db'
)
// Snapshot
process.stdin.resume()
process.stdin.setEncoding('utf8')
process.stdin.on('data', function (chunk) {
if (chunk.trim().toLowerCase() === 's') {
var file = 'db-' + Date.now() + '.json'
_db.save(object, file)
console.log('Saved snapshot to ' + chalk.cyan(file) + '\n')
}
})
// Router
var router = jsonServer.router(filename ? filename : object)
// Watcher
if (argv.watch) {
console.log('Watching', chalk.cyan(source))
var db = router.db
var watchedDir = path.dirname(filename)
var watchedFile = path.basename(filename)
fs.watch(watchedDir, function (event, changedFile) {
// lowdb generates 'rename' event on watchedFile
// using it to know if file has been modified by the user
if ((event === 'change' || event === 'rename') && (changedFile === watchedFile || changedFile === source)) {
console.log(chalk.cyan(source), 'has changed, reloading database')
try {
if (filename) {
db.object = JSON.parse(fs.readFileSync(filename))
} else {
require.cache[jsMockGenerator] = null
db.object = require(jsMockGenerator)()
}
showResources(hostname, port, db.object)
} catch (e) {
console.log('Can\'t parse', chalk.cyan(source))
console.log(e.message)
}
console.log()
}
})
}
console.log()
var server = jsonServer.create()
server.use(jsonServer.defaults)
// Rewriter
if (argv.routes) {
var routes = JSON.parse(fs.readFileSync(process.cwd() + '/' + argv.routes))
var rewriter = jsonServer.rewriter(routes)
server.use(rewriter)
}
server.use(router)
// Custom id
router.db._.id = argv.id
server.listen(port, argv.host)
}
// Set file and port
var source = argv._[0]
// Say hi, load file and start server
console.log(chalk.cyan(' {^_^} Hi!\n'))
console.log('Loading database from ' + chalk.cyan(source))
// Remote source
if (/^(http|https):/.test(source)) {
got(source, function (err, data) {
if (err) {
console.log('Error', err)
process.exit(1)
}
var object = JSON.parse(data)
start(object)
})
// JSON file
} else if (/\.json$/.test(source)) {
var filename = process.cwd() + '/' + source
var object = require(filename)
start(object, filename)
// JS file
} else if (/\.js$/.test(source)) {
jsMockGenerator = process.cwd() + '/' + source
var object = require(jsMockGenerator)()
start(object)
}

View File

@ -2,7 +2,7 @@
"name": "json-server", "name": "json-server",
"version": "0.7.20", "version": "0.7.20",
"description": "Serves JSON files through REST routes.", "description": "Serves JSON files through REST routes.",
"main": "./src/index.js", "main": "./src/server/index.js",
"bin": "./bin/index.js", "bin": "./bin/index.js",
"directories": { "directories": {
"test": "test" "test": "test"
@ -13,7 +13,7 @@
"cors": "^2.3.0", "cors": "^2.3.0",
"errorhandler": "^1.2.0", "errorhandler": "^1.2.0",
"express": "^4.9.5", "express": "^4.9.5",
"got": "^1.2.2", "got": "^3.3.0",
"lodash": "^3.9.2", "lodash": "^3.9.2",
"lowdb": "^0.10.0", "lowdb": "^0.10.0",
"method-override": "^2.1.2", "method-override": "^2.1.2",
@ -21,17 +21,18 @@
"node-uuid": "^1.4.2", "node-uuid": "^1.4.2",
"pluralize": "^1.1.2", "pluralize": "^1.1.2",
"underscore-db": "^0.9.0", "underscore-db": "^0.9.0",
"update-notifier": "^0.2.2", "update-notifier": "^0.5.0",
"yargs": "^3.10.0" "yargs": "^3.10.0"
}, },
"devDependencies": { "devDependencies": {
"husky": "^0.6.1", "husky": "^0.6.1",
"mocha": "^2.2.4", "mocha": "^2.2.4",
"rimraf": "^2.4.1",
"standard": "^3.8.0", "standard": "^3.8.0",
"supertest": "~0.8.1" "supertest": "~0.8.1"
}, },
"scripts": { "scripts": {
"test": "standard && mocha -R spec test", "test": "NODE_ENV=test mocha -R spec test/**/*.js && standard",
"start": "node bin", "start": "node bin",
"prepush": "npm t" "prepush": "npm t"
}, },

48
src/cli/index.js Normal file
View File

@ -0,0 +1,48 @@
var updateNotifier = require('update-notifier')
var yargs = require('yargs')
var run = require('./run')
var pkg = require('../../package.json')
module.exports = function () {
updateNotifier({ pkg: pkg }).notify()
var argv = yargs
.usage('$0 [options] <source>')
.options({
port: {
alias: 'p',
description: 'Set port',
default: 3000
},
host: {
alias: 'H',
description: 'Set host',
default: '0.0.0.0'
},
watch: {
alias: 'w',
description: 'Watch file(s)'
},
routes: {
alias: 'r',
description: 'Load routes file'
},
id: {
description: 'Set database id property (e.g. _id)',
default: 'id'
}
})
.boolean('watch')
.help('help').alias('help', 'h')
.version(pkg.version).alias('version', 'v')
.example('$0 db.json', '')
.example('$0 file.js', '')
.example('$0 http://example.com/db.json', '')
.epilog('https://github.com/typicode/json-server')
.require(1, 'Missing <source> argument')
.argv
run(argv)
}

102
src/cli/run.js Normal file
View File

@ -0,0 +1,102 @@
var fs = require('fs')
var chalk = require('chalk')
var is = require('./utils/is')
var load = require('./utils/load')
var watch = require('./watch')
var jsonServer = require('../server')
function prettyPrint (argv, object, rules) {
var host = argv.host === '0.0.0.0' ? 'localhost' : argv.host
var port = argv.port
var root = 'http://' + host + ':' + port
console.log()
console.log(chalk.bold(' Resources'))
for (var prop in object) {
console.log(' ' + root + '/' + prop)
}
if (rules) {
console.log()
console.log(chalk.bold(' Other routes'))
for (var rule in rules) {
console.log(' ' + rule + ' -> ' + rules[rule])
}
}
console.log()
console.log(chalk.bold(' Home'))
console.log(' ' + root)
console.log()
}
function createServer (source, object, routes) {
var server = jsonServer.create()
var router = jsonServer.router(
is.JSON(source) ?
source :
object
)
server.use(jsonServer.defaults)
if (routes) {
var rewriter = jsonServer.rewriter(routes)
server.use(rewriter)
}
server.use(router)
return server
}
module.exports = function (argv) {
var source = argv._[0]
var server
console.log()
console.log(chalk.cyan(' \\{^_^}/ hi!'))
function start () {
console.log()
console.log(chalk.gray(' Loading', source))
// Load JSON, JS or HTTP database
load(source, function (err, data) {
if (err) throw err
// Load additional routes
if (argv.routes) {
console.log(chalk.gray(' Loading', argv.routes))
var routes = JSON.parse(fs.readFileSync(argv.routes))
}
console.log(chalk.gray(' Done'))
// Create server and listen
server = createServer(source, data, routes).listen(argv.port, argv.host)
// Display server informations
prettyPrint(argv, data, routes)
})
}
// Start server
start()
// Watch files
if (argv.watch) {
console.log(chalk.gray(' Watching...'))
console.log()
watch(argv, function (file) {
console.log(chalk.gray(' ' + file + ' has changed, reloading...'))
// Restart server
server && server.close()
start()
})
}
}

17
src/cli/utils/is.js Normal file
View File

@ -0,0 +1,17 @@
module.exports = {
JSON: isJSON,
JS: isJS,
URL: isURL
}
function isJSON (s) {
return /\.json$/.test(s)
}
function isJS (s) {
return /\.js$/.test(s)
}
function isURL (s) {
return /^(http|https):/.test(s)
}

18
src/cli/utils/load.js Normal file
View File

@ -0,0 +1,18 @@
var path = require('path')
var got = require('got')
var is = require('./is')
module.exports = function (source, cb) {
if (is.URL(source)) {
// Load URL
got(source, { json: true }, function (err, data) {
cb(err, data)
})
} else {
// Load JS or JSON
var filename = path.resolve(source)
delete require.cache[filename]
var data = is.JSON(source) ? require(filename) : require(filename)()
cb(null, data)
}
}

44
src/cli/watch.js Normal file
View File

@ -0,0 +1,44 @@
var fs = require('fs')
var path = require('path')
var is = require('./utils/is')
module.exports = watch
// Because JSON file can be modified by the server, we need to be able to
// distinguish between user modification vs server modification.
// When the server modifies the JSON file, it generates a rename event.
// When the user modifies the JSON file, it generate a change event.
function watchDB (file, cb) {
var watchedDir = path.dirname(file)
var watchedFile = path.basename(file)
fs.watch(watchedDir, function (event, changedFile) {
if (event === 'change' && changedFile === watchedFile) cb()
})
}
function watchJS (file, cb) {
fs.watchFile(file, cb)
}
function watchSource (source, cb) {
if (is.JSON(source)) {
return watchDB(source, cb)
}
if (is.JS(source)) return watchJS(source, cb)
if (is.URL(source)) throw new Error('Can\'t watch URL')
}
function watch (argv, cb) {
var source = argv._[0]
watchSource(source, function () {
cb(source)
})
if (argv.routes) {
fs.watchFile(argv.routes, function () {
cb(argv.routes)
})
}
}

View File

@ -8,7 +8,10 @@ var arr = []
// Logger // Logger
arr.push(logger('dev', { arr.push(logger('dev', {
skip: function (req, res) { return req.path === '/favicon.ico' } skip: function (req, res) {
return process.env.NODE_ENV === 'test' ||
req.path === '/favicon.ico'
}
})) }))
// Enable CORS for all the requests, including static files // Enable CORS for all the requests, including static files

View File

Before

Width:  |  Height:  |  Size: 318 B

After

Width:  |  Height:  |  Size: 318 B

View File

Before

Width:  |  Height:  |  Size: 278 B

After

Width:  |  Height:  |  Size: 278 B

View File

@ -0,0 +1,3 @@
module.exports = function () {
return { posts: [] }
}

123
test/cli/index.js Normal file
View File

@ -0,0 +1,123 @@
var os = require('os')
var fs = require('fs')
var path = require('path')
var cp = require('child_process')
var request = require('supertest')
var rmrf = require('rimraf')
var pkg = require('../../package.json')
request = request('http://localhost:3000')
var tmpDir = path.join(__dirname, '../../tmp')
var dbFile = path.join(tmpDir, 'db.json')
var routesFile = path.join(tmpDir, 'routes.json')
function cli (args) {
var bin = path.join(__dirname, '../..', pkg.bin)
return cp.spawn('node', [bin].concat(args), {
stdio: 'inherit',
cwd: __dirname
})
}
/* global beforeEach, afterEach, describe, it */
describe('cli', function () {
var child
beforeEach(function () {
fs.mkdirSync(tmpDir)
fs.writeFileSync(dbFile, JSON.stringify({ posts: [] }))
fs.writeFileSync(routesFile, JSON.stringify({ '/blog/': '/' }))
})
afterEach(function (done) {
rmrf.sync(tmpDir)
child.kill()
setTimeout(done, 1000)
})
describe('db.json', function () {
beforeEach(function (done) {
child = cli([dbFile])
setTimeout(done, 1000)
})
it('should support JSON dbFile', function (done) {
request.get('/posts').expect(200, done)
})
})
describe('seed.js', function () {
beforeEach(function (done) {
child = cli(['fixtures/seed.js'])
setTimeout(done, 1000)
})
it('should support JS file', function (done) {
request.get('/posts').expect(200, done)
})
})
describe('http://jsonplaceholder.typicode.com/db', function () {
beforeEach(function (done) {
this.timeout(6000)
child = cli(['http://jsonplaceholder.typicode.com/db'])
setTimeout(done, 5000)
})
it('should support URL file', function (done) {
request.get('/posts').expect(200, done)
})
})
describe('db.json -r routes.json', function () {
beforeEach(function (done) {
child = cli([dbFile, '-r', routesFile])
setTimeout(done, 1000)
})
it('should use routes.json', function (done) {
request.get('/blog/posts').expect(200, done)
})
})
// FIXME test fails on OS X and maybe on Windows
// But manually updating db.json works...
if (os.platform() === 'linux') {
describe('--watch db.json -r routes.json', function () {
beforeEach(function (done) {
child = cli(['--watch', dbFile, '-r', routesFile])
setTimeout(done, 1000)
})
it('should watch db file', function (done) {
fs.writeFileSync(dbFile, JSON.stringify({ foo: [] }))
setTimeout(function () {
request.get('/foo').expect(200, done)
}, 1000)
})
it('should watch routes file', function (done) {
// Can be very slow
this.timeout(10000)
fs.writeFileSync(routesFile, JSON.stringify({ '/api/': '/' }))
setTimeout(function () {
request.get('/api/posts').expect(200, done)
}, 9000)
})
})
}
})

View File

@ -1,6 +1,6 @@
var request = require('supertest') var request = require('supertest')
var assert = require('assert') var assert = require('assert')
var jsonServer = require('../src/') var jsonServer = require('../../src/server')
/* global beforeEach, describe, it */ /* global beforeEach, describe, it */

View File

@ -1,7 +1,7 @@
var assert = require('assert') var assert = require('assert')
var mixins = require('../src/mixins')
var _ = require('lodash') var _ = require('lodash')
var _db = require('underscore-db') var _db = require('underscore-db')
var mixins = require('../../src/server/mixins')
/* global describe, it */ /* global describe, it */

View File

@ -1,5 +1,5 @@
var assert = require('assert') var assert = require('assert')
var utils = require('../src/utils') var utils = require('../../src/server/utils')
/* global describe, it */ /* global describe, it */