├── .editorconfig ├── .eslintrc ├── .gitignore ├── README.md ├── bin └── dev-server.sh ├── index.js ├── nodemon.json ├── package.json ├── src ├── app │ ├── Application │ │ ├── drink.png │ │ ├── index.jsx │ │ └── styles.less │ ├── Atom.js │ ├── HomePage │ │ ├── buildingclock.jpg │ │ └── index.jsx │ ├── NavMenu │ │ └── index.jsx │ ├── Todos │ │ ├── AddTodo.jsx │ │ ├── TodoItem.jsx │ │ ├── TodoList.jsx │ │ ├── TodoModel.js │ │ ├── TodoService.js │ │ ├── Todos.less │ │ └── index.jsx │ ├── favicon.ico │ ├── index.jsx │ └── routes.jsx ├── config │ └── databases.js └── server │ ├── environments.js │ ├── getHTML.js │ ├── index.html │ └── index.js ├── webpack.config.js ├── webpack.prod-node.config.js └── webpack.prod-web.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 2 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | // I want to use babel-eslint for parsing! 3 | "parser": "babel-eslint", 4 | "ecmaFeatures": { 5 | "jsx": true, 6 | "arrowFunctions": true, 7 | "blockBindings": true, 8 | "generators": true 9 | }, 10 | // To give you an idea how to override rule options: 11 | "rules": { 12 | "strict": 0, 13 | "no-underscore-dangle": 0, 14 | "no-unused-vars": 0, 15 | "curly": 0, 16 | "no-multi-spaces": 0, 17 | "key-spacing": 0, 18 | "no-return-assign": 0, 19 | "consistent-return": 0, 20 | "no-shadow": 0, 21 | "no-comma-dangle": 0, 22 | "no-use-before-define": 0, 23 | "no-empty": 0, 24 | "new-parens": 0, 25 | "no-cond-assign": 0, 26 | "quotes": [2, "single", "avoid-escape"], 27 | "camelcase": 0, 28 | "new-cap": [1, { "capIsNew": false }], 29 | "no-undef": 2 30 | }, 31 | "env": { 32 | "node": true, 33 | "browser": true 34 | }, 35 | "globals": { 36 | "IS_SERVER": true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | 4 | /npm-debug.log 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Isomorphic React Example 2 | 3 | > A basic example of using webpack to build isomorphic react modules 4 | 5 | ## Goals 6 | 7 | - [x] Isomorphic 8 | - [x] Uni-directional data flow 9 | - [x] Reactive/realtime database everywhere (PouchDB/CouchDB) 10 | - [x] `react-hot-loader` for development 11 | - [x] Material UI 12 | - [ ] Immutable centralised state (Atom concept, using Immutable-js) 13 | - [ ] Route is state 14 | - [ ] Test infrastructure 15 | - [ ] Production config. Docker? 16 | 17 | There's three different webpack configs: one for dev (with hot loading), and two for production. 18 | Production builds two packages, a CommonJS compatible package to be consumed by node (for server-side rendering), and an optimised packing for sending the the client. 19 | 20 | A basic Todos app is being built to demonstrate usage. See `src/app/Todos`. 21 | 22 | ## Usage 23 | 24 | **Development** 25 | 26 | ``` 27 | $ npm run dev-server 28 | ``` 29 | 30 | This spins up the app server and webpack dev server 31 | 32 | 33 | **Production** 34 | 35 | ``` 36 | $ npm run build // build webpack packages for node and client 37 | $ npm run prod-server // boot up the prod server with React rendering 38 | ``` -------------------------------------------------------------------------------- /bin/dev-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Change to bin 4 | cd $(dirname $([ -L $0 ] && readlink -f $0 || echo $0)) 5 | # Then change up 6 | cd ../ 7 | 8 | npm install 9 | 10 | webpack-dev-server --port 7007 -c --progress --config ./webpack.config.js --devtool eval & 11 | nodemon ./index.js 12 | 13 | trap "kill 0" SIGINT SIGTERM EXIT 14 | 15 | wait -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('babel/register')({ 2 | ignore: /node_modules\/(?!react-resolver)/ 3 | }); 4 | require('./src/server'); 5 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "execMap": { 3 | "js": "node --harmony --harmony_arrow_functions" 4 | } 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-editor", 3 | "version": "0.0.1", 4 | "description": "A simple document editor", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "npm install && webpack --progress --colours --config webpack.prod-node.config.js && webpack --progress --colours --config webpack.prod-web.config.js && npm run build-favicon", 9 | "prod-server": "ENVIRONMENT='prod' node --harmony index.js", 10 | "dev-server": "./bin/dev-server.sh && npm run build-favicon", 11 | "build-favicon": "cp ./src/app/favicon.ico build/" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/webpack/react-starter.git" 16 | }, 17 | "keywords": [ 18 | "reactjs", 19 | "isomorphic", 20 | "alt", 21 | "flux", 22 | "scribe" 23 | ], 24 | "author": "Andrew Cobby (https://github.com/cobbweb)", 25 | "license": "ISC", 26 | "bugs": { 27 | "url": "https://github.com/webpack/react-starter/issues" 28 | }, 29 | "homepage": "https://github.com/webpack/react-starter", 30 | "dependencies": { 31 | "alt": "^0.13.10", 32 | "autoprefixer-core": "^5.1.7", 33 | "babel": "^4.3.0", 34 | "babel-core": "^4.4.5", 35 | "babel-loader": "^4.0.0", 36 | "bluebird": "^1.2.4", 37 | "co": "^4.3.1", 38 | "css-loader": "^0.9.1", 39 | "csswring": "^3.0.1", 40 | "eventemitter3": "^0.1.6", 41 | "extract-text-webpack-plugin": "^0.3.8", 42 | "file-loader": "^0.8.1", 43 | "immutable": "^3.6.2", 44 | "koa": "^0.17.0", 45 | "koa-better-body": "^1.0.17", 46 | "koa-favicon": "^1.2.0", 47 | "koa-mount": "^1.3.0", 48 | "koa-render": "^0.2.1", 49 | "koa-router": "^4.0.1", 50 | "koa-static": "^1.4.9", 51 | "less": "^2.4.0", 52 | "less-loader": "^2.0.0", 53 | "leveldown": "^0.10.4", 54 | "lodash": "^3.3.1", 55 | "material-ui": "^0.7.0", 56 | "null-loader": "^0.1.0", 57 | "postcss-loader": "^0.3.0", 58 | "pouchdb": "^3.3.1", 59 | "react": "^0.12.2", 60 | "react-resolver": "0.0.2", 61 | "react-router": "^0.12.4", 62 | "react-tap-event-plugin": "^0.1.4", 63 | "style-loader": "^0.8.3", 64 | "underscore": "^1.7.0", 65 | "url-loader": "^0.5.5", 66 | "uuid": "^2.0.1", 67 | "webpack": "^1.5.3", 68 | "webpack-dev-server": "^1.7.0" 69 | }, 70 | "devDependencies": { 71 | "nodemon": "^1.3.7", 72 | "react-hot-loader": "^1.1.4" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/app/Application/drink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cobbweb/isomorphic-react-example/d5315a29080b9650e3a12d8e15c5f95908662f25/src/app/Application/drink.png -------------------------------------------------------------------------------- /src/app/Application/index.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react/addons'); 2 | var NavMenu = require('../NavMenu'); 3 | var { AppCanvas, AppBar, EnhancedButton } = require('material-ui'); 4 | var { RouteHandler } = require('react-router'); 5 | var Atom = require('../Atom'); 6 | 7 | require('./styles.less'); 8 | 9 | var icon = require('./drink.png'); 10 | 11 | var Application = React.createClass({ 12 | 13 | componentDidMount() { 14 | Atom.onChange(this.forceUpdate.bind(this)); 15 | }, 16 | 17 | componentWillUnmount() { 18 | Atom.offChange(this.forceUpdate.bind(this)); 19 | }, 20 | 21 | render() { 22 | return ( 23 | 24 | 25 | 26 | Isomorphic React Icon 27 | 28 | 29 | 30 | 31 | 32 |
33 | 34 |
35 |
36 | ); 37 | }, 38 | 39 | onMenuIconButtonTouchTap() { 40 | this.refs.navMenu.toggle(); 41 | } 42 | 43 | }); 44 | 45 | module.exports = Application; 46 | -------------------------------------------------------------------------------- /src/app/Application/styles.less: -------------------------------------------------------------------------------- 1 | @import '~material-ui/src/less/scaffolding'; 2 | @import '~material-ui/src/less/components'; 3 | 4 | body, html { 5 | font-family: Helvetica, sans-serif; 6 | background: darken(white, 10%); 7 | } 8 | 9 | .app-inner { 10 | padding-top: 80px; 11 | } 12 | 13 | .app-bar-icon-size() { 14 | @height: 36px; 15 | 16 | position: absolute; 17 | top: 50%; 18 | right: 20px; 19 | 20 | height: @height; 21 | margin-top: -@height / 2; 22 | } 23 | 24 | .app-bar-icon-button { 25 | .app-bar-icon-size(); 26 | } 27 | 28 | .app-bar-icon { 29 | .app-bar-icon-size(); 30 | display: block; 31 | filter: invert(1); 32 | } -------------------------------------------------------------------------------- /src/app/Atom.js: -------------------------------------------------------------------------------- 1 | const Immutable = require('immutable'); 2 | const Cursor = require('immutable/contrib/cursor'); 3 | 4 | class Atom { 5 | 6 | constructor() { 7 | this.callbacks = []; 8 | this.data = Immutable.fromJS({ data: { todos: {} }, state: { url: '/' } }); 9 | } 10 | 11 | getState() { 12 | return this.data; 13 | } 14 | 15 | setState(data) { 16 | this.data = data; 17 | this.emitChange(); 18 | } 19 | 20 | getBasicState() { 21 | return this.data.toJS(); 22 | } 23 | 24 | setBasicState(state) { 25 | this.data = Immutable.fromJS(state); 26 | } 27 | 28 | emitChange() { 29 | this.callbacks.forEach(cb => cb()); 30 | } 31 | 32 | onChange(cb) { 33 | this.callbacks.push(cb); 34 | } 35 | 36 | offChange(cb) { 37 | var index = this.callbacks.indexOf(cb); 38 | if (index !== -1) { 39 | delete this.callbacks[index]; 40 | } 41 | } 42 | 43 | stop() { 44 | // TODO Prevent other stuff happening after stop() is executed 45 | this.callbacks = []; 46 | } 47 | 48 | getCursor(path) { 49 | return Cursor.from(this.data, path, newData => { 50 | console.log('newData (should be false)', newData === this.data); 51 | this.data = newData; 52 | this.emitChange(); 53 | }); 54 | } 55 | 56 | } 57 | 58 | module.exports = new Atom(); 59 | 60 | if (typeof window !== 'undefined') { 61 | window.__app__state = module.exports; 62 | } 63 | -------------------------------------------------------------------------------- /src/app/HomePage/buildingclock.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cobbweb/isomorphic-react-example/d5315a29080b9650e3a12d8e15c5f95908662f25/src/app/HomePage/buildingclock.jpg -------------------------------------------------------------------------------- /src/app/HomePage/index.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var backgroundImage = require('./buildingclock.jpg'); 4 | 5 | var HomePage = React.createClass({ 6 | 7 | render() { 8 | return ( 9 |
10 | Build Clock 11 |
12 | ); 13 | } 14 | 15 | }); 16 | 17 | module.exports = HomePage; 18 | -------------------------------------------------------------------------------- /src/app/NavMenu/index.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var { LeftNav } = require('material-ui'); 3 | var { Navigation } = require('react-router'); 4 | 5 | var NavMenu = React.createClass({ 6 | 7 | mixins: [Navigation], 8 | 9 | menuItems: [ 10 | { route: 'app', text: 'Home' }, 11 | { route: 'todos', text: 'Todos' } 12 | ], 13 | 14 | onMenuItem(event, index, item) { 15 | this.transitionTo(item.route, item.params, item.query); 16 | }, 17 | 18 | render() { 19 | return 20 | }, 21 | 22 | toggle() { 23 | this.refs.leftNav.toggle(); 24 | } 25 | 26 | }); 27 | 28 | module.exports = NavMenu; -------------------------------------------------------------------------------- /src/app/Todos/AddTodo.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var TodoService = require('./TodoService'); 3 | 4 | var AddTodo = React.createClass({ 5 | 6 | addTodo(event) { 7 | event.preventDefault(); 8 | var input = this.refs.input.getDOMNode(); 9 | 10 | if (!input.value) { 11 | return; 12 | } 13 | 14 | TodoService.create(input.value); 15 | input.value = ''; 16 | }, 17 | 18 | render() { 19 | return ( 20 |
21 | 22 | 23 |
24 | ); 25 | } 26 | 27 | }); 28 | 29 | module.exports = AddTodo; -------------------------------------------------------------------------------- /src/app/Todos/TodoItem.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var TodoService = require('./TodoService'); 3 | 4 | var TodoItem = React.createClass({ 5 | 6 | render() { 7 | return ( 8 |
  • 9 | {this.props.todo.text} 10 | 11 |
  • ); 12 | }, 13 | 14 | remove() { 15 | TodoService.remove(this.props.todo); 16 | } 17 | 18 | }); 19 | 20 | module.exports = TodoItem; -------------------------------------------------------------------------------- /src/app/Todos/TodoList.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var TodoItem = require('./TodoItem'); 3 | 4 | var TodoList = React.createClass({ 5 | 6 | componentWillMount() { 7 | console.log('willMount'); 8 | }, 9 | 10 | shouldComponentUpdate() { 11 | console.log('shouldUpdate'); 12 | return true; 13 | }, 14 | 15 | render() { 16 | var todos = this.props.todos; 17 | return ( 18 | 23 | ); 24 | } 25 | 26 | }); 27 | 28 | module.exports = TodoList; 29 | -------------------------------------------------------------------------------- /src/app/Todos/TodoModel.js: -------------------------------------------------------------------------------- 1 | const TodoService = require('./TodoService'); 2 | const PouchDB = require('pouchdb'); 3 | const config = require('../../config/databases').todos; 4 | const Atom = require('../Atom'); 5 | const { List } = require('immutable'); 6 | const sortedIndex = require('lodash/array/sortedIndex'); 7 | 8 | const TODOS_PATH = ['data', 'todos']; 9 | 10 | 11 | class TodoModel { 12 | 13 | constructor() { 14 | this.db = new PouchDB(config.database); 15 | this.cursor = Atom.getCursor(['data', 'todos']); 16 | 17 | if (config.replicateTo) { 18 | this.sync = PouchDB.sync(config.database, config.replicateTo, { live: true }); 19 | } 20 | 21 | this.loaded = this.db.allDocs({ include_docs: true }).then((response) => { 22 | this.initializeData(response); 23 | 24 | if (this.sync) { 25 | this.sync.on('change', (info) => { 26 | if (info.direction === 'pull' && info.change.docs.length > 0) { 27 | this.updateData(info); 28 | } 29 | }); 30 | } 31 | 32 | this.db.changes({ live: true, include_docs: true, since: 'now' }).on('change', this.updateData.bind(this)); 33 | }); 34 | } 35 | 36 | updateData(info) { 37 | if (info.deleted) { 38 | this.cursor = this.cursor.delete(info.id); 39 | } else { 40 | // Insert or update event 41 | this.cursor = this.cursor.set(info.id, info.doc); 42 | } 43 | } 44 | 45 | initializeData(response) { 46 | const updated = this.cursor.withMutations(map => { 47 | response.rows.forEach(row => map.set(row.id, row.doc)); 48 | }); 49 | 50 | this.cursor = this.cursor.merge(updated); 51 | } 52 | 53 | insert(doc) { 54 | doc.createdAt = String(Date.now()); 55 | doc._id = doc.text.substring(0, 16) + '_' + doc.createdAt; 56 | this.db.put(doc); 57 | } 58 | 59 | remove(doc) { 60 | this.db.remove(doc).then((response) => { 61 | var info = { deleted: true, id: response.id }; 62 | this.updateData(info); 63 | }); 64 | } 65 | 66 | removeAll() { 67 | let docs = this.cursor.toArray(); 68 | docs.forEach(doc => doc._deleted = true); 69 | this.db.bulkDocs(docs, (err, response) => { 70 | if (err) { 71 | throw err; 72 | } 73 | console.log(arguments); 74 | }); 75 | } 76 | 77 | } 78 | 79 | module.exports = new TodoModel(); 80 | -------------------------------------------------------------------------------- /src/app/Todos/TodoService.js: -------------------------------------------------------------------------------- 1 | var TodoModel = require('./TodoModel'); 2 | 3 | class TodoService { 4 | 5 | create(text) { 6 | TodoModel.insert({ text: text }); 7 | } 8 | 9 | remove(todo) { 10 | TodoModel.remove(todo); 11 | } 12 | 13 | removeAll() { 14 | TodoModel.removeAll(); 15 | } 16 | 17 | loaded() { 18 | return TodoModel.loaded; 19 | } 20 | 21 | } 22 | 23 | module.exports = new TodoService(); 24 | -------------------------------------------------------------------------------- /src/app/Todos/Todos.less: -------------------------------------------------------------------------------- 1 | @import '~material-ui/src/less/scaffolding'; 2 | @import '~material-ui/src/less/components'; 3 | 4 | .todos { 5 | width: 100%; 6 | max-width: 500px; 7 | margin: 0 auto; 8 | background-color: @white; 9 | 10 | .mui-paper-container { 11 | padding: 24px; 12 | } 13 | } -------------------------------------------------------------------------------- /src/app/Todos/index.jsx: -------------------------------------------------------------------------------- 1 | // Libs 2 | const React = require('react'); 3 | const Resolver = require('react-resolver'); 4 | const { Paper } = require('material-ui'); 5 | 6 | // App 7 | const Atom = require('../Atom'); 8 | const AddTodo = require('./AddTodo'); 9 | const TodoList = require('./TodoList'); 10 | const TodoService = require('./TodoService'); 11 | 12 | require('./Todos.less'); 13 | 14 | 15 | const Todos = React.createClass({ 16 | 17 | mixins: [Resolver.mixin], 18 | 19 | statics: { 20 | resolve: { 21 | todos() { 22 | return TodoService.loaded(); 23 | } 24 | } 25 | }, 26 | 27 | removeAll() { 28 | TodoService.removeAll(); 29 | }, 30 | 31 | render() { 32 | const todos = Atom.getState().getIn(['data', 'todos']); 33 | return ( 34 | 35 |

    Todos!

    36 | 37 | 38 | 39 |
    40 | ); 41 | } 42 | 43 | }); 44 | 45 | module.exports = Todos; 46 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cobbweb/isomorphic-react-example/d5315a29080b9650e3a12d8e15c5f95908662f25/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/index.jsx: -------------------------------------------------------------------------------- 1 | // Browser entry point 2 | require('react-tap-event-plugin')(); 3 | 4 | var React = require('react/addons'); 5 | var Router = require('react-router'); 6 | var resolver = require('react-resolver').create(); 7 | var routes = resolver.route(require('./routes')); 8 | 9 | var appContainer = document.getElementById('app'); 10 | 11 | React.render(
    Loading
    , appContainer); 12 | 13 | Router.run(routes, Router.HistoryLocation, (Handler) => { 14 | resolver.resolve().then(function() { 15 | React.render(, appContainer); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/app/routes.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var { Route, DefaultRoute } = require('react-router'); 3 | 4 | var Application = require('./Application'); 5 | var HomePage = require('./HomePage'); 6 | var Todos = require('./Todos'); 7 | 8 | module.exports = ( 9 | 10 | 11 | 12 | 13 | ); -------------------------------------------------------------------------------- /src/config/databases.js: -------------------------------------------------------------------------------- 1 | var CLIENT = { 2 | todos: { 3 | database: 'todos', 4 | replicateTo: 'http://localhost:5984/todos' 5 | } 6 | }; 7 | 8 | var SERVER = { 9 | todos: { 10 | database: 'http://localhost:5984/todos' 11 | } 12 | }; 13 | 14 | // IS_SERVER is defined in Webpack config 15 | module.exports = IS_SERVER ? SERVER : CLIENT; 16 | -------------------------------------------------------------------------------- /src/server/environments.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | PROD: 'prod', 3 | LOCAL: 'local' 4 | }; 5 | -------------------------------------------------------------------------------- /src/server/getHTML.js: -------------------------------------------------------------------------------- 1 | var envs = require('./environments'); 2 | 3 | module.exports = function *(url, env) { 4 | var d = Promise.defer(); 5 | if (env !== envs.LOCAL) { 6 | // App crap 7 | var React = require('react'); 8 | var Router = require('react-router'); 9 | var resolver = require('react-resolver').create(); 10 | var routes = resolver.route(require('../../build/main.commonjs.js')); 11 | 12 | Router.run(routes, url, function (Handler) { 13 | resolver.handle(Handler).then(() => { 14 | d.resolve(React.renderToString(React.createElement(Handler))); 15 | }); 16 | }); 17 | } else { 18 | d.resolve(''); 19 | } 20 | return d.promise; 21 | }; 22 | -------------------------------------------------------------------------------- /src/server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | React Editor 4 | 5 | 6 | <%= stylesheets %> 7 | 8 | 9 |
    <%= html %>
    10 | 11 | <%= scripts %> 12 | 13 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | // Server crap 2 | var app = require('koa')(); 3 | var serve = require('koa-static'); 4 | var views = require('koa-render'); 5 | var mount = require('koa-mount'); 6 | var router = require('koa-router')(); 7 | var getHTML = require('./getHTML'); 8 | var envs = require('./environments'); 9 | var favicon = require('koa-favicon'); 10 | var path = require('path'); 11 | 12 | const FAVICON_PATH = path.resolve(__dirname, '../../build/favicon.ico'); 13 | 14 | 15 | app.env = process.env.ENVIRONMENT || envs.LOCAL; 16 | 17 | app.use(views(__dirname, { map: { html: 'underscore' } })); 18 | app.use(mount('/assets', serve(path.resolve(__dirname, '../../build')))); 19 | 20 | 21 | const scripts = { 22 | prod: '', 23 | local: '' 24 | }; 25 | 26 | const stylesheets = { 27 | prod: '', 28 | local: '' 29 | }; 30 | 31 | app.use(favicon(FAVICON_PATH)); 32 | 33 | app.use(function *(next) { 34 | this.html = yield getHTML(this.url, app.env); 35 | yield next; 36 | }); 37 | 38 | router.get('/:page*', function *() { 39 | var js = scripts[app.env]; 40 | var css = stylesheets[app.env]; 41 | this.body = yield this.render('index.html', { scripts: js, html: this.html, stylesheets: css }); 42 | }); 43 | 44 | app.use(router.routes()) 45 | .use(router.allowedMethods()); 46 | 47 | app.listen(7001); 48 | console.log('Server running at http://localhost:7001'); 49 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var autoprefixer = require('autoprefixer-core'); 3 | 4 | module.exports = { 5 | entry: [ 6 | 'webpack-dev-server/client?http://localhost:7007/', 7 | 'webpack/hot/only-dev-server', 8 | './src/app/Application/index.jsx', 9 | './src/app/index.jsx' 10 | ], 11 | output: { 12 | path: __dirname + '/build/', 13 | filename: '[name].js', 14 | contentBase: 'http://localhost:7001', 15 | publicPath: 'http://localhost:7007/assets/' 16 | }, 17 | module: { 18 | loaders: [ 19 | { test: /\.jsx?$/, loaders: ['react-hot', 'babel'], exclude: /node_modules(?!\/react-resolver)/ }, 20 | { test: /\.less$/, loader: 'style!css!postcss!less' }, 21 | { test: /\.(?:jpe?g|png|gif|svg)$/, loader: 'url?limit=10000&name=[name].[sha512:hash:base64:7].[ext]' } 22 | ] 23 | }, 24 | postcss: [autoprefixer], 25 | plugins: [ 26 | new webpack.HotModuleReplacementPlugin(), 27 | new webpack.NoErrorsPlugin(), 28 | new webpack.DefinePlugin({ IS_SERVER: false }) 29 | ], 30 | resolve: { 31 | extensions: ['', '.js', '.jsx'], 32 | alias: { 33 | pouchdb: 'pouchdb/dist/pouchdb.js' 34 | } 35 | }, 36 | devtool: '#inline-source-map' 37 | }; -------------------------------------------------------------------------------- /webpack.prod-node.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | module.exports = { 4 | target: 'node', 5 | entry: [ 6 | './src/app/routes.jsx' 7 | ], 8 | output: { 9 | path: __dirname + '/build/', 10 | filename: '[name].commonjs.js', 11 | library: true, 12 | libraryTarget: 'commonjs2', 13 | publicPath: '/assets/' 14 | }, 15 | module: { 16 | loaders: [ 17 | { test: /\.jsx?$/, loader: 'babel', exclude: /node_modules(?!\/react-resolver)/ }, 18 | { test: /\.less$/, loader: 'null' }, 19 | { test: /\.(?:jpe?g|png|gif|svg)$/, loader: 'url?limit=10000&name=[name].[sha512:hash:base64:7].[ext]' } 20 | ] 21 | }, 22 | resolve: { 23 | extensions: ['', '.js', '.jsx'] 24 | }, 25 | externals: [ /^(?!\.)/ ], 26 | plugins: [ 27 | new webpack.optimize.DedupePlugin(), 28 | new webpack.DefinePlugin({ IS_SERVER: true }) 29 | ] 30 | }; -------------------------------------------------------------------------------- /webpack.prod-web.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 3 | var autoprefixer = require('autoprefixer-core'); 4 | 5 | module.exports = { 6 | entry: [ 7 | './src/app/index.jsx' 8 | ], 9 | output: { 10 | path: __dirname + '/build/', 11 | filename: '[name].js', 12 | publicPath: '/assets/' 13 | }, 14 | module: { 15 | loaders: [ 16 | { test: /\.jsx?$/, loader: 'babel', exclude: /node_modules(?!\/react-resolver)/ }, 17 | { test: /\.less$/, loader: ExtractTextPlugin.extract('style', 'css!postcss!less') }, 18 | { test: /\.(?:jpe?g|png|gif|svg)$/, loader: 'url?limit=10000&name=[name].[sha512:hash:base64:7].[ext]' } 19 | ] 20 | }, 21 | postcss: [autoprefixer], 22 | resolve: { 23 | extensions: ['', '.js', '.jsx'], 24 | alias: { 25 | pouchdb: 'pouchdb/dist/pouchdb.js' 26 | } 27 | }, 28 | plugins: [ 29 | new ExtractTextPlugin('[name].css'), 30 | new webpack.DefinePlugin({ IS_SERVER: false }), 31 | new webpack.optimize.UglifyJsPlugin(), 32 | new webpack.optimize.DedupePlugin(), 33 | new webpack.NoErrorsPlugin() 34 | ] 35 | }; --------------------------------------------------------------------------------