├── public └── .gitkeep ├── Procfile ├── index.js ├── client ├── styles.scss ├── modules │ └── foo │ │ ├── actions.js │ │ ├── actions │ │ ├── sample.js │ │ └── sampleAsync.js │ │ ├── components │ │ └── sampleComponent.js │ │ └── reducers.js ├── config.dev.js ├── config.prod.js ├── reducers.js ├── test │ └── tautological.spec.js ├── components │ ├── layouts │ │ └── mainLayout.js │ ├── widgets │ │ └── header.js │ └── pages │ │ ├── info.js │ │ ├── home.js │ │ └── credits.js ├── routes.js ├── app.js └── store.js ├── tests └── helpers │ └── plugins.js ├── server ├── views │ ├── error.ejs │ └── index.ejs ├── test │ └── tautological.spec.js ├── routes │ └── index.js ├── config.js ├── app.js ├── routes.js └── bin │ └── www ├── conf └── platforms │ ├── targets │ ├── development.js │ ├── production.js │ └── default.js │ └── index.js ├── app.json ├── tasks ├── watch.js ├── copyHtml.js ├── build.js ├── eslint.js ├── watchPackageJSON.js ├── watchClient.js ├── mocha.js ├── css.js ├── buildSrc.js └── watchBrowserify.js ├── .editorconfig ├── .gitignore ├── Gulpfile.js ├── .eslintrc ├── .babelrc ├── package.json └── README.md /public/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node index.js 2 | 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('./server/bin/www'); -------------------------------------------------------------------------------- /client/styles.scss: -------------------------------------------------------------------------------- 1 | body { 2 | //background: darkcyan; 3 | } -------------------------------------------------------------------------------- /tests/helpers/plugins.js: -------------------------------------------------------------------------------- 1 | var should = require('chai').should(); -------------------------------------------------------------------------------- /server/views/error.ejs: -------------------------------------------------------------------------------- 1 |

<%= message %>

2 |

<%= error.status %>

3 |
<%= error.stack %>
4 | -------------------------------------------------------------------------------- /client/modules/foo/actions.js: -------------------------------------------------------------------------------- 1 | export { sampleAction } from './actions/sample'; 2 | export { sampleAsyncAction } from './actions/sampleAsync'; 3 | -------------------------------------------------------------------------------- /client/config.dev.js: -------------------------------------------------------------------------------- 1 | export default { 2 | server: { 3 | url: '//localhost:5000' 4 | }, 5 | dev: { 6 | logActions: true 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /client/config.prod.js: -------------------------------------------------------------------------------- 1 | export default { 2 | server: { 3 | url: '//localhost:5000' 4 | }, 5 | dev: { 6 | logActions: false 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /client/reducers.js: -------------------------------------------------------------------------------- 1 | // import fooReducer from 'modules/foo/reducers'; 2 | // Add here the reducers 3 | export default { 4 | // foo: fooReducer 5 | }; 6 | -------------------------------------------------------------------------------- /client/modules/foo/actions/sample.js: -------------------------------------------------------------------------------- 1 | export function sampleAction() { 2 | return (dispatch) => { 3 | dispatch({ 4 | type: 'FOO:ACTION_SAMPLE' 5 | }); 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /client/test/tautological.spec.js: -------------------------------------------------------------------------------- 1 | describe('Demo test suite for the client', () => { 2 | it('Should pass the tautological test', () => { 3 | (true).should.be.equal(true); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /server/test/tautological.spec.js: -------------------------------------------------------------------------------- 1 | describe('Demo test suite for the server', () => { 2 | it('Should pass the tautological test', () => { 3 | (true).should.be.equal(true); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /conf/platforms/targets/development.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | const defaultsDeep = require('lodash.defaultsdeep'); 4 | const defaultValues = require('./default'); 5 | 6 | module.exports = () => defaultsDeep({}, defaultValues); 7 | -------------------------------------------------------------------------------- /client/modules/foo/components/sampleComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class SampleComponent extends React.Component { 4 | render() { 5 | return ( 6 |
This is a sample component
7 | ); 8 | } 9 | } 10 | 11 | export default SampleComponent; 12 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Node.js Getting Started", 3 | "description": "A barebones Node.js app using Express 4", 4 | "repository": "https://github.com/heroku/node-js-getting-started", 5 | "logo": "http://node-js-sample.herokuapp.com/node.svg", 6 | "keywords": ["node", "express", "static"], 7 | "image": "heroku/nodejs" 8 | } 9 | -------------------------------------------------------------------------------- /tasks/watch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = () => ({ 4 | watch: { 5 | seq: [ 6 | 'build', 7 | [ 8 | 'watchClient', 9 | 'watchPackageJSON', 10 | 'watchBrowserify' 11 | ] 12 | ], 13 | description: 'Build the sources and run the watcher with hot reload features.' 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /client/components/layouts/mainLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from '../widgets/header'; 3 | 4 | class MainLayout extends React.Component { 5 | render() { 6 | return ( 7 |
8 |
9 | {this.props.children} 10 |
11 | ); 12 | } 13 | } 14 | 15 | export default MainLayout; 16 | -------------------------------------------------------------------------------- /tasks/copyHtml.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (gulp, config) => { 4 | const tasks = { 5 | 'copy-html': { 6 | fn: copyHtmlTask, 7 | help: 'Copy the html files' 8 | } 9 | }; 10 | return tasks; 11 | 12 | function copyHtmlTask() { 13 | gulp.src('client/*.html') 14 | .pipe(gulp.dest(config.folders.build)); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /client/modules/foo/reducers.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | const defaultState = Immutable.Map( 3 | { 4 | bar: 10, 5 | baz: 20 6 | } 7 | ); 8 | 9 | export default (state = defaultState, action) => { 10 | switch (action.type) { 11 | case 'FOO:ACTION_SAMPLE': 12 | return state.set('pop', 30); 13 | default : 14 | return state; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /conf/platforms/targets/production.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const defaultsDeep = require('lodash.defaultsdeep'); 3 | const defaultValues = require('./default'); 4 | 5 | module.exports = () => defaultsDeep({}, defaultValues, { 6 | build: { 7 | uglify: { 8 | global: true, 9 | sourcemap: false, 10 | mangle: true, 11 | compress: true 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /tasks/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = () => { 4 | const tasks = { 5 | build: { 6 | seq: [ 7 | 'css', 8 | 'build-src', 9 | 'copy-html' 10 | ], 11 | description: 'Bundle the client package and copy it in the public folder. ' + 12 | 'Run \'npm run build\' to prepare a production environment.' 13 | } 14 | }; 15 | return tasks; 16 | }; 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # Matches multiple files with brace expansion notation 12 | # Set default charset 13 | [*.{js,html,css,scss}] 14 | charset = utf-8 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const router = express.Router(); 5 | const config = require('simple-registry').get('config'); 6 | 7 | /* GET home page. */ 8 | router.get('/', (req, res) => { 9 | res.render('index', { 10 | title: config.app.title, 11 | links: config.services.html.links, 12 | scripts: config.services.html.scripts 13 | }); 14 | }); 15 | 16 | module.exports = router; 17 | -------------------------------------------------------------------------------- /tasks/eslint.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (gulp) => { 4 | const tasks = { 5 | eslint: { 6 | fn: eslintTask, 7 | description: 'Run linting on the project sources.' 8 | } 9 | }; 10 | 11 | return tasks; 12 | 13 | function eslintTask() { 14 | const eslint = require('gulp-eslint'); 15 | return gulp.src(['client/**/*.js', 'tasks/**/*.js', 'server/**/*.js']) 16 | .pipe(eslint()) 17 | .pipe(eslint.format()); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # npm ignore 2 | npm-debug.log 3 | node_modules 4 | mocha.json 5 | coverage 6 | 7 | .tmp 8 | 9 | # Sublime Text 10 | *.sublime-workspace 11 | 12 | # vim 13 | *.swp 14 | 15 | # kdiff3 16 | *.orig 17 | 18 | # OSX 19 | Thumbs.db 20 | .DS_Store 21 | 22 | # Packages 23 | *.jar 24 | *.zip 25 | 26 | # Eclipse files 27 | *.classpath 28 | *.project 29 | *.settings 30 | .checkstyle 31 | 32 | # IntelliJ IDEA files 33 | .idea/ 34 | *.iml 35 | /.metadata 36 | 37 | #floobits 38 | .floo 39 | .flooignore 40 | 41 | -------------------------------------------------------------------------------- /tasks/watchPackageJSON.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (gulp) => { 4 | return { 5 | watchPackageJSON: { 6 | fn: watchPackageJSON, 7 | help: 'Watches the package.json for changes.' 8 | } 9 | }; 10 | 11 | function watchPackageJSON() { 12 | gulp 13 | .watch('./package.json', event => { 14 | console.log(`File ${event.path} was ${event.type}, running tasks...`); 15 | }) 16 | .on('error', error => { 17 | console.error(error); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /tasks/watchClient.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (gulp) => { 4 | return { 5 | watchClient: { 6 | fn: watchClient, 7 | help: 'Watches the client files for changes.' 8 | } 9 | }; 10 | 11 | function watchClient() { 12 | gulp 13 | .watch('client/**/*.html', ['copy-html']) 14 | .on('error', error => { 15 | console.error(error); 16 | }); 17 | 18 | gulp 19 | .watch('client/**/*.css', ['css']) 20 | .on('error', error => { 21 | console.error(error); 22 | }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /server/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= title %> 5 | <% for(var i = 0; i < links.length; i++) { %> 6 | <%= key %>="<%= links[i][key] %>" <% }) %> /><% } %> 7 | 8 | 9 |
10 |

The app is working

11 |
12 | 13 | <% for(var i = 0; i < scripts.length; i++) { %> 14 | <% } %> 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const gulp = require('gulp'); 4 | const taskLoader = require('gulp-commonjs-tasks/task-loader'); 5 | 6 | const consolePrefix = require('console-prefix'); 7 | const platforms = require('./conf/platforms')({ 8 | console: require('console-prefix')('[platforms-bootstrap]') 9 | }); 10 | const options = { 11 | console: consolePrefix('[platforms]') 12 | }; 13 | const config = platforms.current()(options); 14 | 15 | // load tasks 16 | const tasksContext = taskLoader.load('./tasks', gulp, config); 17 | 18 | // Add the gulp help task 19 | tasksContext.addHelpTask(); 20 | -------------------------------------------------------------------------------- /client/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router'; 3 | 4 | import MainLayout from './components/layouts/mainLayout'; 5 | import HomePage from './components/pages/home'; 6 | import InfoPage from './components/pages/info'; 7 | import CreditsPage from './components/pages/credits'; 8 | 9 | export default ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /client/app.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | import injectTapEventPlugin from 'react-tap-event-plugin'; 7 | 8 | import { Provider } from 'react-redux'; 9 | import { Router } from 'react-router'; 10 | 11 | import { store, history } from './store'; 12 | import routes from './routes'; 13 | 14 | // Needed before react 1.0 release 15 | injectTapEventPlugin(); 16 | 17 | ReactDOM.render( 18 | 19 | 20 | {routes} 21 | 22 | 23 | , document.getElementById('app') 24 | ); 25 | -------------------------------------------------------------------------------- /conf/platforms/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (options) => { 4 | const newOptions = 5 | require('lodash') 6 | .merge({ 7 | default: 'development', 8 | targets: require('path').join(__dirname, 'targets'), 9 | transformResult: (config, platformTarget) => 10 | () => { 11 | const args = Array.prototype.splice.call(arguments, 0); 12 | return require('lodash').merge({}, config.apply(null, args), { 13 | name: platformTarget 14 | }); 15 | } 16 | }, options); 17 | 18 | return require('platform-config')(newOptions); 19 | }; 20 | -------------------------------------------------------------------------------- /server/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const consolePrefix = require('console-prefix'); 3 | 4 | const platforms = require('../conf/platforms')({ 5 | console: require('console-prefix')('[platforms-bootstrap]') 6 | }); 7 | 8 | const options = { 9 | console: consolePrefix('[platforms]') 10 | }; 11 | 12 | const config = platforms.current()(options); 13 | 14 | console.log('-------------------------------------------------------------'); 15 | console.log('Great, as far as we can tell we have a valid config:', config.name); 16 | console.log('Let\'s move on'); 17 | console.log('-------------------------------------------------------------'); 18 | module.exports = config; 19 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "jasmine": true, 7 | "phantomjs": true, 8 | "es6": true 9 | }, 10 | "rules": { 11 | "no-use-before-define": 0, 12 | "no-console": 0, 13 | "new-cap": 0, 14 | "consistent-return": 0, 15 | "comma-dangle": [ 16 | 2, 17 | "never" 18 | ], 19 | "strict": 0, 20 | "max-len": [ 21 | 1, 22 | 120 23 | ], 24 | "react/prop-types": 0, 25 | "react/jsx-boolean-value": [ 26 | 1, 27 | "always" 28 | ], 29 | "react/no-multi-comp": 0, 30 | "react/jsx-no-bind": 0, 31 | "react/prefer-stateless-function": 0 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const express = require('express'); 3 | const path = require('path'); 4 | const logger = require('morgan'); 5 | const cookieParser = require('cookie-parser'); 6 | const bodyParser = require('body-parser'); 7 | const app = express(); 8 | 9 | // view engine setup 10 | app.set('views', path.join(__dirname, 'views')); 11 | app.set('view engine', 'ejs'); 12 | 13 | // uncomment after placing your favicon in /public 14 | app.use(logger('dev')); 15 | app.use(bodyParser.json()); 16 | app.use(bodyParser.urlencoded({ extended: false })); 17 | app.use(cookieParser()); 18 | app.use(express.static(path.join(__dirname, '../public'))); 19 | 20 | require('./routes')(app); 21 | 22 | module.exports = app; 23 | -------------------------------------------------------------------------------- /tasks/mocha.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (gulp) => { 4 | const tasks = { 5 | mocha: { 6 | fn: mochaTask, 7 | description: 'Run the unit test.' 8 | } 9 | }; 10 | return tasks; 11 | 12 | function mochaTask(done) { 13 | const mocha = require('gulp-mocha-co'); 14 | const babel = require('babel-register'); 15 | 16 | gulp.src(['./tests/helpers/plugins.js', 17 | './client/**/test/*.spec.js', 18 | './server/**/test/*.spec.js', 19 | './tests/**/*.spec.js' 20 | ], { read: false }) 21 | .pipe( 22 | mocha({ 23 | reporter: 'spec', 24 | compilers: { 25 | js: babel 26 | } 27 | }) 28 | ) 29 | .on('end', done) 30 | .on('error', err => { 31 | console.log(err); 32 | done(); 33 | }); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /server/routes.js: -------------------------------------------------------------------------------- 1 | const routes = require('./routes/index'); 2 | 3 | module.exports = (app) => { 4 | app.use('/', routes); 5 | 6 | // catch 404 and forward to error handler 7 | app.use((req, res, next) => { 8 | const err = new Error('Not Found'); 9 | err.status = 404; 10 | next(err); 11 | }); 12 | 13 | // development error handler 14 | // will print stacktrace 15 | if (app.get('env') === 'development') { 16 | app.use((err, req, res) => { 17 | res.status(err.status || 500); 18 | res.render('error', { 19 | message: err.message, 20 | error: err 21 | }); 22 | }); 23 | } 24 | 25 | // production error handler 26 | // no stacktraces leaked to user 27 | app.use((err, req, res) => { 28 | res.status(err.status || 500); 29 | res.render('error', { 30 | message: err.message, 31 | error: {} 32 | }); 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /conf/platforms/targets/default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | app: { 6 | name: 'Some application name', 7 | title: 'Some application title' 8 | }, 9 | server: { 10 | port: 5000 11 | }, 12 | folders: { 13 | build: path.join(__dirname, '../../../public') 14 | }, 15 | build: {}, 16 | services: { 17 | html: { 18 | links: [ 19 | { 20 | rel: 'stylesheet', 21 | crossorigin: 'anonymous', 22 | href: 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css', 23 | integrity: 'sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7' 24 | }, 25 | { rel: 'stylesheet', type: 'text/css', href: '/styles.css' } 26 | ], 27 | scripts: [ 28 | { type: 'text/javascript', src: '/main.js' } 29 | ] 30 | } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /client/modules/foo/actions/sampleAsync.js: -------------------------------------------------------------------------------- 1 | import qwest from 'qwest'; 2 | import config from 'config'; 3 | 4 | export function sampleAsyncAction(value) { 5 | return (dispatch) => { 6 | dispatch({ 7 | type: 'FOO:ASYNC_ACTION_START' 8 | }); 9 | const url = `${config.server.url}/foo?value=${value}`; 10 | qwest 11 | .get(url) 12 | .then( 13 | (xhr, response) => { 14 | console.log(response); 15 | if (xhr.status !== 200) { 16 | return dispatch({ 17 | type: 'FOO:ASYNC_ACTION_FAILED' 18 | }); 19 | } 20 | // Manage the response 21 | if (response.success === true) { 22 | dispatch({ 23 | type: 'FOO:ASYNC_ACTION_SUCCESS', 24 | val1: response.val1, 25 | val2: response.val2 26 | }); 27 | } 28 | } 29 | ); 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /client/components/widgets/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PageHeader, Grid, Row, Col, Nav, NavItem } from 'react-bootstrap'; 3 | import { Link } from 'react-router'; 4 | 5 | class Header extends React.Component { 6 | render() { 7 | return ( 8 | 9 | 10 | 11 | 12 | Sample 13 | A sample app 14 | 15 | 16 | 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | } 28 | 29 | export default Header; 30 | -------------------------------------------------------------------------------- /tasks/css.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (gulp, config) => { 4 | const tasks = { 5 | css: { 6 | fn: cssTask, 7 | help: 'Create the css' 8 | } 9 | }; 10 | return tasks; 11 | 12 | function cssTask() { 13 | const sass = require('gulp-sass'); 14 | const cssGlobbing = require('gulp-css-globbing'); 15 | 16 | return gulp.src(['client/styles.scss']) 17 | .pipe(cssGlobbing({ 18 | extensions: ['.css', '.scss'], 19 | ignoreFolders: ['../styles'], 20 | autoReplaceBlock: { 21 | onOff: true, 22 | globBlockBegin: 'cssGlobbingBegin', 23 | globBlockEnd: 'cssGlobbingEnd', 24 | globBlockContents: '../**/*.scss' 25 | }, 26 | scssImportPath: { 27 | leading_underscore: false, 28 | filename_extension: false 29 | } 30 | })) 31 | .pipe(sass().on('error', sass.logError)) 32 | .pipe(gulp.dest(config.folders.build)); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /client/store.js: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import thunkMiddleware from 'redux-thunk'; 3 | import logMiddleware from 'redux-logger'; 4 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux'; 5 | import { syncHistoryWithStore, routerReducer } from 'react-router-redux'; 6 | import { useRouterHistory } from 'react-router'; 7 | import { createHashHistory } from 'history'; 8 | 9 | import reducers from './reducers'; 10 | 11 | const reducer = combineReducers(Object.assign({}, reducers, { 12 | routing: routerReducer 13 | })); 14 | 15 | const history = useRouterHistory(createHashHistory)({ queryKey: false }); 16 | 17 | 18 | const middlewares = []; 19 | // middlewares.push(reduxRouterMiddleware); 20 | middlewares.push(thunkMiddleware); 21 | if (config.dev.logActions) { 22 | middlewares.push(logMiddleware()); 23 | } 24 | export const store = compose( 25 | applyMiddleware.apply(null, middlewares) 26 | )(createStore)(reducer); 27 | 28 | syncHistoryWithStore(history, store); 29 | -------------------------------------------------------------------------------- /client/components/pages/info.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Jumbotron, Grid, Row, Col } from 'react-bootstrap'; 3 | 4 | class InfoPage extends React.Component { 5 | render() { 6 | return ( 7 |
8 | 9 | 10 | 11 | 12 |

Info

13 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore 14 | et 15 | dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut 16 | aliquip ex 17 | ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu 18 | fugiat 19 | nulla pariatur.

20 |
21 | 22 |
23 |
24 |
25 | ); 26 | } 27 | } 28 | 29 | export default InfoPage; 30 | -------------------------------------------------------------------------------- /tasks/buildSrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (gulp, config) => { 4 | const tasks = { 5 | 'build-src': { 6 | fn: buildSrcTask, 7 | help: 'Build the src of the project' 8 | } 9 | }; 10 | 11 | function buildSrcTask(done) { 12 | const browserify = require('browserify'); 13 | const babelify = require('babelify'); 14 | const uglifyify = require('uglifyify'); 15 | const vinylSourceStream = require('vinyl-source-stream'); 16 | const depCaseVerify = require('dep-case-verify'); 17 | 18 | const browserifyBundle = 19 | browserify('client/app.js', { debug: false }) 20 | .transform(babelify) 21 | .plugin(depCaseVerify); 22 | 23 | if (config.build.uglify) { 24 | browserifyBundle.transform(config.build.uglify, uglifyify); 25 | } 26 | 27 | browserifyBundle.bundle() 28 | .pipe(vinylSourceStream('main.js')) 29 | .pipe(gulp.dest(config.folders.build)) 30 | .on('end', done); 31 | } 32 | 33 | return tasks; 34 | }; 35 | -------------------------------------------------------------------------------- /client/components/pages/home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Jumbotron, Grid, Row, Col } from 'react-bootstrap'; 3 | 4 | class HomePage extends React.Component { 5 | render() { 6 | return ( 7 |
8 | 9 | 10 | 11 | 12 |

Edit me!!

13 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore 14 | et 15 | dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut 16 | aliquip ex 17 | ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu 18 | fugiat 19 | nulla pariatur.

20 |
21 | 22 |
23 |
24 |
25 | ); 26 | } 27 | } 28 | 29 | export default HomePage; 30 | -------------------------------------------------------------------------------- /client/components/pages/credits.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { store } from '../../store'; 3 | import { routeActions } from 'react-router-redux'; 4 | import { Jumbotron, Grid, Row, Col, Button } from 'react-bootstrap'; 5 | 6 | class CreditsPage extends React.Component { 7 | 8 | handleGoToInfo() { 9 | store.dispatch(routeActions.replace('/info')); 10 | } 11 | 12 | render() { 13 | return ( 14 |
15 | 16 | 17 | 18 | 19 |

Credits

20 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore 21 | et 22 | dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut 23 | aliquip ex 24 | ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu 25 | fugiat 26 | nulla pariatur.

27 |
28 | 29 | 30 |
31 |
32 |
33 | ); 34 | } 35 | } 36 | 37 | export default CreditsPage; 38 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0", 5 | "react" 6 | ], 7 | "env": { 8 | "development": { 9 | "plugins": [ 10 | [ 11 | "babel-plugin-module-alias", 12 | [ 13 | { 14 | "src": "./client/modules", 15 | "expose": "modules" 16 | }, 17 | { 18 | "src": "./client/config.dev.js", 19 | "expose": "config" 20 | } 21 | ] 22 | ], 23 | [ 24 | "react-transform", 25 | { 26 | "transforms": [ 27 | { 28 | "transform": "react-transform-hmr", 29 | "imports": [ 30 | "react" 31 | ], 32 | "locals": [ 33 | "module" 34 | ] 35 | }, 36 | { 37 | "transform": "react-transform-catch-errors", 38 | "imports": [ 39 | "react", 40 | "redbox-react" 41 | ] 42 | } 43 | ] 44 | } 45 | ] 46 | ] 47 | }, 48 | "production": { 49 | "plugins": [ 50 | [ 51 | "babel-plugin-module-alias", 52 | [ 53 | { 54 | "src": "./client/modules", 55 | "expose": "modules" 56 | }, 57 | { 58 | "src": "./client/config.prod.js", 59 | "expose": "config" 60 | } 61 | ] 62 | ] 63 | ] 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tasks/watchBrowserify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (gulp, config) => { 4 | return { 5 | watchBrowserify: { 6 | fn: watchBrowserify, 7 | help: 'Watches the browserify build.' 8 | } 9 | }; 10 | 11 | function watchBrowserify() { 12 | const babelify = require('babelify'); 13 | const browserify = require('browserify'); 14 | const browserifyHMR = require('browserify-hmr'); 15 | const uglifyify = require('uglifyify'); 16 | const vinylSourceStream = require('vinyl-source-stream'); 17 | const watchify = require('watchify'); 18 | const gutil = require('gulp-util'); 19 | const depCaseVerify = require('dep-case-verify'); 20 | 21 | const browserifyBundle = browserify('client/app.js', { 22 | cache: {}, 23 | packageCache: {}, 24 | debug: true 25 | }); 26 | 27 | // Transforms 28 | browserifyBundle.transform(babelify); 29 | 30 | if (config.uglify) { 31 | browserifyBundle.transform(config.uglify, uglifyify); 32 | } 33 | 34 | // Plugins 35 | browserifyBundle.plugin(depCaseVerify); 36 | browserifyBundle.plugin(browserifyHMR); 37 | browserifyBundle.plugin(watchify); 38 | 39 | function bundle() { 40 | browserifyBundle.bundle() 41 | .pipe(vinylSourceStream('main.js')) 42 | .pipe(gulp.dest(config.folders.build)) 43 | .on('end', error => { 44 | if (error) { 45 | console.error(error); 46 | } 47 | }); 48 | } 49 | 50 | browserifyBundle 51 | .on('log', (message) => { 52 | gutil.log('browserify', message); 53 | }); 54 | 55 | browserifyBundle 56 | .on('update', bundle); 57 | 58 | bundle(); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /server/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | /** 5 | * Module dependencies. 6 | */ 7 | const config = require('../config'); 8 | const registry = require('simple-registry'); 9 | registry.set('config', config); 10 | const app = require('../app'); 11 | const debug = require('debug')(config.app.name + ':server'); 12 | const http = require('http'); 13 | 14 | /** 15 | * Get port from environment and store in Express. 16 | */ 17 | const port = normalizePort(process.env.PORT || '5000'); 18 | app.set('port', port); 19 | 20 | /** 21 | * Create HTTP server. 22 | */ 23 | 24 | const server = http.createServer(app); 25 | 26 | /** 27 | * Listen on provided port, on all network interfaces. 28 | */ 29 | app.listen( 30 | app.get('port'), 31 | () => console.log('Node app is running on port', app.get('port')) 32 | ); 33 | server.on('error', onError); 34 | server.on('listening', onListening); 35 | 36 | 37 | /** 38 | * Normalize a port into a number, string, or false. 39 | */ 40 | 41 | function normalizePort(val) { 42 | const tmpPort = parseInt(val, 10); 43 | 44 | if (isNaN(tmpPort)) { 45 | // named pipe 46 | return val; 47 | } 48 | 49 | if (tmpPort >= 0) { 50 | // port number 51 | return tmpPort; 52 | } 53 | 54 | return false; 55 | } 56 | 57 | /** 58 | * Event listener for HTTP server "error" event. 59 | */ 60 | 61 | function onError(error) { 62 | if (error.syscall !== 'listen') { 63 | throw error; 64 | } 65 | 66 | const bind = typeof port === 'string' 67 | ? 'Pipe ' + port 68 | : 'Port ' + port; 69 | 70 | // handle specific listen errors with friendly messages 71 | switch (error.code) { 72 | case 'EACCES': 73 | console.error(bind + ' requires elevated privileges'); 74 | process.exit(1); 75 | break; 76 | case 'EADDRINUSE': 77 | console.error(bind + ' is already in use'); 78 | process.exit(1); 79 | break; 80 | default: 81 | throw error; 82 | } 83 | } 84 | 85 | /** 86 | * Event listener for HTTP server "listening" event. 87 | */ 88 | 89 | function onListening() { 90 | const addr = server.address(); 91 | const bind = typeof addr === 'string' 92 | ? 'pipe ' + addr 93 | : 'port ' + addr.port; 94 | debug('Listening on ' + bind); 95 | } 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-scaffold", 3 | "version": "1.1.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "NODE_ENV=production ./node_modules/gulp/bin/gulp.js mocha", 9 | "build": "NODE_ENV=production ./node_modules/gulp/bin/gulp.js build" 10 | }, 11 | "author": "Davide Fiorello ", 12 | "license": "ISC", 13 | "dependencies": { 14 | "babel-polyfill": "^6.5.0", 15 | "bluebird": "^3.2.2", 16 | "body-parser": "^1.15.0", 17 | "console-prefix": "^1.0.0", 18 | "cookie-parser": "^1.4.0", 19 | "ejs": "^2.4.1", 20 | "express": "^4.13.4", 21 | "history": "^2.0.0", 22 | "immutable": "^3.7.6", 23 | "lodash": "^4.3.0", 24 | "lodash.defaultsdeep": "^4.1.0", 25 | "morgan": "^1.6.1", 26 | "platform-config": "^0.1.5", 27 | "qwest": "^4.0.0", 28 | "react": "^0.14.7", 29 | "react-bootstrap": "^0.28.3", 30 | "react-cookie": "^0.4.3", 31 | "react-dom": "^0.14.7", 32 | "react-redux": "^4.4.0", 33 | "react-router": "^2.0.0", 34 | "react-router-redux": "^4.0.1", 35 | "react-tap-event-plugin": "^0.2.2", 36 | "react-transform-catch-errors": "^1.0.2", 37 | "react-transform-hmr": "^1.0.2", 38 | "redux": "^3.3.1", 39 | "redux-logger": "^2.5.2", 40 | "redux-thunk": "^2.0.1", 41 | "services-config": "^0.1.6", 42 | "simple-registry": "^0.2.2" 43 | }, 44 | "devDependencies": { 45 | "babel": "^6.5.2", 46 | "babel-core": "^6.5.1", 47 | "babel-eslint": "^6.0.2", 48 | "babel-plugin-module-alias": "^1.2.0", 49 | "babel-plugin-react-transform": "^2.0.0", 50 | "babel-preset-es2015": "^6.5.0", 51 | "babel-preset-react": "^6.5.0", 52 | "babel-preset-stage-0": "^6.5.0", 53 | "babel-register": "^6.5.2", 54 | "babelify": "^7.2.0", 55 | "browserify": "^13.0.0", 56 | "browserify-hmr": "^0.3.1", 57 | "browserify-transform-tools": "^1.5.0", 58 | "chai": "^3.5.0", 59 | "chai-as-promised": "^5.2.0", 60 | "del": "^2.2.0", 61 | "dep-case-verify": "^2.0.0", 62 | "eslint-config-airbnb": "^6.2.0", 63 | "eslint-plugin-react": "^4.3.0", 64 | "gulp": "^3.9.1", 65 | "gulp-commonjs-tasks": "^1.2.0", 66 | "gulp-css-globbing": "^0.1.8", 67 | "gulp-eslint": "^2.0.0", 68 | "gulp-mocha-co": "^0.4.1-co.3", 69 | "gulp-sass": "^2.2.0", 70 | "redbox-react": "^1.2.2", 71 | "rewire": "^2.5.1", 72 | "sinon": "^1.17.3", 73 | "sinon-chai": "^2.8.0", 74 | "uglifyify": "^3.0.1", 75 | "vinyl-source-stream": "^1.1.0", 76 | "watchify": "^3.6.1" 77 | }, 78 | "engines": { 79 | "node": "5.3.0" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-redux-scaffold 2 | 3 | ## TLTR 4 | A React/Redux scaffold project with a nodeJs server and a babel/browserify building system. 5 | 6 | ** requires node > 5.x ** 7 | 8 | # Quick Start 9 | 10 | ### Just to check that everything is working 11 | ``` 12 | # Install the node dependencies 13 | npm install 14 | 15 | # Bundle the client code 16 | npm run build 17 | 18 | # Run the node server 19 | npm start 20 | 21 | # Open the web browser at http://localhost:5000 22 | # You should then see a sample 'bootstrap' app 23 | ``` 24 | 25 | ### Ok, is working but... I want to start to write my code ASAP 26 | 27 | Everything you need to start to do some experiment and start to write your react app is: 28 | 29 | ##### Run the server 30 | Open a terminal and in the app folder run: 31 | 32 | ```npm start``` 33 | 34 | ##### Run the watcher 35 | Open another terminal and in the app folder 36 | 37 | ```gulp watch``` 38 | 39 | ##### Open your browser at http://localhost:5000 40 | And you should see a sample boostrap app with a "Edit me!!" title in the center of the page. 41 | 42 | ##### Edit the code 43 | * Open your preferred IDE and start to open the ```client``` folder. 44 | * Open the `client/components/pages/home.js` file. 45 | * Change the `

Edit me!!

` text in something different. 46 | * Check the browser (the content should automatically refresh) 47 | 48 | ##### Have a lot of fun!! 49 | 50 | # The project explained 51 | 52 | The project is built using many technologies. 53 | 54 | * Server. Is a simple nodeJs/express app that just serves the index file that load the client app. It could be used as well as a complete server app, adding all the features needed. 55 | * Client. Is a redux/react-router/react basic app. It uses ```qwest``` module as a Ajax library and `react-bootstrap` module as a front-end component framework. 56 | * Conf. The configuration is managed by the `platform-config` and the `service-config` modules. 57 | * Gulp tasks. The Gulp task are managed by the `gulp-commonjs-tasks` module, that allow keep the tasks clean and easy to mantain. 58 | * Test. The test environment is built using mocha. 59 | * Bundle. The bundle is made using a browserify/babel workflow that provide react hot reloading. 60 | 61 | ### File and folder structure 62 | 63 | ``` 64 | . 65 | +-- client // React App 66 | | +-- ... 67 | +-- conf // System configration 68 | | +-- platforms // production/development/etc.. 69 | | +-- ... 70 | | +-- services // Additional services configuration 71 | | +-- ... 72 | +-- public // Public folder, static files 73 | +-- server // NodeJs App 74 | | +-- ... 75 | +-- tasks // Gulp tasks 76 | | +-- ... 77 | +-- tests // Common test files 78 | | +-- helpers 79 | | +-- ... 80 | +-- index.js // NodeJS entry point 81 | +-- Gulpfile.js // Gulp entrypoint 82 | +-- .babelrc // Babel config file 83 | +-- .editornconfig // Editor config file 84 | +-- .eslintrc // Eslint config file 85 | +-- README.md // This file 86 | 87 | ``` 88 | 89 | ## Build your bundle 90 | The development enviroment uses browserify to build the bundle. At the end of the process the client files are copied in the ```public``` folder. 91 | 92 | * main.js (Contains the app) 93 | * build.js (Contains all the external libs) 94 | * styles.css (Contains all the styles) 95 | 96 | #### Start the dev watch/build 97 | 98 | To start the watch task just run: 99 | 100 | ``` 101 | gulp watch 102 | ``` 103 | The task run a build process and enable the watcher. The task is run by default in `development` mode and add a `react-transform-hmr` transform to the babel bundle process for the hot reloading process.
104 | The build file doesn't work if the watcher is down.
105 | To build a no/watcher version run ```npm run build``` that simply uses the `production` environment and doesn't add the hot reload features to the code. 106 | 107 | #### Environments 108 | 109 | There are currently configured 2 environment: 110 | 111 | * production 112 | * development (default) 113 | 114 | To set the environment just add NODE_ENV=some_env before the commands. 115 | 116 | Eg. For the production building of the client 117 | ``` 118 | NODE_ENV=production gulp build 119 | ``` 120 | ##### Production 121 | * the client is uglified 122 | * the client doesn't require the watcher to run, hot reloader is disabled 123 | 124 | ##### Development 125 | * the client is not uglified 126 | * the client require the watcher to run, hot reloader is enabled 127 | 128 | The environments could be changed in the `config/platforms/targets` folder. 129 | 130 | * folders/build (The public folder location) 131 | * build/uglify (The uglify params, not uglified if missing) 132 | 133 | The babel parameters could be changed in the `.babelrc` file. 134 | 135 | 136 | ## Config 137 | The config is defined in th `config` folder. 138 | 139 | The app uses the node modules `platform-config` (https://github.com/sytac/platform-config) 140 | 141 | ``` 142 | // The data related to the application 143 | app: { 144 | name: 'Some application name', 145 | title: 'Some application title' 146 | }, 147 | // The server data used in node/express 148 | server: { 149 | port: 5000 150 | }, 151 | // The folder containing the static files 152 | folders: { 153 | build: path.join(__dirname, '../../../public') 154 | }, 155 | // The build/bundle parameter 156 | build: { 157 | uglify: { 158 | global: true, 159 | sourcemap: false, 160 | mangle: true, 161 | compress: true 162 | } 163 | }, 164 | // Additional services 165 | services: { 166 | html: { 167 | // The list of -link- tags added to the index.html server file 168 | links: [ 169 | { 170 | rel: 'stylesheet', 171 | crossorigin: 'anonymous', 172 | href: 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css', 173 | integrity: 'sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7' 174 | }, 175 | { rel: 'stylesheet', type: 'text/css', href: '/styles.css' } 176 | ], 177 | // The list of -scripts- tags added to the index.html server file 178 | scripts: [ 179 | { type: 'text/javascript', src: '/main.js' } 180 | ] 181 | } 182 | } 183 | ``` 184 | 185 | ## Gulp / Bundle 186 | All the automation are created using Gulp with the task manager module `gulp-commonjs-tasks` (https://github.com/sytac/gulp-commonjs-tasks) 187 | 188 | The tasks are placed in `tasks` folder. 189 | 190 | Run `gulp help` to see le list of the tasks. 191 | 192 | * build Bundle the client package and copy it in the public folder. Run 'npm run build' to prepare a production environment. 193 | * eslint Run linting on the project sources. 194 | * mocha Run the unit test. 195 | * watch Build the sources and run the watcher with hot reload features. 196 | --------------------------------------------------------------------------------