diff --git a/.eslintrc b/.eslintrc index 2b091631d..a629f53c1 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,4 +1,5 @@ -{ "extends": "eslint-config-airbnb", +{ + "extends": "eslint-config-airbnb", "env": { "browser": true, "node": true, @@ -36,6 +37,10 @@ "__SERVER__": true, "__DISABLE_SSR__": true, "__DEVTOOLS__": true, + "__META__": {}, + "__API_ENDPOINT__": '', + "__API_HOST__": '', + "__API_PORT__": '', "socket": true, "webpackIsomorphicTools": true } diff --git a/.gitignore b/.gitignore index a7a8d08ee..097716ab4 100755 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist/ webpack-assets.json webpack-stats.json npm-debug.log +.DS_Store diff --git a/.travis.yml b/.travis.yml index fd6e5d5c5..0c5c083a2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,4 +15,4 @@ before_script: script: - npm run lint - npm test - - npm run test-node + - npm test-node diff --git a/api/api.js b/api/api.js index 7cee64671..d793877fd 100644 --- a/api/api.js +++ b/api/api.js @@ -1,13 +1,15 @@ import express from 'express'; import session from 'express-session'; import bodyParser from 'body-parser'; -import config from '../src/config'; import * as actions from './actions/index'; import {mapUrl} from 'utils/url.js'; import PrettyError from 'pretty-error'; import http from 'http'; import SocketIo from 'socket.io'; +const apiPort = process.env.APIPORT; +const apiHost = process.env.APIHOST || 'localhost'; + const pretty = new PrettyError(); const app = express(); @@ -24,9 +26,7 @@ app.use(session({ })); app.use(bodyParser.json()); - app.use((req, res) => { - const splittedUrlPath = req.url.split('?')[0].split('/').slice(1); const {action, params} = mapUrl(actions, splittedUrlPath); @@ -57,13 +57,13 @@ const bufferSize = 100; const messageBuffer = new Array(bufferSize); let messageIndex = 0; -if (config.apiPort) { - const runnable = app.listen(config.apiPort, (err) => { +if (apiPort) { + const runnable = app.listen(apiPort, (err) => { if (err) { console.error(err); } - console.info('----\n==> ๐ŸŒŽ API is running on port %s', config.apiPort); - console.info('==> ๐Ÿ’ป Send requests to http://%s:%s', config.apiHost, config.apiPort); + console.info('----\n==> ๐ŸŒŽ API is running on port %s', apiPort); + console.info('==> ๐Ÿ’ป Send requests to http://%s:%s', apiHost, apiPort); }); io.on('connection', (socket) => { @@ -87,7 +87,6 @@ if (config.apiPort) { }); }); io.listen(runnable); - } else { console.error('==> ERROR: No PORT environment variable has been specified'); } diff --git a/api/utils/url.js b/api/utils/url.js index 6e3e78ca8..ec2d8ca68 100644 --- a/api/utils/url.js +++ b/api/utils/url.js @@ -1,5 +1,4 @@ export function mapUrl(availableActions = {}, url = []) { - const notFound = {action: null, params: []}; // test for empty input diff --git a/bin/server.js b/bin/server.js deleted file mode 100644 index d4792622e..000000000 --- a/bin/server.js +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env node -require('../server.babel'); // babel registration (runtime transpilation for node) -var path = require('path'); -var rootDir = path.resolve(__dirname, '..'); -/** - * Define isomorphic constants. - */ -global.__CLIENT__ = false; -global.__SERVER__ = true; -global.__DISABLE_SSR__ = false; // <----- DISABLES SERVER SIDE RENDERING FOR ERROR DEBUGGING -global.__DEVELOPMENT__ = process.env.NODE_ENV !== 'production'; - -if (__DEVELOPMENT__) { - if (!require('piping')({ - hook: true, - ignore: /(\/\.|~$|\.json|\.scss$)/i - })) { - return; - } -} - -// https://github.com/halt-hammerzeit/webpack-isomorphic-tools -var WebpackIsomorphicTools = require('webpack-isomorphic-tools'); -global.webpackIsomorphicTools = new WebpackIsomorphicTools(require('../webpack/webpack-isomorphic-tools')) - .development(__DEVELOPMENT__) - .server(rootDir, function() { - require('../src/server'); - }); diff --git a/bin/start.js b/bin/start.js new file mode 100644 index 000000000..a0660c279 --- /dev/null +++ b/bin/start.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node +require('../server.babel'); // babel registration (runtime transpilation for node) +require('./start_es6.js'); diff --git a/bin/start_es6.js b/bin/start_es6.js new file mode 100644 index 000000000..72455b471 --- /dev/null +++ b/bin/start_es6.js @@ -0,0 +1,60 @@ +#!/usr/bin/env node +import path from 'path'; +import http from 'http'; +import renderer from 'universal-redux'; +import httpProxy from 'http-proxy'; +import config from '../config/universal-redux.config.js'; +import SocketIo from 'socket.io'; + +const isProduction = process.env.NODE_ENV !== 'production'; +const apiPort = process.env.APIPORT; +const apiHost = process.env.APIHOST || 'localhost'; +const apiEndpoint = '/api'; + +function setupProxy(app) { + + const proxy = httpProxy.createProxyServer({ + target: 'http://' + apiHost + ':' + apiPort, + ws: true + }); + + // Proxy to API server + app.use(`${apiEndpoint}`, (req, res) => { + proxy.web(req, res); + }); + + // added the error handling to avoid https://github.com/nodejitsu/node-http-proxy/issues/527 + proxy.on('error', (error, req, res) => { + let json; + if (error.code !== 'ECONNRESET') { + console.error('proxy error', error); + } + if (!res.headersSent) { + res.writeHead(500, {'content-type': 'application/json'}); + } + + json = {error: 'proxy_error', reason: error.message}; + res.end(JSON.stringify(json)); + }); +} + +const app = renderer.app(); + +setupProxy(app); + +renderer.setup(config); + +const server = new http.Server(app); + +if (!isProduction) { + const io = new SocketIo(server); + io.path(`${__API_ENDPOINT__}/ws`); +} + +server.listen(config.server.port, (err) => { + if (err) { + console.error(err); + } + console.info('==> ๐ŸŒŽ API calls will be received at:', config.server.host + ':' + config.server.port + apiEndpoint); + console.info('==> ๐Ÿ’ป Open http://localhost:%s in a browser to view the app.', config.server.port); +}); diff --git a/config/meta.config.js b/config/meta.config.js new file mode 100644 index 000000000..e6539aa75 --- /dev/null +++ b/config/meta.config.js @@ -0,0 +1,22 @@ +module.exports = { + title: 'React Redux Example', + description: 'All the modern best practices in one example.', + meta: { + charSet: 'utf-8', + property: { + 'og:site_name': 'React Redux Example', + 'og:image': 'https://react-redux.herokuapp.com/logo.jpg', + 'og:locale': 'en_US', + 'og:title': 'React Redux Example', + 'og:description': 'All the modern best practices in one example.', + 'twitter:card': 'summary', + 'twitter:site': '@erikras', + 'twitter:creator': '@erikras', + 'twitter:title': 'React Redux Example', + 'twitter:description': 'All the modern best practices in one example.', + 'twitter:image': 'https://react-redux.herokuapp.com/logo.jpg', + 'twitter:image:width': '200', + 'twitter:image:height': '200' + } + } +} diff --git a/config/universal-redux.config.js b/config/universal-redux.config.js new file mode 100644 index 000000000..183c07527 --- /dev/null +++ b/config/universal-redux.config.js @@ -0,0 +1,184 @@ +/* eslint-disable */ +const path = require('path'); +const isProduction = process.env.NODE_ENV === 'production'; +const projectRoot = path.resolve(__dirname, '..'); +const sourceRoot = path.resolve(__dirname, '../src'); +const apiPort = process.env.APIPORT; +const apiHost = process.env.APIHOST || 'localhost'; +const meta = require('./meta.config.js'); + +module.exports = Object.assign({ + + /* + // Express configuration + */ + server: { + /* + // The host to run the Express universal renderer. See src/server.js. + // + // Expects: String + */ + host: process.env.HOST || 'localhost', + + /* + // The port to run Express universal renderer will run on. See src/server.js. + // + // Expects: Number + */ + port: process.env.PORT, + }, + + /* + // Globals available to both serverside and clientside rendering. + // You may also add your own here. + */ + globals: { + + /* + // Whether or not to run redux-logger + // + // Expects: Boolean + */ + __LOGGER__: false, + + /* + // Whether or not to run redux-devtools + // + // Expects: Boolean + */ + __DEVTOOLS__: !isProduction, + + __API_ENDPOINT__: '/api', + __API_PORT__: apiPort, + __API_HOST__: apiHost, + __META__: meta + }, + + /* + // Enable eslint checks per Webpack build. Will not be run + // on production. + // + // Expects: Boolean + */ + lint: { + enabled: true, + config: projectRoot + '/.eslintrc' + }, + + /* + // Enable native desktop notifications for Webpack build events. + // Will not be run on production. + // + // Expects: Boolean + */ + notifications: false, + + /* + // Path to a file with customizations for the default + // webpack-isomorphic-tools configuration. Optional. + // + // Expects: String + */ + toolsConfigPath: __dirname + '/webpack-isomorphic-tools.config.js', + + /* + // When eneabled, will output Webpack and Webpack Isomorphic + // Tools configurations at startup + // + // Expects: Boolean + */ + verbose: true, + + /* + // The react-router Routes file, Required. Will be added to Webpack aliases. + */ + routes: sourceRoot + '/routes.js', + + redux: { + /* + // The path to the index of your Redux reducers. Required. Will be added + // to Webpcak aliases. + */ + reducers: sourceRoot + '/redux/modules/reducer.js', + + /* + // A path to an index of middleware functions. On the serverside, these will + // be called with the Express request and response. Optional. + // + // Expects: String + */ + middleware: sourceRoot + '/redux/middleware/index.js' + }, + + /* + // The path to your replacement for the default HTML shell. Optional. + // If not provided, the default used will be that in src/helpers/Html.js. + // Will be added to Webpack aliases. + */ + // htmlShell: sourceRoot + '/helpers/Html.js', + + webpack: { + + /* + // Whether to merge into the default webpack configuration using + // webpack-config-merger. + // + // If the `merge` parameter is `true`, properties with the same name + // will be overwritten. Arrays will be concatenated. Objects will + // be merged. + // + // If the `merge` parameter is `false`, default webpack settings + // will not be used and the config specified here will need to + // be the complete settings required for building. + */ + merge: true, + + /* + // Webpack configuration cusomtizations. There are more parameters + // available than specified here. For the full list, see + // https://webpack.github.io/docs/configuration.html. + */ + config: { + + /* + // The Webpack devtool configuration. May affect build times. + // See https://webpack.github.io/docs/configuration.html#devtool + */ + devtool: 'inline-eval-cheap-source-map', + + entry: { + main: [ + 'bootstrap-sass!' + sourceRoot + '/theme/bootstrap.config' + (isProduction ? '.prod' : '') + '.js', + 'font-awesome-webpack!' + sourceRoot + '/theme/font-awesome.config' + (isProduction ? '.prod' : '') + '.js' + ] + }, + + /* + // Not recommended to change. + */ + context: projectRoot, + + /* + // Not recommended to change. + */ + output: { + + /* + // Not recommended to change. + */ + path: projectRoot + '/static/dist' + + }, + + resolve: { + + /* + // Not recommended to change. + */ + root: sourceRoot + } + } + } + +}); +/* eslint-enable */ diff --git a/config/webpack-isomorphic-tools.config.js b/config/webpack-isomorphic-tools.config.js new file mode 100644 index 000000000..cee7bcd0a --- /dev/null +++ b/config/webpack-isomorphic-tools.config.js @@ -0,0 +1,32 @@ +const WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin'); + +module.exports = { + assets: { + // this whole "bootstrap" asset type is only used once in development mode. + // the only place it's used is the Html.js file + // where a