diff --git a/api/index.coffee b/api/index.coffee index 64c32b52b..5adf50ac3 100644 --- a/api/index.coffee +++ b/api/index.coffee @@ -47,4 +47,4 @@ app.use notFound app.use errorHandler # Start the test server if run directly -app.listen(5000, -> debug "Listening on 5000") if module is require.main \ No newline at end of file +app.listen(5000, -> debug "Listening on 5000") if module is require.main diff --git a/boot.js b/boot.js index ed78f765e..81e537cd9 100644 --- a/boot.js +++ b/boot.js @@ -4,8 +4,10 @@ import artsyXapp from 'artsy-xapp' import express from 'express' -import { IpFilter } from 'express-ipfilter' import newrelic from 'artsy-newrelic' +import path from 'path' +import { IpFilter } from 'express-ipfilter' +import { createReloadable, isDevelopment } from 'lib/reloadable' const debug = require('debug')('app') const app = module.exports = express() @@ -16,23 +18,32 @@ app.use( ) // Get an xapp token -artsyXapp.init({ +const xappConfig = { url: process.env.ARTSY_URL, id: process.env.ARTSY_ID, secret: process.env.ARTSY_SECRET -}, () => { +} + +artsyXapp.init(xappConfig, () => { app.use(newrelic) - // Put client/api together - app.use('/api', require('./api')) + if (isDevelopment) { + const reloadAndMount = createReloadable(app) - // TODO: Possibly a terrible hack to not share `req.user` between both. - app.use((req, rest, next) => { - req.user = null - next() - }) + // Enable server-side code hot-swapping on change + app.use('/api', reloadAndMount(path.resolve(__dirname, 'api'), { + mountPoint: '/api' + })) - app.use(require('./client')) + invalidateUserMiddleware(app) + reloadAndMount(path.resolve(__dirname, 'client')) + + // Staging, Prod + } else { + app.use('/api', require('./api')) + invalidateUserMiddleware(app) + app.use(require('./client')) + } // Start the server and send a message to IPC for the integration test // helper to hook into. @@ -50,3 +61,10 @@ artsyXapp.on('error', (error) => { console.warn(error) process.exit(1) }) + +const invalidateUserMiddleware = (app) => { + app.use((req, rest, next) => { + req.user = null + next() + }) +} diff --git a/client/apps/articles_list/routes.coffee b/client/apps/articles_list/routes.coffee index b92d4c779..68e086d6f 100644 --- a/client/apps/articles_list/routes.coffee +++ b/client/apps/articles_list/routes.coffee @@ -34,4 +34,4 @@ renderArticles = (res, req, result, published) -> res.locals.sd.HAS_PUBLISHED = published res.render 'index', articles: result.articles || [] - current_channel: req.user?.get('current_channel') \ No newline at end of file + current_channel: req.user?.get('current_channel') diff --git a/client/apps/edit/routes.coffee b/client/apps/edit/routes.coffee index ed4fefd1e..dffc864f6 100644 --- a/client/apps/edit/routes.coffee +++ b/client/apps/edit/routes.coffee @@ -38,4 +38,4 @@ render = (req, res, article) -> setChannelIndexable = (channel, article) -> noIndex = sd.NO_INDEX_CHANNELS.split '|' if noIndex.includes channel.get('id') - article.set 'indexable', false \ No newline at end of file + article.set 'indexable', false diff --git a/client/index.coffee b/client/index.coffee index 900301f12..02e639ffc 100755 --- a/client/index.coffee +++ b/client/index.coffee @@ -6,4 +6,4 @@ setup = require "./lib/setup" express = require "express" app = module.exports = express() -setup app \ No newline at end of file +setup app diff --git a/lib/reloadable.js b/lib/reloadable.js new file mode 100644 index 000000000..c239e7768 --- /dev/null +++ b/lib/reloadable.js @@ -0,0 +1,80 @@ +/** + * Dev utility for server-side code reloading without the restart. In development, + * watch for file-system changes and clear cached modules when a change occurs, + * thus effectively reloading the entire app on request. + */ + +export const isDevelopment = process.env.NODE_ENV === 'development' + +export function createReloadable (app) { + return (folderPath, options = {}) => { + if (isDevelopment) { + const { + mountPoint = '/' + } = options + + const onReload = (req, res, next) => require(folderPath)(req, res, next) + + if (typeof onReload !== 'function') { + throw new Error( + '(lib/reloadable.js) Error initializing reloadable: `onReload` is ' + + 'undefined. Did you forget to pass a callback function?' + ) + } + + const watcher = require('chokidar').watch(folderPath) + + watcher.on('ready', () => { + watcher.on('all', () => { + Object.keys(require.cache).forEach(id => { + if (id.startsWith(folderPath)) { + delete require.cache[id] + } + }) + }) + }) + + let currentResponse = null + let currentNext = null + + app.use((req, res, next) => { + currentResponse = res + currentNext = next + + res.on('finish', () => { + currentResponse = null + currentNext = null + }) + + next() + }) + + /** + * In case of an uncaught exception show it to the user and proceed, rather + * than exiting the process. + */ + process.on('uncaughtException', (error) => { + if (currentResponse) { + currentNext(error) + currentResponse = null + currentNext = null + } else { + process.abort() + } + }) + + app.use(mountPoint, (req, res, next) => { + onReload(req, res, next) + }) + + return onReload + + // Node env not 'development', exit + } else { + throw new Error( + '(lib/reloadable.js) NODE_ENV must be set to "development" to use ' + + 'reloadable.js' + ) + } + } +} diff --git a/package.json b/package.json index f4a99b073..d16b7dd39 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "babel-preset-react": "^6.24.1", "babel-preset-stage-3": "^6.24.1", "babelify": "^7.3.0", + "babel-runtime": "^6.25.0", "backbone": "^1.2.3", "backbone-super-sync": "^1.1.1", "bcryptjs": "^2.4.3", @@ -102,6 +103,7 @@ "browserify": "^14.1.0", "browserify-dev-middleware": "^1.5.0", "caching-coffeeify": "^0.5.1", + "chokidar": "^1.7.0", "cssify": "^0.7.0", "eslint": "^3.19.0", "eslint-config-standard": "^10.2.1", diff --git a/test-reload/index.js b/test-reload/index.js new file mode 100644 index 000000000..e963def50 --- /dev/null +++ b/test-reload/index.js @@ -0,0 +1,7 @@ +import express from 'express' + +const app = module.exports = express() + +app.get('/bar', (req, res, next) => { + res.send('working! 41') +}) diff --git a/yarn.lock b/yarn.lock index 3f55122d9..b4814ecf5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1956,9 +1956,9 @@ cheerio@^0.19.0: htmlparser2 "~3.8.1" lodash "^3.2.0" -chokidar@^1.0.0, chokidar@^1.0.1, chokidar@^1.4.3, chokidar@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.6.1.tgz#2f4447ab5e96e50fb3d789fd90d4c72e0e4c70c2" +chokidar@^1.0.0, chokidar@^1.0.1, chokidar@^1.4.3, chokidar@^1.6.1, chokidar@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" dependencies: anymatch "^1.3.0" async-each "^1.0.0"