├── app ├── images │ └── bg.png ├── styles │ ├── about.css │ ├── navbar.css │ ├── global.css │ └── todos.css ├── store │ └── index.js ├── components │ ├── not-found.jsx │ ├── about.jsx │ ├── todos │ │ ├── finished-todo.jsx │ │ ├── pending-todo.jsx │ │ ├── create.jsx │ │ └── index.jsx │ └── app.jsx ├── routes.jsx ├── actions │ └── todos.js └── index.js ├── .eslintignore ├── spec ├── helpers │ ├── setup-browser-env.js │ └── mount.js ├── core │ ├── hydrate.spec.js │ ├── provide-insert-css.spec.js │ ├── fetch-data.spec.js │ └── connect.spec.js └── app.spec.js ├── circle.yml ├── .gitignore ├── core ├── fetch-data.js ├── hydrate.js ├── config.js ├── provide-insert-css.js ├── connect.js └── universal-render.js ├── webpack ├── test.config.babel.js ├── server.config.babel.js ├── shared.config.js └── client.config.babel.js ├── run ├── dev.js └── utils │ ├── start-webpack-server.js │ ├── write-stats.js │ └── start-render-server.js ├── constants.js ├── .babelrc ├── server ├── server-html.jsx └── koa.js ├── .eslintrc ├── package.json └── README.md /app/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iam4x/futureRX/HEAD/app/images/bg.png -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | server/build.js 3 | node_modules/ 4 | coverage/ 5 | .nyc_output/ 6 | -------------------------------------------------------------------------------- /app/styles/about.css: -------------------------------------------------------------------------------- 1 | .aboutContainer { 2 | background-color: #FFF; 3 | padding: 20px; 4 | } 5 | -------------------------------------------------------------------------------- /app/styles/navbar.css: -------------------------------------------------------------------------------- 1 | .github { 2 | padding-top: 12px; 3 | 4 | iframe:first-child { 5 | margin-right: 15px; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /spec/helpers/setup-browser-env.js: -------------------------------------------------------------------------------- 1 | global.document = require('jsdom').jsdom('') 2 | global.window = document.defaultView 3 | global.navigator = window.navigator 4 | -------------------------------------------------------------------------------- /app/store/index.js: -------------------------------------------------------------------------------- 1 | import MobxStore from 'mobx-store' 2 | 3 | const defaultState = { 4 | todos: [] 5 | } 6 | 7 | export default (state = defaultState) => new MobxStore(state) 8 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 6.3.0 4 | dependencies: 5 | pre: 6 | - npm set progress=false 7 | test: 8 | post: 9 | - npm run coverage 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/webpack-stats.json 2 | coverage 3 | node_modules 4 | npm-debug.log 5 | dist 6 | .tmp 7 | .DS_Store 8 | .sass-cache 9 | .env 10 | .c9 11 | .nyc_output 12 | server/build.js 13 | coverage.lcov 14 | -------------------------------------------------------------------------------- /app/components/not-found.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | 4 | const NotFound = () => ( 5 |
6 |

Page not found

7 | Back to home 8 |
9 | ) 10 | 11 | export default NotFound 12 | -------------------------------------------------------------------------------- /core/fetch-data.js: -------------------------------------------------------------------------------- 1 | import { isFunction } from 'lodash' 2 | 3 | export default async (store, { components, params, location: { query } }) => 4 | await Promise.all( 5 | components 6 | .filter(c => isFunction(c.fetchData)) 7 | .map(c => c.fetchData(store, params, query)) 8 | ) 9 | -------------------------------------------------------------------------------- /core/hydrate.js: -------------------------------------------------------------------------------- 1 | import { each } from 'lodash' 2 | import jsonStringifySafe from 'json-stringify-safe' 3 | 4 | export const dehydrate = (store) => jsonStringifySafe(store.contents()) 5 | 6 | export const rehydrate = (store) => each( 7 | window.__appState__, 8 | (data, storeKey) => store.set(storeKey, data) 9 | ) 10 | -------------------------------------------------------------------------------- /app/styles/global.css: -------------------------------------------------------------------------------- 1 | /* PUT YOUR GLOBAL SCOPE CSS HERE, it should only be external CSS */ 2 | @import 'normalize.css/normalize'; 3 | @import 'bootstrap/dist/css/bootstrap'; 4 | 5 | html, 6 | body { 7 | min-height: 100%; 8 | width: 100%; 9 | } 10 | 11 | body { 12 | background: #EEE; 13 | padding-top: 70px; 14 | padding-bottom: 20px; 15 | } 16 | -------------------------------------------------------------------------------- /app/routes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route } from 'react-router' 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /webpack/test.config.babel.js: -------------------------------------------------------------------------------- 1 | export default { 2 | devtool: 'inline-source-map', 3 | module: { 4 | loaders: [ 5 | { test: /\.json$/, loader: 'json' }, 6 | { test: /\.css$/, loader: 'css?modules!postcss' }, 7 | { test: /\.md$/, loader: 'raw' } 8 | ] 9 | }, 10 | output: { libraryTarget: 'commonjs2' }, 11 | resolve: { 12 | extensions: [ '', '.js', '.json', '.jsx' ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /run/dev.js: -------------------------------------------------------------------------------- 1 | process.env.BABEL_ENV = 'browser' 2 | process.env.NODE_ENV = 'development' 3 | 4 | require('babel-register') 5 | require('babel-polyfill') 6 | 7 | const debug = require('debug') 8 | 9 | const startRenderServer = require('./utils/start-render-server') 10 | const startWebpackServer = require('./utils/start-webpack-server') 11 | 12 | debug.enable('dev,koa') 13 | 14 | startWebpackServer(startRenderServer) 15 | -------------------------------------------------------------------------------- /core/config.js: -------------------------------------------------------------------------------- 1 | const { PORT, NODE_ENV } = process.env 2 | 3 | const APP_CONFIG = { 4 | shared: { 5 | 6 | }, 7 | 8 | development: { 9 | PORT: parseInt(PORT, 10) || 3000 10 | }, 11 | 12 | production: { 13 | PORT: parseInt(PORT, 10) || 3010 14 | } 15 | } 16 | 17 | // default to development config 18 | export default { 19 | ...APP_CONFIG.shared, 20 | ...(APP_CONFIG[NODE_ENV] || APP_CONFIG.development) 21 | } 22 | -------------------------------------------------------------------------------- /app/components/about.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import withStyles from 'isomorphic-style-loader/lib/withStyles' 3 | import { markdown } from 'markdown' 4 | 5 | import styles from 'app/styles/about.css' 6 | import readme from 'README.md' 7 | 8 | const about = () => 9 |
12 | 13 | export default withStyles(styles)(about) 14 | -------------------------------------------------------------------------------- /app/actions/todos.js: -------------------------------------------------------------------------------- 1 | import { times, random } from 'lodash' 2 | import debug from 'debug' 3 | 4 | export const load = (store) => new Promise(resolve => { 5 | debug('dev')('start load todos') 6 | 7 | setTimeout(() => { 8 | store('todos').replace( 9 | times( 10 | random(9) + 1, 11 | (id) => ({ id, title: `todo #${id}`, finished: !!random(1) }) 12 | ) 13 | ) 14 | 15 | resolve() 16 | }, 300) 17 | 18 | debug('dev')('end load todos') 19 | }) 20 | -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | const { NODE_ENV = 'development' } = process.env 2 | 3 | // WEBPACK BUILD CONSTANTS 4 | export const JS_REGEX = /\.js$|\.jsx$|\.es6$|\.babel$/ 5 | export const EXCLUDE_REGEX = /node_modules/ 6 | 7 | export const AUTOPREFIXER_BROWSERS = [ 8 | 'Android 2.3', 9 | 'Android >= 4', 10 | 'Chrome >= 35', 11 | 'Firefox >= 31', 12 | 'Explorer >= 9', 13 | 'iOS >= 7', 14 | 'Opera >= 12', 15 | 'Safari >= 7.1' 16 | ] 17 | 18 | export const DEV = NODE_ENV === 'development' 19 | -------------------------------------------------------------------------------- /core/provide-insert-css.js: -------------------------------------------------------------------------------- 1 | import { Component, PropTypes } from 'react' 2 | 3 | class ProvideInsertCss extends Component { 4 | 5 | props: { 6 | children: any; 7 | insertCss: Function; 8 | }; 9 | 10 | static childContextTypes = { 11 | insertCss: PropTypes.func.isRequired 12 | } 13 | 14 | getChildContext() { 15 | const { insertCss } = this.props 16 | return { insertCss } 17 | } 18 | 19 | render() { 20 | const { children } = this.props 21 | return children 22 | } 23 | 24 | } 25 | 26 | export default ProvideInsertCss 27 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["save"], 3 | "plugins": [ 4 | "typecheck", 5 | "react-hot-loader/babel", 6 | "lodash" 7 | ], 8 | "env": { 9 | "test": { 10 | "plugins": [ 11 | [ "webpack-loaders", { "config": "${CONFIG}" } ], 12 | [ "resolver", { "resolveDirs": [ "./" ] } ] 13 | ] 14 | }, 15 | "production": { 16 | "plugins": [ 17 | "react-remove-prop-types", 18 | "transform-react-constant-elements", 19 | "transform-react-inline-elements", 20 | "transform-react-pure-class-to-function" 21 | ] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /spec/core/hydrate.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import createStore from 'app/store' 4 | import { dehydrate, rehydrate } from 'core/hydrate' 5 | 6 | test('it should dehydrate store data', t => { 7 | const store = createStore({ todos: [ { title: 'foo' } ] }) 8 | const json = dehydrate(store) 9 | t.is(json, '{"todos":[{"title":"foo"}]}') 10 | }) 11 | 12 | test('it should rehydrate store data', t => { 13 | const store = createStore() 14 | 15 | window.__appState__ = JSON.parse('{"todos":[{"title":"foo"}]}') 16 | rehydrate(store) 17 | 18 | const [ todo ] = store('todos') 19 | t.is(todo.title, 'foo') 20 | }) 21 | -------------------------------------------------------------------------------- /spec/helpers/mount.js: -------------------------------------------------------------------------------- 1 | import { noop } from 'lodash' 2 | 3 | import React from 'react' 4 | import { mount } from 'enzyme' 5 | import { Provider } from 'react-tunnel' 6 | 7 | import createStore from 'app/store' 8 | import ProvideInsertCss from 'core/provide-insert-css' 9 | 10 | export default (Component, props = {}, store = createStore()) => { 11 | const wrapper = mount( 12 | 13 | { () => 14 | 15 | 16 | } 17 | 18 | ) 19 | return { store, wrapper } 20 | } 21 | -------------------------------------------------------------------------------- /spec/core/provide-insert-css.spec.js: -------------------------------------------------------------------------------- 1 | import { noop } from 'lodash' 2 | 3 | import test from 'ava' 4 | import React, { PropTypes } from 'react' 5 | import { mount } from 'enzyme' 6 | 7 | import ProvideInsertCss from 'core/provide-insert-css' 8 | 9 | const App = () =>

Foobar

10 | App.contextTypes = { insertCss: PropTypes.func.isRequired } 11 | 12 | const Example = () => 13 | 14 | 15 | 16 | 17 | test('it should provide `insertCss` function through context', t => { 18 | const wrapper = mount() 19 | t.is(wrapper.find('App').node.context.insertCss, noop) 20 | }) 21 | -------------------------------------------------------------------------------- /app/components/todos/finished-todo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import connect from 'core/connect' 3 | 4 | import styles from 'app/styles/todos.css' 5 | 6 | type Props = { 7 | todo: { 8 | id: number, 9 | title: string, 10 | finished: boolean 11 | }, 12 | onRemove: Function 13 | } 14 | 15 | const FinishedTodo = ({ todo, onRemove }: Props) => 16 |
  • 17 | { todo.title } 18 | 19 |
    22 | 23 |
    24 |
  • 25 | 26 | export default connect()(FinishedTodo) 27 | -------------------------------------------------------------------------------- /spec/core/fetch-data.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import createStore from 'app/store' 4 | import fetchData from 'core/fetch-data' 5 | 6 | const ExampleComponent = { 7 | fetchData: (store) => new Promise(resolve => { 8 | setTimeout(() => { 9 | store('todos').replace([ { title: 'foo' } ]) 10 | resolve() 11 | }, 100) 12 | }) 13 | } 14 | 15 | test('it should resolve `fetchData` promises', async (t) => { 16 | const store = createStore() 17 | const components = [ ExampleComponent ] 18 | await fetchData(store, { components, params: {}, location: { query: '' } }) 19 | t.is(store('todos').length, 1) 20 | t.is(store('todos')[0].title, 'foo') 21 | }) 22 | -------------------------------------------------------------------------------- /spec/app.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import React from 'react' 3 | import chai, { expect } from 'chai' 4 | import chaiEnzyme from 'chai-enzyme' 5 | 6 | import mount from 'spec/helpers/mount' 7 | import App from 'app/components/app' 8 | 9 | chai.use(chaiEnzyme()) 10 | 11 | test('it should display basic layout', () => { 12 | const { wrapper } = mount(App) 13 | expect(wrapper.find('.navbar-brand')).to.exist 14 | expect(wrapper.find('.navbar-right')).to.exist 15 | }) 16 | 17 | test('it should render child', () => { 18 | const { wrapper } = mount(App, { children:

    Foobar

    }) 19 | expect(wrapper.find('h1')).to.exist 20 | expect(wrapper.find('h1')).to.have.text('Foobar') 21 | }) 22 | -------------------------------------------------------------------------------- /app/components/todos/pending-todo.jsx: -------------------------------------------------------------------------------- 1 | import { memoize } from 'lodash' 2 | 3 | import React from 'react' 4 | import connect from 'core/connect' 5 | 6 | import styles from 'app/styles/todos.css' 7 | 8 | type Props = { 9 | todo: { 10 | id: number, 11 | title: string, 12 | finished: boolean 13 | } 14 | } 15 | 16 | const handleClick = memoize((todo) => () => { todo.finished = true }) 17 | 18 | const PendingTodo = ({ todo }: Props) => 19 |
  • 22 | 26 | 27 | { todo.title } 28 |
  • 29 | 30 | export default connect()(PendingTodo) 31 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | // global defined styles 2 | import 'app/styles/global.css' 3 | 4 | import { after } from 'lodash' 5 | import { browserHistory, match } from 'react-router' 6 | 7 | import routes from 'app/routes' 8 | 9 | import createStore from 'app/store' 10 | 11 | import renderApp from 'core/universal-render' 12 | import fetchData from 'core/fetch-data' 13 | 14 | const store = createStore() 15 | renderApp({ store }) 16 | 17 | // we have to define the `browserHistory` listener here since we need to 18 | // dispose the listener on hot module reload 19 | const unlisten = browserHistory.listen(after(2, ({ pathname }) => 20 | match({ routes, location: pathname }, (error, redirect, props) => 21 | props ? fetchData(store, props) : undefined 22 | ) 23 | )) 24 | 25 | if (module.hot) { 26 | module.hot.dispose(unlisten) 27 | module.hot.accept(renderApp) 28 | } 29 | -------------------------------------------------------------------------------- /app/styles/todos.css: -------------------------------------------------------------------------------- 1 | .todosContainer { 2 | background-image: url('../images/bg.png'); 3 | margin-top: 20px; 4 | padding: 20px 20px 50px 20px; 5 | position: relative; 6 | width: 100%; 7 | } 8 | 9 | .todosList { 10 | border-top: 1px solid #EAEAEA; 11 | padding-top: 20px; 12 | padding-left: 0; 13 | list-style-type: none; 14 | vertical-align: middle; 15 | 16 | li { 17 | border-bottom: 1px solid #EAEAEA; 18 | padding-bottom: 5px; 19 | padding-top: 5px; 20 | width: 100%; 21 | 22 | &:last-child { 23 | border-bottom: none; 24 | } 25 | 26 | input[type='checkbox'] { 27 | margin-right: 10px; 28 | } 29 | } 30 | } 31 | 32 | .count { 33 | background-color: #cff3cf; 34 | bottom: 0; 35 | left: 0; 36 | padding: 5px 20px; 37 | position: absolute; 38 | width: 100%; 39 | } 40 | 41 | .pendingTodo { 42 | cursor: pointer; 43 | } 44 | 45 | .finishedTodo { 46 | cursor: default; 47 | text-decoration: line-through; 48 | } 49 | -------------------------------------------------------------------------------- /run/utils/start-webpack-server.js: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | 3 | import Koa from 'koa' 4 | import convert from 'koa-convert' 5 | import webpack from 'webpack' 6 | 7 | import clientConfig from '../../webpack/client.config.babel' 8 | import { PORT } from '../../core/config' 9 | 10 | export default (afterBundle) => { 11 | const webpackServer = new Koa() 12 | const webpackClientConfig = clientConfig(afterBundle) 13 | const compiler = webpack(webpackClientConfig) 14 | 15 | const config = { 16 | port: PORT + 1, 17 | options: { 18 | publicPath: `http://localhost:${PORT + 1}/assets/`, 19 | hot: true, 20 | stats: { 21 | assets: true, 22 | colors: true, 23 | version: false, 24 | hash: false, 25 | timings: true, 26 | chunks: false, 27 | chunkModules: false 28 | } 29 | } 30 | } 31 | 32 | webpackServer.use(convert(require('koa-webpack-dev-middleware')(compiler, config.options))) 33 | webpackServer.use(convert(require('koa-webpack-hot-middleware')(compiler))) 34 | 35 | webpackServer.listen(config.port, '0.0.0.0', () => 36 | debug('dev')('`webpack-dev-server` listening on port %s', config.port)) 37 | } 38 | -------------------------------------------------------------------------------- /app/components/todos/create.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { observable } from 'mobx' 3 | 4 | import connect from 'core/connect' 5 | 6 | @connect('todos') 7 | class CreateTodo extends Component { 8 | 9 | props: { 10 | todos: { 11 | push: Function 12 | } 13 | } 14 | 15 | @observable title = '' 16 | 17 | handleChange = ({ target: { value } }) => { 18 | this.title = value 19 | } 20 | 21 | handleSubmit = (e) => { 22 | e.preventDefault() 23 | 24 | const { title } = this 25 | const { todos } = this.props 26 | 27 | if (title && title.trim()) { 28 | // fire `onSubmit` with new Todo 29 | todos.push({ title, id: Math.random(), finished: false }) 30 | 31 | // reset title to empty string for next todo 32 | this.title = '' 33 | } 34 | } 35 | 36 | render() { 37 | const { title } = this 38 | 39 | return ( 40 |
    43 | 49 |
    50 | ) 51 | } 52 | 53 | } 54 | 55 | export default CreateTodo 56 | -------------------------------------------------------------------------------- /server/server-html.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | type Props = { 4 | assets: {}; 5 | locale: string; 6 | body: string; 7 | appState: string; 8 | css: string; 9 | }; 10 | 11 | const ServerHTML = ({ assets, locale, body, appState, css }: Props) => ( 12 | 13 | 14 | 15 | 16 | 17 | { /* Global bundled styles */ } 18 | { assets.style.map((href, idx) => 19 | ) } 20 | 21 | { /* Critical rendered styles */ } 22 |