├── CHANGELOG.md
├── .eslintignore
├── .gitignore
├── karma.entry.js
├── .babelrc
├── src
├── js
│ ├── ducks
│ │ ├── reducers.js
│ │ ├── counter.js
│ │ └── counter.test.js
│ ├── components
│ │ ├── Reset.js
│ │ ├── Decrement.js
│ │ ├── Increment.js
│ │ ├── Button.js
│ │ ├── Counter.js
│ │ ├── App.test.js
│ │ ├── App.js
│ │ ├── Counter.test.js
│ │ ├── Reset.test.js
│ │ ├── Decrement.test.js
│ │ ├── Increment.test.js
│ │ └── Button.test.js
│ ├── containers
│ │ ├── CounterContainer.js
│ │ ├── ResetContainer.js
│ │ ├── DecrementContainer.js
│ │ ├── IncrementContainer.js
│ │ ├── ResetContainer.test.js
│ │ ├── DecrementContainer.test.js
│ │ └── IncrementContainer.test.js
│ ├── index.js
│ └── store.js
└── index.html
├── karma.config.js
├── webpack.config.js
├── config
├── karma
│ ├── single.js
│ ├── watch.js
│ ├── ci.js
│ └── _base.js
├── webpack
│ ├── stage.js
│ ├── development.js
│ ├── plugins
│ │ └── html-inject.js
│ ├── production.js
│ └── _base.js
└── index.js
├── .travis.yml
├── .eslintrc
├── bin
├── changelog.js
└── webpack-dev-server.js
├── server.babel.js
├── Makefile
├── package.json
└── README.md
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/*
2 | dist/*
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules/
3 | coverage/
4 | npm-debug.log
5 | selenium-debug.log
6 | dist/
7 | .DS_Store
8 |
--------------------------------------------------------------------------------
/karma.entry.js:
--------------------------------------------------------------------------------
1 | const context = require.context('./src/js/', true, /.+\.test\.jsx?$/);
2 | context.keys().forEach(context);
3 | module.exports = context;
4 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react", "es2015", "stage-0"],
3 | "env": {
4 | "development": {
5 | "presets": ["react-hmre"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/js/ducks/reducers.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | import counter from './counter';
4 |
5 | export default combineReducers({
6 | counter,
7 | });
8 |
--------------------------------------------------------------------------------
/karma.config.js:
--------------------------------------------------------------------------------
1 | require('babel-register');
2 | const config = require('./config').default;
3 | module.exports = require('./config/karma/' + config.get('globals').TEST_ENV).default;
4 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | require('babel-register');
2 | const config = require('./config/index').default;
3 | module.exports = require('./config/webpack/' + config.get('globals').NODE_ENV).default;
4 |
--------------------------------------------------------------------------------
/config/karma/single.js:
--------------------------------------------------------------------------------
1 | export default config => {
2 | const base = require('./_base').default(config);
3 |
4 | config.set({
5 | ...base,
6 | autoWatch: false,
7 | singleRun: true,
8 | reporters: ['progress'],
9 | });
10 |
11 | return config;
12 | };
13 |
--------------------------------------------------------------------------------
/src/js/components/Reset.js:
--------------------------------------------------------------------------------
1 | import { h } from 'react-hyperscript-helpers';
2 |
3 | import Button from 'components/Button';
4 |
5 |
6 | const Reset = (props) => (
7 | h(Button, {
8 | ...props,
9 | onClick: props.handleOnClick,
10 | },
11 | 'Reset')
12 | );
13 |
14 |
15 | export default Reset;
16 |
--------------------------------------------------------------------------------
/src/js/components/Decrement.js:
--------------------------------------------------------------------------------
1 | import { h } from 'react-hyperscript-helpers';
2 |
3 | import Button from 'components/Button';
4 |
5 |
6 | const Decrement = (props) => (
7 | h(Button, {
8 | ...props,
9 | onClick: props.handleOnClick,
10 | },
11 | 'Minus')
12 | );
13 |
14 |
15 | export default Decrement;
16 |
--------------------------------------------------------------------------------
/src/js/components/Increment.js:
--------------------------------------------------------------------------------
1 | import { h } from 'react-hyperscript-helpers';
2 |
3 | import Button from 'components/Button';
4 |
5 |
6 | const Increment = (props) => (
7 | h(Button, {
8 | ...props,
9 | onClick: props.handleOnClick,
10 | },
11 | 'Plus')
12 | );
13 |
14 |
15 | export default Increment;
16 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 'stable'
4 | sudo: false
5 | notifications:
6 | email: false
7 | cache:
8 | directories:
9 | - node_modules
10 | before_install:
11 | - npm config set spin false
12 | after_script:
13 | - npm install codecov
14 | - ./node_modules/.bin/codecov < coverage/lcov.info
15 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | React Webpack Example
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/js/components/Button.js:
--------------------------------------------------------------------------------
1 | import { PropTypes } from 'react';
2 | import { button } from 'react-hyperscript-helpers';
3 |
4 | const Button = ({ children, ...rest }) => (
5 | button(
6 | { ...rest },
7 | children
8 | )
9 | );
10 |
11 |
12 | Button.propTypes = {
13 | children: PropTypes.node,
14 | };
15 |
16 |
17 | export default Button;
18 |
--------------------------------------------------------------------------------
/config/karma/watch.js:
--------------------------------------------------------------------------------
1 | export default config => {
2 | const base = require('./_base').default(config);
3 |
4 | config.set({
5 | ...base,
6 | autoWatch: true,
7 | singleRun: false,
8 | reporters: ['mocha'],
9 | mochaReporter: {
10 | output: 'autowatch',
11 | showDiff: true,
12 | },
13 | });
14 |
15 | return config;
16 | };
17 |
--------------------------------------------------------------------------------
/src/js/components/Counter.js:
--------------------------------------------------------------------------------
1 | import { PropTypes } from 'react';
2 | import { div } from 'react-hyperscript-helpers';
3 |
4 | const Counter = ({ count, operation, ...rest }) => (
5 | div(
6 | { ...rest },
7 | `${operation} Count: ${count}`
8 | )
9 | );
10 |
11 |
12 | Counter.propTypes = {
13 | children: PropTypes.node,
14 | };
15 |
16 |
17 | export default Counter;
18 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "parser": "babel-eslint",
4 | "rules": {
5 | "strict": 0
6 | },
7 | "env": {
8 | "browser": true,
9 | "mocha": true,
10 | "jasmine": true
11 | },
12 | "plugins": [
13 | "react"
14 | ],
15 | "globals": {
16 | "__BASE__": true,
17 | "__DEBUG__": true,
18 | "__DEV__": true,
19 | "sinon": true
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/js/containers/CounterContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { h } from 'react-hyperscript-helpers';
3 | import Counter from 'components/Counter';
4 |
5 | const CounterContainer = (props) => h(Counter, props);
6 |
7 | const mapStateToProps = ({ counter: { count, operation } }) => ({ count, operation });
8 |
9 | export default connect(mapStateToProps)(CounterContainer);
10 |
--------------------------------------------------------------------------------
/src/js/index.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 | import ReactDOM from 'react-dom';
3 | import { h } from 'react-hyperscript-helpers';
4 | import { Provider } from 'react-redux';
5 |
6 | import configureStore from './store';
7 | import App from 'components/App';
8 |
9 | const store = configureStore({});
10 |
11 | ReactDOM.render(
12 | h(Provider, { store }, h(App)),
13 | document.getElementById('react-webpack-example')
14 | );
15 |
--------------------------------------------------------------------------------
/bin/changelog.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | var fs = require('graceful-fs');
4 | var join = require('path').join;
5 | var cc = require('conventional-changelog');
6 | var wstream = fs.createWriteStream(join(__dirname, '../CHANGELOG.md'), 'utf-8');
7 |
8 | cc(
9 | {
10 | preset: 'angular',
11 | releaseCount: 0
12 | }, // options
13 | {}, // context
14 | {}, // gitRawCommitsOpts
15 | {}, // parserOpts
16 | {}
17 | )
18 | .pipe(wstream);
19 |
--------------------------------------------------------------------------------
/src/js/components/App.test.js:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import { h } from 'react-hyperscript-helpers';
3 |
4 | import App from './App';
5 |
6 | describe('', () => {
7 | it('should exist', () => {
8 | const wrapper = shallow(h(App));
9 | expect(wrapper.type()).to.equal('div');
10 | });
11 |
12 | it('should contain top level components', () => {
13 | const wrapper = shallow(h(App));
14 | expect(wrapper.children()).to.have.length(4);
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/server.babel.js:
--------------------------------------------------------------------------------
1 | require('babel-register');
2 |
3 | const chalk = require('chalk');
4 | const config = require('./config').default;
5 | const devServer = require('./bin/webpack-dev-server').default;
6 |
7 | const host = config.get('webpack_host');
8 | const port = config.get('webpack_port');
9 |
10 |
11 | devServer.listen(port, host, () => {
12 | console.log(`⚡ Server running at ${chalk.white(`${host}:${port}`)}`);
13 | console.log(` Proxying to API running at ${chalk.white(config.get('proxy'))}`);
14 | });
15 |
--------------------------------------------------------------------------------
/src/js/containers/ResetContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { h } from 'react-hyperscript-helpers';
3 |
4 | import Reset from 'components/Reset';
5 |
6 | import { reset } from 'ducks/counter';
7 |
8 | export const ResetContainer = (props) => (
9 | h(Reset, {
10 | ...props,
11 | handleOnClick: props.reset,
12 | })
13 | );
14 |
15 | export const mapDispatchToProps = (dispatch) => ({
16 | reset: () => dispatch(reset()),
17 | });
18 |
19 | export default connect(
20 | () => ({}),
21 | mapDispatchToProps,
22 | )(ResetContainer);
23 |
--------------------------------------------------------------------------------
/src/js/components/App.js:
--------------------------------------------------------------------------------
1 | import { div, h } from 'react-hyperscript-helpers';
2 |
3 | import IncrementContainer from 'containers/IncrementContainer';
4 | import DecrementContainer from 'containers/DecrementContainer';
5 | import ResetContainer from 'containers/ResetContainer';
6 | import CounterContainer from 'containers/CounterContainer';
7 |
8 |
9 | const App = () => (
10 | div({
11 | children: [
12 | h(IncrementContainer),
13 | h(DecrementContainer),
14 | h(ResetContainer),
15 | h(CounterContainer),
16 | ],
17 | })
18 | );
19 |
20 | export default App;
21 |
--------------------------------------------------------------------------------
/src/js/components/Counter.test.js:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import { div, h } from 'react-hyperscript-helpers';
3 |
4 | import Counter from './Counter';
5 |
6 | describe('', () => {
7 | it('should exist', () => {
8 | const wrapper = shallow(h(Counter));
9 | expect(wrapper.type()).to.equal('div');
10 | });
11 |
12 | it('should take props', () => {
13 | const wrapper = shallow(h(Counter, {
14 | operation: 'Increment',
15 | count: 5,
16 | }, ''));
17 | expect(wrapper.equals(div('Increment Count: 5'))).to.equal(true);
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/js/containers/DecrementContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { h } from 'react-hyperscript-helpers';
3 |
4 | import Decrement from 'components/Decrement';
5 |
6 | import { decrement } from 'ducks/counter';
7 |
8 | export const DecrementContainer = (props) => (
9 | h(Decrement, {
10 | ...props,
11 | handleOnClick: props.decrement,
12 | })
13 | );
14 |
15 | export const mapDispatchToProps = (dispatch) => ({
16 | decrement: () => dispatch(decrement()),
17 | });
18 |
19 | export default connect(
20 | () => ({}),
21 | mapDispatchToProps,
22 | )(DecrementContainer);
23 |
--------------------------------------------------------------------------------
/src/js/containers/IncrementContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { h } from 'react-hyperscript-helpers';
3 |
4 | import Increment from 'components/Increment';
5 |
6 | import { increment } from 'ducks/counter';
7 |
8 | export const IncrementContainer = (props) => (
9 | h(Increment, {
10 | ...props,
11 | handleOnClick: props.increment,
12 | })
13 | );
14 |
15 | export const mapDispatchToProps = (dispatch) => ({
16 | increment: () => dispatch(increment()),
17 | });
18 |
19 | export default connect(
20 | () => ({}),
21 | mapDispatchToProps,
22 | )(IncrementContainer);
23 |
--------------------------------------------------------------------------------
/src/js/components/Reset.test.js:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import { h } from 'react-hyperscript-helpers';
3 |
4 | import Button from 'components/Button';
5 | import Reset from './Reset';
6 |
7 | describe('', () => {
8 | it('should exist', () => {
9 | const wrapper = shallow(h(Reset));
10 | expect(wrapper.type()).to.equal(Button);
11 | });
12 |
13 | it('should handle onClick', () => {
14 | const props = {
15 | handleOnClick: sinon.spy(),
16 | };
17 | shallow(h(Reset, props)).simulate('click');
18 | expect(props.handleOnClick.called).to.equal(true);
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/js/components/Decrement.test.js:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import { h } from 'react-hyperscript-helpers';
3 |
4 | import Button from 'components/Button';
5 | import Decrement from './Decrement';
6 |
7 | describe('', () => {
8 | it('should exist', () => {
9 | const wrapper = shallow(h(Decrement));
10 | expect(wrapper.type()).to.equal(Button);
11 | });
12 |
13 | it('should handle onClick', () => {
14 | const props = {
15 | handleOnClick: sinon.spy(),
16 | };
17 | shallow(h(Decrement, props)).simulate('click');
18 | expect(props.handleOnClick.called).to.equal(true);
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/js/components/Increment.test.js:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import { h } from 'react-hyperscript-helpers';
3 |
4 | import Button from 'components/Button';
5 | import Increment from './Increment';
6 |
7 | describe('', () => {
8 | it('should exist', () => {
9 | const wrapper = shallow(h(Increment));
10 | expect(wrapper.type()).to.equal(Button);
11 | });
12 |
13 | it('should handle onClick', () => {
14 | const props = {
15 | handleOnClick: sinon.spy(),
16 | };
17 | shallow(h(Increment, props)).simulate('click');
18 | expect(props.handleOnClick.called).to.equal(true);
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/config/webpack/stage.js:
--------------------------------------------------------------------------------
1 | import webpack from 'webpack';
2 |
3 | import HtmlInject from './plugins/html-inject';
4 |
5 | import config from '../';
6 | import webpackConfig from './_base';
7 |
8 | const LIBS_BUNDLE = 'libs';
9 |
10 | export default {
11 | ...webpackConfig,
12 | entry: {
13 | ...webpackConfig.entry,
14 | [LIBS_BUNDLE]: config.get('dependencies')
15 | },
16 | output: {
17 | ...webpackConfig.output,
18 | filename: '[name].[hash].js',
19 | chunkFilename: '[id].js'
20 | },
21 | plugins: [
22 | ...webpackConfig.plugins,
23 | new webpack.optimize.CommonsChunkPlugin(LIBS_BUNDLE, `${LIBS_BUNDLE}.[hash].js`),
24 | new HtmlInject()
25 | ]
26 | };
27 |
--------------------------------------------------------------------------------
/src/js/containers/ResetContainer.test.js:
--------------------------------------------------------------------------------
1 | import { shallow, mount } from 'enzyme';
2 | import { h } from 'react-hyperscript-helpers';
3 |
4 | import Reset from 'components/Reset';
5 | import { ResetContainer } from './ResetContainer';
6 |
7 | describe('', () => {
8 | it('should exist', () => {
9 | const wrapper = shallow(h(ResetContainer));
10 | expect(wrapper.type()).to.equal(Reset);
11 | });
12 |
13 | it('should handle onClick', () => {
14 | const props = {
15 | reset: sinon.spy(),
16 | };
17 | const wrapper = mount(h(ResetContainer, props));
18 | wrapper.find(Reset).simulate('click');
19 | expect(props.reset.called).to.equal(true);
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/js/components/Button.test.js:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import { span, button, h } from 'react-hyperscript-helpers';
3 |
4 | import Button from './Button';
5 |
6 | describe('', () => {
7 | it('should exist', () => {
8 | const wrapper = shallow(h(Button));
9 | expect(wrapper.type()).to.equal('button');
10 | });
11 |
12 | it('should allow text label', () => {
13 | const wrapper = shallow(h(Button, 'children'));
14 | expect(wrapper.equals(button('children'))).to.equal(true);
15 | });
16 |
17 | it('should allow dom children', () => {
18 | const wrapper = shallow(h(Button, {}, span('foo')));
19 | expect(wrapper.children().equals(span('foo'))).to.equal(true);
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/js/containers/DecrementContainer.test.js:
--------------------------------------------------------------------------------
1 | import { shallow, mount } from 'enzyme';
2 | import { h } from 'react-hyperscript-helpers';
3 |
4 | import Decrement from 'components/Decrement';
5 | import { DecrementContainer } from './DecrementContainer';
6 |
7 | describe('', () => {
8 | it('should exist', () => {
9 | const wrapper = shallow(h(DecrementContainer));
10 | expect(wrapper.type()).to.equal(Decrement);
11 | });
12 |
13 | it('should handle onClick', () => {
14 | const props = {
15 | decrement: sinon.spy(),
16 | };
17 | const wrapper = mount(h(DecrementContainer, props));
18 | wrapper.find(Decrement).simulate('click');
19 | expect(props.decrement.called).to.equal(true);
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/js/containers/IncrementContainer.test.js:
--------------------------------------------------------------------------------
1 | import { shallow, mount } from 'enzyme';
2 | import { h } from 'react-hyperscript-helpers';
3 |
4 | import Increment from 'components/Increment';
5 | import { IncrementContainer } from './IncrementContainer';
6 |
7 | describe('', () => {
8 | it('should exist', () => {
9 | const wrapper = shallow(h(IncrementContainer));
10 | expect(wrapper.type()).to.equal(Increment);
11 | });
12 |
13 | it('should handle onClick', () => {
14 | const props = {
15 | increment: sinon.spy(),
16 | };
17 | const wrapper = mount(h(IncrementContainer, props));
18 | wrapper.find(Increment).simulate('click');
19 | expect(props.increment.called).to.equal(true);
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/config/webpack/development.js:
--------------------------------------------------------------------------------
1 | import webpack from 'webpack';
2 |
3 | import config from '../';
4 | import webpackConfig from './_base';
5 |
6 | const devServer = {
7 | contentBase: config.get('dir_src'),
8 | stats: {
9 | colors: true,
10 | hash: false,
11 | timings: true,
12 | chunks: false,
13 | chunkModules: false,
14 | modules: false,
15 | },
16 | publicPath: webpackConfig.output.publicPath,
17 | };
18 |
19 | export default {
20 | ...webpackConfig,
21 | devtool: 'cheap-module-eval-source-map',
22 | entry: {
23 | ...webpackConfig.entry,
24 | bundle: [
25 | 'webpack-hot-middleware/client?reload=true',
26 | ...webpackConfig.entry.bundle,
27 | ],
28 | },
29 | plugins: [
30 | new webpack.HotModuleReplacementPlugin(),
31 | ...webpackConfig.plugins,
32 | ],
33 | devServer,
34 | };
35 |
--------------------------------------------------------------------------------
/src/js/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 |
3 | import reducers from 'ducks/reducers';
4 |
5 | let middleware = [];
6 | // $FlowIgnore
7 | if (__DEBUG__) {
8 | // $FlowIgnore
9 | const createLogger = require('redux-logger');
10 | middleware = [...middleware, createLogger({
11 | collapsed: true,
12 | })];
13 | }
14 |
15 | const finalCreateStore = compose(
16 | applyMiddleware(...middleware),
17 | window.devToolsExtension ? window.devToolsExtension() : f => f
18 | )(createStore);
19 |
20 | export default function configureStore(initialState: Object): Object {
21 | const store = finalCreateStore(reducers, initialState);
22 | if (__DEV__ && module.hot) {
23 | // Enable Webpack hot module replacement for reducers
24 | module.hot.accept('ducks/reducers', () => {
25 | const nextRootReducer = require('ducks/reducers');
26 | store.replaceReducer(nextRootReducer);
27 | });
28 | }
29 | return store;
30 | }
31 |
--------------------------------------------------------------------------------
/src/js/ducks/counter.js:
--------------------------------------------------------------------------------
1 | export const COUNTER_INCREMENT = 'COUNTER_INCREMENT';
2 | export const COUNTER_DECREMENT = 'COUNTER_DECREMENT';
3 | export const COUNTER_RESET = 'COUNTER_RESET';
4 |
5 | export const increment = () => ({ type: COUNTER_INCREMENT });
6 | export const decrement = () => ({ type: COUNTER_DECREMENT });
7 | export const reset = () => ({ type: COUNTER_RESET });
8 |
9 | export const INITIAL_STATE = {
10 | operation: COUNTER_RESET,
11 | count: 0,
12 | };
13 |
14 | export default function reducer(state = INITIAL_STATE, action) {
15 | switch (action.type) {
16 | case COUNTER_INCREMENT:
17 | return {
18 | operation: COUNTER_INCREMENT,
19 | count: state.count + 1,
20 | };
21 | case COUNTER_DECREMENT:
22 | return {
23 | operation: COUNTER_DECREMENT,
24 | count: state.count - 1,
25 | };
26 | case COUNTER_RESET:
27 | return {
28 | operation: COUNTER_RESET,
29 | count: 0,
30 | };
31 | default:
32 | return state;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/config/karma/ci.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | export default config => {
4 | const single = require('./single').default(config);
5 |
6 | config.set({
7 | ...single,
8 | reporters: [...single.reporters, 'coverage'],
9 | plugins: [...single.plugins, 'karma-coverage'],
10 | coverageReporter: {
11 | dir: 'coverage',
12 | reporters: [
13 | { type: 'lcov', subdir: '.', file: 'lcov.info' },
14 | ],
15 | },
16 | webpack: {
17 | ...single.webpack,
18 | isparta: {
19 | embedSource: true,
20 | noAutoWrap: true,
21 | },
22 | module: {
23 | ...single.webpack.module,
24 | preLoaders: [
25 | ...single.webpack.module.preLoaders,
26 | // transpile and instrument testing files with isparta
27 | {
28 | test: /\.js$/,
29 | include: path.resolve('src/js/'),
30 | exclude: /test.js$/,
31 | loader: 'isparta',
32 | },
33 | ],
34 | },
35 | },
36 | });
37 |
38 | return config;
39 | };
40 |
--------------------------------------------------------------------------------
/config/webpack/plugins/html-inject.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import fs from 'graceful-fs';
3 |
4 | import config from '../..';
5 | import webpackConfig from '../_base';
6 |
7 | function HtmlInject() {}
8 | HtmlInject.prototype.apply = compiler => {
9 | compiler.plugin('done', stats => {
10 | const assets = stats.toJson().assets.filter(a => a.name.endsWith('.js'));
11 | assets.reverse();
12 |
13 | const buffer = fs.readFileSync(path.join(config.get('dir_src'), 'index.html'), 'utf8');
14 | const html = buffer.replace(
15 | /[\s\S]*/,
16 | assets.map(asset => ``).join('')
17 | );
18 | fs.writeFileSync(path.join(config.get('dir_dist'), config.get('globals').__BASE__, 'index.html'), html, 'utf8');
19 |
20 | // const favico = fs.readFileSync(path.join(config.get('dir_src'), 'favicon.ico'));
21 | // fs.writeFileSync(path.join(config.get('dir_dist'), config.get('globals').__BASE__, 'favicon.ico'), favico);
22 | });
23 | };
24 |
25 | export default HtmlInject;
26 |
--------------------------------------------------------------------------------
/config/webpack/production.js:
--------------------------------------------------------------------------------
1 | import webpack from 'webpack';
2 | import CompressionPlugin from 'compression-webpack-plugin';
3 |
4 | import webpackConfig from './stage';
5 |
6 | export default {
7 | ...webpackConfig,
8 | bail: true,
9 | debug: false,
10 | profile: false,
11 | pathInfo: false,
12 | output: {
13 | ...webpackConfig.output,
14 | pathInfo: false,
15 | },
16 | plugins: [
17 | ...webpackConfig.plugins,
18 | new webpack.optimize.DedupePlugin(),
19 | new webpack.optimize.UglifyJsPlugin({
20 | mangle: {
21 | except: ['require', 'export', '$super'],
22 | },
23 | compress: {
24 | warnings: false,
25 | sequences: true,
26 | dead_code: true,
27 | conditionals: true,
28 | booleans: true,
29 | unused: true,
30 | if_return: true,
31 | join_vars: true,
32 | drop_console: false,
33 | },
34 | }),
35 | new CompressionPlugin({
36 | asset: '[path].gz[query]',
37 | algorithm: 'gzip',
38 | test: /\.js$|\.html$/,
39 | threshold: 10240,
40 | minRatio: 0.8,
41 | }),
42 | ],
43 | };
44 |
--------------------------------------------------------------------------------
/config/karma/_base.js:
--------------------------------------------------------------------------------
1 | import webpack from '../webpack/development';
2 |
3 | const KARMA_ENTRY_FILE = 'karma.entry.js';
4 |
5 | export default config => {
6 | config.set({
7 | browsers: ['PhantomJS'],
8 | // karma only needs to know about the test bundle
9 | files: [
10 | 'node_modules/babel-polyfill/dist/polyfill.js',
11 | 'node_modules/phantomjs-polyfill/bind-polyfill.js',
12 | KARMA_ENTRY_FILE,
13 | ],
14 | // run the bundle through the webpack and sourcemap plugins
15 | preprocessors: {
16 | [KARMA_ENTRY_FILE]: ['webpack', 'sourcemap'],
17 | },
18 | frameworks: ['chai-sinon', 'mocha'],
19 | plugins: [
20 | 'karma-phantomjs-launcher',
21 | 'karma-chai-sinon',
22 | 'karma-mocha',
23 | 'karma-mocha-reporter',
24 | 'karma-sourcemap-loader',
25 | 'karma-webpack',
26 | ],
27 | colors: true,
28 |
29 | // level of logging
30 | // possible values:
31 | // LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
32 | logLevel: config.LOG_INFO,
33 |
34 | webpack,
35 | webpackMiddleware: {
36 | ...webpack.devServer,
37 | quiet: true,
38 | },
39 | });
40 |
41 | return config;
42 | };
43 |
--------------------------------------------------------------------------------
/bin/webpack-dev-server.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import express from 'express';
3 | import proxy from 'express-http-proxy';
4 | import gzipStatic from 'connect-gzip-static';
5 | import webpack from 'webpack';
6 |
7 | import config from '../config';
8 | import webpackConfig from '../webpack.config';
9 |
10 | const app = express();
11 |
12 | const isDevelopment = config.get('env').NODE_ENV === 'development';
13 | const staticDir = config.get(isDevelopment ? 'dir_src' : 'dir_dist');
14 | const indexFile = path.join(isDevelopment ? '' : config.get('globals').__BASE__, 'index.html');
15 |
16 | app.use(gzipStatic(staticDir));
17 |
18 | if (isDevelopment) {
19 | const compiler = webpack(webpackConfig);
20 |
21 | app.use(require('webpack-dev-middleware')(compiler, webpackConfig.devServer));
22 | app.use(require('webpack-hot-middleware')(compiler));
23 |
24 | console.log('⌛ Webpack bundling assets for the first time...');
25 | }
26 |
27 | app.use('/api', proxy(config.get('proxy'), {
28 | forwardPath: req => (
29 | require('url').parse(req.url).path
30 | ),
31 | }));
32 |
33 | app.get(/^((?!(.js|.css|.ico)).)*$/, (req, res) => {
34 | res.sendFile(path.join(staticDir, indexFile));
35 | });
36 |
37 | export default app;
38 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PROJECT = "React Webpack Example"
2 |
3 | PATH := node_modules/.bin:$(PATH)
4 | SHELL := /bin/bash
5 |
6 | ifndef VERBOSE
7 | Q := @
8 | NIL := > /dev/null 2>&1
9 | endif
10 |
11 | NODE_ENV ?= development
12 |
13 | NO_COLOR=\033[0m
14 | OK_COLOR=\033[32;01m
15 | OK_STRING=$(OK_COLOR)[OK]$(NO_COLOR)
16 | AWK_CMD = awk '{ printf "%-30s %-10s\n",$$1, $$2; }'
17 | PRINT_OK = printf "$@ $(OK_STRING)\n" | $(AWK_CMD)
18 | NODE_ENV_STRING = $(OK_COLOR)[$(NODE_ENV)]$(NO_COLOR)
19 | PRINT_ENV = printf "$@ $(NODE_ENV_STRING)\n" | $(AWK_CMD)
20 |
21 | all: install dist
22 |
23 | server:
24 | $(Q) node server.babel
25 |
26 | install:
27 | $(Q) npm install --loglevel error
28 | @$(PRINT_OK)
29 |
30 | build: clean-dist
31 | @$(PRINT_ENV)
32 | $(Q) webpack
33 | @$(PRINT_OK)
34 |
35 | dist: prepare build
36 | @$(PRINT_OK)
37 |
38 | prepare: lint test-once
39 | @$(PRINT_OK)
40 |
41 | test:
42 | TEST_ENV=watch karma start karma.config.js
43 |
44 | test-once:
45 | TEST_ENV=single karma start karma.config.js
46 | @$(PRINT_OK)
47 |
48 | lint:
49 | $(Q) eslint src --ext .js,.jsx
50 | @$(PRINT_OK)
51 |
52 | clean: clean-dist clean-deps
53 | @$(PRINT_OK)
54 |
55 | clean-dist:
56 | $(Q) rm -rf dist
57 | @$(PRINT_OK)
58 |
59 | clean-deps:
60 | $(Q) rm -rf node_modules
61 | @$(PRINT_OK)
62 |
63 | update:
64 | $(Q) david
65 | @$(PRINT_OK)
66 |
67 | upgrade:
68 | $(Q) david update
69 | @$(PRINT_OK)
70 |
71 | logs:
72 | $(Q) node ./bin/changelog.js
73 | @$(PRINT_OK)
74 |
75 | .PHONY: install start server build dist prepare test test-once lint clean clean-dist clean-deps update upgrade logs
76 |
--------------------------------------------------------------------------------
/src/js/ducks/counter.test.js:
--------------------------------------------------------------------------------
1 | import * as counter from './counter';
2 | const reducer = counter.default;
3 |
4 | describe('counter', () => {
5 | describe('actions', () => {
6 | it('should create action to increment count', () => {
7 | expect(counter.increment()).to.deep.equal({ type: counter.COUNTER_INCREMENT });
8 | });
9 | it('should create action to decrement count', () => {
10 | expect(counter.decrement()).to.deep.equal({ type: counter.COUNTER_DECREMENT });
11 | });
12 | it('should create action to reset count', () => {
13 | expect(counter.reset()).to.deep.equal({ type: counter.COUNTER_RESET });
14 | });
15 | });
16 |
17 | describe('reducer', () => {
18 | it('should return initial state', () => {
19 | expect(reducer(undefined, {})).to.deep.equal(counter.INITIAL_STATE);
20 | });
21 | it('should handle COUNTER_INCREMENT', () => {
22 | expect(
23 | reducer(counter.INITIAL_STATE, { type: counter.COUNTER_INCREMENT })
24 | ).to.deep.equal(
25 | { operation: counter.COUNTER_INCREMENT, count: 1 }
26 | );
27 | });
28 | it('should handle COUNTER_DECREMENT', () => {
29 | expect(
30 | reducer(counter.INITIAL_STATE, { type: counter.COUNTER_DECREMENT })
31 | ).to.deep.equal(
32 | { operation: counter.COUNTER_DECREMENT, count: -1 }
33 | );
34 | });
35 | it('should handle COUNTER_RESET', () => {
36 | expect(
37 | reducer({ count: 55 }, { type: counter.COUNTER_RESET })
38 | ).to.deep.equal(
39 | { operation: counter.COUNTER_RESET, count: 0 }
40 | );
41 | });
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/config/webpack/_base.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import webpack from 'webpack';
3 |
4 | import config from '../';
5 |
6 | export default {
7 | target: 'web',
8 | devtool: '#source-map',
9 | entry: {
10 | bundle: [path.join(config.get('dir_src'), 'js', 'index.js')],
11 | },
12 | output: {
13 | path: path.join(config.get('dir_dist'), config.get('globals').__BASE__, 'js'),
14 | pathInfo: true,
15 | publicPath: `/${path.join(config.get('globals').__BASE__, 'js/')}`,
16 | filename: 'bundle.js',
17 | },
18 | module: {
19 | preLoaders: [],
20 | loaders: [
21 | {
22 | test: /\.jsx?$/,
23 | loader: 'babel',
24 | exclude: ['node_modules'],
25 | include: `${config.get('dir_src')}/js`,
26 | },
27 | {
28 | test: /\.json$/,
29 | loader: 'json',
30 | },
31 | ],
32 | noParse: [/\.min\.js$/],
33 | },
34 | resolve: {
35 | extentions: ['', '.js', '.jsx'],
36 | modulesDirectories: ['web_modules', 'node_modules'],
37 | alias: {
38 | react: path.resolve(path.join(config.get('path_project'), 'node_modules', 'react')),
39 | ducks: path.resolve(path.join(config.get('path_project'), 'src', 'js', 'ducks')),
40 | components: path.resolve(path.join(config.get('path_project'), 'src', 'js', 'components')),
41 | containers: path.resolve(path.join(config.get('path_project'), 'src', 'js', 'containers')),
42 | },
43 | },
44 | plugins: [
45 | new webpack.DefinePlugin({
46 | 'process.env': config.get('globals')['process.env'],
47 | __DEV__: JSON.stringify(config.get('globals').__DEV__),
48 | __PROD__: JSON.stringify(config.get('globals').__PROD__),
49 | __DEBUG__: JSON.stringify(config.get('globals').__DEBUG__),
50 | __BASE__: JSON.stringify(config.get('globals').__BASE__),
51 | }),
52 | ],
53 | };
54 |
--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | const config = new Map();
4 |
5 | // ------------------------------------
6 | // Project
7 | // ------------------------------------
8 | config.set('path_project', path.resolve(__dirname, '..'));
9 |
10 | // ------------------------------------
11 | // User Configuration
12 | // ------------------------------------
13 | // NOTE: Due to limitations with Webpack's custom require, which is used for
14 | // looking up all *.spec.js files, if you edit dir_test you must also edit
15 | // the path in ~/karma.entry.js.
16 | config.set('dir_src', path.join(config.get('path_project'), 'src'));
17 | config.set('dir_dist', path.join(config.get('path_project'), 'dist'));
18 |
19 | config.set('webpack_host', process.env.HOST || 'localhost');
20 | config.set('webpack_port', process.env.PORT || 8080);
21 | config.set('proxy', process.env.PROXY || 'http://localhost:5000');
22 |
23 | /* *********************************************
24 | -------------------------------------------------
25 |
26 | All Internal Configuration Below
27 | Edit at Your Own Risk
28 |
29 | -------------------------------------------------
30 | ************************************************/
31 | // ------------------------------------
32 | // Environment
33 | // ------------------------------------
34 | config.set('env', process.env);
35 | config.set('globals', {
36 | 'process.env': {
37 | NODE_ENV: JSON.stringify(process.env.NODE_ENV),
38 | },
39 | NODE_ENV: process.env.NODE_ENV || 'stage',
40 | __DEV__: process.env.NODE_ENV === 'development',
41 | __PROD__: process.env.NODE_ENV === 'production',
42 | __DEBUG__: process.env.NODE_ENV === 'development' && parseInt(process.env.DEBUG, 10) === 1,
43 | TEST_ENV: process.env.CI ? 'ci' : (process.env.TEST_ENV || 'single'),
44 | __BASE__: process.env.BASE || '',
45 | });
46 |
47 | // ------------------------------------
48 | // Webpack
49 | // ------------------------------------
50 | config.set('webpack_public_path',
51 | `http://${config.get('webpack_host')}:${config.get('webpack_port')}/`
52 | );
53 |
54 | // ------------------------------------
55 | // Utilities
56 | // ------------------------------------
57 | const packageJSON = require(path.join(config.get('path_project'), 'package.json'));
58 | const dependencies = Object.keys(packageJSON.dependencies);
59 |
60 | config.set('dependencies', dependencies);
61 |
62 | export default config;
63 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-webpack-example",
3 | "private": true,
4 | "version": "0.0.1",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/shanewilson/react-webpack-example.git"
8 | },
9 | "homepage": "https://github.com/shanewilson/react-webpack-example",
10 | "author": {
11 | "name": "Shane Wilson",
12 | "email": "shanezilla@gmail.com",
13 | "url": "https://github.com/shanewilson/"
14 | },
15 | "dependencies": {
16 | "react": "^15.0.0",
17 | "react-dom": "^15.0.0"
18 | },
19 | "devDependencies": {
20 | "babel": "^6.3.26",
21 | "babel-core": "^6.4.0",
22 | "babel-eslint": "^6.0.0-beta.6",
23 | "babel-loader": "^6.2.1",
24 | "babel-plugin-lodash": "^2.1.0",
25 | "babel-plugin-react-transform": "^2.0.0",
26 | "babel-plugin-transform-regenerator": "^6.6.0",
27 | "babel-polyfill": "^6.3.14",
28 | "babel-preset-es2015": "^6.3.13",
29 | "babel-preset-react": "^6.3.13",
30 | "babel-preset-react-hmre": "^1.0.1",
31 | "babel-preset-stage-0": "^6.3.13",
32 | "babel-register": "^6.3.13",
33 | "babel-runtime": "^6.3.19",
34 | "chai": "^3.5.0",
35 | "chalk": "^1.1.1",
36 | "compression-webpack-plugin": "^0.3.0",
37 | "connect-gzip-static": "^1.0.0",
38 | "conventional-changelog": "^1.1.0",
39 | "david": "^7.0.1",
40 | "enzyme": "^2.1.0",
41 | "escope": "^3.5.0",
42 | "eslint": "^2.2.0",
43 | "eslint-config-airbnb": "^6.0.2",
44 | "eslint-loader": "^1.3.0",
45 | "eslint-plugin-babel": "^3.0.0",
46 | "eslint-plugin-react": "^4.1.0",
47 | "expect": "^1.14.0",
48 | "express": "^4.13.4",
49 | "express-http-proxy": "^0.6.0",
50 | "extract-text-webpack-plugin": "^1.0.1",
51 | "graceful-fs": "^4.1.3",
52 | "isparta-instrumenter-loader": "^1.0.0",
53 | "isparta-loader": "^2.0.0",
54 | "json-loader": "^0.5.4",
55 | "karma": "^0.13.21",
56 | "karma-chai": "^0.1.0",
57 | "karma-chai-sinon": "^0.1.5",
58 | "karma-chrome-launcher": "^0.2.2",
59 | "karma-cli": "^0.1.2",
60 | "karma-coverage": "^0.5.5",
61 | "karma-mocha": "^0.2.2",
62 | "karma-mocha-reporter": "^2.0.0",
63 | "karma-phantomjs-launcher": "^1.0.0",
64 | "karma-sauce-launcher": "^0.3.0",
65 | "karma-sourcemap-loader": "^0.3.7",
66 | "karma-webpack": "^1.7.0",
67 | "mocha": "^2.4.5",
68 | "phantomjs-polyfill": "^0.0.2",
69 | "phantomjs-prebuilt": "^2.1.4",
70 | "react-addons-test-utils": "^15.0.0",
71 | "react-hot-loader": "^1.3.0",
72 | "react-hyperscript-helpers": "^1.0.1",
73 | "react-redux": "^4.4.1",
74 | "react-transform-catch-errors": "^1.0.2",
75 | "react-transform-hmr": "^1.0.2",
76 | "redbox-react": "^1.2.2",
77 | "redux": "^3.3.1",
78 | "redux-logger": "^2.6.1",
79 | "sinon": "^1.17.3",
80 | "sinon-chai": "^2.8.0",
81 | "webpack": "^1.12.14",
82 | "webpack-dev-middleware": "^1.5.1",
83 | "webpack-dev-server": "^1.14.1",
84 | "webpack-hot-middleware": "^2.8.1"
85 | },
86 | "engines": {
87 | "node": ">=0.10.0"
88 | },
89 | "scripts": {
90 | "start": "NODE_ENV=development make -j test server",
91 | "server:dev": "NODE_ENV=development make server",
92 | "server:stage": "NODE_ENV=stage make build server",
93 | "server:prod": "NODE_ENV=production make build server",
94 | "build": "make build",
95 | "test": "make prepare"
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | `Buzzwords: #redux #reactjs #webpack #es6 #babeljs #hyperscript #enzyme`
2 |
3 | - [Version with PostCSS](https://github.com/shanewilson/react-webpack-example/tree/e461a63c7b09d1f57c895be187159caa8ed82fba)
4 | - [Version with Stylus](https://github.com/shanewilson/react-webpack-example/tree/64e435063f6e9f8aa880965f7ea5099d28e7bf50)
5 | - [Version with Gulp](https://github.com/shanewilson/react-webpack-example/tree/8132c077870d41fbb08c9b2562b6204ea5cc4a75)
6 | - [Version with Browser-sync](https://github.com/shanewilson/react-webpack-example/tree/d7d251bea5935ceafdd89700ad6ff986c32c506c)
7 |
8 | Technologies
9 | =
10 |
11 | - [React](http://facebook.github.io/react/) - A Javascript Library For Building User Interfaces
12 | - [Redux](http://redux.js.org/) - Redux is a predictable state container for JavaScript apps.
13 | - [Webpack](http://webpack.github.io/) - Module Bundler
14 | - [Babel](https://babeljs.io/) - Babel will turn your ES6+ code into ES5 friendly code
15 | - [Enzyme](http://airbnb.io/enzyme/) - makes it easier to assert, manipulate, and traverse your React Components.
16 |
17 | Development
18 | =
19 |
20 | The development server is setup using Webpack
21 |
22 | ```
23 | ❯ npm start
24 | ...
25 | TEST_ENV=watch karma start karma.config.js
26 | ⌛ Webpack bundling assets for the first time...
27 | ⚡ Server running at localhost:8080
28 | Proxying to API running at http://localhost:5000
29 | webpack built f93ce65a51c93393a327 in 1759ms
30 | Version: webpack 1.12.14
31 | Time: 1759ms
32 | Asset Size Chunks Chunk Names
33 | bundle.js 2.86 MB 0 [emitted] bundle
34 | webpack: bundle is now VALID.
35 | 02 04 2016 01:58:39.968:WARN [karma]: No captured browser, open http://localhost:9876/
36 | 02 04 2016 01:58:39.975:INFO [karma]: Karma v0.13.22 server started at http://localhost:9876/
37 | 02 04 2016 01:58:39.979:INFO [launcher]: Starting browser PhantomJS
38 | 02 04 2016 01:58:40.471:INFO [PhantomJS 2.1.1 (Mac OS X 0.0.0)]: Connected on socket /#dNDHkKK04rMIhQEGAAAA with id 10265082
39 | ...
40 | SUMMARY:
41 | ✔ 26 tests completed
42 | ```
43 |
44 | Tests
45 | =
46 |
47 | Unit tests are run using Karma.
48 |
49 | ```
50 | ❯ npm test
51 | ...
52 | 02 04 2016 01:59:43.148:INFO [karma]: Karma v0.13.22 server started at http://localhost:9876/
53 | 02 04 2016 01:59:43.154:INFO [launcher]: Starting browser PhantomJS
54 | 02 04 2016 01:59:43.617:INFO [PhantomJS 2.1.1 (Mac OS X 0.0.0)]: Connected on socket /#OVwa16Xpcdld6HxyAAAA with id 84226691
55 | PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 26 of 26 SUCCESS (0.037 secs / 0.02 secs)
56 | ```
57 |
58 | Production
59 | =
60 |
61 | Webpack bundles all the assets in production mode
62 |
63 | ```
64 | ❯ NODE_ENV=production npm run build
65 | ...
66 | Hash: 581483788fa821923595
67 | Version: webpack 1.12.14
68 | Time: 6040ms
69 | Asset Size Chunks Chunk Names
70 | bundle.581483788fa821923595.js 106 kB 0 [emitted] bundle
71 | libs.581483788fa821923595.js 133 kB 1 [emitted] libs
72 | bundle.581483788fa821923595.js.map 847 kB 0 [emitted] bundle
73 | libs.581483788fa821923595.js.map 1.55 MB 1 [emitted] libs
74 | bundle.581483788fa821923595.js.gz 32.7 kB [emitted]
75 | libs.581483788fa821923595.js.gz 38.3 kB [emitted]
76 | [0] multi bundle 28 bytes {0} [built]
77 | [0] multi libs 40 bytes {1} [built]
78 | + 482 hidden modules
79 | ```
80 |
81 | Resources
82 | =
83 |
84 | - https://github.com/petehunt/webpack-howto
85 |
--------------------------------------------------------------------------------