├── .babelrc
├── .eslintignore
├── .eslintrc
├── .gitignore
├── README.md
├── app
├── actions
│ └── todos.js
├── components
│ ├── about.jsx
│ ├── app.jsx
│ ├── not-found.jsx
│ └── todos
│ │ ├── create.jsx
│ │ ├── finished-todo.jsx
│ │ ├── index.jsx
│ │ └── pending-todo.jsx
├── images
│ └── bg.png
├── index.js
├── routes.jsx
├── store
│ └── index.js
└── styles
│ ├── about.css
│ ├── global.css
│ ├── navbar.css
│ └── todos.css
├── circle.yml
├── constants.js
├── core
├── config.js
├── connect.js
├── fetch-data.js
├── hydrate.js
├── provide-insert-css.js
└── universal-render.js
├── package.json
├── run
├── dev.js
└── utils
│ ├── start-render-server.js
│ ├── start-webpack-server.js
│ └── write-stats.js
├── server
├── koa.js
└── server-html.jsx
├── spec
├── app.spec.js
├── core
│ ├── connect.spec.js
│ ├── fetch-data.spec.js
│ ├── hydrate.spec.js
│ └── provide-insert-css.spec.js
└── helpers
│ ├── mount.js
│ └── setup-browser-env.js
└── webpack
├── client.config.babel.js
├── server.config.babel.js
├── shared.config.js
└── test.config.babel.js
/.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 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist/
2 | server/build.js
3 | node_modules/
4 | coverage/
5 | .nyc_output/
6 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": "eslint-config-airbnb",
4 | "plugins": ["react", "jsx-a11y"],
5 | "env": {
6 | "browser": true,
7 | "node": true,
8 | "es6": true,
9 | "mocha": true
10 | },
11 | "globals": {
12 | "chai": true,
13 | "sinon": true,
14 | "expect": true
15 | },
16 | "rules": {
17 | "react/jsx-curly-spacing": [2, "always"],
18 | "react/jsx-no-duplicate-props": 2,
19 | "react/jsx-no-undef": 2,
20 | "react/jsx-quotes": 0,
21 | "react/jsx-uses-react": 2,
22 | "react/jsx-uses-vars": 2,
23 | "react/jsx-closing-bracket-location": [2, "after-props" ],
24 | "react/no-did-mount-set-state": 2,
25 | "react/no-did-update-set-state": 2,
26 | "react/no-multi-comp": 2,
27 | "react/no-unknown-property": 2,
28 | "react/prop-types": 2,
29 | "react/react-in-jsx-scope": 2,
30 | "react/require-extension": 2,
31 | "react/self-closing-comp": 2,
32 | "react/wrap-multilines": 2,
33 | "react/sort-comp": 0,
34 | "react/jsx-no-bind": 0,
35 | "react/jsx-boolean-value": [2, "always"],
36 |
37 | "quotes": [2, "single", "avoid-escape"],
38 | "jsx-quotes": [2, "prefer-single"],
39 | "comma-dangle": [2, "never"],
40 | "indent": [2, 2],
41 | "object-curly-spacing": [2, "always"],
42 | "array-bracket-spacing": [2, "always"],
43 | "no-undef": 2,
44 | "no-underscore-dangle": 0,
45 | "func-names": 0,
46 | "no-else-return": 0,
47 | "no-console": 1,
48 | "no-throw-literal": 0,
49 | "id-length": 0,
50 | "no-unused-expressions": 0,
51 | "no-param-reassign": 0,
52 | "max-len": [1, 100],
53 | "camelcase": 0,
54 | "space-before-function-paren": [2, "never"],
55 | "consistent-return": 0,
56 | "semi": [2, "never"],
57 | "no-confusing-arrow": 0,
58 | "global-require": 0,
59 | "import/no-unresolved": 0
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ⚛ futureRX
2 |
3 | > 🚀 The futuristic stack to create universal React applications with MobX as state manager. With supporting async data fetching out of the box on server side rendering.
4 |
5 | [](https://david-dm.org/iam4x/futureRX#info=dependencies&view=table)
6 | [](https://david-dm.org/iam4x/futureRX#info=devDependencies&view=table)
7 | [](https://circleci.com/gh/iam4x/futureRX)
8 | [](https://codecov.io/gh/iam4x/futureRX)
9 |
10 |
11 | WARNING: This is a proof of concept running on bleeding edge libraries, it has not been running in production yet. If you do [contact me](http://twitter.com/iam4x).
12 |
13 | This project aim frontend applications which are using an external API to work with data. This boilerplate does not include an API with it to fetch/persist data.
14 |
15 | ## Libraries used
16 |
17 | > **nodejs ^6.2** / **npm ^3.9**
18 |
19 | * #### react
20 | * [mobx](https://github.com/mobxjs/mobx)
21 | * [mobx-store](https://github.com/AriaFallah/mobx-store)
22 | * [react ^15](https://facebook.github.io/react/)
23 | * [react-router ^2.0.0](https://github.com/rackt/react-router)
24 | * [react-hot-loader ^3.0.0-beta](https://github.com/gaearon/react-hot-loader)
25 | * [why-did-you-update](https://github.com/garbles/why-did-you-update)
26 |
27 | * #### css styles
28 | * [css-modules](https://github.com/css-modules/css-modules) 🌟
29 | * [postcss ^5.0.21](https://github.com/postcss/postcss)
30 | * [postcss-import ^8.1.2](https://github.com/postcss/postcss-import)
31 | * [postcss-url ^5.1.2](https://github.com/postcss/postcss-url)
32 | * [autoprefixer ^6.3.6](https://github.com/postcss/autoprefixer)
33 | * [precss ^1.4.0](https://github.com/jonathantneal/precss)
34 | * [isomorphic-style-loader ^1.0.0](https://github.com/kriasoft/isomorphic-style-loader)
35 | * _Supports also global CSS for real world usage with:_
36 | * [extract-text-webpack-plugin ^1.0.1](https://github.com/webpack/extract-text-webpack-plugin)
37 | * [style-loader ^0.13](https://github.com/webpack/style-loader)
38 |
39 | * #### server side rendering
40 | * [koa ^2.0.0](http://koajs.com/)
41 |
42 | * #### testing tools
43 | * [ava ^0.15.2](https://github.com/sindresorhus/ava)
44 | * [jsdom ^9.4.0](https://github.com/tmpvar/jsdom)
45 | * [enzyme ^2.3.0](https://github.com/airbnb/enzyme)
46 | * [chai-enzyme ^0.5.0](https://github.com/producthunt/chai-enzyme)
47 | * [faux-jax ^5.0.1](https://github.com/algolia/faux-jax)
48 | * [sinon ^1.17.4](https://github.com/sinonjs/sinon)
49 | * [sinon-chai ^2.8.0](https://github.com/domenic/sinon-chai)
50 | * [nyc ^7.0.0-alpha.5](https://github.com/bcoe/nyc)
51 |
52 | * #### dev/building tools
53 | * [webpack ^2.1.0-beta.15](http://webpack.github.io/)
54 | * [webpack-hot-middleware](https://github.com/glenjamin/webpack-hot-middleware)
55 | * [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware)
56 | * [babeljs ^6.9](https://babeljs.io/)
57 | * [flowtype (via babel-plugin-typecheck)](https://github.com/codemix/babel-plugin-typecheck)
58 | * [browser-sync](https://www.browsersync.io)
59 |
60 | ## Setup
61 |
62 | * `$ git clone -o upstream https://github.com/iam4x/futureRX.git [myApp]`
63 | * `$ cd [myApp] && npm install`
64 |
65 | ## Run
66 |
67 | * ### dev
68 | * `$ npm run dev` OR
69 | * `$ PORT=xxxx npm run dev`
70 | * (Append `?debugRender` to your URL to enable `why-did-you-update`)
71 |
72 | * ### test
73 | * `$ npm test` OR
74 | * `$ npm test -- --watch`
75 |
76 | * ### build
77 | * `$ NODE_ENV=production npm run build`
78 | * `$ NODE_ENV=production node dist/server`
79 |
80 | ## Update
81 |
82 | * `$ git fetch upstream`
83 | * `$ git merge upstream/master`
84 | * `$ npm install`
85 |
86 | ### TODO
87 |
88 | * [x] allow override of listening ports via config ([7e9ae4a](https://github.com/iam4x/futureRX/commit/7e9ae4ac73fdd562fed5d39dda9325b4541217af))
89 | * [x] styles with css-modules and global styles ([#1](https://github.com/iam4x/futureRX/pull/1))
90 | * [x] test with ava and airbnb-enzyme ([#2](https://github.com/iam4x/futureRX/pull/2))
91 | * [x] webpack build config
92 | * [ ] production server optimization
93 | * [ ] refactor server code
94 |
95 | ### Learn more
96 |
97 | * [Official ReactJS website](http://facebook.github.io/react/)
98 | * [Official ReactJS wiki](https://github.com/facebook/react/wiki)
99 | * [Learn ES6](https://babeljs.io/docs/learn-es6/)
100 | * [ES6 Features](https://github.com/lukehoban/es6features#readme)
101 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/components/app.jsx:
--------------------------------------------------------------------------------
1 | /* eslint max-len: 0 */
2 |
3 | import React from 'react'
4 | import withStyles from 'isomorphic-style-loader/lib/withStyles'
5 | import { Link } from 'react-router'
6 |
7 | import styles from 'app/styles/navbar.css'
8 |
9 | type Props = {
10 | children: any
11 | }
12 |
13 | const App = ({ children }: Props) =>
14 |
15 |
42 |
43 |
44 |
{ children }
45 |
46 |
47 | export default withStyles(styles)(App)
48 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
50 | )
51 | }
52 |
53 | }
54 |
55 | export default CreateTodo
56 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/components/todos/index.jsx:
--------------------------------------------------------------------------------
1 | import { filter } from 'lodash'
2 | import pluralize from 'pluralize'
3 |
4 | import React from 'react'
5 | import withStyles from 'isomorphic-style-loader/lib/withStyles'
6 |
7 | import connect from 'core/connect'
8 | import { load } from 'app/actions/todos'
9 |
10 | import FinishedTodo from './finished-todo'
11 | import PendingTodo from './pending-todo'
12 | import CreateTodo from './create'
13 |
14 | import styles from 'app/styles/todos.css'
15 |
16 | type Props = {
17 | todos: {
18 | map: Function
19 | }
20 | }
21 |
22 | const handleMarkAllDone = (todos) => () =>
23 | todos.replace(todos.map(t => ({ ...t, finished: true })))
24 |
25 | const Todos = ({ todos }: Props) => {
26 | const pendingTodos = filter(todos, { finished: false })
27 | const finishedTodos = filter(todos, { finished: true })
28 |
29 | return (
30 |
31 | { /* unfinished todos */ }
32 |
33 |
34 |
Todos
35 |
36 |
37 |
38 |
39 |
42 | Mark all done
43 |
44 |
45 |
46 |
47 | { pendingTodos.map(todo =>
48 | ) }
51 |
52 |
53 |
54 | { pendingTodos.length ?
55 |
56 | { pendingTodos.length }
57 | { pluralize('todo', pendingTodos.length) } remaining
58 | :
59 | Congrats it's all done! }
60 |
61 |
62 |
63 |
64 | { /* finished todos */ }
65 |
66 |
67 |
Already done
68 |
69 |
70 | { finishedTodos.map(todo =>
71 | todos.remove(todo) } />) }
75 |
76 |
77 |
78 |
79 | )
80 | }
81 |
82 |
83 | export default Object.assign(
84 | withStyles(styles)(connect('todos')(Todos)),
85 | { fetchData: load }
86 | )
87 |
--------------------------------------------------------------------------------
/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/images/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iam4x/futureRX/b1dd1b65a031ceb79acce2150225786c8f7ff294/app/images/bg.png
--------------------------------------------------------------------------------
/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/routes.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Route } from 'react-router'
3 |
4 | export default (
5 |
6 |
7 |
8 |
9 |
10 | )
11 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/styles/about.css:
--------------------------------------------------------------------------------
1 | .aboutContainer {
2 | background-color: #FFF;
3 | padding: 20px;
4 | }
5 |
--------------------------------------------------------------------------------
/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/styles/navbar.css:
--------------------------------------------------------------------------------
1 | .github {
2 | padding-top: 12px;
3 |
4 | iframe:first-child {
5 | margin-right: 15px;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/core/connect.js:
--------------------------------------------------------------------------------
1 | import { isFunction, isString, isArray } from 'lodash'
2 | import { observer } from 'mobx-react'
3 | import { inject } from 'react-tunnel'
4 |
5 | const nativeObjectMethods = Object.getOwnPropertyNames(Function)
6 |
7 | // borrowed from `react-redux/connect`
8 | // https://github.com/rackt/react-redux/blob/master/src/components/connect.js#L17
9 | const getDisplayName = ({ displayName, name }) => displayName || name || 'Component'
10 |
11 | export default (params) => (Component) => {
12 | // * connect('foo' || [ 'foo', 'bar' ]) -> provide storeKey data into props
13 | //
14 | // in 'foo' out ({ foo: store('foo') })
15 | // in ['foo', 'bar'] out ({ foo: store('foo'), bar: store('bar') })
16 | const injectStoresIntoProps = inject(({ store }) => {
17 | if (isString(params)) {
18 | return ({ [params]: store(params) })
19 | }
20 |
21 | if (isArray(params)) {
22 | return params.reduce((result, storeKey) =>
23 | ({ ...result, [storeKey]: store(storeKey) }), {})
24 | }
25 |
26 | return ({ store })
27 | })
28 |
29 | // * observe decorated component by `mobx-react::observer`
30 | // * connect props from context onto decorated component
31 | const FinalComponent = injectStoresIntoProps(observer(Component))
32 |
33 | // copy missing static methods from `Component` to `FinalComponent`
34 | Object.getOwnPropertyNames(Component).forEach(method => {
35 | const isNotNativeMethod = !nativeObjectMethods.includes(method)
36 | const isNotAlreadyDefined = !isFunction(FinalComponent[method])
37 | const methodIsFunction = isFunction(Component[method])
38 |
39 | if (isNotNativeMethod && methodIsFunction && isNotAlreadyDefined) {
40 | FinalComponent[method] = Component[method]
41 | }
42 | })
43 |
44 | // set `connect(Component)` as displayName
45 | FinalComponent.displayName = `connect(${getDisplayName(Component)})`
46 |
47 | return FinalComponent
48 | }
49 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/core/universal-render.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import DevTools from 'mobx-react-devtools'
3 | import { render } from 'react-dom'
4 | import { renderToString } from 'react-dom/server'
5 | import { Provider } from 'react-tunnel'
6 | import { Router, browserHistory, RouterContext, match } from 'react-router'
7 |
8 | import routes from 'app/routes'
9 |
10 | import fetchData from './fetch-data'
11 | import ProvideInsertCss from './provide-insert-css'
12 | import { rehydrate, dehydrate } from './hydrate'
13 |
14 | const { NODE_ENV, BROWSER } = process.env
15 |
16 | export default async ({ store, location, assets } = {}) => {
17 | const css = []
18 | const insertCss = BROWSER ?
19 | (styles) => styles._insertCss() :
20 | (styles) => css.push(styles._getCss())
21 |
22 | if (BROWSER) {
23 | rehydrate(store)
24 |
25 | // track potentially unnecessary re-renders
26 | // if we find `debugRender` in the query string
27 | if (NODE_ENV === 'development' &&
28 | window.location.search.includes('debugRender')) {
29 | const { whyDidYouUpdate } = require('why-did-you-update')
30 | whyDidYouUpdate(React)
31 | }
32 |
33 | const App = (
34 |
35 | { () =>
36 |
37 |
40 | }
41 |
42 | )
43 |
44 | const container = document.getElementById('app--container')
45 |
46 | if (NODE_ENV !== 'development') return render(App, container)
47 |
48 | // special render in development:
49 | // * enable react-hot-loader
50 | // * enable mobx-react-devtools
51 | const { AppContainer } = require('react-hot-loader')
52 | const Dev = (
53 |
57 | )
58 |
59 | render(Dev, container)
60 | } else {
61 | // server side rendering
62 | const ServerHTML = require('server/server-html')
63 |
64 | // promisify `match` from `react-router`
65 | const asyncMatch = () => new Promise((resolve) =>
66 | match({ routes, location }, (...args) => resolve(args))
67 | )
68 |
69 | const [ routerError, redirect, renderProps ] = await asyncMatch(location, routes)
70 | if (routerError || redirect) throw ({ error: routerError, redirect })
71 |
72 | await fetchData(store, renderProps)
73 | const appState = dehydrate(store)
74 |
75 | const body = renderToString(
76 |
77 | { () =>
78 |
79 |
80 | }
81 |
82 | )
83 |
84 | return renderToString(
85 |
91 | )
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "futureRX",
3 | "version": "1.0.0",
4 | "description": "🚀 The futuristic stack to create universal React applications with MobX as state manager. With supporting async data fetching out of the box on server side rendering.",
5 | "main": "index.js",
6 | "scripts": {
7 | "lint": "eslint . --ext .jsx,.js",
8 | "dev": "node run/dev",
9 | "ava": "CONFIG=$(pwd)/webpack/test.config.babel.js BABEL_DISABLE_CACHE=1 NODE_ENV=test ava spec/**/*.spec.js",
10 | "test": "nyc npm run ava",
11 | "coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov -t 8bba035a-a292-4daa-b6d4-68acc439495f",
12 | "build:client": "BABEL_ENV=production webpack --config ./webpack/client.config.babel.js",
13 | "build:server": "BABEL_ENV=production webpack --config ./webpack/server.config.babel.js",
14 | "build": "npm run build:server && npm run build:client"
15 | },
16 | "author": "iam4x",
17 | "license": "ISC",
18 | "ava": {
19 | "babel": "inherit",
20 | "failFast": true,
21 | "verbose": true,
22 | "require": [
23 | "./spec/helpers/setup-browser-env",
24 | "babel-register",
25 | "babel-polyfill"
26 | ]
27 | },
28 | "nyc": {
29 | "extension": [
30 | ".jsx",
31 | ".js"
32 | ],
33 | "exclude": [
34 | "spec/",
35 | "webpack/",
36 | "node_modules/",
37 | "dist/"
38 | ]
39 | },
40 | "dependencies": {
41 | "autoprefixer": "^6.3.6",
42 | "axios": "^0.13.0",
43 | "babel-core": "^6.9.1",
44 | "babel-eslint": "^6.0.4",
45 | "babel-loader": "^6.2.4",
46 | "babel-plugin-lodash": "^3.2.0",
47 | "babel-plugin-react-remove-prop-types": "^2.0.2",
48 | "babel-plugin-transform-react-constant-elements": "^6.9.1",
49 | "babel-plugin-transform-react-inline-elements": "^6.8.0",
50 | "babel-plugin-transform-react-pure-class-to-function": "^1.0.1",
51 | "babel-plugin-typecheck": "^3.9.0",
52 | "babel-polyfill": "^6.9.1",
53 | "babel-preset-save": "^1.1.5",
54 | "bootstrap": "^3.3.6",
55 | "css-loader": "^0.23.1",
56 | "debug": "^2.2.0",
57 | "eslint": "^3.1.1",
58 | "eslint-config-airbnb": "^9.0.1",
59 | "eslint-loader": "^1.3.0",
60 | "eslint-plugin-import": "^1.8.1",
61 | "eslint-plugin-jsx-a11y": "^2.0.1",
62 | "eslint-plugin-react": "^5.1.1",
63 | "extract-text-webpack-plugin": "^1.0.1",
64 | "file-loader": "^0.9.0",
65 | "image-webpack-loader": "^1.8.0",
66 | "isomorphic-style-loader": "^1.0.0",
67 | "json-loader": "^0.5.4",
68 | "json-stringify-safe": "^5.0.1",
69 | "koa": "^2.0.0",
70 | "koa-compress": "^1.0.9",
71 | "koa-html-minifier": "^1.0.1",
72 | "koa-mount": "^1.3.0",
73 | "koa-proxy": "^0.6.0",
74 | "koa-static-cache": "^3.1.7",
75 | "lodash": "^4.13.1",
76 | "markdown": "^0.5.0",
77 | "mobx": "^2.3.6",
78 | "mobx-react": "^3.3.1",
79 | "mobx-store": "^3.3.1",
80 | "normalize.css": "^4.2.0",
81 | "pluralize": "^3.0.0",
82 | "postcss": "^5.1.0",
83 | "postcss-import": "8.1.0",
84 | "postcss-loader": "^0.9.1",
85 | "postcss-url": "^5.1.2",
86 | "precss": "^1.4.0",
87 | "raw-loader": "^0.5.1",
88 | "react": "^15.1.0",
89 | "react-dom": "^15.1.0",
90 | "react-hot-loader": "^3.0.0-beta.2",
91 | "react-router": "^2.4.1",
92 | "react-tunnel": "^0.1.0",
93 | "style-loader": "^0.13.1",
94 | "webpack": "^2.1.0-beta.15",
95 | "webpack-node-externals": "^1.2.0"
96 | },
97 | "devDependencies": {
98 | "ava": "^0.15.2",
99 | "babel-plugin-resolver": "0.0.7",
100 | "babel-plugin-webpack-loaders": "^0.7.0",
101 | "browser-sync": "^2.13.0",
102 | "browser-sync-webpack-plugin": "^1.0.3",
103 | "chai": "^3.5.0",
104 | "chai-enzyme": "^0.5.0",
105 | "codecov": "^1.0.1",
106 | "enzyme": "^2.3.0",
107 | "faux-jax": "^5.0.1",
108 | "jsdom": "^9.4.0",
109 | "koa-webpack-dev-middleware": "^1.2.1",
110 | "koa-webpack-hot-middleware": "^1.0.3",
111 | "mobx-react-devtools": "^4.2.0",
112 | "node-watch": "^0.4.0",
113 | "nyc": "^7.0.0-alpha.5",
114 | "open": "0.0.5",
115 | "sinon": "^1.17.4",
116 | "sinon-chai": "^2.8.0",
117 | "source-map-support": "^0.4.1",
118 | "webpack-dev-middleware": "^1.6.1",
119 | "webpack-hot-middleware": "^2.10.0",
120 | "why-did-you-update": "0.0.8"
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/run/utils/start-render-server.js:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path'
2 | import { exec, fork } from 'child_process'
3 |
4 | import debug from 'debug'
5 | import open from 'open'
6 |
7 | import browserSync from 'browser-sync'
8 | import watch from 'node-watch'
9 |
10 | import { PORT } from '../../core/config'
11 |
12 | let server = null
13 | let serverReload = false
14 | let firstServerStart = true
15 |
16 | const asyncExec = (cmd, opts) => new Promise(r =>
17 | exec(cmd, opts, (e) => { if (e) { throw e } else { r() } }).stdout.pipe(process.stdout))
18 |
19 | const startServer = async () => {
20 | const env = { ...process.env, DEBUG: 'dev,koa', BABEL_ENV: 'server' }
21 |
22 | // compile first the server code,
23 | // it will be re-compiled at every server restart
24 | const webpackServerConfig = resolve(__dirname, '../../webpack/server.config.babel.js')
25 | await asyncExec(`webpack --config ${webpackServerConfig}`, { env, encoding: 'utf8' })
26 |
27 | debug('dev')('compiled server code updated')
28 |
29 | // define `restartServer` function
30 | const restartServer = () => {
31 | if (server) {
32 | debug('dev')('restarting koa application')
33 | serverReload = true
34 | server.kill('SIGTERM')
35 | server = null
36 | startServer()
37 | }
38 | }
39 |
40 | server = fork(resolve(__dirname, '../../dist/server.js'), { env })
41 |
42 | server.once('message', (message) => {
43 | if (message.match(/^online$/)) {
44 | // tell `browserSync` to reload the browser,
45 | // server code has restarted
46 | if (serverReload) {
47 | serverReload = false
48 | browserSync.get('bs-webpack-plugin').reload()
49 | }
50 |
51 | // open browser after server started
52 | if (firstServerStart) {
53 | firstServerStart = false
54 | open(`http://localhost:${PORT + 2}`)
55 |
56 | // watch for `rs` into console to restart server
57 | process.stdin.setEncoding('utf8')
58 | process.stdin.on('data', (data) => {
59 | const parsedData = `${data}`.trim().toLowerCase()
60 | if (parsedData === 'rs') restartServer()
61 | })
62 |
63 | // watch for `./server` changes and auto-reload server & browser
64 | watch(resolve(__dirname, '../../server'), () => restartServer())
65 | }
66 | }
67 | })
68 | }
69 |
70 | process.on('exit', () => server ? server.kill('SIGTERM') : undefined)
71 |
72 | export default startServer
73 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/run/utils/write-stats.js:
--------------------------------------------------------------------------------
1 | // borrowed from https://github.com/gpbl/isomorphic500/blob/master/webpack%2Futils%2Fwrite-stats.js
2 | import fs from 'fs'
3 | import debug from 'debug'
4 | import { extname, resolve } from 'path'
5 |
6 | const filepath = resolve(__dirname, '../../dist/webpack-stats.json')
7 |
8 | function writeStats(stats) {
9 | const publicPath = this.options.output.publicPath
10 | const json = stats.toJson()
11 |
12 | // get chunks by name and extensions
13 | const getChunks = function(name, ext = /.js$/) {
14 | let chunks = json.assetsByChunkName[name]
15 |
16 | // a chunk could be a string or an array, so make sure it is an array
17 | if (!(Array.isArray(chunks))) {
18 | chunks = [ chunks ]
19 | }
20 |
21 | return chunks
22 | .filter(chunk => ext.test(extname(chunk))) // filter by extension
23 | .map(chunk => `${publicPath}${chunk}`) // add public path to it
24 | }
25 |
26 | const script = getChunks('app', /js/)
27 | const style = getChunks('app', /css/)
28 |
29 | // Find compiled images in modules
30 | // it will be used to map original filename to the compiled one
31 | // for server side rendering
32 | const imagesRegex = /\.(jpe?g|png|gif|svg)$/
33 | const images = json.modules
34 | .filter(module => imagesRegex.test(module.name))
35 | .map(image =>
36 | ({
37 | original: image.name,
38 | compiled: `${publicPath}${image.assets[0]}`
39 | })
40 | )
41 |
42 | const content = { script, style, images }
43 |
44 | fs.writeFileSync(filepath, JSON.stringify(content))
45 | debug('dev')('`webpack-stats.json` updated')
46 | }
47 |
48 | export default function() { this.plugin('done', writeStats) }
49 |
--------------------------------------------------------------------------------
/server/koa.js:
--------------------------------------------------------------------------------
1 | import debug from 'debug'
2 | import { resolve } from 'path'
3 |
4 | import Koa from 'koa'
5 | import convert from 'koa-convert'
6 | import mount from 'koa-mount'
7 | import staticCache from 'koa-static-cache'
8 |
9 | import createStore from 'app/store'
10 | import render from 'core/universal-render'
11 | import { PORT } from 'core/config'
12 |
13 | const { NODE_ENV } = process.env
14 |
15 | const app = new Koa()
16 |
17 | debug.enable('koa')
18 |
19 | // production middlewares
20 | if (NODE_ENV !== 'development') {
21 | app.use(convert(require('koa-compress')()))
22 | app.use(convert(require('koa-html-minifier')({
23 | caseSensitive: true,
24 | collapseWhitespace: true,
25 | collapseBooleanAttributes: true,
26 | collapseInlineTagWhitespace: true,
27 | decodeEntities: true,
28 | minifyCSS: true,
29 | minifyJS: true,
30 | minifyURLs: true,
31 | removeAttributeQuotes: true,
32 | removeEmptyAttributes: true,
33 | removeRedundantAttributes: true,
34 | removeScriptTypeAttributes: true,
35 | removeStyleLinkTypeAttributes: true,
36 | useShortDoctype: true
37 | })))
38 | }
39 |
40 | // Proxy asset folder to webpack development server in development mode
41 | if (NODE_ENV === 'development') {
42 | const proxy = require('koa-proxy')({
43 | host: `http://0.0.0.0:${PORT + 1}`,
44 | map: (filePath) => `assets/${filePath}`
45 | })
46 | app.use(convert(mount('/assets', proxy)))
47 | } else {
48 | const cacheOpts = { maxAge: 86400000, gzip: true }
49 | app.use(convert(mount('/assets', staticCache(resolve('./dist'), cacheOpts))))
50 | }
51 |
52 | app.use(async (ctx) => {
53 | try {
54 | ctx.status = 200
55 | ctx.body = await render({
56 | assets: require('./webpack-stats.json'),
57 | store: createStore(),
58 | location: ctx.request.url
59 | })
60 |
61 | // Don't cache assets name on dev
62 | if (NODE_ENV === 'development') {
63 | delete require.cache[require.resolve('./webpack-stats.json')]
64 | }
65 | } catch (err) {
66 | // Render 500 error page from server
67 | const { error, redirect } = err
68 | if (error) throw error
69 | // Handle component `onEnter` transition
70 | if (redirect) {
71 | const { pathname, search } = redirect
72 | ctx.redirect(pathname + search)
73 | } else {
74 | throw err
75 | }
76 | }
77 | })
78 |
79 | app.listen(PORT)
80 |
81 | // Tell parent process koa-server is started
82 | if (process.send) process.send('online')
83 | debug('koa')('`koa-render-server` started on port %s', PORT)
84 |
--------------------------------------------------------------------------------
/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 |
25 |
26 |
27 |
28 |
29 | { /* bundled js + app state */ }
30 |
31 |
32 |
33 | { /* external js */ }
34 |
39 |
40 |
41 | )
42 |
43 | export default ServerHTML
44 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/spec/core/connect.spec.js:
--------------------------------------------------------------------------------
1 | import { noop } from 'lodash'
2 |
3 | import test from 'ava'
4 | import React from 'react'
5 | import chai, { expect } from 'chai'
6 | import chaiEnzyme from 'chai-enzyme'
7 |
8 | import createStore from 'app/store'
9 | import mount from 'spec/helpers/mount'
10 | import connect from 'core/connect'
11 |
12 | chai.use(chaiEnzyme())
13 |
14 | const Example = ({ todos }: { todos: any }) =>
15 | { todos.map(({ title }) => - { title }
) }
16 |
17 | Example.fetchData = noop
18 |
19 | const Example2 = ({ store }: { store: any }) =>
20 | { store('todos').map(({ title }) => - { title }
) }
21 |
22 | const data = { todos: [ { title: 'foo' } ] }
23 |
24 | test('it should decorate component', t => {
25 | const Decorated = connect('todos')(Example)
26 | t.is(Decorated.displayName, 'connect(Example)')
27 | t.is(Decorated.fetchData, Example.fetchData)
28 | })
29 |
30 | test('it should connect store with store key', () => {
31 | const store = createStore(data)
32 | const Decorated = connect('todos')(Example)
33 | const { wrapper } = mount(Decorated, {}, store)
34 | expect(wrapper.find('li')).to.exist
35 | expect(wrapper.find('li')).to.have.text('foo')
36 | })
37 |
38 | test('it should connect whole store without params', () => {
39 | const store = createStore(data)
40 | const Decorated = connect()(Example2)
41 | const { wrapper } = mount(Decorated, {}, store)
42 | expect(wrapper.find('li')).to.exist
43 | expect(wrapper.find('li')).to.have.text('foo')
44 | })
45 |
46 | test('it should connect with an array as params', () => {
47 | const store = createStore(data)
48 | const Decorated = connect([ 'todos' ])(Example)
49 | const { wrapper } = mount(Decorated, {}, store)
50 | expect(wrapper.find('li')).to.exist
51 | expect(wrapper.find('li')).to.have.text('foo')
52 | })
53 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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/helpers/setup-browser-env.js:
--------------------------------------------------------------------------------
1 | global.document = require('jsdom').jsdom('')
2 | global.window = document.defaultView
3 | global.navigator = window.navigator
4 |
--------------------------------------------------------------------------------
/webpack/client.config.babel.js:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path'
2 | import { once } from 'lodash'
3 |
4 | import webpack from 'webpack'
5 | import BrowserSyncPlugin from 'browser-sync-webpack-plugin'
6 | import ExtractTextPlugin from 'extract-text-webpack-plugin'
7 |
8 | import sharedConfig from './shared.config'
9 | import writeStats from '../run/utils/write-stats'
10 | import { DEV } from '../constants'
11 | import { PORT } from '../core/config'
12 |
13 | const {
14 | BUILD_HASH = 'DEFAULT',
15 | NODE_ENV = 'development'
16 | } = process.env
17 |
18 | const clean = (arr) => arr.filter(i => i !== false)
19 |
20 | export default (afterBundle) => ({
21 | ...sharedConfig,
22 |
23 | devtool: DEV ? 'eval' : 'cheap-module-source-map',
24 |
25 | entry: {
26 | app: clean([
27 | 'babel-polyfill',
28 | DEV && 'react-hot-loader/patch',
29 | DEV && `webpack-hot-middleware/client?path=http://localhost:${PORT + 1}/__webpack_hmr`,
30 | resolve(__dirname, '../app/index.js')
31 | ])
32 | },
33 |
34 | output: {
35 | path: resolve(__dirname, '../dist'),
36 | filename: `[name]-${BUILD_HASH}.js`,
37 | chunkFilename: `[name]-${BUILD_HASH}.js`,
38 | publicPath: '/assets/',
39 | pathinfo: true
40 | },
41 |
42 | plugins: clean([
43 | new webpack.LoaderOptionsPlugin({ minimize: !DEV, debug: DEV }),
44 |
45 | !DEV && new ExtractTextPlugin(`[name]-${BUILD_HASH}.css`),
46 |
47 | DEV && new BrowserSyncPlugin({
48 | host: 'localhost',
49 | port: PORT + 2,
50 | proxy: `0.0.0.0:${PORT}`,
51 | open: false
52 | }, { reload: false }),
53 |
54 | DEV && new webpack.HotModuleReplacementPlugin(),
55 | DEV && new webpack.NoErrorsPlugin(),
56 |
57 | new webpack.DefinePlugin({
58 | 'process.env': {
59 | BROWSER: JSON.stringify(true),
60 | NODE_ENV: JSON.stringify(NODE_ENV)
61 | }
62 | }),
63 |
64 | // production build optimizations
65 | !DEV && new webpack.optimize.DedupePlugin(),
66 | !DEV && new webpack.optimize.UglifyJsPlugin({
67 | compress: {
68 | warnings: false,
69 | screw_ie8: true,
70 | sequences: true,
71 | dead_code: true,
72 | drop_debugger: true,
73 | comparisons: true,
74 | conditionals: true,
75 | evaluate: true,
76 | booleans: true,
77 | loops: true,
78 | unused: true,
79 | hoist_funs: true,
80 | if_return: true,
81 | join_vars: true,
82 | cascade: true,
83 | drop_console: true
84 | },
85 | output: {
86 | comments: false
87 | }
88 | }),
89 |
90 | writeStats,
91 |
92 | DEV && function() { this.plugin('done', once(() => afterBundle())) }
93 | ])
94 | })
95 |
--------------------------------------------------------------------------------
/webpack/server.config.babel.js:
--------------------------------------------------------------------------------
1 | // set process.env.SERVER to true for `shared.config.babel.js`
2 | process.env.SERVER = true
3 |
4 | import path from 'path'
5 | import webpack from 'webpack'
6 |
7 | const {
8 | resolve,
9 | postcss,
10 | module: { loaders }
11 | } = require('./shared.config')
12 |
13 | export default {
14 | target: 'node',
15 | devtool: 'source-map',
16 |
17 | externals: [
18 | require('webpack-node-externals')(),
19 | './webpack-stats.json'
20 | ],
21 |
22 | entry: {
23 | app: [
24 | 'babel-polyfill',
25 | path.resolve(__dirname, '../server/koa')
26 | ]
27 | },
28 |
29 | output: {
30 | path: path.resolve(__dirname, '../dist'),
31 | filename: 'server.js',
32 | libraryTarget: 'commonjs2',
33 | publicPath: '/assets/',
34 | pathinfo: true
35 | },
36 |
37 | node: {
38 | console: true,
39 | global: true,
40 | process: true,
41 | Buffer: true,
42 | __filaname: true,
43 | __dirname: true,
44 | fs: true,
45 | path: true
46 | },
47 |
48 | module: { loaders },
49 |
50 | plugins: [
51 | new webpack.BannerPlugin({
52 | banner: 'require("source-map-support").install();',
53 | raw: true,
54 | entryOnly: false
55 | })
56 | ],
57 |
58 | postcss,
59 | resolve
60 | }
61 |
--------------------------------------------------------------------------------
/webpack/shared.config.js:
--------------------------------------------------------------------------------
1 | /* eslint max-len: 0 */
2 | import { resolve } from 'path'
3 | import ExtractTextPlugin from 'extract-text-webpack-plugin'
4 |
5 | import {
6 | DEV,
7 | JS_REGEX,
8 | EXCLUDE_REGEX,
9 | AUTOPREFIXER_BROWSERS
10 | } from '../constants'
11 |
12 | const { BUILD_HASH = 'DEFAULT', SERVER } = process.env
13 |
14 | const clean = (arr) => arr.filter(i => i !== false)
15 |
16 | export default {
17 | module: {
18 | preLoaders: [
19 | { test: JS_REGEX, exclude: EXCLUDE_REGEX, loader: 'eslint' }
20 | ],
21 |
22 | loaders: [
23 | { test: /\.json$/, exclude: EXCLUDE_REGEX, loader: 'json' },
24 | { test: JS_REGEX, exclude: EXCLUDE_REGEX, loader: 'babel' },
25 | {
26 | test: /\.(woff|woff2|eot|ttf|svg)(\?v=[0-9].[0-9].[0-9])?$/,
27 | loader: `file?name=[path][name]_${BUILD_HASH}.[ext]`
28 | },
29 | {
30 | test: /\.(jpe?g|png|gif|svg)$/,
31 | loaders: clean([
32 | `file?name=[path][name]_${BUILD_HASH}.[ext]`,
33 | // optimize image for production client build
34 | !DEV && !SERVER && 'image-webpack?optimizationLevel=7&progressive&interlaced'
35 | ])
36 | },
37 | {
38 | test: /\.css$/,
39 | exclude: /global\.css$/,
40 | loaders: [
41 | 'isomorphic-style',
42 | `css?sourceMap&modules&localIdentName=[name]_[local]_${BUILD_HASH}`,
43 | 'postcss'
44 | ]
45 | },
46 | {
47 | test: /global\.css$/,
48 | loader: DEV ? 'style!css!postcss' : ExtractTextPlugin.extract('style', 'css!postcss')
49 | },
50 | { test: /\.md$/, loader: 'raw' }
51 | ]
52 | },
53 |
54 | postcss: (webpackInstance) => [
55 | require('postcss-import')({ addDependencyTo: webpackInstance }),
56 | require('postcss-url')(),
57 | require('precss')(),
58 | require('autoprefixer')({ browsers: AUTOPREFIXER_BROWSERS })
59 | ],
60 |
61 | resolve: {
62 | extensions: [ '', '.js', '.json', '.jsx' ],
63 | modules: [ resolve('./'), 'node_modules' ]
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------