├── .eslintignore ├── src ├── index.js ├── common │ ├── components │ │ └── App │ │ │ └── index.js │ ├── reducers │ │ ├── example │ │ │ └── index.js │ │ └── index.js │ ├── config.js │ ├── actions │ │ └── example │ │ │ └── index.js │ ├── routes │ │ └── index.js │ ├── lib │ │ ├── switchcase │ │ │ ├── index.js │ │ │ ├── index.test.js │ │ │ └── switchcase.md │ │ ├── shouldFetchAction.js │ │ ├── slugify │ │ │ ├── README.md │ │ │ ├── __tests__ │ │ │ │ └── index-test.js │ │ │ └── index.js │ │ ├── connectApp.js │ │ └── connectApp.md │ ├── containers │ │ └── App.js │ ├── store │ │ └── configureStore.js │ └── api │ │ └── index.js ├── server │ ├── logs │ │ ├── index.js │ │ └── masker │ │ │ └── index.js │ ├── _utils │ │ └── http-utils.js │ ├── index.js │ └── routes │ │ ├── template │ │ └── index.js │ │ └── index.js └── client │ └── index.js ├── .env.development ├── .gitignore ├── README.md ├── .babelrc ├── .eslintrc ├── webpack ├── webpack-dev-server.js ├── webpack-isomorphic-tools.js ├── prod.config.js └── dev.config.js └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | webpack/* 2 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | require = require("@std/esm")(module) 2 | module.exports = require("./server/index.js").default 3 | 4 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | API= 2 | APPLICATION_PORT=3000 3 | NODE_ENV=development 4 | HOST_NAME=http://localhost:3000 5 | 6 | -------------------------------------------------------------------------------- /src/common/components/App/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const App = () => ( 4 |
5 |

Welcome to the Node, React, Redux Isomorphic Boilerplate

6 |
7 | ); 8 | 9 | export default App; 10 | 11 | -------------------------------------------------------------------------------- /src/common/reducers/example/index.js: -------------------------------------------------------------------------------- 1 | import switchcase from './../../lib/switchcase'; 2 | 3 | export default function exampleReducer(state = {}, action) { 4 | return switchcase({ 5 | EXAMPLE: action.example, 6 | })(state)(action.type); 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/common/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | // example 4 | import exampleReducer from './example'; 5 | 6 | const rootReducer = combineReducers({ 7 | exampleReducer, 8 | }); 9 | 10 | export default rootReducer; 11 | 12 | -------------------------------------------------------------------------------- /src/common/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | hostName: process.env.HOST_NAME, 3 | api: { 4 | timeout: 30000, 5 | }, 6 | application: { 7 | port: process.env.APPLICATION_PORT, 8 | }, 9 | pathGroper: process.env.REDIRECTS_BASE_URL, 10 | }; 11 | 12 | -------------------------------------------------------------------------------- /src/common/actions/example/index.js: -------------------------------------------------------------------------------- 1 | const exampleActionType = () => ({ 2 | type: 'EXAMPLE', 3 | example: { test: true }, 4 | createAt: Date.now(), 5 | }); 6 | 7 | export default function exampleAction() { 8 | return dispatch => dispatch(exampleActionType()); 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/server/logs/index.js: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | 3 | const logger = new (winston.Logger)({ 4 | transports: [ 5 | new (winston.transports.Console)({ 6 | formatter: options => options.message, 7 | }), 8 | ], 9 | }); 10 | 11 | export default logger; 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | /.env 5 | 6 | .esm-cache 7 | 8 | public/* 9 | !public/robots.txt 10 | !public/favicon.ico 11 | !public/fonts 12 | !public/images 13 | !public/sitemap.xml 14 | 15 | dist 16 | 17 | webpack-assets.json 18 | webpack-stats.json 19 | 20 | logs/** 21 | 22 | -------------------------------------------------------------------------------- /src/common/routes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Switch } from 'react-router'; 3 | 4 | import App from './../containers/App'; 5 | 6 | export default ( 7 | 8 | 9 |

Page Not Found

} /> 10 |
11 | ); 12 | 13 | -------------------------------------------------------------------------------- /src/server/logs/masker/index.js: -------------------------------------------------------------------------------- 1 | import cnsr from 'cnsr'; 2 | 3 | export default function masker(obj) { 4 | const attrToMask = [ 5 | 'cpf', 6 | 'password', 7 | 'postcode', 8 | 'number', 9 | 'authCode', 10 | 'verificationCode', 11 | 'holder', 12 | 'expirationMonth', 13 | 'expirationYear', 14 | 'taxDocument', 15 | ]; 16 | 17 | return JSON.stringify(cnsr(obj, attrToMask)); 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/common/lib/switchcase/index.js: -------------------------------------------------------------------------------- 1 | const ifFunction = (f) => { 2 | if (typeof f === 'function') { 3 | return f(); 4 | } 5 | return f; 6 | }; 7 | 8 | const switchcaseF = cases => defaultCase => (key) => { 9 | if (key in cases) { 10 | return cases[key]; 11 | } 12 | 13 | return defaultCase; 14 | }; 15 | 16 | const switchcase = (cases = {}) => (defaultCase = {}) => (key = '') => 17 | ifFunction(switchcaseF(cases)(defaultCase)(key)); 18 | 19 | export default switchcase; 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-Redux-NodeJS Isomorphic Boilerplate 2 | 3 | > This is not yet ready for production, the build is not properly optimized for running with safety. If you wanna help me out with that, get the code and open a Pull Request. 4 | 5 | A boilerplate and/or guide to a fully functional Isomorphic app using React, Redux and NodeJS 6 | 7 | ## Dependencies installations 8 | 9 | ``` 10 | yarn 11 | ``` 12 | 13 | ## Running the app (development build) 14 | 15 | ``` 16 | yarn dev 17 | ``` 18 | 19 | -------------------------------------------------------------------------------- /src/server/_utils/http-utils.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export default function fetchComponentsData({ 4 | dispatch, 5 | components, 6 | params, 7 | query, 8 | }) { 9 | const promises = components.map(current => { 10 | const component = current.WrappedComponent ? current.WrappedComponent : current; 11 | 12 | return component.fetchData ? component.fetchData({ 13 | dispatch, 14 | params, 15 | query, 16 | }) : null; 17 | }); 18 | 19 | return axios.all(promises); 20 | } 21 | -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { hydrate } from 'react-dom'; 3 | import { BrowserRouter as Router } from 'react-router-dom'; 4 | import { Provider } from 'react-redux'; 5 | import routes from '../common/routes'; 6 | import configureStore from '../common/store/configureStore'; 7 | 8 | const preloadedState = window.__PRELOADED_STATE__; 9 | const store = configureStore(preloadedState); 10 | 11 | hydrate( 12 | 13 | 14 | {routes} 15 | 16 | , 17 | document.getElementById('app'), 18 | ); 19 | 20 | -------------------------------------------------------------------------------- /src/common/lib/shouldFetchAction.js: -------------------------------------------------------------------------------- 1 | export default function shouldFetchAction(state, origin) { 2 | if (origin) { 3 | const originHasS = (origin.filter(i => i === 's').length === 1); 4 | const originHasC = (origin.filter(i => i === 'c').length === 1); 5 | 6 | const originComplete = (originHasS && originHasC); 7 | const canFetch = (!origin.length || originComplete || state.loading); 8 | return canFetch; 9 | } else if ((Object.keys(state).length !== 0 && 10 | state.constructor === Object) || state.length) { 11 | return false; 12 | } 13 | return true; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-0"], 3 | 4 | "plugins": [ 5 | "transform-runtime", 6 | "add-module-exports", 7 | "transform-decorators-legacy", 8 | "transform-react-display-name" 9 | ], 10 | 11 | "env": { 12 | "development": { 13 | "plugins": [ 14 | "typecheck", 15 | ["react-transform", { 16 | "transforms": [{ 17 | "transform": "react-transform-catch-errors", 18 | "imports": ["react", "redbox-react"] 19 | } 20 | ] 21 | }] 22 | ] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/common/containers/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | import connectApp from './../lib/connectApp'; 6 | import App from './../components/App'; 7 | 8 | import exampleAction from './../actions/example'; 9 | 10 | const AppContainer = props => (); 11 | 12 | AppContainer.propTypes = { 13 | example: PropTypes.object, 14 | }; 15 | 16 | function mapStateToProps(state) { 17 | return { 18 | example: state.exampleReducer, 19 | }; 20 | } 21 | 22 | export default connect(mapStateToProps)(connectApp(AppContainer, [exampleAction])); 23 | 24 | -------------------------------------------------------------------------------- /src/common/lib/slugify/README.md: -------------------------------------------------------------------------------- 1 | # Slugify 2 | 3 | `slugify` takes a string as argument and return it slugged for many uses. First written for parse country names from API resonses and apply a proper CSS Class for showing the country flag in product areas. 4 | 5 | ## Usage 6 | 7 | Import it in your code and: 8 | 9 | ```javascript 10 | slugify('África do Sul'); // africa-do-sul 11 | 12 | // second argument is optional. See Doc section 13 | slugify('África do Sul', '_'); // africa_do_sul 14 | ``` 15 | 16 | ## Doc 17 | 18 | ### `slugify(String, separator)` 19 | 20 | *String:* a string argument to be slugified. Required argument; 21 | 22 | *separator:* the separator for blank spaces. Optional argument. 23 | 24 | -------------------------------------------------------------------------------- /src/common/lib/switchcase/index.test.js: -------------------------------------------------------------------------------- 1 | import switchcase from './index'; 2 | 3 | describe('switchcase utility', () => { 4 | it('pass single case and returns proper value', () => { 5 | const s = switchcase( 6 | { TRIGGER_EXAMPLE: { example: true } } 7 | )({})('TRIGGER_EXAMPLE'); 8 | expect(JSON.stringify(s)).toBe(JSON.stringify({ example: true })); 9 | }); 10 | 11 | it('pass multiple cases and returns proper value', () => { 12 | const s = switchcase( 13 | { 14 | TRIGGER_EXAMPLE: { example: true }, 15 | TRIGGER_EXAMPLE_FALSE: { example: false } 16 | } 17 | )({})('TRIGGER_EXAMPLE'); 18 | expect(JSON.stringify(s)).toBe(JSON.stringify({ example: true })); 19 | }); 20 | }); 21 | 22 | -------------------------------------------------------------------------------- /src/common/lib/slugify/__tests__/index-test.js: -------------------------------------------------------------------------------- 1 | import slugify from './../index'; 2 | 3 | describe('slugify lib', () => { 4 | it('is returning slugged string', () => { 5 | expect(slugify('Bélgica')).toEqual('belgica'); 6 | expect(slugify('Eslovênia')).toEqual('eslovenia'); 7 | expect(slugify('Estados Unidos')).toEqual('estados-unidos'); 8 | expect(slugify('Vários Países')).toEqual('varios-paises'); 9 | expect(slugify('Escócia')).toEqual('escocia'); 10 | expect(slugify('República Tcheca')).toEqual('republica-tcheca'); 11 | }); 12 | 13 | it('is returning custom separator', () => { 14 | expect(slugify('República Tcheca', '_')).toEqual('republica_tcheca'); 15 | }) 16 | 17 | it('is returning empty string in case of empty string be passed', () => { 18 | expect(slugify('')).toEqual(''); 19 | }); 20 | }); 21 | 22 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | }, 7 | "extends": "airbnb", 8 | "rules": { 9 | "no-underscore-dangle": "off", 10 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 11 | "react/require-default-props": [0], 12 | "react/forbid-prop-types": [0], 13 | "function-paren-newline": ["error", "consistent"], 14 | "jsx-a11y/anchor-is-valid": [ 15 | "error", 16 | { 17 | "components": [ 18 | "a" 19 | ], 20 | "specialLink": [ 21 | "hrefLeft", 22 | "hrefRight" 23 | ], 24 | "aspects": [ 25 | "noHref", 26 | "invalidHref", 27 | "preferButton" 28 | ] 29 | } 30 | ] 31 | }, 32 | "globals": { 33 | "__DEVELOPMENT__": true, 34 | "__CLIENT__": true, 35 | "__SERVER__": true, 36 | "__DISABLE_SSR__": true, 37 | "__DEVTOOLS__": true, 38 | "webpackIsomorphicTools": true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /webpack/webpack-dev-server.js: -------------------------------------------------------------------------------- 1 | const Express = require('express'); 2 | const webpack = require('webpack'); 3 | 4 | const config = {}; 5 | const webpackConfig = require('./dev.config'); 6 | const compiler = webpack(webpackConfig); 7 | 8 | const host = config.host || 'localhost'; 9 | const port = (Number(config.port) + 1) || 3001; 10 | 11 | const serverOptions = { 12 | contentBase: 'http://' + host + ':' + port, 13 | quiet: true, 14 | noInfo: true, 15 | hot: true, 16 | inline: true, 17 | lazy: false, 18 | publicPath: webpackConfig.output.publicPath, 19 | headers: { 'Access-Control-Allow-Origin': '*' }, 20 | stats: { colors: true } 21 | }; 22 | 23 | const app = new Express(); 24 | 25 | app.use(require('webpack-dev-middleware')(compiler, serverOptions)); 26 | app.use(require('webpack-hot-middleware')(compiler)); 27 | 28 | app.listen(port, function onAppListening(err) { 29 | if (err) { 30 | console.error(err); 31 | } else { 32 | /* eslint-disable */ 33 | console.log(`PORT ${port} ready`) 34 | /* eslint-enable */ 35 | } 36 | }); 37 | 38 | -------------------------------------------------------------------------------- /src/common/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunkMiddleware from 'redux-thunk'; 3 | import Cookies from 'cookies-js'; 4 | import { getCookiesMiddleware } from 'redux-cookies'; 5 | 6 | import rootReducer from '../reducers'; 7 | 8 | export default function configureStore(preloadedState) { 9 | const store = createStore( 10 | rootReducer, 11 | preloadedState, 12 | compose( 13 | applyMiddleware(thunkMiddleware), 14 | applyMiddleware(getCookiesMiddleware(Cookies)), 15 | // for redux devTools chrome extension 16 | (typeof window === 'object' && typeof window.devToolsExtension !== 'undefined') ? 17 | window.devToolsExtension() : f => f, 18 | ), 19 | ); 20 | 21 | if (module.hot) { 22 | // Enable Webpack hot module replacement for reducers 23 | module.hot.accept('../reducers/index', () => { 24 | const nextReducer = require('../reducers/index').default; // eslint-disable-line 25 | store.replaceReducer(nextReducer); 26 | }); 27 | } 28 | 29 | return store; 30 | } 31 | -------------------------------------------------------------------------------- /src/common/lib/slugify/index.js: -------------------------------------------------------------------------------- 1 | export default function slugify(str, separator = '-') { 2 | // lower case original string to a new one 3 | let s = str.toLowerCase(); 4 | // replace special characters based in a 5 | s = s.replace(/[\u00C0-\u00C5]/ig, 'a'); 6 | // replace special characters based in e 7 | s = s.replace(/[\u00C8-\u00CB]/ig, 'e'); 8 | // replace special characters based in i 9 | s = s.replace(/[\u00CC-\u00CF]/ig, 'i'); 10 | // replace special characters based in o 11 | s = s.replace(/[\u00D2-\u00D6]/ig, 'o'); 12 | // replace special characters based in u 13 | s = s.replace(/[\u00D9-\u00DC]/ig, 'u'); 14 | // replace special characters based in n 15 | s = s.replace(/[\u00D1]/ig, 'n'); 16 | // replace ç for c 17 | s = s.replace('ç', 'c'); 18 | // remove non parsed symbols 19 | s = s.replace(/[^a-z0-9 ]+/gi, ''); 20 | // replace blank spaces for separator argument 21 | s = s.trim().replace(/ /g, separator); 22 | // regex for cleaning unnecessary separators from original string 23 | const regex = new RegExp('/[^a-z\\' + separator + '/ ]*/ig'); // eslint-disable-line 24 | return (s.replace(regex, '')); 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/common/lib/switchcase/switchcase.md: -------------------------------------------------------------------------------- 1 | # switchcase 2 | 3 | `switchcase` is an internal Redux utility built for avoiding use the the `switch` statement, because "it's not immutable, it can't be composed with other functions, and it's a little side effecty". Concepts for this use is located in https://hackernoon.com/rethinking-javascript-eliminate-the-switch-statement-for-better-code-5c81c044716d. 4 | 5 | ## `switchcase(cases)(defaultCase)(key)` 6 | 7 | `switchcase` is a curried function which executes its curried callbacks in three levels, and its arguments are: 8 | 9 | *`cases`*: an object for receiving the possible cases to be matched following this model: 10 | 11 | ``` 12 | { 13 | TRIGGER_EXAMPLE: action.example, 14 | USER_STATUS: action.status, 15 | } 16 | ``` 17 | 18 | *`defaultCase`*: the defaultCase is the state itself. From a reducer, its execution is: 19 | 20 | ``` 21 | export default function exampleReducer(state = {}, action) { 22 | return switchcase({ 23 | TRIGGER_EXAMPLE: action.example 24 | })(state)(action.type); 25 | } 26 | ``` 27 | 28 | *`key`*: used for receiving the action type in the reducer as showed in `defaultCase` argument example. It is used to match the case and return the value from matched object key in `cases`. 29 | 30 | -------------------------------------------------------------------------------- /src/common/api/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { application, api } from '../config.js'; 3 | 4 | export default function api(url, token, obj, method, paramsData) { 5 | const AUTH_TOKEN = 6 | (token === 'null' || token === null || token === undefined) ? '' : token; 7 | const baseURL = 8 | (typeof location !== 'undefined') ? 9 | '/api' : 10 | `http://localhost:${application.port}/api`; 11 | 12 | const rooturl = typeof window !== 'undefined' && window.location 13 | ? window.location.pathname 14 | : global.__CLIENT_URL__; 15 | 16 | return axios({ 17 | baseURL, 18 | url: (url || ''), 19 | method: (method || 'get'), 20 | data: obj, 21 | timeout: api.timeout, 22 | headers: { 23 | Authorization: AUTH_TOKEN, 24 | rooturl, 25 | }, 26 | params: paramsData, 27 | 28 | transformResponse: [(resp) => { 29 | const r = (json) => { 30 | try { 31 | return JSON.parse(json); 32 | } catch (e) { 33 | return {}; 34 | } 35 | }; 36 | 37 | if (!r(resp).data) { 38 | return r(resp); 39 | } 40 | if (typeof window === 'undefined') { 41 | r(resp).data.requestedOrigin = ['s']; 42 | } else { 43 | r(resp).data.requestedOrigin = ['s', 'c']; 44 | } 45 | return r(resp); 46 | }], 47 | }); 48 | } 49 | 50 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import path from 'path'; 3 | import dotenv from 'dotenv'; 4 | import WebpackIsomorphicTools from 'webpack-isomorphic-tools'; 5 | 6 | import WebpackIsomorphicToolsConfig from './../../webpack/webpack-isomorphic-tools'; 7 | 8 | import routes from './routes'; 9 | import log from './logs'; 10 | 11 | dotenv.config(); 12 | 13 | /* GLOBALS */ 14 | // TODO: move all this global values to config file using dotenv to access it 15 | global.__CLIENT__ = false; 16 | global.__SERVER__ = true; 17 | global.__DISABLE_SSR__ = false; // <----- DISABLES SERVER SIDE RENDERING FOR ERROR DEBUGGING 18 | // __DEVELOPMENT__ is used for enabling development tools in server side 19 | // TODO: move it to config file 20 | global.__DEVELOPMENT__ = process.env.NODE_ENV === 'development'; 21 | 22 | const rootDir = path.resolve(__dirname, '../../'); 23 | const app = express(); 24 | app.use(express.static('public')); 25 | 26 | app.use('/fonts', express.static('public/fonts')); 27 | 28 | const PORT = process.env.APPLICATION_PORT; 29 | 30 | //const server = app.listen(PORT, () => { 31 | // log.info(`L I S T E N I N G A T: ${PORT}`); 32 | //}); 33 | 34 | global.webpackIsomorphicTools = new WebpackIsomorphicTools(WebpackIsomorphicToolsConfig) 35 | .server(rootDir, () => { 36 | app.use(routes); 37 | app.listen(PORT, () => log.info(`L I S T E N I G A T: ${PORT}`)) 38 | }); 39 | 40 | -------------------------------------------------------------------------------- /src/server/routes/template/index.js: -------------------------------------------------------------------------------- 1 | function javascripts(assets = {}) { 2 | const js = assets.javascript || {}; 3 | const mainJS = js.main || ''; 4 | const path = 5 | (mainJS.length) ? `${mainJS.replace('./dist', '/dist')}` : '/dist/main.js'; 6 | 7 | return ``; 8 | } 9 | 10 | const stylesheets = ''; 11 | 12 | function renderHTML(html, preloadedState, assets, head) { 13 | return ` 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ${(head.title) ? head.title.toString() : ''} 22 | 25 | ${(head.meta) ? head.meta.toString().replace(/\/>/g, '/>\n ') : ''} 26 | ${(head.script) ? head.script.toString() : ''} 27 | ${(head.link) ? head.link.toString() : ''} 28 | 29 | 30 | 31 |
${html}
32 | 35 | ${javascripts(assets)} 36 | 37 | 38 | `; 39 | } 40 | 41 | export default renderHTML; 42 | 43 | -------------------------------------------------------------------------------- /webpack/webpack-isomorphic-tools.js: -------------------------------------------------------------------------------- 1 | const WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin'); 2 | 3 | module.exports = { 4 | assets: { 5 | style_modules: { 6 | extensions: ['styl'], 7 | filter: function(module, regex, options, log) { 8 | if (options.development) { 9 | // in development mode there's webpack "style-loader", 10 | // so the module.name is not equal to module.name 11 | return WebpackIsomorphicToolsPlugin.style_loader_filter(module, regex, options, log); 12 | } else { 13 | // in production mode there's no webpack "style-loader", 14 | // so the module.name will be equal to the asset path 15 | return regex.test(module.name); 16 | } 17 | }, 18 | path: function(module, options, log) { 19 | if (options.development) { 20 | // in development mode there's webpack "style-loader", 21 | // so the module.name is not equal to module.name 22 | return WebpackIsomorphicToolsPlugin.style_loader_path_extractor(module, options, log); 23 | } else { 24 | // in production mode there's no webpack "style-loader", 25 | // so the module.name will be equal to the asset path 26 | return module.name; 27 | } 28 | }, 29 | parser: function(module, options, log) { 30 | if (options.development) { 31 | return WebpackIsomorphicToolsPlugin.css_modules_loader_parser(module, options, log); 32 | } else { 33 | // in production mode there's Extract Text Loader which extracts CSS text away 34 | return module.source; 35 | } 36 | } 37 | } 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/common/lib/connectApp.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import axios from 'axios'; 4 | import Helmet from 'react-helmet'; 5 | 6 | let IS_FIRST_MOUNT_AFTER_LOAD = true; 7 | 8 | export default function connectApp(Container, actionCreators = []) { 9 | class DataFetchersWrapper extends Component { 10 | static fetchData({ 11 | dispatch, 12 | params = {}, 13 | query = {}, 14 | }) { 15 | return axios.all( 16 | actionCreators.map(actionCreator => dispatch(actionCreator({ params, query }))), 17 | ); 18 | } 19 | 20 | componentDidMount() { 21 | if (!IS_FIRST_MOUNT_AFTER_LOAD) { 22 | this._fetchDataOnClient(); 23 | } 24 | 25 | IS_FIRST_MOUNT_AFTER_LOAD = false; 26 | } 27 | 28 | componentDidUpdate(prevProps) { 29 | const { location } = this.props; 30 | const { location: prevLocation } = prevProps; 31 | 32 | const isUrlChanged = (location.pathname !== prevLocation.pathname) 33 | || (location.search !== prevLocation.search); 34 | 35 | if (isUrlChanged) { 36 | this._fetchDataOnClient(); 37 | } 38 | } 39 | 40 | _fetchDataOnClient() { 41 | DataFetchersWrapper.fetchData({ 42 | dispatch: this.props.dispatch, 43 | params: this.props.params, 44 | query: this.props.location.query, 45 | }); 46 | } 47 | 48 | render() { 49 | return ( 50 |
51 | 52 | 53 | 54 | 55 | Titulo 56 | 57 | 58 |
59 | ); 60 | } 61 | } 62 | 63 | DataFetchersWrapper.propTypes = { 64 | dispatch: PropTypes.func.isRequired, 65 | params: PropTypes.object, 66 | location: PropTypes.object, 67 | }; 68 | 69 | return DataFetchersWrapper; 70 | } 71 | 72 | -------------------------------------------------------------------------------- /src/common/lib/connectApp.md: -------------------------------------------------------------------------------- 1 | # Connect Data Fetchers docs 2 | 3 | It's a High Order Component (HOC) for our React containers responsible for work isomorphically with our components. 4 | 5 | ## Arguments 6 | 7 | It receives 4 arguments, which are Container, actionCreators and authentication. 8 | 9 | ### Container 10 | 11 | We separate components as Containers and Components. Containers are the main components called by the router, they receive `props` and distributed as it is necessary across the children components. 12 | 13 | Path to all Containers is `./common/containers`. And the children components are located at `./common/components`. 14 | 15 | ### actionCreators 16 | 17 | It's an `Array` and receive all the actions that must be fetched to its Container. 18 | 19 | It'll be fetched and passed as `...this.props` to the `Container`. 20 | 21 | ### authentication 22 | 23 | It's a `Object` which receives from `./common/lib/authUserRedirect` two values: 24 | 25 | #### auth 26 | 27 | Receives `true` for say that it is necessary authenticate the user for having access to its `Container`. 28 | 29 | #### redirect 30 | 31 | A string with the path to redirect the user in case of his/her status be equal `'not-loggedin'`. 32 | 33 | It uses the Router Context to push user to somewhere in the application. 34 | 35 | ### redirect 36 | 37 | Argument used for Checkout Container manage to redirect user to Cart if this user has no item in his cart. 38 | 39 | ## Usage in Containers 40 | 41 | ```javascript 42 | // mapStateToProps is a function to be passed as argument to 43 | // React-Redux Connect function that works telling which 44 | // states are going to be props in the current Container 45 | function mapStateToProps(state) { 46 | return { 47 | userAuthentication: state.userAuthentication, 48 | }; 49 | } 50 | 51 | // as connect callback we pass our HOC with its arguments and 52 | // using authUserRedirect to specify the authentication 53 | // need and its path to redirect the user 54 | export default connect(mapStateToProps)( 55 | connectDataFetchers(Example, [authenticateUser], authUserRedirect(true, '/')) 56 | ); 57 | ``` 58 | 59 | -------------------------------------------------------------------------------- /src/server/routes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Cookies from 'cookies'; 3 | import Helmet from 'react-helmet'; 4 | import uaParser from 'ua-parser-js'; 5 | import { Provider } from 'react-redux'; 6 | import thunkMiddleware from 'redux-thunk'; 7 | import { renderToString } from 'react-dom/server'; 8 | import { StaticRouter } from 'react-router'; 9 | import { getCookiesMiddleware } from 'redux-cookies'; 10 | import { createStore, applyMiddleware, compose } from 'redux'; 11 | 12 | import log from './../logs'; 13 | import renderHTML from './template'; 14 | import routes from './../../common/routes'; 15 | import fetchComponentsData from './../_utils/http-utils'; 16 | import rootReducer from './../../common/reducers'; 17 | 18 | export default function serverRouting(req, res) { 19 | if (__DEVELOPMENT__) { 20 | // Do not cache webpack stats: the script file would change since 21 | // hot module replacement is enabled in the development env 22 | webpackIsomorphicTools.refresh(); 23 | console.log(webpackIsomorphicTools.refresh); 24 | } 25 | 26 | const context = {}; 27 | const cookies = new Cookies(req, res); 28 | const store = createStore( 29 | rootReducer, 30 | applyMiddleware(getCookiesMiddleware(cookies)), 31 | compose( 32 | applyMiddleware(thunkMiddleware), 33 | ) 34 | ); 35 | 36 | const routeList = routes.props.children.filter(_ => _.props.path === req.url); 37 | const currentRoute = routeList.length ? routeList[0].props : {}; 38 | const currentComponent = currentRoute.component || {}; 39 | 40 | fetchComponentsData({ 41 | dispatch: store.dispatch, 42 | components: [currentComponent], 43 | params: req.params, 44 | query: req.query, 45 | }).then(() => { 46 | const componentHTML = renderToString( 47 | 48 | 52 | {routes} 53 | 54 | 55 | ); 56 | 57 | const head = Helmet.renderStatic(); 58 | const html = renderHTML( 59 | componentHTML, 60 | store.getState(), 61 | webpackIsomorphicTools.assets(), 62 | head, 63 | ); 64 | 65 | return { html }; 66 | }).then(({ html }) => { 67 | if (req.url === '/404') { 68 | res.status(404).send(html); 69 | return; 70 | } 71 | 72 | res.end(html); 73 | return; 74 | }).catch(err => { 75 | log.error(JSON.stringify({ 76 | message: `Request in ${req.location} failed.`, 77 | response: err, 78 | })); 79 | }); 80 | } 81 | 82 | -------------------------------------------------------------------------------- /webpack/prod.config.js: -------------------------------------------------------------------------------- 1 | // Webpack config for creating the production bundle. 2 | require('dotenv').config(); 3 | 4 | var path = require('path'); 5 | var webpack = require('webpack'); 6 | var CleanPlugin = require('clean-webpack-plugin'); 7 | var strip = require('strip-loader'); 8 | var CompressionPlugin = require('compression-webpack-plugin'); 9 | 10 | var projectRootPath = path.resolve(__dirname, '../'); 11 | var assetsPath = path.resolve(projectRootPath, './public/dist'); 12 | var distPath = path.resolve(projectRootPath, './dist/'); 13 | 14 | var WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin'); 15 | var webpackIsomorphicToolsPlugin = new WebpackIsomorphicToolsPlugin(require('./webpack-isomorphic-tools')); 16 | 17 | module.exports = { 18 | devtool: 'source-map', 19 | context: path.resolve(__dirname, '..'), 20 | entry: { 21 | 'main': [ 22 | './client/index.js', 23 | ] 24 | }, 25 | output: { 26 | path: assetsPath, 27 | filename: '[name].js', 28 | chunkFilename: '[name].js', 29 | publicPath: './dist/' 30 | }, 31 | module: { 32 | rules: [ 33 | { 34 | test: /\.jsx?$/, 35 | exclude: /node_modules/, 36 | use: [ 37 | { loader: strip.loader('debug') }, 38 | { loader: 'babel-loader' }, 39 | ], 40 | }, 41 | { test: /\.json$/, loader: 'json-loader' }, 42 | ] 43 | }, 44 | resolve: { 45 | modules: ['common', 'node_modules'], 46 | extensions: ['.js', '.jsx'] 47 | }, 48 | plugins: [ 49 | new CleanPlugin([assetsPath, distPath], { root: projectRootPath }), 50 | 51 | new webpack.DefinePlugin({ 52 | 'process.env': { 53 | NODE_ENV: JSON.stringify(process.env.ENVIRONMENT), 54 | REDIRECTS_BASE_URL: JSON.stringify(process.env.REDIRECTS_BASE_URL), 55 | ZED_AUTH_SECRET: JSON.stringify(process.env.ZED_AUTH_SECRET), 56 | FACEBOOK_API_KEY: JSON.stringify(process.env.FACEBOOK_API_KEY), 57 | HOST_NAME: JSON.stringify(process.env.HOST_NAME), 58 | }, 59 | __CLIENT__: true, 60 | __SERVER__: false, 61 | __DEVELOPMENT__: false, 62 | __DEVTOOLS__: false 63 | }), 64 | 65 | // ignore dev config 66 | new webpack.IgnorePlugin(/\.\/dev/, /\/config$/), 67 | 68 | new webpack.optimize.UglifyJsPlugin({ 69 | sourceMap: true, 70 | compress: { 71 | warnings: true 72 | } 73 | }), 74 | new CompressionPlugin({ 75 | asset: '[path].gz[query]', 76 | algorithm: 'gzip', 77 | test: /\.js$/, 78 | threshold: 500, 79 | }), 80 | webpackIsomorphicToolsPlugin 81 | ] 82 | }; 83 | 84 | -------------------------------------------------------------------------------- /webpack/dev.config.js: -------------------------------------------------------------------------------- 1 | // Webpack config for development 2 | require('dotenv').config(); 3 | 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const webpack = require('webpack'); 7 | const assetsPath = path.resolve(__dirname, '../public/dist'); 8 | const host = (process.env.HOST || 'localhost'); 9 | const port = (+process.env.PORT + 1) || 3001; 10 | 11 | // https://github.com/halt-hammerzeit/webpack-isomorphic-tools 12 | const WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin'); 13 | const webpackIsomorphicToolsPlugin = new WebpackIsomorphicToolsPlugin(require('./webpack-isomorphic-tools')); 14 | 15 | const babelrc = fs.readFileSync('./.babelrc'); 16 | let babelrcObject = {}; 17 | 18 | try { 19 | babelrcObject = JSON.parse(babelrc); 20 | } catch (err) { 21 | console.error('==> ERROR: Error parsing your .babelrc.'); 22 | console.error(err); 23 | } 24 | 25 | const babelrcObjectDevelopment = babelrcObject.env && babelrcObject.env.development || {}; 26 | 27 | // merge global and dev-only plugins 28 | let combinedPlugins = babelrcObject.plugins || []; 29 | combinedPlugins = combinedPlugins.concat(babelrcObjectDevelopment.plugins); 30 | 31 | const babelLoaderQuery = Object.assign({}, babelrcObjectDevelopment, babelrcObject, {plugins: combinedPlugins}); 32 | delete babelLoaderQuery.env; 33 | 34 | // make sure react-transform is enabled 35 | babelLoaderQuery.plugins = babelLoaderQuery.plugins || []; 36 | 37 | module.exports = { 38 | devtool: 'inline-source-map', 39 | context: path.resolve(__dirname, '..'), 40 | entry: { 41 | 'main': [ 42 | 'webpack-hot-middleware/client?path=http://' + host + ':' + port + '/__webpack_hmr', 43 | 'webpack/hot/only-dev-server', 44 | './src/client/index.js', 45 | ] 46 | }, 47 | devServer: { 48 | hot: true 49 | }, 50 | output: { 51 | path: assetsPath, 52 | filename: '[name]-[hash].js', 53 | chunkFilename: '[name]-[chunkhash].js', 54 | publicPath: 'http://' + host + ':' + port + '/' 55 | }, 56 | module: { 57 | rules: [ 58 | { 59 | test: /\.jsx?$/, 60 | exclude: [/node_modules/, /public/], 61 | use: [ 62 | { loader: 'babel-loader?' + JSON.stringify(babelLoaderQuery) }, 63 | { loader: 'eslint-loader' }, 64 | ], 65 | }, 66 | { test: /\.json$/, loader: 'json-loader' }, 67 | ] 68 | }, 69 | resolve: { 70 | modules: ['common', 'node_modules'], 71 | extensions: ['.js', '.jsx', '.json'], 72 | }, 73 | plugins: [ 74 | // hot reload 75 | new webpack.HotModuleReplacementPlugin(), 76 | new webpack.IgnorePlugin(/webpack-stats\.json$/), 77 | new webpack.DefinePlugin({ 78 | 'process.env': { 79 | NODE_ENV: '"development"', 80 | HOST_NAME: JSON.stringify(process.env.HOST_NAME), 81 | }, 82 | __CLIENT__: true, 83 | __SERVER__: false, 84 | __DEVELOPMENT__: true, 85 | __DEVTOOLS__: true // <-------- DISABLE redux-devtools HERE 86 | }), 87 | webpackIsomorphicToolsPlugin.development() 88 | ] 89 | }; 90 | 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "@std/esm": { 3 | "esm": "js" 4 | }, 5 | "name": "", 6 | "version": "0.0.1", 7 | "description": "", 8 | "main": "./src/server/index.js", 9 | "scripts": { 10 | "start": "npm run production", 11 | "production": "better-npm-run build-js && better-npm-run build-app && better-npm-run start-prod", 12 | "dev": "concurrently --kill-others \"better-npm-run watch-client\" \"better-npm-run start-dev\"", 13 | "dev-server": "concurrently --kill-others \"better-npm-run start-dev\"", 14 | "dev-assets": "concurrently --kill-others \"better-npm-run watch-client\"", 15 | "lint": "eslint common server client --ignore-pattern __tests__ --ignore-pattern '/tests/'" 16 | }, 17 | "jest": { 18 | "testResultsProcessor": "./node_modules/jest-junit", 19 | "unmockedModulePathPatterns": [ 20 | "react", 21 | "react-dom", 22 | "react-addons-test-utils", 23 | "enzyme", 24 | "lodash" 25 | ] 26 | }, 27 | "betterScripts": { 28 | "start-dev": { 29 | "command": "babel-node --inspect ./src/server/index", 30 | "env": { 31 | "NODE_PATH": "./src", 32 | "NODE_ENV": "development", 33 | "PORT": 3000 34 | } 35 | }, 36 | "watch-client": { 37 | "command": "babel-node ./webpack/webpack-dev-server.js", 38 | "env": { 39 | "UV_THREADPOOL_SIZE": 100, 40 | "PORT": 3000 41 | } 42 | }, 43 | "start-prod": { 44 | "command": "node ./dist/server/index", 45 | "env": { 46 | "NODE_PATH": "./src", 47 | "PORT": 8080 48 | } 49 | }, 50 | "build-js": { 51 | "command": "webpack --verbose --colors --display-error-details --config webpack/prod.config.js" 52 | }, 53 | "build-app": { 54 | "command": "babel ./server -d ./dist/server && babel ./client -d ./dist/client && babel ./webpack -d ./dist/webpack && babel ./common -d ./dist/common && cp webpack-assets.json ./dist/webpack-assets.json" 55 | } 56 | }, 57 | "repository": { 58 | "type": "git", 59 | "url": "" 60 | }, 61 | "keywords": [], 62 | "author": "", 63 | "license": "MIT", 64 | "homepage": "", 65 | "dependencies": { 66 | "@std/esm": "0.14.0", 67 | "axios": "0.17.1", 68 | "babel-cli": "6.26.0", 69 | "babel-core": "6.26.0", 70 | "babel-eslint": "8.0.2", 71 | "babel-jest": "21.2.0", 72 | "babel-loader": "7.1.2", 73 | "babel-plugin-add-module-exports": "0.2.1", 74 | "babel-plugin-react-transform": "3.0.0", 75 | "babel-plugin-transform-decorators-legacy": "1.3.4", 76 | "babel-plugin-transform-react-display-name": "6.8.0", 77 | "babel-plugin-transform-runtime": "6.15.0", 78 | "babel-plugin-typecheck": "3.9.0", 79 | "babel-preset-es2015": "6.13.2", 80 | "babel-preset-react": "6.11.1", 81 | "babel-preset-stage-0": "6.5.0", 82 | "babel-runtime": "6.26.0", 83 | "better-npm-run": "0.1.0", 84 | "clean-webpack-plugin": "0.1.10", 85 | "clipboard": "1.7.1", 86 | "cnsr": "0.2.3", 87 | "compression-webpack-plugin": "1.0.1", 88 | "concat": "1.0.3", 89 | "concurrently": "3.5.0", 90 | "cookies": "0.7.0", 91 | "cookies-js": "1.2.3", 92 | "css-loader": "0.28.7", 93 | "css-mqpacker": "6.0.1", 94 | "dotenv": "4.0.0", 95 | "enzyme": "3.1.1", 96 | "express": "4.15.4", 97 | "file-loader": "1.1.5", 98 | "install": "0.10.1", 99 | "jest": "21.2.1", 100 | "jest-junit": "3.1.0", 101 | "json-loader": "0.5.7", 102 | "json-stringify-safe": "5.0.1", 103 | "pretty-error": "2.0.0", 104 | "prop-types": "15.5.10", 105 | "react": "16.1.0", 106 | "react-dom": "16.1.0", 107 | "react-helmet": "5.2.0", 108 | "react-redux": "5.0.6", 109 | "react-router": "4.2.0", 110 | "react-router-dom": "4.2.2", 111 | "redux": "3.7.2", 112 | "redux-cookies": "1.0.1", 113 | "redux-thunk": "2.2.0", 114 | "strip-loader": "0.1.2", 115 | "sudo": "1.0.3", 116 | "ua-parser-js": "0.7.14", 117 | "webpack": "3.8.1", 118 | "webpack-isomorphic-tools": "3.0.5", 119 | "winston": "2.3.1" 120 | }, 121 | "devDependencies": { 122 | "eslint": "4.11.0", 123 | "eslint-config-airbnb": "16.1.0", 124 | "eslint-loader": "1.9.0", 125 | "eslint-plugin-import": "2.8.0", 126 | "eslint-plugin-jsx-a11y": "6.0.2", 127 | "eslint-plugin-react": "7.4.0", 128 | "eslint-watch": "3.1.3", 129 | "react-addons-test-utils": "15.4.2", 130 | "react-hot-loader": "3.1.2", 131 | "react-test-renderer": "16.1.0", 132 | "react-transform-catch-errors": "1.0.2", 133 | "react-transform-hmr": "1.0.4", 134 | "redbox-react": "1.5.0", 135 | "webpack-dev-middleware": "2.0.4", 136 | "webpack-hot-middleware": "2.21.0" 137 | } 138 | } 139 | --------------------------------------------------------------------------------