├── .gitignore ├── .jshintrc ├── Makefile ├── README.md ├── nodemon.json ├── package.json ├── preview.png ├── src ├── app.js ├── client │ ├── entry.js │ └── init.js ├── server │ ├── app.js │ ├── routes │ │ ├── index.js │ │ ├── middleware.js │ │ └── views │ │ │ └── appIndex.js │ └── webpack.js └── shared │ ├── Flux.js │ ├── actions │ └── StargazerActions.js │ ├── components │ ├── AppHandler.js │ ├── StargazerGridHandler.js │ └── StargazerGridView.js │ ├── init.js │ ├── performRouteHandlerStaticMethod.js │ ├── routes.js │ └── stores │ └── StargazerStore.js ├── views └── app.jade ├── webpack.config.dev.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 27 | node_modules 28 | 29 | /lib 30 | /public 31 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "indent": 2, 3 | "globalstrict": true, 4 | "newcap": false, 5 | 6 | "node": true, 7 | "browser": true, 8 | 9 | "unused": "vars", 10 | "esnext": true, 11 | "laxbreak": true, 12 | "expr": true, 13 | 14 | "globals": { 15 | "Promise": true, 16 | 17 | "describe": true, 18 | "before": true, 19 | "after": true, 20 | "beforeEach": true, 21 | "afterEach": true, 22 | "it": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | WEBPACK_CMD = node_modules/.bin/webpack 2 | NODEMON_CMD = node_modules/.bin/nodemon 3 | BABEL_CMD = node_modules/.bin/babel 4 | 5 | BABEL_ARGS = --experimental --source-maps-inline 6 | 7 | SRC_JS = $(shell find src -name "*.js") 8 | LIB_JS = $(patsubst src/%.js,lib/%.js,$(SRC_JS)) 9 | 10 | # Build application 11 | build: js webpack 12 | 13 | clean: 14 | rm -rf lib/ 15 | rm -rf public/js 16 | 17 | watch: 18 | NODE_ENV=development $(MAKE) -j3 watch-js nodemon webpack-dev 19 | 20 | nodemon: build 21 | nodemon lib/server/app.js 22 | 23 | # Build application quickly 24 | # Faster on first build, but not after that 25 | fast-build: fast-js build 26 | 27 | # Transpile JavaScript using babel 28 | js: $(LIB_JS) 29 | 30 | $(LIB_JS): lib/%.js: src/%.js 31 | mkdir -p $(dir $@) && $(BABEL_CMD) $< -o $@ $(BABEL_ARGS) 32 | 33 | fast-js: 34 | $(BABEL_CMD) src -d lib $(BABEL_ARGS) 35 | 36 | watch-js: 37 | $(BABEL_CMD) src -d lib $(BABEL_ARGS) -w 38 | 39 | webpack: public/js/app.js 40 | 41 | webpack-dev: $(LIB_JS) 42 | node --harmony ./lib/server/webpack 43 | 44 | public/js/app.js: $(SRC_JS) 45 | $(WEBPACK_CMD) 46 | 47 | .PHONY: build test watch nodemon js clean fast-js watch-js webpack webpack-dev 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##No longer maintained. Check out the source for Flummox's [documentation](https://github.com/acdlite/flummox/tree/master/docs) for an updated version of this project.## 2 | 3 | # Flummox Isomorphic Demo 4 | 5 | **Very much a work in progress** 6 | 7 | Demo of how to create isomorphic apps using Flummox and react-router. Fetches and displays the stargazers for a given GitHub repo. The server does the initial render and sends the HTML to the client, where it continues. (This was the easiest, quickest example I could think of.) 8 | 9 | Needs more work, but it should get the basic idea across until I find time to make it better. Pull requests definitely welcome :) 10 | 11 | ![Preview](preview.png) 12 | 13 | ## Tools/technologies used: 14 | 15 | - Flummox 16 | - React 17 | - react-router 18 | - webpack 19 | - react-hot-loader 20 | - koa 21 | - iojs 22 | - superagent 23 | - babel 24 | - jade 25 | - make 26 | 27 | ...and more 28 | 29 | ## Start the app 30 | 31 | Clone this repo, then run 32 | 33 | ``` 34 | $ npm install 35 | $ npm install -g nodemon 36 | $ make watch 37 | ``` 38 | 39 | to start a dev server (with hot reload enabled) on port 3000. Then navigate to http://localhost:3000/stargazers/acdlite/flummox (or any url of the form `/stargazers/:owner/:repo`) 40 | 41 | Make sure you're running on iojs or node v0.11, for generator support. 42 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "execMap": { 3 | "js": "node --harmony" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flummox-isomorphic-demo", 3 | "version": "1.0.0", 4 | "description": "Demo of how to create isomorphic apps using Flummox and react-router", 5 | "main": "app.js", 6 | "scripts": { 7 | "prestart": "make fast-build", 8 | "start": "node lib/server/app.js" 9 | }, 10 | "engines": { 11 | "node": ">= 0.11.16", 12 | "iojs": ">= 1.0.0" 13 | }, 14 | "engineStrict": true, 15 | "keywords": [ 16 | "flummox", 17 | "flux", 18 | "react", 19 | "es6", 20 | "react-router", 21 | "koa" 22 | ], 23 | "author": "Andrew Clark ", 24 | "license": "ISC", 25 | "dependencies": { 26 | "flummox": "~2.13.0", 27 | "immutable": "~3.6.2", 28 | "koa": "~0.16.0", 29 | "koa-conditional-get": "~1.0.2", 30 | "koa-etag": "~2.0.0", 31 | "koa-fresh": "0.0.3", 32 | "koa-gzip": "~0.1.0", 33 | "koa-router": "~3.8.0", 34 | "koa-static": "~1.4.9", 35 | "koa-views": "~2.1.2", 36 | "react": "~0.12.2", 37 | "react-document-title": "~0.1.3", 38 | "react-router": "~0.11.6", 39 | "superagent": "~0.21.0", 40 | "then-jade": "~2.2.1" 41 | }, 42 | "devDependencies": { 43 | "babel": "^4.4.5", 44 | "babel-core": "^4.4.5", 45 | "babel-loader": "^4.0.0", 46 | "nodemon": "~1.3.6", 47 | "react-hot-loader": "~1.1.4", 48 | "source-map-support": "~0.2.9", 49 | "webpack": "~1.5.3", 50 | "webpack-dev-server": "~1.7.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acdlite/flummox-isomorphic-demo/b6f67f0928052f34537e73c17a29c8567f026b82/preview.png -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acdlite/flummox-isomorphic-demo/b6f67f0928052f34537e73c17a29c8567f026b82/src/app.js -------------------------------------------------------------------------------- /src/client/entry.js: -------------------------------------------------------------------------------- 1 | require('../shared/init'); 2 | 3 | import React from 'react'; 4 | import Router from 'react-router'; 5 | import routes from '../shared/routes'; 6 | 7 | import performRouteHandlerStaticMethod from '../shared/performRouteHandlerStaticMethod'; 8 | 9 | import Flux from '../shared/Flux'; 10 | 11 | /** 12 | * On the client, we only need one instance of Flux, created at page load. This 13 | * actually is, effectively, a singleton, but since the code has to run on both 14 | * the server and the client, we still need it down to our components as either 15 | * props or context. We'll use context, in case we have deeply-nested views. 16 | * 17 | * TODO: implement dehydration/rehydration 18 | */ 19 | let flux = new Flux(); 20 | 21 | Router.run(routes, Router.HistoryLocation, (Handler, state) => { 22 | 23 | async function run() { 24 | /** 25 | * Like we did on the server, wait for data to be fetched before rendering. 26 | */ 27 | await performRouteHandlerStaticMethod(state.routes, 'routerWillRun', state, flux); 28 | 29 | /** 30 | * Pass flux instance as context 31 | */ 32 | React.withContext( 33 | { flux }, 34 | () => React.render(, document.getElementById('app')) 35 | ); 36 | } 37 | 38 | /** 39 | * Don't gobble errors. (This is the worst feature of promises, IMO.) 40 | */ 41 | run().catch(error => { 42 | throw error; 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/client/init.js: -------------------------------------------------------------------------------- 1 | // Remove 300ms tap delay on mobile devices 2 | import attachFastClick from 'fastclick'; 3 | attachFastClick(document.body); 4 | 5 | // Expose globally 6 | import React from 'react'; 7 | window.React = React; 8 | -------------------------------------------------------------------------------- /src/server/app.js: -------------------------------------------------------------------------------- 1 | // Initialization 2 | require('../shared/init'); 3 | import sourceMapSupport from 'source-map-support'; 4 | sourceMapSupport.install(); 5 | 6 | // Create koa app 7 | import koa from 'koa'; 8 | let app = koa(); 9 | export default app; 10 | 11 | // Middleware 12 | import routes from './routes'; 13 | routes(app); 14 | 15 | // Start listening 16 | let port = process.env.PORT || 3000; 17 | app.listen(port); 18 | console.log(`App started listening on port ${port}`); 19 | -------------------------------------------------------------------------------- /src/server/routes/index.js: -------------------------------------------------------------------------------- 1 | import middleware from './middleware'; 2 | import appIndex from './views/appIndex'; 3 | 4 | export default function(app) { 5 | middleware(app); 6 | appIndex(app); 7 | } 8 | -------------------------------------------------------------------------------- /src/server/routes/middleware.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import gzip from 'koa-gzip'; 4 | import fresh from 'koa-fresh'; 5 | import conditional from 'koa-conditional-get'; 6 | import etag from 'koa-etag'; 7 | import serve from 'koa-static'; 8 | import views from 'koa-views'; 9 | import router from 'koa-router'; 10 | 11 | export default function(app) { 12 | // Error handling 13 | app.use(function *(next) { 14 | try { 15 | yield next; 16 | } catch (error) { 17 | this.status = error.status || 500; 18 | this.body = error.message; 19 | this.app.emit('error', error, this); 20 | } 21 | }); 22 | 23 | // gzip compression 24 | app.use(gzip()); 25 | 26 | // Conditional GET 27 | app.use(conditional()); 28 | 29 | // Freshness testing 30 | app.use(fresh()); 31 | 32 | // etags 33 | app.use(etag()); 34 | 35 | // Serve static assets from `public` directory 36 | app.use(serve('public')); 37 | 38 | // Add jade rendering 39 | app.use(views(path.join(process.cwd(), 'views'), { 40 | cache: true, 41 | default: 'jade', 42 | })); 43 | 44 | app.use(router(app)); 45 | } 46 | -------------------------------------------------------------------------------- /src/server/routes/views/appIndex.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Router from 'react-router'; 3 | import routes from '../../../shared/routes'; 4 | 5 | import DocumentTitle from 'react-document-title'; 6 | 7 | /** 8 | * Import the app's Flux class. Note that this returns a class, NOT a singleton 9 | * like in most Flux libraries. 10 | */ 11 | import Flux from '../../../shared/Flux'; 12 | 13 | import performRouteHandlerStaticMethod from '../../../shared/performRouteHandlerStaticMethod'; 14 | 15 | export default function(app) { 16 | app.get(/.*/, function *() { 17 | 18 | let { Handler, state } = yield new Promise((resolve, reject) => { 19 | Router.run(routes, this.url, (Handler, state) => resolve({ Handler, state })); 20 | }); 21 | 22 | /** 23 | * Create a new flux instance on every request 24 | */ 25 | let flux = new Flux(); 26 | 27 | /** 28 | * Wait for stores to fetch data before continuing. 29 | */ 30 | yield performRouteHandlerStaticMethod(state.routes, 'routerWillRun', state, flux); 31 | 32 | /** 33 | * Add flux instance to context so deeply-nested views can easily access it. 34 | * Note that "context" here refers to React contexts, not Fluxible contexts. 35 | * Just want to be super clear in case you're coming from that library :) 36 | * For more info on contexts, see: 37 | * https://www.tildedave.com/2014/11/15/introduction-to-contexts-in-react-js.html 38 | */ 39 | let appString = React.withContext( 40 | { flux }, 41 | () => React.renderToString() 42 | ); 43 | 44 | /** 45 | * Cool library that lets us extract a title from the React component tree 46 | * so we can render it on the server, which is very important for SEO 47 | */ 48 | let title = DocumentTitle.rewind(); 49 | 50 | /** 51 | * Pass the initial render of the app to a Jade template 52 | */ 53 | yield this.render('app', { 54 | title, 55 | appString, 56 | env: process.env, 57 | }); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /src/server/webpack.js: -------------------------------------------------------------------------------- 1 | require('../shared/init'); 2 | 3 | import webpack from 'webpack'; 4 | import WebpackDevServer from 'webpack-dev-server'; 5 | import config from '../../webpack.config.dev'; 6 | 7 | new WebpackDevServer(webpack(config), { 8 | publicPath: config.output.publicPath, 9 | hot: true, 10 | }) 11 | .listen(8080, 'localhost', function (err, result) { 12 | if (err) console.log(err); 13 | 14 | console.log('Dev server listening at localhost:8080'); 15 | }); 16 | -------------------------------------------------------------------------------- /src/shared/Flux.js: -------------------------------------------------------------------------------- 1 | import { Flummox } from 'flummox'; 2 | 3 | import StargazerActions from './actions/StargazerActions'; 4 | import StargazerStore from './stores/StargazerStore'; 5 | 6 | export default class Flux extends Flummox { 7 | constructor() { 8 | super(); 9 | 10 | this.createActions('stargazers', StargazerActions); 11 | this.createStore('stargazers', StargazerStore, this); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/shared/actions/StargazerActions.js: -------------------------------------------------------------------------------- 1 | import { Actions } from 'flummox'; 2 | import request from 'superagent'; 3 | 4 | export default class StargazerActions extends Actions { 5 | 6 | async getStargazersByRepo(owner, repo) { 7 | let response = await request 8 | .get(`https://api.github.com/repos/${owner}/${repo}/stargazers`) 9 | .query({ 10 | per_page: 50, 11 | }) 12 | .exec(); 13 | 14 | return { 15 | owner, 16 | repo, 17 | stargazers: response.body, 18 | }; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/shared/components/AppHandler.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RouteHandler } from 'react-router'; 3 | 4 | let AppHandler = React.createClass({ 5 | 6 | render() { 7 | return ( 8 |
9 |
10 |

Isomorphic Flummox App

11 |

12 | This is an simple app demonstrating how to use Flummox and 13 | react-router to create isomorphic React applications. 14 |

15 |

16 | It's a work in progress. Right now, it shows the first 50 stargazers 17 | for a given GitHub repo. Pretty bare-bones, as you can see, but it 18 | gets the basic idea across. 19 |

20 |

21 | Check out the page source to see that HTML is being rendered on the 22 | server. 23 |

24 |
25 |
26 | 27 |
28 |
29 | ); 30 | }, 31 | 32 | }); 33 | 34 | export default AppHandler; 35 | -------------------------------------------------------------------------------- /src/shared/components/StargazerGridHandler.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { State } from 'react-router'; 3 | import Immutable from 'immutable'; 4 | 5 | import StargazerGridView from './StargazerGridView'; 6 | 7 | let StargazerGrid = React.createClass({ 8 | 9 | mixins: [State], 10 | 11 | statics: { 12 | async routerWillRun(state, flux) { 13 | let { owner, repo } = state.params; 14 | let stargazerActions = flux.getActions('stargazers'); 15 | 16 | return await stargazerActions.getStargazersByRepo(owner, repo); 17 | } 18 | }, 19 | 20 | contextTypes: { 21 | flux: React.PropTypes.object.isRequired, 22 | }, 23 | 24 | getInitialState() { 25 | this.stargazerStore = this.context.flux.getStore('stargazers'); 26 | 27 | let { owner, repo } = this.getParams(); 28 | 29 | return { 30 | stargazers: this.stargazerStore.getStargazersByRepo(owner, repo), 31 | }; 32 | }, 33 | 34 | componentDidMount() { 35 | this.stargazerStore.addListener('change', this.onStargazerStoreChange); 36 | }, 37 | 38 | componentWillUnmount() { 39 | this.stargazerStore.removeListener('change', this.onStargazerStoreChange); 40 | }, 41 | 42 | onStargazerStoreChange() { 43 | let { owner, repo } = this.getParams(); 44 | 45 | this.setState({ 46 | stargazers: this.stargazerStore.getStargazersByRepo(owner, repo), 47 | }); 48 | }, 49 | 50 | render() { 51 | let { stargazers } = this.state; 52 | let { owner, repo } = this.getParams(); 53 | 54 | if (!Immutable.List.isList(stargazers)) return 'No stargazers found'; 55 | 56 | return ( 57 | 62 | ); 63 | } 64 | 65 | }); 66 | 67 | export default StargazerGrid; 68 | -------------------------------------------------------------------------------- /src/shared/components/StargazerGridView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Immutable from 'immutable'; 3 | 4 | let StargazerGridView = React.createClass({ 5 | 6 | getDefaultProps() { 7 | return { 8 | stargazers: Immutable.List(), 9 | }; 10 | }, 11 | 12 | render() { 13 | let items = this.props.stargazers 14 | .toArray() 15 | .map(stargazer => ); 16 | 17 | return ( 18 |
19 | {items} 20 |
21 | ); 22 | }, 23 | 24 | }); 25 | 26 | let StargazerItem = React.createClass({ 27 | 28 | render() { 29 | let { stargazer } = this.props; 30 | 31 | return ( 32 | 40 | ); 41 | }, 42 | 43 | }); 44 | 45 | export default StargazerGridView; 46 | -------------------------------------------------------------------------------- /src/shared/init.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Polyfills 3 | */ 4 | // require('babel/runtime'); 5 | import 'babel/polyfill'; 6 | 7 | /* 8 | * Superagent promisification 9 | */ 10 | import { Request } from 'superagent'; 11 | 12 | Request.prototype.exec = function() { 13 | let req = this; 14 | 15 | return new Promise ((resolve, reject) => { 16 | req.end((error, res) => { 17 | if (error) return reject(error); 18 | resolve(res); 19 | }); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /src/shared/performRouteHandlerStaticMethod.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Accepts an array of matched routes as returned from react-router's 3 | * `Router.run()` and calls the given static method on each. The methods may 4 | * return a promise. 5 | * 6 | * Returns a promise that resolves after any promises returned by the routes 7 | * resolve. The practical uptake is that you can wait for your data to be 8 | * fetched before continuing. Based off react-router's async-data example 9 | * https://github.com/rackt/react-router/blob/master/examples/async-data/app.js#L121 10 | * @param {array} routes - Matched routes 11 | * @param {string} methodName - Name of static method to call 12 | * @param {...any} ...args - Arguments to pass to the static method 13 | */ 14 | export default async function performRouteHandlerStaticMethod(routes, methodName, ...args) { 15 | return Promise.all(routes 16 | .map(route => route.handler[methodName]) 17 | .filter(method => typeof method === 'function') 18 | .map(method => method(...args)) 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/shared/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, DefaultRoute } from 'react-router'; 3 | import AppHandler from './components/AppHandler'; 4 | import StargazerGridHandler from './components/StargazerGridHandler'; 5 | 6 | let Routes = ( 7 | 8 | 9 | 10 | ); 11 | 12 | export default Routes; 13 | -------------------------------------------------------------------------------- /src/shared/stores/StargazerStore.js: -------------------------------------------------------------------------------- 1 | import { Store } from 'flummox'; 2 | import Immutable from 'immutable'; 3 | 4 | export default class StargazersStore extends Store { 5 | 6 | constructor(flux) { 7 | super(); 8 | 9 | let stargazerActionIds = flux.getActionIds('stargazers'); 10 | 11 | this.register(stargazerActionIds.getStargazersByRepo, this.handleGetStargazersByRepo); 12 | 13 | this.state = { 14 | stargazers: Immutable.Map(), 15 | stargazersByRepo: Immutable.Map(), 16 | }; 17 | } 18 | 19 | handleGetStargazersByRepo({ owner, repo, stargazers }) { 20 | let key = repoKey(owner, repo); 21 | 22 | stargazers = Immutable.fromJS(stargazers); 23 | 24 | let stargazersMap = stargazers.reduce((result, stargazer) => { 25 | result = result.set(stargazer.get('id'), stargazer); 26 | return result; 27 | }, Immutable.Map()); 28 | 29 | this.setState({ 30 | stargazersByRepo: this.state.stargazersByRepo.set(key, stargazers), 31 | stargazers: this.state.stargazers.merge(stargazersMap), 32 | }); 33 | } 34 | 35 | getStargazersByRepo(owner, repo) { 36 | let key = repoKey(owner, repo); 37 | return this.state.stargazersByRepo.get(key); 38 | } 39 | 40 | } 41 | 42 | function repoKey(owner, repo) { 43 | return `${owner}/${repo}`; 44 | } 45 | -------------------------------------------------------------------------------- /views/app.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(class='no-js') 3 | head 4 | meta(charset='utf-8') 5 | meta(http-equiv='X-UA-Compatible', content='IE=edge') 6 | 7 | title=title 8 | 9 | meta(name='description', content='') 10 | meta(name='viewport', content='width=device-width, initial-scale=1') 11 | 12 | body 13 | #app!=appString 14 | 15 | script. 16 | WebFontConfig = { 17 | google: { 18 | families: [ ], 19 | }, 20 | }; 21 | 22 | if (env.NODE_ENV == 'development') 23 | 24 | script(src='http://0.0.0.0:8080/js/app.js', defer) 25 | else 26 | script(src='/js/app.min.js', defer) 27 | -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack'); 4 | 5 | module.exports = { 6 | devtool: 'inline-source-map', 7 | entry: [ 8 | 'webpack-dev-server/client?http://localhost:8080', 9 | 'webpack/hot/only-dev-server', 10 | './src/client/entry', 11 | ], 12 | output: { 13 | path: __dirname + '/public/js/', 14 | filename: 'app.js', 15 | publicPath: 'http://localhost:8080/js/', 16 | }, 17 | plugins: [ 18 | new webpack.HotModuleReplacementPlugin(), 19 | new webpack.NoErrorsPlugin(), 20 | ], 21 | resolve: { 22 | extensions: ['', '.js'] 23 | }, 24 | module: { 25 | loaders: [ 26 | { test: /\.jsx?$/, loaders: ['react-hot', 'babel-loader?experimental'], exclude: /node_modules/ } 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack'); 4 | 5 | module.exports = { 6 | entry: './src/client/entry', 7 | output: { 8 | path: __dirname + '/public/js/', 9 | filename: 'app.min.js', 10 | publicPath: '/js/' 11 | }, 12 | plugins: [ 13 | new webpack.optimize.UglifyJsPlugin({ 14 | compress: { 15 | warnings: false 16 | } 17 | }), 18 | ], 19 | resolve: { 20 | extensions: ['', '.js'] 21 | }, 22 | module: { 23 | loaders: [ 24 | { test: /\.jsx?$/, loaders: ['babel-loader?experimental'], exclude: /node_modules/ } 25 | ] 26 | } 27 | }; 28 | --------------------------------------------------------------------------------