-
Notifications
You must be signed in to change notification settings - Fork 40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement server-side code hot-swapping on file change #1225
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
app.use((req, rest, next) => { | ||
req.user = null | ||
next() | ||
}) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah right this comment can probably be removed since it's pretty old and we actually benefit from not sharing req.user because it keeps our API stateless. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh yeah, gotta fix this |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,4 +6,4 @@ setup = require "./lib/setup" | |
express = require "express" | ||
|
||
app = module.exports = express() | ||
setup app | ||
setup app |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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' | ||
) | ||
} | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Love this! Very clear and thoughtful error handling |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import express from 'express' | ||
|
||
const app = module.exports = express() | ||
|
||
app.get('/bar', (req, res, next) => { | ||
res.send('working! 41') | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍