├── .babelrc
├── .gitignore
├── gulp
├── index.js
└── tasks
│ ├── babel.js
│ ├── images.js
│ ├── nodemon.js
│ ├── scripts.js
│ ├── styles.js
│ └── symlink.js
├── gulpfile.js
├── package.json
└── src
├── app
├── bundles.js
├── components
│ └── App.jsx
├── constants.js
├── errors
│ ├── AuthorizationError.js
│ └── index.js
├── history.js
├── reducers
│ ├── index.js
│ └── session.js
├── store.js
└── utils
│ ├── callApi.js
│ ├── createApiCaller.js
│ ├── createApiMiddleware.js
│ ├── createCustomStore.js
│ └── renderFullPage.js
├── client
├── index.jsx
├── routes.js
└── styles
│ └── index.scss
└── server
├── index.jsx
└── routes.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [ "es2015", "react", "stage-1" ]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
3 |
--------------------------------------------------------------------------------
/gulp/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const globby = require('globby');
4 | const gulp = require('gulp');
5 | const gutil = require('gulp-util');
6 |
7 | globby.sync(__dirname + '/tasks/*.js').forEach(task => require(task));
8 |
9 | const defaultTasks = [ 'babel', 'images', 'scripts', 'styles', 'symlink' ];
10 | if (gutil.env.dev) {
11 | defaultTasks.push('nodemon');
12 | }
13 |
14 | gulp.task('default', defaultTasks);
15 |
--------------------------------------------------------------------------------
/gulp/tasks/babel.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const gulp = require('gulp');
4 | const gutil = require('gulp-util');
5 | const c = gutil.colors;
6 |
7 | gulp.task('babel', () => {
8 | const babel = require('gulp-babel');
9 | const del = require('del');
10 | const src = [
11 | 'src/**/*.{js,jsx}',
12 | '!src/client/*.{js,jsx}'
13 | ];
14 |
15 | const run = (e, path) => {
16 | let runSrc = src;
17 |
18 | // If this function was triggered by a watch we only need
19 | // to transform the changed files. If a file was deleted we
20 | // need to delete the corresponding transformed file
21 | if (e) {
22 | runSrc = e.path.replace(process.cwd() + '/', '');
23 |
24 | if (e.type === 'deleted') {
25 | return del(runSrc.replace(/^src/, 'dist'));
26 | }
27 |
28 | gutil.log(`${c.cyan('babel')}: ${c.yellow(runSrc)} ${e.type}, converting`);
29 | } else {
30 | gutil.log(`${c.cyan('babel')}: converting`);
31 | }
32 |
33 | return gulp.src(runSrc, { base: 'src' })
34 | .pipe(babel())
35 | .pipe(gulp.dest('dist'));
36 | }
37 |
38 | if (gutil.env.dev) {
39 | gutil.log(`${c.cyan('babel')}: watching`);
40 | gulp.watch(src, run);
41 | }
42 |
43 | return run();
44 | });
45 |
--------------------------------------------------------------------------------
/gulp/tasks/images.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const gulp = require('gulp');
4 | const gutil = require('gulp-util');
5 | const c = gutil.colors;
6 |
7 | /**
8 | * Pull images through imagemin
9 | */
10 | gulp.task('images', function() {
11 | const src = [
12 | 'src/client/images/*.{gif,jpg,png,svg}',
13 | 'src/client/images/**/*.{gif,jpg,png,svg}',
14 | 'src/modules/*/images/*.{gif,jpg,png,svg}',
15 | 'src/modules/*/images/**/*.{gif,jpg,png,svg}'
16 | ];
17 |
18 | if (gutil.env.dev) {
19 | gutil.log(`${c.cyan('images')}: watching`);
20 | gulp.watch(src, e => run(src, e));
21 | }
22 |
23 | return run(src);
24 | });
25 |
26 | function run(src, e) {
27 | if (e) {
28 | src = e.path.replace(`${process.cwd()}/`, '');
29 | gutil.log(`${c.cyan('images')}: ${c.yellow(src)} ${e.type}, minifying`);
30 | } else {
31 | gutil.log(`${c.cyan('images')}: minifying`);
32 | }
33 |
34 | return gulp.src(src, { base: 'src' })
35 | .pipe(require('gulp-rename')(path => {
36 | const m = path.dirname.match(/^modules\/([^\/]+)/);
37 | path.dirname = m ?
38 | path.dirname.replace(`${m[0]}/images`, m[1]) :
39 | path.dirname.replace('client/images', 'app');
40 | }))
41 | .pipe(require('gulp-imagemin')())
42 | .pipe(gulp.dest('dist/public/img'));
43 | };
44 |
--------------------------------------------------------------------------------
/gulp/tasks/nodemon.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const gulp = require('gulp');
4 | const gutil = require('gulp-util');
5 | const c = gutil.colors;
6 |
7 | /**
8 | * Development server
9 | */
10 | gulp.task('nodemon', [ 'babel' ], (cb) => {
11 | const nodemon = require('nodemon');
12 |
13 | nodemon({
14 | script: './dist/server/index.js',
15 | watch: './dist'
16 | })
17 | .once('start', cb)
18 | .on('start', () => {
19 | gutil.log(`${c.cyan('nodemon')}: started`);
20 | })
21 | .on('restart', (files) => {
22 | gutil.log(`${c.cyan('nodemon')}: ${c.yellow(files[0].replace(__dirname + '/', ''))} changed - restarting`);
23 | })
24 | });
25 |
26 |
--------------------------------------------------------------------------------
/gulp/tasks/scripts.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const gulp = require('gulp');
4 | const gutil = require('gulp-util');
5 | const c = gutil.colors;
6 |
7 | /**
8 | * Bundle scripts for browser usage
9 | */
10 | gulp.task('scripts', () => {
11 | const browserify = require('browserify');
12 |
13 | const bundleOpts = {
14 | debug: true,
15 | extensions: [ '.jsx' ],
16 | transform: [ require('babelify') ]
17 | };
18 |
19 | // app bundler
20 | let appBundler = browserify('src/client', bundleOpts);
21 |
22 | // expose app modules
23 | exposeDir(appBundler, './src/app', 'app');
24 |
25 | // this will hold all bundlers and their outputs
26 | let bundlers = [ appBundler ];
27 | const outputs = [ 'app.js' ];
28 |
29 | // create bundlers for each module, excluding modules found in the app bundler
30 | // these don't have entry points since the files are just `require()`d
31 | const inputs = require('globby').sync('./src/modules/*');
32 | let moduleBundler, moduleName;
33 | inputs.forEach(input => {
34 | moduleName = input.match(/\/([^\/]+)$/)[1];
35 |
36 | moduleBundler = browserify(bundleOpts).external(appBundler);
37 | exposeDir(moduleBundler, input, `app/${moduleName}`);
38 |
39 | bundlers.push(moduleBundler);
40 | outputs.push(`${moduleName}.js`);
41 | });
42 |
43 | // wrap the app bundle with watchify in dev mode
44 | if (gutil.env.dev) {
45 | gutil.log(`${c.cyan('scripts')}: watching`);
46 |
47 | bundlers = bundlers.map((bundler, index) => {
48 | bundler = require('watchify')(bundler);
49 | bundler.on('update', files => {
50 | run(bundler, outputs[index], files);
51 | });
52 | return bundler;
53 | });
54 | }
55 |
56 | return require('merge-stream')(
57 | bundlers.map((bundle, index) => run(bundle, outputs[index]))
58 | );
59 | });
60 |
61 | /**
62 | * Recursively expose a directory on given bundler
63 | *
64 | * @param {Object} bundler
65 | * @param {String} dir
66 | * @param {String} namespace
67 | */
68 | function exposeDir(bundler, dir, namespace) {
69 | const fs = require('fs');
70 |
71 | fs.readdirSync(dir).forEach(file => {
72 | if (/^\./.test(file)) return;
73 |
74 | if (fs.statSync(`${dir}/${file}`).isDirectory()) {
75 | return exposeDir(bundler, `${dir}/${file}`, `${namespace}/${file}`);
76 | }
77 |
78 | const m = file.match(/^(.*)\.jsx?$/);
79 | if (!m) return;
80 |
81 | const exposeAs = m[1] === 'index' ? namespace : `${namespace}/${m[1]}`;
82 | bundler.require(`${dir}/${file}`, { expose: exposeAs });
83 | });
84 | }
85 |
86 | /**
87 | * Bundle given bundler
88 | *
89 | * @param {Object} bundler
90 | * @param {String} bundleName
91 | * @param {Array} files
92 | */
93 | function run(bundler, bundleName, files) {
94 | const sourcemaps = require('gulp-sourcemaps');
95 |
96 | if (files) {
97 | gutil.log(`${c.cyan('scripts')}: ${c.yellow(files[0].replace(process.cwd(), '.'))} changed - bundling ${c.yellow(bundleName)}`);
98 | } else {
99 | gutil.log(`${c.cyan('scripts')}: bundling ${c.yellow(bundleName)}`);
100 | }
101 |
102 | return bundler.bundle()
103 | .pipe(require('vinyl-source-stream')(bundleName))
104 | .pipe(require('vinyl-buffer')())
105 | .pipe(sourcemaps.init({ loadMaps: true }))
106 | .pipe(gutil.env.dev ? gutil.noop() : require('gulp-uglify')())
107 | .pipe(sourcemaps.write('.'))
108 | .pipe(gulp.dest('dist/public/js'));
109 | }
110 |
--------------------------------------------------------------------------------
/gulp/tasks/styles.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const gulp = require('gulp');
4 | const gutil = require('gulp-util');
5 | const c = gutil.colors;
6 |
7 | gulp.task('styles', () => {
8 | const src = [
9 | 'src/client/styles/index.scss',
10 | 'src/modules/*/styles/index.scss'
11 | ];
12 |
13 | if (gutil.env.dev) {
14 | gutil.log(`${c.cyan('styles')}: watching`);
15 |
16 | require('globby').sync(src).forEach(src => {
17 | gulp.watch([
18 | src.replace(/([^\/]+)\.scss$/, '*.scss'),
19 | src.replace(/([^\/]+)\.scss$/, '**/*.scss')
20 | ], e => run(src, e));
21 | });
22 | }
23 |
24 | return run(src);
25 | });
26 |
27 | function run(src, e) {
28 | const sourcemaps = require('gulp-sourcemaps');
29 |
30 | if (e) {
31 | gutil.log(`${c.cyan('styles')}: ${c.yellow(e.path.replace(process.cwd(), '.'))} ${e.type}, processing`);
32 | } else {
33 | gutil.log(`${c.cyan('styles')}: processing`);
34 | }
35 |
36 | return gulp.src(src, { base: 'src' })
37 | .pipe(sourcemaps.init())
38 | .pipe(require('gulp-sass')())
39 | .pipe(require('gulp-autoprefixer')({
40 | browsers: [ 'last 2 versions' ]
41 | }))
42 | .pipe(gutil.env.dev ? gutil.noop() : require('gulp-cssnano')({
43 | zindex: false
44 | }))
45 | .pipe(require('gulp-rename')(path => {
46 | const m = path.dirname.match(/^modules\/([^\/]+)/);
47 | path.basename = m ? m[1] : 'app';
48 | path.dirname = '';
49 | }))
50 | .pipe(sourcemaps.write('.'))
51 | .pipe(gulp.dest('dist/public/css'));
52 | };
53 |
--------------------------------------------------------------------------------
/gulp/tasks/symlink.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const gulp = require('gulp');
4 |
5 | /**
6 | * Symlink app into node_modules
7 | */
8 | gulp.task('symlink', [ 'babel' ], () => {
9 | const symlink = require('gulp-symlink');
10 |
11 | const appStream = gulp.src('dist/app')
12 | .pipe(symlink('node_modules/app', { force: true }));
13 |
14 | const modStream = gulp.src('dist/modules/*')
15 | .pipe(symlink(file => (
16 | new symlink.File({
17 | path: `dist/app/${file.relative}`,
18 | cwd: process.cwd()
19 | })
20 | ), { force: true }));
21 |
22 | return require('merge-stream')(appStream, modStream);
23 | });
24 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('./gulp');
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-boilerplate",
3 | "version": "0.0.0",
4 | "description": "",
5 | "author": "chielkunkels",
6 | "license": "MIT",
7 | "homepage": "https://github.com/chielkunkels/react-redux-boilerplate#readme",
8 | "bugs": {
9 | "url": "https://github.com/chielkunkels/react-redux-boilerplate/issues"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/chielkunkels/react-redux-boilerplate.git"
14 | },
15 | "keywords": [
16 | "react",
17 | "redux",
18 | "react-router"
19 | ],
20 | "dependencies": {
21 | "babel-preset-stage-1": "^6.3.13",
22 | "compression": "^1.6.0",
23 | "cookie-parser": "^1.4.1",
24 | "express": "^4.13.3",
25 | "fg-loadcss": "^0.2.4",
26 | "fg-loadjs": "^0.2.3",
27 | "history": "^1.17.0",
28 | "isomorphic-fetch": "^2.2.1",
29 | "normalize.css": "^3.0.3",
30 | "react": "^0.14.6",
31 | "react-dom": "^0.14.6",
32 | "react-redux": "^4.0.6",
33 | "react-router": "^1.0.3",
34 | "redux": "^3.0.5",
35 | "serve-static": "^1.10.0"
36 | },
37 | "devDependencies": {
38 | "babel-preset-es2015": "^6.3.13",
39 | "babel-preset-react": "^6.3.13",
40 | "babelify": "^7.2.0",
41 | "browserify": "^13.0.0",
42 | "del": "^2.2.0",
43 | "globby": "^4.0.0",
44 | "gulp": "^3.9.0",
45 | "gulp-autoprefixer": "^3.1.0",
46 | "gulp-babel": "^6.1.1",
47 | "gulp-cssnano": "^2.1.0",
48 | "gulp-imagemin": "^2.4.0",
49 | "gulp-rename": "^1.2.2",
50 | "gulp-sass": "^2.1.1",
51 | "gulp-sourcemaps": "^1.6.0",
52 | "gulp-symlink": "^2.1.3",
53 | "gulp-uglify": "^1.5.1",
54 | "gulp-util": "^3.0.7",
55 | "merge-stream": "^1.0.0",
56 | "nodemon": "^1.8.1",
57 | "vinyl-buffer": "^1.0.0",
58 | "vinyl-source-stream": "^1.1.0",
59 | "watchify": "^3.7.0"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/app/bundles.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const bundles = {};
4 |
5 | export default bundles;
6 |
--------------------------------------------------------------------------------
/src/app/components/App.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React from 'react';
4 | import { connect } from 'react-redux';
5 |
6 | function App(props) {
7 | return (
8 |
9 | Hello {props.currentUser ? props.currentUser.firstname : 'world'}.
10 |
11 | );
12 | }
13 |
14 | export default connect(state => ({
15 | currentUser: state.session.user
16 | }))(App);
17 |
--------------------------------------------------------------------------------
/src/app/constants.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | export const SET_SESSION_TOKEN = 'SET_SESSION_TOKEN';
4 | export const SET_SESSION_USER = 'SET_SESSION_USER';
5 |
--------------------------------------------------------------------------------
/src/app/errors/AuthorizationError.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | export default function AuthorizationError(message) {
4 | this.name = 'AuthorizationError';
5 | this.message = message;
6 | this.status = 401;
7 | }
8 |
9 | AuthorizationError.prototype.toString = function() {
10 | return this.message;
11 | };
12 |
--------------------------------------------------------------------------------
/src/app/errors/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | export Authorization from 'app/errors/AuthorizationError';
4 |
--------------------------------------------------------------------------------
/src/app/history.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import createBrowserHistory from 'history/lib/createBrowserHistory';
4 |
5 | export default createBrowserHistory();
6 |
--------------------------------------------------------------------------------
/src/app/reducers/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { session } from 'app/reducers/session';
4 | import { combineReducers } from 'redux';
5 |
6 | let reducers = { session };
7 |
8 | export function addReducers(newReducers) {
9 | reducers = { ...reducers, ...newReducers };
10 | }
11 |
12 | export default function createReducer() {
13 | return combineReducers(reducers);
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/reducers/session.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import * as c from 'app/constants';
4 |
5 | const initialState = {
6 | token: null,
7 | user: null
8 | };
9 |
10 | /**
11 | * Session reducer
12 | *
13 | * @param {Object} state
14 | * @param {Object} action
15 | *
16 | * @return {Object}
17 | */
18 | export function session(state = initialState, action) {
19 | switch (action.type) {
20 | case c.SET_SESSION_TOKEN:
21 | return { ...state,
22 | token: action.token
23 | };
24 |
25 | case c.SET_SESSION_USER:
26 | return { ...state,
27 | user: action.user
28 | };
29 |
30 | default:
31 | return state;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/store.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import history from 'app/history';
4 | import createReducer from 'app/reducers';
5 | import createApiCaller from 'app/utils/createApiCaller';
6 | import createApiMiddleware from 'app/utils/createApiMiddleware';
7 | import createCustomStore from 'app/utils/createCustomStore';
8 |
9 | const apiToken = window.__initialState.session.token;
10 |
11 | const store = createCustomStore(
12 | createReducer(),
13 | [ createApiMiddleware(createApiCaller(apiToken), history) ],
14 | window.__initialState
15 | );
16 |
17 | export default store;
18 |
--------------------------------------------------------------------------------
/src/app/utils/callApi.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('isomorphic-fetch');
4 |
5 | import * as errors from 'app/errors';
6 |
7 | const API_URL = typeof window !== 'undefined' ?
8 | window.__config.API_URL :
9 | process.env.API_URL;
10 |
11 | /**
12 | * Call given api endpoint with given options
13 | *
14 | * This is a lightweight wrapper around fetch that sets content-type and
15 | * accept headers if they've not been defined yet and does some standard
16 | * error handling that should always be implemented.
17 | *
18 | * @param {String} endpoint
19 | * @param {Object} options
20 | *
21 | * @return {Promise}
22 | */
23 | export default function callApi(endpoint, options) {
24 | const url = API_URL + endpoint;
25 |
26 | options = options || {};
27 | options.headers = options.headers || {};
28 |
29 | if (!options.headers.accept) {
30 | options.headers.accept = 'application/json';
31 | }
32 |
33 | if (!options.headers['content-type']) {
34 | options.headers['content-type'] = 'application/json';
35 | }
36 |
37 | return fetch(url, options)
38 | .then(res => {
39 | if (res.status >= 200 && res.status < 300) {
40 | return res.json();
41 | } else if (res.status === 401) {
42 | return res.json().then(json => (
43 | Promise.reject(new errors.Authorization(json.error.message))
44 | ));
45 | }
46 | });
47 | }
48 |
--------------------------------------------------------------------------------
/src/app/utils/createApiCaller.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import callApi from 'app/utils/callApi';
4 |
5 | /**
6 | * Create an api calling function with given token
7 | *
8 | * If a token is passed into this function, the api calls will receive
9 | * an Authorization header; if not, the calls will be unauthorized.
10 | *
11 | * @param {String} token
12 | *
13 | * @return {Function}
14 | */
15 | export default function createApiCaller(token) {
16 | if (!token) return callApi;
17 |
18 | /**
19 | * Wrap callApi to mix the Authorization header with the token into
20 | * the request's options. Takes the same arguments as `callApi`
21 | *
22 | * @param {String} endpoint
23 | * @param {Object} options
24 | */
25 | return function callApiWithToken(endpoint, options) {
26 | if (!options) options = {};
27 |
28 | const headers = options.headers || {};
29 |
30 | return callApi(endpoint, { ...options,
31 | headers: { ...headers,
32 | Authorization: `Bearer ${token}`
33 | }
34 | });
35 | };
36 | }
37 |
--------------------------------------------------------------------------------
/src/app/utils/createApiMiddleware.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | export default function createApiMiddleware(callApiWithToken, history) {
4 | return function apiMiddleware({ dispatch, getState }) {
5 | return next => action =>
6 | typeof action === 'function' ?
7 | action(dispatch, getState, callApiWithToken, history) :
8 | next(action);
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/utils/createCustomStore.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { applyMiddleware,
4 | createStore } from 'redux';
5 |
6 | /**
7 | * Create a store with given reducer, middleware and initial state
8 | *
9 | * @param {Function} reducer
10 | * @param {Function[]} middleware
11 | * @param {Object} initialState
12 | */
13 | export default function createCustomStore(reducer, middleware, initialState) {
14 | return applyMiddleware(...middleware)(createStore)(reducer, initialState);
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/utils/renderFullPage.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * Render given html string into full page markup
5 | *
6 | * @param {String} html
7 | * @param {Object} initialState
8 | * @param {String[]} styleSheets
9 | *
10 | * @return {String}
11 | */
12 | export default function renderFullPage(html, initialState, styleSheets) {
13 | styleSheets = styleSheets.map(sheet => (
14 | ``
15 | ));
16 |
17 | const config = {
18 | API_URL: process.env.API_URL
19 | };
20 |
21 | const analytics = !process.env.GOOGLE_ANALYTICS ? '' : ``;
29 |
30 | return (
31 | `
32 |
33 |
34 |
35 |
36 |
37 | React + Redux boilerplate
38 |
39 | ${styleSheets}
40 |
41 |
42 | ${html}
43 |
44 |
45 |
46 | ${analytics}
47 |
48 | `
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/src/client/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import routes from './routes';
4 | import App from 'app/components/App';
5 | import history from 'app/history';
6 | import React from 'react';
7 | import ReactDOM from 'react-dom';
8 | import { match, Router } from 'react-router';
9 | import { Provider } from 'react-redux';
10 |
11 | history.listen(location => {
12 | match({ routes, location: location.pathname }, (err, redirectLocation, renderProps) => {
13 | ReactDOM.render((
14 |
15 |
16 |
17 | ), document.getElementById('app-root'));
18 | });
19 | })();
20 |
--------------------------------------------------------------------------------
/src/client/routes.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import bundles from 'app/bundles';
4 | import App from 'app/components/App';
5 | import createReducer, {
6 | addReducers } from 'app/reducers';
7 | import loadCSS from 'fg-loadcss';
8 | import loadJS from 'fg-loadjs';
9 |
10 | const routes = [
11 | {
12 | getChildRoutes: getChildRoutes
13 | },
14 | {
15 | path: '*',
16 | component: App
17 | }
18 | ];
19 |
20 | const loaded = [];
21 |
22 | function getChildRoutes(location, cb) {
23 | let bundle = location.pathname.replace(/^[\/\s]+/, '').split('/')[0];
24 | if (!bundle || !bundles[bundle]) return cb();
25 |
26 | if (bundles[bundle].partOf) {
27 | bundle = bundles[bundle].partOf;
28 | }
29 |
30 | if (loaded.indexOf(bundle) !== -1) {
31 | return cb(null, require(`app/${bundle}/routes`).default);
32 | }
33 |
34 | if (bundles[bundle].styles) {
35 | const sheets = document.styleSheets;
36 | let found = false;
37 |
38 | for (let i = 0; i < sheets.length; i++) {
39 | if (!sheets[i].href) continue;
40 |
41 | if (new RegExp(`\/css\/${bundle}\.css`).test(sheets[i].href)) {
42 | found = true;
43 | break;
44 | }
45 | }
46 |
47 | if (!found) {
48 | loadCSS(`/css/${bundle}.css`);
49 | }
50 | }
51 |
52 | loadJS(`/js/${bundle}.js`, function() {
53 | loaded.push(bundle);
54 |
55 | if (bundles[bundle].reducers) {
56 | addReducers(require(`app/${bundle}/reducers`));
57 | require('app/store').default.replaceReducer(createReducer());
58 | }
59 | cb(null, require(`app/${bundle}/routes`).default);
60 | });
61 | }
62 |
63 | export default routes;
64 |
--------------------------------------------------------------------------------
/src/client/styles/index.scss:
--------------------------------------------------------------------------------
1 | @charset 'utf-8';
2 |
3 | @import '../../../node_modules/normalize.css/normalize';
4 |
--------------------------------------------------------------------------------
/src/server/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import routes from './routes';
4 | import bundles from 'app/bundles';
5 | import App from 'app/components/App';
6 | import * as sessionReducers from 'app/reducers/session';
7 | import createApiCaller from 'app/utils/createApiCaller';
8 | import createApiMiddleware from 'app/utils/createApiMiddleware';
9 | import createCustomStore from 'app/utils/createCustomStore';
10 | import renderFullPage from 'app/utils/renderFullPage';
11 | import express from 'express';
12 | import createMemoryHistory from 'history/lib/createMemoryHistory';
13 | import React from 'react';
14 | import { renderToString } from 'react-dom/server';
15 | import { Provider } from 'react-redux';
16 | import { match,
17 | RoutingContext } from 'react-router';
18 | import { combineReducers } from 'redux';
19 |
20 | const app = express();
21 | app.use(require('compression')());
22 | app.use(require('serve-static')(__dirname + '/../public'));
23 | app.use(require('cookie-parser')());
24 |
25 | app.get('*', (req, res, next) => {
26 | let bundle = req.path.replace(/^[\/\s]+/, '').split('/')[0];
27 | if (bundles[bundle] && bundles[bundle].partOf) {
28 | bundle = bundles[bundle].partOf;
29 | }
30 |
31 | let reducers = { ...sessionReducers };
32 |
33 | const styleSheets = [];
34 | if (bundles[bundle]) {
35 | if (bundles[bundle].styles) {
36 | styleSheets.push(`/css/${bundle}.css`);
37 | }
38 |
39 | if (bundles[bundle].reducers) {
40 | reducers = { ...reducers, ...require(`app/${bundle}/reducers`) };
41 | }
42 | }
43 |
44 | const history = createMemoryHistory();
45 |
46 | const store = createCustomStore(
47 | combineReducers(reducers),
48 | [ createApiMiddleware(createApiCaller(req.cookies.api_token), history) ]
49 | );
50 |
51 | match({ routes, location: req.url, history }, (err, redirectLocation, renderProps) => {
52 | if (err) {
53 | return next(err);
54 | }
55 |
56 | if (redirectLocation) {
57 | return res.redirect(302, redirectLocation.pathname + redirectLocation.search);
58 | }
59 |
60 | const render = () => (
61 | res.send(renderFullPage(renderToString(
62 |
63 |
64 |
65 | ), store.getState(), styleSheets))
66 | );
67 |
68 | const promises = renderProps.components
69 | .filter(component => component && !!component.fetchData)
70 | .map(component => component.fetchData(renderProps, store.dispatch));
71 |
72 | if (!promises.length) {
73 | return render();
74 | }
75 |
76 | Promise.all(promises)
77 | .then(render)
78 | .catch(next);
79 | });
80 | });
81 |
82 | const port = process.env.PORT || 4589;
83 | app.listen(port, () => {
84 | console.log(`Server listening on port ${port}`);
85 | });
86 |
--------------------------------------------------------------------------------
/src/server/routes.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import App from 'app/components/App';
4 | import globby from 'globby';
5 |
6 | const moduleRoutes = globby.sync(`${__dirname}/../modules/*/routes.js`)
7 | .map(routesFile => require(routesFile).default);
8 |
9 | const routes = moduleRoutes.map(routes => ({ childRoutes: routes }));
10 |
11 | routes.push({
12 | path: '*',
13 | component: App
14 | });
15 |
16 | export default routes;
17 |
--------------------------------------------------------------------------------