diff --git a/bin/index.js b/bin/index.js index af0f1e8..6337655 100755 --- a/bin/index.js +++ b/bin/index.js @@ -1,165 +1,2 @@ #!/usr/bin/env node -var fs = require('fs') -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] ') - .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 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) -} +require('../src/cli')() diff --git a/package.json b/package.json index 0d278d3..71d6649 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "json-server", "version": "0.7.20", "description": "Serves JSON files through REST routes.", - "main": "./src/index.js", + "main": "./src/server/index.js", "bin": "./bin/index.js", "directories": { "test": "test" @@ -13,7 +13,7 @@ "cors": "^2.3.0", "errorhandler": "^1.2.0", "express": "^4.9.5", - "got": "^1.2.2", + "got": "^3.3.0", "lodash": "^3.9.2", "lowdb": "^0.10.0", "method-override": "^2.1.2", @@ -21,17 +21,18 @@ "node-uuid": "^1.4.2", "pluralize": "^1.1.2", "underscore-db": "^0.9.0", - "update-notifier": "^0.2.2", + "update-notifier": "^0.5.0", "yargs": "^3.10.0" }, "devDependencies": { "husky": "^0.6.1", "mocha": "^2.2.4", + "rimraf": "^2.4.1", "standard": "^3.8.0", "supertest": "~0.8.1" }, "scripts": { - "test": "standard && mocha -R spec test", + "test": "NODE_ENV=test mocha -R spec test/**/*.js && standard", "start": "node bin", "prepush": "npm t" }, diff --git a/src/cli/index.js b/src/cli/index.js new file mode 100644 index 0000000..8365ef7 --- /dev/null +++ b/src/cli/index.js @@ -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] ') + .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 argument') + .argv + + run(argv) + +} diff --git a/src/cli/run.js b/src/cli/run.js new file mode 100644 index 0000000..fd2fda2 --- /dev/null +++ b/src/cli/run.js @@ -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() + }) + } + +} diff --git a/src/cli/utils/is.js b/src/cli/utils/is.js new file mode 100644 index 0000000..9556ae5 --- /dev/null +++ b/src/cli/utils/is.js @@ -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) +} diff --git a/src/cli/utils/load.js b/src/cli/utils/load.js new file mode 100644 index 0000000..98ca5d3 --- /dev/null +++ b/src/cli/utils/load.js @@ -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) + } +} diff --git a/src/cli/watch.js b/src/cli/watch.js new file mode 100644 index 0000000..5b452cb --- /dev/null +++ b/src/cli/watch.js @@ -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) + }) + } +} diff --git a/src/defaults.js b/src/server/defaults.js similarity index 88% rename from src/defaults.js rename to src/server/defaults.js index 3b103bc..633914c 100644 --- a/src/defaults.js +++ b/src/server/defaults.js @@ -8,7 +8,10 @@ var arr = [] // Logger 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 diff --git a/src/index.js b/src/server/index.js similarity index 100% rename from src/index.js rename to src/server/index.js diff --git a/src/mixins.js b/src/server/mixins.js similarity index 100% rename from src/mixins.js rename to src/server/mixins.js diff --git a/src/public/favicon.ico b/src/server/public/favicon.ico similarity index 100% rename from src/public/favicon.ico rename to src/server/public/favicon.ico diff --git a/src/public/images/json.png b/src/server/public/images/json.png similarity index 100% rename from src/public/images/json.png rename to src/server/public/images/json.png diff --git a/src/public/index.html b/src/server/public/index.html similarity index 100% rename from src/public/index.html rename to src/server/public/index.html diff --git a/src/public/stylesheets/style.css b/src/server/public/stylesheets/style.css similarity index 100% rename from src/public/stylesheets/style.css rename to src/server/public/stylesheets/style.css diff --git a/src/rewriter.js b/src/server/rewriter.js similarity index 100% rename from src/rewriter.js rename to src/server/rewriter.js diff --git a/src/router.js b/src/server/router.js similarity index 100% rename from src/router.js rename to src/server/router.js diff --git a/src/utils.js b/src/server/utils.js similarity index 100% rename from src/utils.js rename to src/server/utils.js diff --git a/test/cli/fixtures/seed.js b/test/cli/fixtures/seed.js new file mode 100644 index 0000000..22f00eb --- /dev/null +++ b/test/cli/fixtures/seed.js @@ -0,0 +1,3 @@ +module.exports = function () { + return { posts: [] } +} diff --git a/test/cli/index.js b/test/cli/index.js new file mode 100644 index 0000000..83f5076 --- /dev/null +++ b/test/cli/index.js @@ -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) + }) + + }) + } + +}) diff --git a/test/index.js b/test/server/index.js similarity index 99% rename from test/index.js rename to test/server/index.js index eba72e4..f326343 100644 --- a/test/index.js +++ b/test/server/index.js @@ -1,6 +1,6 @@ var request = require('supertest') var assert = require('assert') -var jsonServer = require('../src/') +var jsonServer = require('../../src/server') /* global beforeEach, describe, it */ diff --git a/test/mixins.js b/test/server/mixins.js similarity index 93% rename from test/mixins.js rename to test/server/mixins.js index 88210fb..e632736 100644 --- a/test/mixins.js +++ b/test/server/mixins.js @@ -1,7 +1,7 @@ var assert = require('assert') -var mixins = require('../src/mixins') var _ = require('lodash') var _db = require('underscore-db') +var mixins = require('../../src/server/mixins') /* global describe, it */ diff --git a/test/utils.js b/test/server/utils.js similarity index 94% rename from test/utils.js rename to test/server/utils.js index 13c7d0a..e353804 100644 --- a/test/utils.js +++ b/test/server/utils.js @@ -1,5 +1,5 @@ var assert = require('assert') -var utils = require('../src/utils') +var utils = require('../../src/server/utils') /* global describe, it */