├── tmp └── .gitkeep ├── Procfile ├── config ├── test.json ├── integration.json ├── production.json ├── development.json ├── env.json ├── application.json └── webpack │ ├── test.js │ ├── development.js │ └── production.js ├── Staticfile ├── app ├── stylesheets │ ├── application.scss │ ├── _layout.scss │ └── postcss.config.js ├── store.js ├── config.js ├── api │ └── fake_posts_api.js ├── dispatchers │ ├── api_dispatcher.js │ └── main_dispatcher.js ├── components │ ├── todo_item.js │ ├── todo_list.js │ ├── user_list_page.js │ ├── use_router.js │ ├── todo_page.js │ ├── api_page.js │ ├── todo_adder.js │ ├── layout.js │ ├── user_create_page.js │ ├── application.js │ └── router.js ├── index.jsx └── index.js ├── Procfile.dev ├── .env ├── manifest.yml ├── spec ├── app │ ├── index.js │ ├── components │ │ ├── todo_item_spec.js │ │ ├── user_list_page_spec.js │ │ ├── todo_list_spec.js │ │ ├── api_page_spec.js │ │ ├── todo_page_spec.js │ │ ├── todo_adder_spec.js │ │ ├── application_spec.js │ │ ├── user_create_page_spec.js │ │ ├── use_router_spec.js │ │ └── router_spec.js │ ├── support │ │ ├── mock_router_spec.js │ │ ├── mock_router.js │ │ └── dispatcher_matchers.js │ ├── dispatchers │ │ ├── main_dispatcher_spec.js │ │ └── api_dispatcher_spec.js │ ├── api │ │ └── fake_posts_api_spec.js │ └── spec_helper.js ├── factories │ └── user.js ├── support │ ├── bluebird.js │ ├── mock_fetch.js │ └── deferred.js ├── integration │ ├── support │ │ ├── selenium.js │ │ └── jasmine_webdriver.js │ ├── features_spec.js │ ├── spec_helper.js │ └── helpers │ │ └── webdriver_helper.js └── spec_helper.js ├── helpers ├── application_helper.js └── fetch_helper.js ├── .gitignore ├── .cfignore ├── tasks ├── default.js ├── react_tools.js ├── deploy.js ├── dev_server.js ├── server.js └── integration.js ├── gulpfile.js ├── server ├── env.js ├── app.js └── bootstrap.js ├── .travis.yml ├── .babelrc ├── index.js ├── .react-tools ├── LICENSE ├── .eslintrc ├── package.json └── README.md /tmp/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start -------------------------------------------------------------------------------- /config/test.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /Staticfile: -------------------------------------------------------------------------------- 1 | pushstate: enabled 2 | 3 | -------------------------------------------------------------------------------- /config/integration.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /config/production.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /config/development.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /config/env.json: -------------------------------------------------------------------------------- 1 | [ 2 | "FOO" 3 | ] 4 | -------------------------------------------------------------------------------- /app/stylesheets/application.scss: -------------------------------------------------------------------------------- 1 | @import '_layout'; 2 | -------------------------------------------------------------------------------- /app/store.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | todoItems: [], 3 | users: [] 4 | }; 5 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: gulp s 2 | jasmine: gulp jasmine 3 | assets: PORT=3000 gulp dev-server -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | { 2 | "NODE_ENV": "development", 3 | "PORT": 3000, 4 | "API_PORT": 3001 5 | } 6 | -------------------------------------------------------------------------------- /app/stylesheets/_layout.scss: -------------------------------------------------------------------------------- 1 | .pui-react-starter { 2 | padding: 20px; 3 | color: darkblue; 4 | } -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: react-starter 4 | path: public 5 | memory: 64MB 6 | -------------------------------------------------------------------------------- /app/stylesheets/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'autoprefixer': {}, 4 | } 5 | }; -------------------------------------------------------------------------------- /config/application.json: -------------------------------------------------------------------------------- 1 | { 2 | "globalNamespace": "MyReactStarter", 3 | "title": "My React Starter" 4 | } 5 | -------------------------------------------------------------------------------- /spec/app/index.js: -------------------------------------------------------------------------------- 1 | const specs = require.context('../app', true, /_spec\.js$/); 2 | specs.keys().forEach(specs); 3 | -------------------------------------------------------------------------------- /spec/factories/user.js: -------------------------------------------------------------------------------- 1 | const Factory = require('rosie').Factory; 2 | 3 | Factory.define('user') 4 | .sequence('name', id => `Bob ${id}`); 5 | -------------------------------------------------------------------------------- /helpers/application_helper.js: -------------------------------------------------------------------------------- 1 | const helpers = { 2 | compact(array) { 3 | return array.filter(Boolean); 4 | } 5 | }; 6 | 7 | module.exports = helpers; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env.json 2 | .idea 3 | /config/local.json 4 | /dist/* 5 | /node_modules 6 | /public/* 7 | /logs/* 8 | /tmp/* 9 | .DS_Store 10 | npm-debug.log 11 | -------------------------------------------------------------------------------- /.cfignore: -------------------------------------------------------------------------------- 1 | .env 2 | .idea 3 | /app/* 4 | /config/* 5 | /lib/* 6 | /logs/* 7 | /node_modules 8 | /scripts/* 9 | /server/* 10 | /spec/* 11 | /tasks/* 12 | /tmp/* 13 | -------------------------------------------------------------------------------- /tasks/default.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const runSequence = require('run-sequence'); 3 | 4 | gulp.task('default', cb => runSequence('lint', 'spec-app', 'spec-integration', cb)); 5 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register'); 2 | require('babel-polyfill'); 3 | 4 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 5 | 6 | const requireDir = require('require-dir'); 7 | requireDir('./tasks'); -------------------------------------------------------------------------------- /spec/support/bluebird.js: -------------------------------------------------------------------------------- 1 | const Bluebird = require('bluebird'); 2 | Bluebird.prototype.catch = function(...args) { 3 | return Bluebird.prototype.then.call(this, i => i, ...args); 4 | }; 5 | global.Promise = Bluebird; 6 | -------------------------------------------------------------------------------- /app/config.js: -------------------------------------------------------------------------------- 1 | const config = require('../pui-react-tools/config')(); 2 | 3 | const {globalNamespace = 'Application'} = config; 4 | 5 | module.exports = (function() { 6 | `window.${globalNamespace} = {config: ${config}}`; 7 | })(); 8 | -------------------------------------------------------------------------------- /server/env.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | try { 3 | Object.entries(require('../.env.json')) 4 | .filter(([key]) => !(key in process.env)) 5 | .forEach(([key, value]) => process.env[key] = value); 6 | } catch(e) { 7 | } 8 | }; -------------------------------------------------------------------------------- /spec/integration/support/selenium.js: -------------------------------------------------------------------------------- 1 | const seleniumStandalone = require('selenium-standalone'); 2 | const thenify = require('thenify'); 3 | 4 | module.exports = { 5 | install: thenify(seleniumStandalone.install), 6 | start: thenify(seleniumStandalone.start) 7 | }; 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 6.9.1 3 | before_script: 4 | - "export DISPLAY=:99.0" 5 | - "sh -e /etc/init.d/xvfb start" 6 | - sleep 3 # give xvfb some time to start 7 | script: 8 | - BROWSER=firefox gulp 9 | cache: 10 | yarn: true 11 | directories: 12 | - node_modules -------------------------------------------------------------------------------- /app/api/fake_posts_api.js: -------------------------------------------------------------------------------- 1 | const {fetchJson} = require('../../helpers/fetch_helper'); 2 | 3 | const apiUrl = 'http://jsonplaceholder.typicode.com'; 4 | 5 | const FakePostsApi = { 6 | fetch() { 7 | return fetchJson(`${apiUrl}/posts`); 8 | } 9 | }; 10 | 11 | module.exports = FakePostsApi; -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | export default function (config) { 4 | const app = express(); 5 | 6 | app.get('/config.js', (req, res) => { 7 | res.type('text/javascript').status(200) 8 | .send(`window.${config.globalNamespace} = {config: ${JSON.stringify(config)}}`); 9 | }); 10 | 11 | return app; 12 | }; -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "plugins": [ 5 | "react-hot-loader/babel" 6 | ] 7 | } 8 | }, 9 | "presets": [["es2015", {"loose": true}], "react", "stage-0"], 10 | "plugins": [ 11 | "add-module-exports", 12 | "transform-object-assign", 13 | "transform-react-display-name" 14 | ] 15 | } -------------------------------------------------------------------------------- /spec/spec_helper.js: -------------------------------------------------------------------------------- 1 | require('babel-polyfill'); 2 | const React = require('react'); 3 | const {Factory} = require('rosie'); 4 | 5 | let globals; 6 | 7 | beforeAll(() => { 8 | globals = { 9 | Factory, 10 | React 11 | }; 12 | Object.assign(global, globals); 13 | }); 14 | 15 | afterAll(() => { 16 | Object.keys(globals).forEach(key => delete global[key]); 17 | }); -------------------------------------------------------------------------------- /spec/support/mock_fetch.js: -------------------------------------------------------------------------------- 1 | let isomorphicFetch; 2 | const nativeFetch = global.fetch; 3 | 4 | module.exports = { 5 | install() { 6 | if (!isomorphicFetch) { 7 | global.fetch = null; 8 | isomorphicFetch = require('isomorphic-fetch'); 9 | } 10 | global.fetch = isomorphicFetch; 11 | }, 12 | 13 | uninstall() { 14 | global.fetch = nativeFetch; 15 | } 16 | }; -------------------------------------------------------------------------------- /spec/app/components/todo_item_spec.js: -------------------------------------------------------------------------------- 1 | require('../spec_helper'); 2 | 3 | describe('TodoItem', () => { 4 | beforeEach(() => { 5 | const TodoItem = require('../../../app/components/todo_item'); 6 | ReactDOM.render(, root); 7 | }); 8 | 9 | it('renders the value of the todoitem', () => { 10 | expect('.todo-item').toHaveText('hey'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /app/dispatchers/api_dispatcher.js: -------------------------------------------------------------------------------- 1 | const FakePostsApi = require('../api/fake_posts_api'); 2 | 3 | const ApiDispatcher = { 4 | fetchPosts(){ 5 | return FakePostsApi.fetch().then((data) => { 6 | this.dispatch({type: 'updatePosts', data}); 7 | }); 8 | }, 9 | updatePosts({data}){ 10 | this.$store.merge({posts: data}); 11 | } 12 | }; 13 | 14 | module.exports = ApiDispatcher; 15 | -------------------------------------------------------------------------------- /app/dispatchers/main_dispatcher.js: -------------------------------------------------------------------------------- 1 | const MainDispatcher = { 2 | setRoute({data}) { 3 | this.router.navigate(data); 4 | }, 5 | todoItemCreate({data}) { 6 | this.$store.refine('todoItems').push(data); 7 | }, 8 | userCreate({data}) { 9 | this.$store.refine('users').push(data); 10 | }, 11 | userSet({data}) { 12 | this.$store.merge({userId: data}); 13 | } 14 | }; 15 | 16 | module.exports = MainDispatcher; 17 | -------------------------------------------------------------------------------- /app/components/todo_item.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | import PropTypes from 'prop-types'; 3 | 4 | class TodoItem extends React.Component{ 5 | static propTypes = { 6 | value: PropTypes.node.isRequired 7 | }; 8 | 9 | render() { 10 | const {value} = this.props; 11 | return ( 12 |
  • 13 | {value} 14 |
  • 15 | ); 16 | } 17 | } 18 | 19 | module.exports = TodoItem; 20 | -------------------------------------------------------------------------------- /spec/app/components/user_list_page_spec.js: -------------------------------------------------------------------------------- 1 | require('../spec_helper'); 2 | 3 | describe('UserListPage', () => { 4 | beforeEach(() => { 5 | const UserListPage = require('../../../app/components/user_list_page'); 6 | const users = [Factory.build('user', {name: 'Felix'})]; 7 | ReactDOM.render(, root); 8 | }); 9 | 10 | it('renders the users', () => { 11 | expect('.user-list').toContainText('Felix'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /app/index.jsx: -------------------------------------------------------------------------------- 1 | import ReactDOMServer from 'react-dom/server'; 2 | import Layout from './components/layout'; 3 | import React from 'react'; 4 | import Application from './components/application'; 5 | 6 | export default function Index(props, done) { 7 | const {config = {}} = props; 8 | const html = `${ReactDOMServer.renderToStaticMarkup()}`; 9 | if (!done) return html; 10 | done(null, html); 11 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-disable no-var */ 3 | var recluster = require('recluster'); 4 | var path = require('path'); 5 | var cluster = recluster(path.join(__dirname, 'server', 'bootstrap.js'), {readyWhen: 'ready', workers: 1}); 6 | /* eslint-enable no-var */ 7 | cluster.run(); 8 | process.on('SIGUSR2', function() { 9 | console.log('Got SIGUSR2, reloading cluster...'); 10 | cluster.reload(); 11 | }); 12 | console.log('spawned cluster, kill -s SIGUSR2', process.pid, 'to reload'); 13 | -------------------------------------------------------------------------------- /tasks/react_tools.js: -------------------------------------------------------------------------------- 1 | import {Assets, Foreman, Jasmine, Lint} from 'pui-react-tools'; 2 | import test from '../config/webpack/test'; 3 | import development from '../config/webpack/development'; 4 | import production from '../config/webpack/production'; 5 | 6 | Assets.install({ 7 | webpack: { 8 | development, 9 | production, 10 | integration: production 11 | } 12 | }); 13 | 14 | Foreman.install(); 15 | Lint.install(); 16 | 17 | Jasmine.install({ 18 | webpack: {test} 19 | }); 20 | -------------------------------------------------------------------------------- /spec/app/components/todo_list_spec.js: -------------------------------------------------------------------------------- 1 | require('../spec_helper'); 2 | 3 | describe('TodoList', () => { 4 | beforeEach(() => { 5 | const TodoList = require('../../../app/components/todo_list'); 6 | ReactDOM.render(, root); 7 | }); 8 | 9 | it('renders the todolist', () => { 10 | expect('.todo-item').toHaveLength(2); 11 | expect('.todo-item:eq(0)').toHaveText('do this'); 12 | expect('.todo-item:eq(1)').toHaveText('do that'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /.react-tools: -------------------------------------------------------------------------------- 1 | const base = require('pui-react-tools/webpack/base'); 2 | const development = require('pui-react-tools/webpack/development'); 3 | 4 | module.exports = { 5 | webpack: { 6 | base: {...base, entry: {application: './app/index.js'}}, 7 | development: { 8 | entry: { 9 | application: ['react-hot-loader/patch', 'webpack-hot-middleware/client', './app/index.js'] 10 | }, 11 | plugins: development.plugins 12 | }, 13 | integration: { 14 | devtool: 'source-map', 15 | } 16 | } 17 | }; -------------------------------------------------------------------------------- /server/bootstrap.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register'); 2 | require('babel-polyfill'); 3 | 4 | /* eslint-disable no-var */ 5 | let app = require('./app')(require('pui-react-tools/assets/config')()); 6 | /* eslint-enable no-var */ 7 | let apiPort = process.env.API_PORT || process.env.PORT || 3001; 8 | /* eslint-disable no-console */ 9 | console.log(`API listening on ${apiPort}`); 10 | /* eslint-enable no-console */ 11 | 12 | app.listen(apiPort, function() { 13 | process.send && process.send({cmd: 'ready'}); 14 | }); 15 | 16 | module.exports = app; 17 | -------------------------------------------------------------------------------- /spec/app/components/api_page_spec.js: -------------------------------------------------------------------------------- 1 | require('../spec_helper'); 2 | 3 | describe('ApiPage', () => { 4 | beforeEach(() => { 5 | const ApiPage = require('../../../app/components/api_page'); 6 | ReactDOM.render(, root); 7 | }); 8 | 9 | it('fetches posts', () => { 10 | expect('fetchPosts').toHaveBeenDispatched(); 11 | }); 12 | 13 | it('renders the post titles result from the api', () => { 14 | expect('.api-page').toContainText('bar'); 15 | expect('.api-page').toContainText('baz'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /app/components/todo_list.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const TodoItem = require('./todo_item'); 3 | import PropTypes from 'prop-types'; 4 | 5 | class TodoList extends React.Component { 6 | static propTypes = { 7 | todoItems: PropTypes.array.isRequired 8 | }; 9 | 10 | render() { 11 | const {todoItems} = this.props; 12 | const todoItemsList = todoItems.map((item, key) => ()); 13 | 14 | return ( 15 |
      16 | {todoItemsList} 17 |
    18 | ); 19 | } 20 | } 21 | 22 | module.exports = TodoList; 23 | -------------------------------------------------------------------------------- /app/components/user_list_page.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | import PropTypes from 'prop-types'; 3 | 4 | class UserListPage extends React.Component { 5 | static propTypes = { 6 | users: PropTypes.array 7 | }; 8 | 9 | render() { 10 | const {users} = this.props; 11 | const userItems = users.map((user, key) =>
  • User name: {user.name}
  • ); 12 | return ( 13 |
    14 |

    List of Users

    15 |
      {userItems}
    16 |
    17 | ); 18 | } 19 | } 20 | 21 | module.exports = UserListPage; 22 | -------------------------------------------------------------------------------- /spec/integration/features_spec.js: -------------------------------------------------------------------------------- 1 | require('./spec_helper'); 2 | 3 | describeWithWebdriver('Features', () => { 4 | let page; 5 | 6 | describe('when viewing the app', () => { 7 | beforeEach.async(async() => { 8 | page = (await visit('/')).page; 9 | await waitForExist(page, '.pui-react-starter'); 10 | }); 11 | 12 | it.async('can add a todoItem', async() => { 13 | await setValue(page, '.todo-adder input', 'DO THIS THING'); 14 | await click(page, '.todo-adder button'); 15 | await waitForText(page, '.todo-list .todo-item', 'DO THIS THING'); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /spec/app/components/todo_page_spec.js: -------------------------------------------------------------------------------- 1 | require('../spec_helper'); 2 | 3 | describe('TodoPage', () => { 4 | beforeEach(() => { 5 | const TodoPage = require('../../../app/components/todo_page'); 6 | ReactDOM.render(, root); 7 | }); 8 | 9 | it('renders a title', () => { 10 | expect('.title').toHaveText('the title'); 11 | }); 12 | 13 | it('renders the todolist', () => { 14 | expect('.todo-item').toHaveLength(2); 15 | expect('.todo-item:eq(0)').toHaveText('do this'); 16 | expect('.todo-item:eq(1)').toHaveText('do that'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /app/components/use_router.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const Grapnel = require('grapnel'); 3 | const {Dispatcher} = require('p-flux'); 4 | 5 | const exports = { 6 | Router: Grapnel, 7 | useRouter: (Component) => class extends React.Component { 8 | constructor(props, context) { 9 | super(props, context); 10 | const {state} = this; 11 | const router = new (exports.Router)({pushState: true}); 12 | Dispatcher.router = router; 13 | this.state = {...state, router}; 14 | } 15 | 16 | render() { 17 | return (); 18 | } 19 | } 20 | }; 21 | 22 | module.exports = exports; 23 | -------------------------------------------------------------------------------- /spec/integration/spec_helper.js: -------------------------------------------------------------------------------- 1 | require('../support/bluebird'); 2 | require('../spec_helper'); 3 | const webdriverHelper = require('./helpers/webdriver_helper'); 4 | const JasmineAsyncSuite = require('jasmine-async-suite'); 5 | 6 | JasmineAsyncSuite.install(); 7 | 8 | const {DEFAULT_TIMEOUT_INTERVAL} = jasmine; 9 | 10 | let globals = {...webdriverHelper}; 11 | Object.assign(global, globals); 12 | 13 | beforeAll(() => { 14 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000; 15 | }); 16 | 17 | afterAll(() => { 18 | jasmine.DEFAULT_TIMEOUT_INTERVAL = DEFAULT_TIMEOUT_INTERVAL; 19 | Object.keys(globals).forEach(key => delete global[key]); 20 | JasmineAsyncSuite.uninstall(); 21 | }); -------------------------------------------------------------------------------- /tasks/deploy.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const runSequence = require('run-sequence'); 3 | const {spawn} = require('child_process'); 4 | 5 | gulp.task('push', (callback) => { 6 | spawn('cf', ['push'], {stdio: 'inherit', env: process.env}).once('close', callback); 7 | }); 8 | 9 | gulp.task('copy-staticfile', () => { 10 | return gulp.src('Staticfile').pipe(gulp.dest('public')); 11 | }); 12 | 13 | gulp.task('deploy', (done) => { 14 | const {NODE_ENV: env} = process.env; 15 | process.env.NODE_ENV = 'production'; 16 | runSequence('clean-assets', 'assets', 'assets-config', 'copy-staticfile', 'push', () => { 17 | process.env.NODE_ENV = env; 18 | done(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /app/components/todo_page.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | import PropTypes from 'prop-types'; 3 | const TodoAdder = require('./todo_adder'); 4 | const TodoList = require('./todo_list'); 5 | 6 | class TodoPage extends React.Component { 7 | static propTypes = { 8 | config: PropTypes.object, 9 | todoItems: PropTypes.array 10 | }; 11 | 12 | render() { 13 | const {config: {title}, todoItems} = this.props; 14 | return ( 15 |
    16 |

    {title}

    17 |

    Things to do

    18 | 19 | 20 |
    21 | ); 22 | } 23 | } 24 | 25 | module.exports = TodoPage; 26 | -------------------------------------------------------------------------------- /app/components/api_page.js: -------------------------------------------------------------------------------- 1 | const {Actions} = require('p-flux'); 2 | const React = require('react'); 3 | import PropTypes from 'prop-types'; 4 | 5 | class ApiPage extends React.Component { 6 | static propTypes = { 7 | posts: PropTypes.array 8 | }; 9 | 10 | componentDidMount() { 11 | Actions.fetchPosts(); 12 | } 13 | 14 | render() { 15 | const {posts = []} = this.props; 16 | const titles = posts.map(({title, id}) =>
    {title}
    ); 17 | return ( 18 |
    19 |

    This page talks to an api

    20 |

    The api has posts with titles:

    21 | {titles} 22 |
    23 | ); 24 | } 25 | } 26 | 27 | module.exports = ApiPage; -------------------------------------------------------------------------------- /spec/support/deferred.js: -------------------------------------------------------------------------------- 1 | const mockPromises = require('mock-promises'); 2 | 3 | const Deferred = function() { 4 | let resolver, rejector; 5 | const promise = new Promise(function(res, rej) { 6 | resolver = res; 7 | rejector = rej; 8 | }); 9 | 10 | const wrapper = Object.assign(promise, { 11 | resolve(...args) { 12 | resolver(...args); 13 | mockPromises.executeForPromise(promise); 14 | return wrapper; 15 | }, 16 | reject(...args) { 17 | rejector(...args); 18 | mockPromises.executeForPromise(promise); 19 | return wrapper; 20 | }, 21 | promise() { 22 | return promise; 23 | } 24 | }); 25 | return wrapper; 26 | }; 27 | 28 | module.exports = Deferred; -------------------------------------------------------------------------------- /spec/app/components/todo_adder_spec.js: -------------------------------------------------------------------------------- 1 | require('../spec_helper'); 2 | 3 | describe('TodoAdder', () => { 4 | beforeEach(() => { 5 | const TodoAdder = require('../../../app/components/todo_adder'); 6 | ReactDOM.render(, root); 7 | }); 8 | 9 | describe('when adding a todo item', () => { 10 | beforeEach(() => { 11 | $('.todo-adder input').val('do this thing').simulate('change'); 12 | $('.todo-adder form').simulate('submit'); 13 | }); 14 | 15 | it('adds the todoItem', () => { 16 | expect('todoItemCreate').toHaveBeenDispatchedWith({data: 'do this thing'}); 17 | }); 18 | 19 | it('clears out the input text', () => { 20 | expect('.todo-adder input').toHaveValue(''); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /spec/app/components/application_spec.js: -------------------------------------------------------------------------------- 1 | require('../spec_helper'); 2 | 3 | describe('Application', () => { 4 | let TodoList; 5 | 6 | beforeEach(() => { 7 | const Application = require('../../../app/components/application'); 8 | TodoList = require('../../../app/components/todo_list'); 9 | spyOn(TodoList.prototype, 'render').and.callThrough(); 10 | const config = {title: 'title'}; 11 | ReactDOM.render(, root); 12 | }); 13 | 14 | it('has a TodoAdder', () => { 15 | expect('.todo-adder').toExist(); 16 | }); 17 | 18 | it('has a TodoList', () => { 19 | expect('.todo-list').toExist(); 20 | }); 21 | 22 | it('has a title', () => { 23 | expect('.title').toHaveText('title'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tasks/dev_server.js: -------------------------------------------------------------------------------- 1 | import WebpackDevServer from 'webpack-dev-server'; 2 | import gulp from 'gulp'; 3 | import webpack from 'webpack'; 4 | 5 | const devServerPort = 3000; 6 | 7 | let server; 8 | function kill() { 9 | if (server) server.close(); 10 | } 11 | 12 | gulp.task('dev-server', done => { 13 | const config = require('../config/webpack/development.js')(); 14 | const compiler = webpack(config); 15 | compiler.plugin('done', () => { 16 | done(); 17 | }); 18 | server = new WebpackDevServer(compiler, config.devServer); 19 | 20 | const port = process.env.PORT || devServerPort; 21 | /* eslint-disable no-console */ 22 | console.log(`dev server listening on port ${port}`); 23 | /* eslint-enable no-console */ 24 | server.listen(port); 25 | }); 26 | 27 | export {kill}; -------------------------------------------------------------------------------- /spec/app/components/user_create_page_spec.js: -------------------------------------------------------------------------------- 1 | require('../spec_helper'); 2 | 3 | describe('UserCreatePage', () => { 4 | beforeEach(() => { 5 | const UserCreatePage = require('../../../app/components/user_create_page'); 6 | ReactDOM.render(, root); 7 | }); 8 | 9 | describe('creating a user', () => { 10 | beforeEach(() => { 11 | $('.user-create-page input').val('Alice').simulate('change'); 12 | $('.user-create-page form').simulate('submit'); 13 | }); 14 | 15 | it('creates a new user', () => { 16 | expect('userCreate').toHaveBeenDispatchedWith({data: {name: 'Alice'}}); 17 | }); 18 | 19 | it('navigates to the user list page', () => { 20 | expect('setRoute').toHaveBeenDispatchedWith({data: '/users/list'}); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /helpers/fetch_helper.js: -------------------------------------------------------------------------------- 1 | function checkStatus(response) { 2 | if (response.status >= 200 && response.status < 400) return response; 3 | const error = new Error(response.statusText); 4 | error.response = response; 5 | throw error; 6 | } 7 | 8 | module.exports = { 9 | fetchJson(url, {accessToken, headers, ...options} = {}) { 10 | require('isomorphic-fetch'); 11 | const acceptHeaders = {accept: 'application/json', 'Content-Type': 'application/json'}; 12 | const authorizationHeaders = accessToken ? {authorization: `Bearer ${accessToken}`} : {}; 13 | options = {credentials: 'same-origin', headers: {...acceptHeaders, ...authorizationHeaders, ...headers}, ...options}; 14 | return fetch(url, options) 15 | .then(checkStatus) 16 | .then(response => [204, 304].includes(response.status) ? {} : response.json()); 17 | } 18 | }; -------------------------------------------------------------------------------- /app/components/todo_adder.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const {Actions} = require('p-flux'); 3 | 4 | class TodoAdder extends React.Component{ 5 | constructor(props, context) { 6 | super(props, context); 7 | this.state = {todoItem: ''}; 8 | } 9 | 10 | submit = e => { 11 | e.preventDefault(); 12 | Actions.todoItemCreate(this.state.todoItem); 13 | this.setState({todoItem: ''}); 14 | }; 15 | 16 | change = e => { 17 | this.setState({[e.currentTarget.name]: e.target.value}); 18 | }; 19 | 20 | render() { 21 | const {todoItem} = this.state; 22 | 23 | return ( 24 |
    25 |
    26 | 27 | 28 |
    29 |
    30 | ); 31 | } 32 | }; 33 | 34 | module.exports = TodoAdder; 35 | -------------------------------------------------------------------------------- /spec/app/components/use_router_spec.js: -------------------------------------------------------------------------------- 1 | require('../spec_helper'); 2 | import PropTypes from 'prop-types'; 3 | 4 | describe('#useRouter', () => { 5 | let routeSpy; 6 | 7 | beforeEach(() => { 8 | const {useRouter} = require('../../../app/components/use_router'); 9 | routeSpy = jasmine.createSpy('route'); 10 | 11 | const Application = ({router}) => { 12 | router.get('/test', routeSpy); 13 | return ( 14 |
    15 | 16 |
    17 | ); 18 | }; 19 | Application.propTypes = { 20 | router: PropTypes.func.isRequired 21 | }; 22 | 23 | const TestRouter = useRouter(Application); 24 | ReactDOM.render(, root); 25 | }); 26 | 27 | it('routes', () => { 28 | $('.application button').simulate('click'); 29 | expect(routeSpy).toHaveBeenCalled(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | const invariant = require('invariant'); 2 | const React = require('react'); 3 | const ReactDOM = require('react-dom'); 4 | const Application = require('./components/application'); 5 | const {AppContainer} = require('react-hot-loader'); 6 | 7 | invariant(global.MyReactStarter, 8 | `globalNamespace in application.json has been changed without updating global variable name. 9 | Please change "MyReactStarter" in app/index.js to your current globalNamespace` 10 | ); 11 | 12 | const {config} = global.MyReactStarter; 13 | ReactDOM.render( 14 | 15 | 16 | , root 17 | ); 18 | 19 | if (module.hot) { 20 | module.hot.accept('./components/application', () => { 21 | const NextApp = require('./components/application'); 22 | ReactDOM.render( 23 | 24 | 25 | , 26 | root 27 | ); 28 | }); 29 | } -------------------------------------------------------------------------------- /app/components/layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ReactDOMServer from 'react-dom/server'; 4 | 5 | export default function Layout({config, children}) { 6 | const configJs = `window.${config.globalNamespace} = {animation: true, config: ${JSON.stringify(config)}}`; 7 | const metas = Layout.metas.map((props, key) => ); 8 | return ( 9 | 10 | {metas} 11 | 12 |
    13 |