Commit 8d668726e7d11fcdfc803f3c47ed305680c7194f
split server into micro-services!
each service will now reload independently in development, should be much faster. also will allow us to create a legit production deploy using docker-compose. also i really like the consistency of using npm scripts to start each service.Michael Williams committed on 1/3/2016, 3:14:24 AM
Parent: d393015f835e882e9ded1efb5f5b459f624ee943
Files changed
app/api.js | changed |
app/package.json | changed |
app/stack/services.js | changed |
app/stack/render.js | deleted |
app/stack/static.js | deleted |
app/client.js | deleted |
app/render-browser.js | added |
app/server.js | deleted |
app/render.js | added |
app/static.js | added |
config/index.js | changed |
package.json | changed |
api.js | added |
client.js | deleted |
server.js | deleted |
render.js | added |
static.js | added |
app/api.js | ||
---|---|---|
@@ -1,0 +1,21 @@ | ||
1 | +const feathers = require('feathers') | |
2 | +const { mapObjIndexed, reduce, toPairs } = require('ramda') | |
3 | + | |
4 | +const services = require('app/services') | |
5 | +const config = require('app/config') | |
6 | + | |
7 | +module.exports = createServer | |
8 | + | |
9 | +function createServer (config) { | |
10 | + const app = feathers() | |
11 | + | |
12 | + useAll(app, services) | |
13 | + | |
14 | + return app | |
15 | +} | |
16 | + | |
17 | +function useAll (app, services) { | |
18 | + return reduce((app, [name, service]) => { | |
19 | + return app.use(`/${name}`, service) | |
20 | + }, app, toPairs(services)) | |
21 | +} |
app/package.json | ||
---|---|---|
@@ -1,7 +1,8 @@ | ||
1 | 1 | { |
2 | 2 | "browser": { |
3 | - "./api.js": "./clients/index.js" | |
3 | + "./render.js": "./render-browser.js", | |
4 | + "./api.js": "./api-browser.js" | |
4 | 5 | }, |
5 | 6 | "browserify": { |
6 | 7 | "transform": [ |
7 | 8 | ["cssify", { "modules": true } ], |
app/stack/services.js | ||
---|---|---|
@@ -1,21 +1,0 @@ | ||
1 | -const feathers = require('feathers') | |
2 | -const { mapObjIndexed, reduce, toPairs } = require('ramda') | |
3 | - | |
4 | -const services = require('app/services') | |
5 | - | |
6 | -module.exports = createServices | |
7 | - | |
8 | -function createServices(config) { | |
9 | - const app = feathers() | |
10 | - | |
11 | - useAll(app, services) | |
12 | - | |
13 | - return app | |
14 | -} | |
15 | - | |
16 | -function useAll (app, services) { | |
17 | - return reduce((app, [name, service]) => { | |
18 | - return app.use(`/${name}`, service) | |
19 | - }, app, toPairs(services)) | |
20 | -} | |
21 | - |
app/stack/render.js | ||
---|---|---|
@@ -1,75 +1,0 @@ | ||
1 | -// https://github.com/jlongster/react-redux-universal-hot-example/blob/master/src/server.js | |
2 | - | |
3 | -const React = require('react') | |
4 | -const { renderToString } = require('react-dom/server') | |
5 | -const { Provider } = require('react-redux') | |
6 | -const { createHistory } = require('history') | |
7 | -const { Router, RoutingContext, match } = require('react-router') | |
8 | - | |
9 | -const createStore = require('app/store') | |
10 | -const routes = require('app/routes') | |
11 | -const fetchAllData = require('app/util/fetch-all-data') | |
12 | - | |
13 | -module.exports = createRender | |
14 | - | |
15 | -function createRender (config) { | |
16 | - return function render (req, res) { | |
17 | - const store = createStore() | |
18 | - | |
19 | - match({ | |
20 | - routes: routes, | |
21 | - location: req.path | |
22 | - }, function (err, redirectLocation, renderProps) { | |
23 | - if (redirectLocation) { | |
24 | - res.redirect(redirectLocation.pathname + redirectLocation.search) | |
25 | - } else if (err) { | |
26 | - res.status(500).send(err.message) | |
27 | - } else if (!renderProps) { | |
28 | - res.status(404).send('Not found') | |
29 | - } else { | |
30 | - fetchAllData( | |
31 | - renderProps.components, | |
32 | - store.getState, store.dispatch, | |
33 | - renderProps.location, | |
34 | - renderProps.params | |
35 | - ).then(function () { | |
36 | - const component = <Provider store={store} key="provider"> | |
37 | - <RoutingContext { ...renderProps } /> | |
38 | - </Provider> | |
39 | - | |
40 | - var innerHtml | |
41 | - try { | |
42 | - innerHtml = renderToString(component) | |
43 | - } catch (err) { | |
44 | - res.setHeader('content-type', 'text/plain') | |
45 | - res.status(500).send(err.stack) | |
46 | - } | |
47 | - | |
48 | - const html = renderFullPage(innerHtml, store.getState()) | |
49 | - | |
50 | - res.send(html) | |
51 | - }) | |
52 | - } | |
53 | - }) | |
54 | - } | |
55 | -} | |
56 | - | |
57 | -function renderFullPage (innerHtml, initialData) { | |
58 | - return ` | |
59 | - | |
60 | - <html lang="en"> | |
61 | - <head> | |
62 | - <meta charset="utf-8" /> | |
63 | - <title>Craftworks TodoMVC</title> | |
64 | - <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
65 | - </head> | |
66 | - <body> | |
67 | - <main>${ innerHtml }</main> | |
68 | - <script> | |
69 | - window.__data = ${ JSON.stringify(initialData) } | |
70 | - </script> | |
71 | - <script src="bundle.js"></script> | |
72 | - </body> | |
73 | - </html> | |
74 | - ` | |
75 | -} |
app/stack/static.js | ||
---|---|---|
@@ -1,8 +1,0 @@ | ||
1 | -const serveStatic = require('serve-static') | |
2 | -const { join } = require('path') | |
3 | - | |
4 | -module.exports = createStatic | |
5 | - | |
6 | -function createStatic (config) { | |
7 | - return serveStatic(config.root, config) | |
8 | -} |
app/client.js | ||
---|---|---|
@@ -1,45 +1,0 @@ | ||
1 | -const React = require('react') | |
2 | -const { render } = require('react-dom') | |
3 | -const { Provider } = require('react-redux') | |
4 | -const { Router } = require('react-router') | |
5 | -const { createHistory } = require('history') | |
6 | -const { syncReduxAndRouter } = require('redux-simple-router') | |
7 | - | |
8 | -const routes = require('app/routes') | |
9 | -const createStore = require('app/store') | |
10 | -const fetchElement = require('app/util/fetch-element') | |
11 | - | |
12 | -if (process.env.NODE_ENV === 'development') { | |
13 | -} | |
14 | - | |
15 | -const store = createStore(window.__data) | |
16 | -const history = createHistory() | |
17 | - | |
18 | -syncReduxAndRouter(history, store) | |
19 | - | |
20 | -const main = ( | |
21 | - <Router createElement={fetchElement} history={history}> | |
22 | - { routes } | |
23 | - </Router> | |
24 | -) | |
25 | - | |
26 | -render( | |
27 | - <Provider store={store} key="provider"> | |
28 | - { main } | |
29 | - </Provider>, | |
30 | - document.querySelector('main') | |
31 | -) | |
32 | - | |
33 | -if (process.env.NODE_ENV === 'development') { | |
34 | - const DevTools = require('app/dev/tools') | |
35 | - | |
36 | - render( | |
37 | - <Provider store={store} key="provider"> | |
38 | - <div> | |
39 | - { main } | |
40 | - <DevTools /> | |
41 | - </div> | |
42 | - </Provider>, | |
43 | - document.querySelector('main') | |
44 | - ) | |
45 | -} |
app/render-browser.js | ||
---|---|---|
@@ -1,0 +1,45 @@ | ||
1 | +const React = require('react') | |
2 | +const { render } = require('react-dom') | |
3 | +const { Provider } = require('react-redux') | |
4 | +const { Router } = require('react-router') | |
5 | +const { createHistory } = require('history') | |
6 | +const { syncReduxAndRouter } = require('redux-simple-router') | |
7 | + | |
8 | +const routes = require('app/routes') | |
9 | +const createStore = require('app/store') | |
10 | +const fetchElement = require('app/util/fetch-element') | |
11 | + | |
12 | +if (process.env.NODE_ENV === 'development') { | |
13 | +} | |
14 | + | |
15 | +const store = createStore(window.__data) | |
16 | +const history = createHistory() | |
17 | + | |
18 | +syncReduxAndRouter(history, store) | |
19 | + | |
20 | +const main = ( | |
21 | + <Router createElement={fetchElement} history={history}> | |
22 | + { routes } | |
23 | + </Router> | |
24 | +) | |
25 | + | |
26 | +render( | |
27 | + <Provider store={store} key="provider"> | |
28 | + { main } | |
29 | + </Provider>, | |
30 | + document.querySelector('main') | |
31 | +) | |
32 | + | |
33 | +if (process.env.NODE_ENV === 'development') { | |
34 | + const DevTools = require('app/dev/tools') | |
35 | + | |
36 | + render( | |
37 | + <Provider store={store} key="provider"> | |
38 | + <div> | |
39 | + { main } | |
40 | + <DevTools /> | |
41 | + </div> | |
42 | + </Provider>, | |
43 | + document.querySelector('main') | |
44 | + ) | |
45 | +} |
app/server.js | ||
---|---|---|
@@ -1,11 +1,0 @@ | ||
1 | -const createStack = require('app/stack') | |
2 | - | |
3 | -module.exports = createServer | |
4 | - | |
5 | -function createServer (config) { | |
6 | - const server = createStack(config) | |
7 | - | |
8 | - server.listen(config.port) | |
9 | - | |
10 | - return server | |
11 | -} |
app/render.js | ||
---|---|---|
@@ -1,0 +1,87 @@ | ||
1 | +// https://github.com/jlongster/react-redux-universal-hot-example/blob/master/src/server.js | |
2 | + | |
3 | +const http =require('http') | |
4 | +const Url = require('url') | |
5 | +const React = require('react') | |
6 | +const { renderToString } = require('react-dom/server') | |
7 | +const { Provider } = require('react-redux') | |
8 | +const { createHistory } = require('history') | |
9 | +const { Router, RoutingContext, match } = require('react-router') | |
10 | +const sendHtml = require('send-data/html') | |
11 | +const sendError = require('send-data/error') | |
12 | +const redirect = require('predirect') | |
13 | + | |
14 | +const createStore = require('app/store') | |
15 | +const routes = require('app/routes') | |
16 | +const fetchAllData = require('app/util/fetch-all-data') | |
17 | + | |
18 | +module.exports = createRender | |
19 | + | |
20 | +function createRender (config) { | |
21 | + const staticUrl = Url.format(config.static.url) | |
22 | + | |
23 | + return http.createServer(render) | |
24 | + | |
25 | + function render (req, res) { | |
26 | + const store = createStore() | |
27 | + | |
28 | + match({ | |
29 | + routes: routes, | |
30 | + location: req.url | |
31 | + }, function (err, redirectLocation, renderProps) { | |
32 | + if (redirectLocation) { | |
33 | + redirect(req, res, redirectLocation.pathname + redirectLocation.search) | |
34 | + } else if (err) { | |
35 | + sendError(req, res, { body: err }) | |
36 | + } else if (!renderProps) { | |
37 | + sendError(req, res, { | |
38 | + statusCode: 404, | |
39 | + body: new Error('Not found') | |
40 | + }) | |
41 | + } else { | |
42 | + fetchAllData( | |
43 | + renderProps.components, | |
44 | + store.getState, store.dispatch, | |
45 | + renderProps.location, | |
46 | + renderProps.params | |
47 | + ).then(function () { | |
48 | + const component = <Provider store={store} key="provider"> | |
49 | + <RoutingContext { ...renderProps } /> | |
50 | + </Provider> | |
51 | + | |
52 | + var innerHtml | |
53 | + try { | |
54 | + innerHtml = renderToString(component) | |
55 | + } catch (err) { | |
56 | + return sendError(req, res, { body: err }) | |
57 | + } | |
58 | + | |
59 | + const html = renderFullPage(innerHtml, store.getState(), config) | |
60 | + | |
61 | + sendHtml(req, res, html) | |
62 | + }) | |
63 | + } | |
64 | + }) | |
65 | + } | |
66 | + | |
67 | + function renderFullPage (innerHtml, initialData) { | |
68 | + return ` | |
69 | + | |
70 | + <html lang="en"> | |
71 | + <head> | |
72 | + <meta charset="utf-8" /> | |
73 | + <title>Craftworks TodoMVC</title> | |
74 | + <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
75 | + </head> | |
76 | + <body> | |
77 | + <main>${ innerHtml }</main> | |
78 | + <script> | |
79 | + window.__data = ${ JSON.stringify(initialData) } | |
80 | + </script> | |
81 | + <script src="${Url.resolve(staticUrl, 'bundle.js')}"></script> | |
82 | + </body> | |
83 | + </html> | |
84 | + ` | |
85 | + } | |
86 | +} | |
87 | + |
app/static.js | ||
---|---|---|
@@ -1,0 +1,12 @@ | ||
1 | +const http = require('http') | |
2 | + | |
3 | +module.exports = createStatic | |
4 | + | |
5 | +function createStatic (config) { | |
6 | + const ecstatic = config.livereload ? | |
7 | + require('ecstatic-lr') : require('ecstatic') | |
8 | + | |
9 | + return http.createServer( | |
10 | + ecstatic(config.static) | |
11 | + ) | |
12 | +} |
config/index.js | ||
---|---|---|
@@ -2,9 +2,27 @@ | ||
2 | 2 | const env = process.env |
3 | 3 | const nodeEnv = env.NODE_ENV |
4 | 4 | |
5 | 5 | module.exports = { |
6 | + render: { | |
7 | + url: { | |
8 | + protocol: 'http:', | |
9 | + hostname: 'localhost', | |
10 | + port: 5000 | |
11 | + } | |
12 | + }, | |
6 | 13 | static: { |
14 | + url: { | |
15 | + protocol: 'http:', | |
16 | + hostname: 'localhost', | |
17 | + port: 5001 | |
18 | + }, | |
7 | 19 | root: join(__dirname, '..', 'build') |
8 | 20 | }, |
9 | - port: env.PORT || 5000 | |
21 | + api: { | |
22 | + url: { | |
23 | + protocol: 'http:', | |
24 | + hostname: 'localhost', | |
25 | + port: 5002 | |
26 | + } | |
27 | + } | |
10 | 28 | } |
package.json | ||
---|---|---|
@@ -1,22 +1,26 @@ | ||
1 | 1 | { |
2 | 2 | "name": "business-stack", |
3 | 3 | "version": "0.0.0", |
4 | 4 | "description": "real-world production-quality TodoMVC example", |
5 | - "main": "server", | |
6 | 5 | "scripts": { |
7 | 6 | "postinstall": "lnfs app node_modules/app", |
8 | 7 | "lint": "snazzy", |
9 | 8 | "format": "snazzy --format", |
10 | 9 | "test": "npm-run-all -p test:*", |
11 | 10 | "test:spec": "node app/spec", |
12 | 11 | "test:feature": "node app/features", |
13 | - "dev:client": "BABEL_ENV=hot watchify client -o build/bundle.js -dv -p browserify-hmr", | |
12 | + "dev:render-browser": "BABEL_ENV=hot watchify app/render -o build/bundle.js -dv -p browserify-hmr", | |
13 | + "dev:render-node": "node-dev render", | |
14 | 14 | "dev:assets": "cpx \"app/assets/**/*\" build -w", |
15 | - "dev:server": "node-dev server", | |
16 | - "prod:client": "browserify client -o build/bundle.js -g envify -g uglifyify", | |
15 | + "dev:livereload": "wtch -d build -e html,css,png,gif,jpg | garnish --level debug", | |
16 | + "dev:api": "node-dev api", | |
17 | + "dev:static": "node-dev static", | |
18 | + "prod:render-browser": "browserify app/render -o build/bundle.js -g envify -g uglifyify", | |
19 | + "prod:render-node": "node render", | |
17 | 20 | "prod:assets": "cpx \"app/assets/**/*\" build", |
18 | - "prod:server": "node server", | |
21 | + "prod:api": "node api", | |
22 | + "prod:static": "node static", | |
19 | 23 | "dev": "NODE_ENV=development npm-run-all -p dev:*", |
20 | 24 | "prod": "NODE_ENV=production npm-run-all -s prod:*" |
21 | 25 | }, |
22 | 26 | "repository": { |
@@ -50,8 +54,10 @@ | ||
50 | 54 | "ava": "^0.8.0", |
51 | 55 | "babel-plugin-react-transform": "^2.0.0", |
52 | 56 | "browserify-hmr": "^0.3.1", |
53 | 57 | "cuke-tap": "^1.0.2", |
58 | + "ecstatic-lr": "^1.0.1", | |
59 | + "garnish": "^5.0.1", | |
54 | 60 | "glob": "^6.0.2", |
55 | 61 | "jsdom": "^7.1.0", |
56 | 62 | "node-dev": "^2.7.1", |
57 | 63 | "react-transform-catch-errors": "^1.0.0", |
@@ -62,9 +68,10 @@ | ||
62 | 68 | "redux-devtools-log-monitor": "^1.0.1", |
63 | 69 | "redux-logger": "^2.0.4", |
64 | 70 | "run-parallel": "^1.1.4", |
65 | 71 | "tape": "^4.4.0", |
66 | - "watchify": "^3.6.1" | |
72 | + "watchify": "^3.6.1", | |
73 | + "wtch": "^4.0.1" | |
67 | 74 | }, |
68 | 75 | "dependencies": { |
69 | 76 | "babel-core": "^6.2.1", |
70 | 77 | "babel-plugin-transform-object-rest-spread": "^6.1.18", |
@@ -78,24 +85,26 @@ | ||
78 | 85 | "bulkify": "^1.1.1", |
79 | 86 | "cpx": "^1.2.1", |
80 | 87 | "css-modules-require-hook": "^2.1.0", |
81 | 88 | "cssify": "github:ahdinosaur/cssify", |
89 | + "ecstatic": "^1.4.0", | |
82 | 90 | "envify": "^3.4.0", |
83 | 91 | "evalify": "^1.0.1", |
84 | 92 | "feathers": "^1.2.0", |
85 | 93 | "history": "^1.13.1", |
86 | 94 | "lnfs-cli": "^1.0.1", |
87 | 95 | "npm-run-all": "^1.3.2", |
88 | 96 | "pinkie-promise": "^2.0.0", |
97 | + "predirect": "^1.1.0", | |
89 | 98 | "ramda": "^0.18.0", |
90 | 99 | "react": "^0.14.3", |
91 | 100 | "react-dom": "^0.14.3", |
92 | 101 | "react-redux": "^4.0.1", |
93 | 102 | "react-router": "^1.0.0", |
94 | 103 | "redux": "^3.0.5", |
95 | 104 | "redux-simple-router": "0.0.10", |
96 | 105 | "redux-thunk": "^1.0.0", |
97 | - "simple-rc": "github:ahdinosaur/simple-rc", | |
98 | - "serve-static": "^1.10.0", | |
106 | + "send-data": "^8.0.0", | |
107 | + "simple-rc": "^1.0.0", | |
99 | 108 | "uglifyify": "^3.0.1" |
100 | 109 | } |
101 | 110 | } |
api.js | ||
---|---|---|
@@ -1,0 +1,12 @@ | ||
1 | +require('babel-core/register') | |
2 | + | |
3 | +const config = require('app/config') | |
4 | +const createApi = require('app/api') | |
5 | +const Url = require('url') | |
6 | + | |
7 | +const server = createApi(config) | |
8 | + | |
9 | +server.listen(config.api.url.port, function () { | |
10 | + const apiUrl = Url.format(config.api.url) | |
11 | + console.log(`api server listening at ${apiUrl}`) | |
12 | +}) |
client.js | ||
---|---|---|
@@ -1,1 +1,0 @@ | ||
1 | -require('app/client') |
server.js | ||
---|---|---|
@@ -1,7 +1,0 @@ | ||
1 | -require('babel-core/register') | |
2 | -require('css-modules-require-hook') | |
3 | - | |
4 | -const createServer = require('app/server') | |
5 | -const config = require('app/config') | |
6 | - | |
7 | -createServer(config) |
render.js | ||
---|---|---|
@@ -1,0 +1,13 @@ | ||
1 | +require('babel-core/register') | |
2 | +require('css-modules-require-hook') | |
3 | + | |
4 | +const config = require('app/config') | |
5 | +const createRender = require('app/render') | |
6 | +const Url = require('url') | |
7 | + | |
8 | +const server = createRender(config) | |
9 | + | |
10 | +server.listen(config.render.url.port, function () { | |
11 | + const renderUrl = Url.format(config.render.url) | |
12 | + console.log(`render server listening at ${renderUrl}`) | |
13 | +}) |
static.js | ||
---|---|---|
@@ -1,0 +1,10 @@ | ||
1 | +const config = require('app/config') | |
2 | +const createStatic = require('app/static') | |
3 | +const Url = require('url') | |
4 | + | |
5 | +const server = createStatic(config) | |
6 | + | |
7 | +server.listen(config.static.url.port, function () { | |
8 | + const staticUrl = Url.format(config.static.url) | |
9 | + console.log(`static server listening at ${staticUrl}`) | |
10 | +}) |
Built with git-ssb-web