├── setupJest.js ├── src ├── components │ ├── Hello.css │ ├── shared │ │ ├── Loading.css │ │ ├── EditableDiv.css │ │ ├── Loading.js │ │ └── EditableDiv.js │ ├── Hello.js │ └── JsonAPI.js ├── config │ └── actionType.js ├── styles │ └── global.css ├── reducers │ ├── index.js │ ├── jsonAPI.js │ └── hello.js ├── actions │ ├── api.js │ ├── jsonAPI.js │ └── hello.js ├── matchConfig.js ├── containers │ ├── JsonAPI.js │ ├── HelloWorld.js │ └── PreloadHelloWorld.js ├── server.js ├── clientRender.js └── serverRender.js ├── .gitignore ├── postcss.config.js ├── .storybook ├── config.js └── webpack.config.js ├── .travis.yml ├── __functional__ └── actions │ └── api.js ├── __tests__ ├── actions │ ├── hello.js │ └── jsonAPI.js ├── reducers │ ├── jsonAPI.js │ └── hello.js ├── components │ └── Hello.js └── containers │ └── HelloWorld.js ├── .babelrc ├── __e2e__ ├── specs │ └── hello_spec.js └── config.js ├── stories └── index.js ├── LICENSE ├── webpack.config.js ├── webpack.prod.config.js ├── package.json ├── README.md └── .eslintrc.js /setupJest.js: -------------------------------------------------------------------------------- 1 | global.fetch = require('jest-fetch-mock'); -------------------------------------------------------------------------------- /src/components/Hello.css: -------------------------------------------------------------------------------- 1 | 2 | .title { 3 | color: red; 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | build 4 | public 5 | .DS_Store 6 | dist 7 | public -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer')({browsers: ['Android >= 4.1', 'ie >= 9']}) 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/config/actionType.js: -------------------------------------------------------------------------------- 1 | export const HELLO_WORLD = 'HELLO_WORLD' 2 | export const SET_MESSAGE = 'SET_MESSAGE' 3 | 4 | export const SET_API_DATA = 'SET_API_DATA' -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | function loadStories() { 4 | require('../stories'); 5 | } 6 | 7 | configure(loadStories, module); 8 | -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | :global(body) { 2 | font-family: PingFangTC, Helvetica Neue, Helvetica, Arial, STHeiti, Microsoft JhengHei, 微軟正黑體, sans-serif; 3 | background: green; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/shared/Loading.css: -------------------------------------------------------------------------------- 1 | .loaderBox { 2 | position: absolute; 3 | z-index: 1; 4 | } 5 | 6 | .loaderBox img { 7 | position: fixed; 8 | left: 50%; 9 | top: 45%; 10 | margin: 0 auto; 11 | width: 50px; 12 | } -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import hello from './hello' 3 | import jsonAPI from './jsonAPI' 4 | 5 | const reducer = combineReducers({ 6 | jsonAPI, 7 | hello 8 | }) 9 | 10 | export default reducer 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "6" 5 | 6 | cache: 7 | directories: 8 | - node_modules 9 | 10 | script: 11 | - npm run test 12 | 13 | notifications: 14 | email: 15 | - wahengchang@gmail.com 16 | -------------------------------------------------------------------------------- /src/actions/api.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch' 2 | 3 | const actionCreator = { 4 | getJsonAPI: (_username) => { 5 | return ( 6 | fetch(`https://jsonplaceholder.typicode.com/posts`) 7 | .then(response => response.json()) 8 | ) 9 | } 10 | } 11 | 12 | export default actionCreator 13 | -------------------------------------------------------------------------------- /__functional__/actions/api.js: -------------------------------------------------------------------------------- 1 | import apis from '../../src/actions/api.js' 2 | 3 | describe('api action creator', () => { 4 | it('get github user', (done) => { 5 | apis.getJsonAPI().then( 6 | (res) => { 7 | expect(res[0].title).toMatch(''); 8 | done(); 9 | }, 10 | (err) => done(err) 11 | ) 12 | }) 13 | }) -------------------------------------------------------------------------------- /__tests__/actions/hello.js: -------------------------------------------------------------------------------- 1 | import helloActionCreator from '../../src/actions/hello.js' 2 | 3 | describe('helloActionCreator', () => { 4 | it('should create an action for HelloWorld', () => { 5 | const dispatch = jest.fn(); 6 | helloActionCreator.helloWorld()(dispatch, {}); 7 | expect(dispatch).toBeCalledWith({ type: 'HELLO_WORLD' }); 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/reducers/jsonAPI.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_API_DATA 3 | } from '../config/actionType' 4 | 5 | const apiReducer = (state = {products: []}, action) => { 6 | switch (action.type) { 7 | case SET_API_DATA: 8 | return Object.assign({}, state, { products: action.products }) 9 | default: 10 | return state 11 | } 12 | } 13 | 14 | export default apiReducer 15 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets":[ 3 | "es2015", "react", "stage-2" 4 | ], 5 | "env": { 6 | "development": { 7 | "presets": ["es2015", "react", "stage-0"], 8 | "plugins": ["transform-runtime", "transform-object-rest-spread"], 9 | "presets": ["react-hmre"] 10 | } 11 | }, 12 | "env": { 13 | "build": { 14 | "presets": ["es2015", "react", "stage-0"] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/actions/jsonAPI.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_API_DATA 3 | } from '../config/actionType' 4 | 5 | import apis from './api.js' 6 | 7 | const jsonAPIActionCreator = { 8 | getJsonAPI: () => { 9 | return function (dispatch, getState) { 10 | return apis.getJsonAPI().then((res) => { 11 | dispatch({ 12 | type: SET_API_DATA, 13 | products: res 14 | }) 15 | }) 16 | } 17 | } 18 | } 19 | export default jsonAPIActionCreator -------------------------------------------------------------------------------- /src/reducers/hello.js: -------------------------------------------------------------------------------- 1 | import { HELLO_WORLD, SET_MESSAGE } from '../config/actionType' 2 | 3 | const helloWorld = (state = { message: 'Hello' }, action) => { 4 | switch (action.type) { 5 | case HELLO_WORLD: 6 | return Object.assign({}, state, { message: 'Hello, World!' }) 7 | case SET_MESSAGE: 8 | return Object.assign({}, state, { message: action.message }) 9 | default: 10 | return state 11 | } 12 | } 13 | 14 | export default helloWorld 15 | -------------------------------------------------------------------------------- /__e2e__/specs/hello_spec.js: -------------------------------------------------------------------------------- 1 | // spec.js 2 | 3 | var url = 'http://localhost:3000/topic' 4 | 5 | describe('Protractor Demo App1', function() { 6 | browser.driver.get(url); 7 | 8 | beforeEach(function() { 9 | return browser.ignoreSynchronization = true; 10 | }); 11 | 12 | it('should greet the named user', function() { 13 | expect($('h1').getText()).toEqual('Hello'); 14 | $('button').click(); 15 | expect($('h1').getText()).toEqual('Hello, World!'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /__tests__/reducers/jsonAPI.js: -------------------------------------------------------------------------------- 1 | import apiReducer from '../../src/reducers/jsonAPI.js' 2 | 3 | import { createStore } from 'redux' 4 | let store = createStore(apiReducer, {}) 5 | 6 | describe('github reducer', () => { 7 | it('should return the initial state', (done) => { 8 | const _products = [{a: 'a'}]; 9 | 10 | store.dispatch({ 11 | type: 'SET_API_DATA', 12 | products: _products 13 | }); 14 | 15 | expect(store.getState().products).toBe(_products) 16 | done() 17 | }) 18 | }) -------------------------------------------------------------------------------- /src/actions/hello.js: -------------------------------------------------------------------------------- 1 | import { HELLO_WORLD, SET_MESSAGE } from '../config/actionType' 2 | 3 | const helloActionCreator = { 4 | helloWorld: () => { 5 | return (dispatch, getState) => { 6 | return dispatch({ 7 | type: HELLO_WORLD, 8 | }) 9 | } 10 | }, 11 | setMessage: (_message) => { 12 | return (dispatch, getState) => { 13 | return dispatch({ 14 | type: SET_MESSAGE, 15 | message: _message 16 | }) 17 | } 18 | } 19 | } 20 | export default helloActionCreator -------------------------------------------------------------------------------- /src/components/Hello.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | import style from './Hello.css' 5 | 6 | const Hello = ({ onClick, message }) => { 7 | return ( 8 |
9 |

{ message }

10 | 11 |
12 | ) 13 | } 14 | 15 | Hello.propTypes = { 16 | onClick: PropTypes.func.isRequired, 17 | message: PropTypes.string.isRequired 18 | } 19 | 20 | export default Hello 21 | -------------------------------------------------------------------------------- /src/matchConfig.js: -------------------------------------------------------------------------------- 1 | 2 | import HelloWorld from './containers/HelloWorld' 3 | import PreloadHelloWorld from './containers/PreloadHelloWorld' 4 | import JsonAPI from './containers/JsonAPI' 5 | 6 | const matchConfig = [ 7 | { 8 | path: '/api', 9 | component: JsonAPI, 10 | initState: JsonAPI.initState 11 | }, 12 | { 13 | path: '/preload', 14 | component: PreloadHelloWorld, 15 | initState: PreloadHelloWorld.initState 16 | }, 17 | { 18 | path: '/', 19 | component: HelloWorld, 20 | initState: HelloWorld.initState, 21 | exact: false 22 | } 23 | ] 24 | 25 | export default matchConfig 26 | -------------------------------------------------------------------------------- /src/components/shared/EditableDiv.css: -------------------------------------------------------------------------------- 1 | .single-line { 2 | white-space: nowrap; 3 | overflow: hidden; 4 | text-overflow: ellipsis; 5 | display: inline-block; 6 | vertical-align: top; 7 | border: 1px solid #f3f3f3; 8 | } 9 | 10 | [contenteditable="true"].single-line { 11 | white-space: nowrap; 12 | width:200px; 13 | overflow-y: hidden; 14 | padding: 5px 10px; 15 | } 16 | [contenteditable="true"].single-line br { 17 | display:none; 18 | 19 | } 20 | [contenteditable="true"].single-line * { 21 | display:inline; 22 | white-space:nowrap; 23 | } 24 | 25 | .single-line:focus { 26 | text-overflow: inherit; 27 | } -------------------------------------------------------------------------------- /__tests__/actions/jsonAPI.js: -------------------------------------------------------------------------------- 1 | import jsonAPIActionCreator from '../../src/actions/jsonAPI' 2 | import configureStore from 'redux-mock-store'; 3 | import thunk from 'redux-thunk' 4 | const mockStore = configureStore([thunk]) 5 | 6 | describe('api action creator', () => { 7 | it('get apiJSON title', (done) => { 8 | var mockData = [{title:'_title'}] 9 | fetch.mockResponse(JSON.stringify(mockData)) 10 | 11 | const store = mockStore({}) 12 | const expectedActions = [ 13 | { type: 'SET_API_DATA', products: mockData } 14 | ] 15 | 16 | store.dispatch(jsonAPIActionCreator.getJsonAPI()) 17 | .then((res) => { 18 | // expect(store.getActions()).toEqual(expectedActions) 19 | done() 20 | }) 21 | }, 5000) 22 | }) -------------------------------------------------------------------------------- /src/components/shared/Loading.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import classNames from 'classnames' 5 | import style from './Loading.css' 6 | 7 | const LOADING_ICON = 'https://digitalsynopsis.com/wp-content/uploads/2016/06/loading-animations-preloader-gifs-ui-ux-effects-3.gif' 8 | 9 | const renderLoading = () => ( 10 |
11 | 12 |
13 | ) 14 | 15 | class Loading extends Component { 16 | render () { 17 | const { isLoading } = this.props 18 | return (isLoading) ? renderLoading() : null 19 | } 20 | } 21 | 22 | Loading.propTypes = { 23 | isLoading: PropTypes.bool 24 | } 25 | 26 | export default Loading -------------------------------------------------------------------------------- /src/containers/JsonAPI.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import jsonAPIActionCreator from '../actions/jsonAPI' 3 | import JsonAPI from '../components/JsonAPI' 4 | 5 | const mapStateToProps = (state, ownProps) => { 6 | return { 7 | products: state.jsonAPI.products 8 | } 9 | } 10 | 11 | const mapDispatchToProps = (dispatch, ownProps) => { 12 | return { 13 | fetchData: () => { 14 | dispatch(jsonAPIActionCreator.getJsonAPI()); 15 | } 16 | } 17 | } 18 | 19 | const jsonAPIContainer = connect( 20 | mapStateToProps, 21 | mapDispatchToProps 22 | )(JsonAPI) 23 | 24 | 25 | jsonAPIContainer.initState = (store, req, res) => { 26 | return (dispatch, getState) => { 27 | return new Promise((resolve, reject) => { 28 | resolve() 29 | }) 30 | } 31 | } 32 | 33 | export default jsonAPIContainer -------------------------------------------------------------------------------- /src/containers/HelloWorld.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import Hello from './../components/Hello' 3 | import helloActionCreator from '../actions/hello' 4 | import globalStyle from '../styles/global.css' 5 | 6 | const mapStateToProps = (state, ownProps) => { 7 | return { 8 | message: state.hello.message 9 | } 10 | } 11 | 12 | const mapDispatchToProps = (dispatch, ownProps) => { 13 | return { 14 | onClick: () => { 15 | dispatch(helloActionCreator.helloWorld()); 16 | } 17 | } 18 | } 19 | 20 | const hello = connect( 21 | mapStateToProps, 22 | mapDispatchToProps 23 | )(Hello) 24 | 25 | hello.initState = (store, req, res) => { 26 | return (dispatch, getState) => { 27 | return new Promise((resolve, reject) => { 28 | resolve() 29 | }) 30 | } 31 | } 32 | 33 | 34 | export default hello -------------------------------------------------------------------------------- /stories/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { storiesOf } from '@storybook/react'; 4 | import { action } from '@storybook/addon-actions'; 5 | import { linkTo } from '@storybook/addon-links'; 6 | 7 | import EditableDiv from '../src/components/shared/EditableDiv'; 8 | import Loading from '../src/components/shared/Loading'; 9 | 10 | storiesOf('Shared Component', module) 11 | .add('EditableDiv', () => { 12 | const onChange = _v => console.log('updated : ', _v) 13 | const onUpdate = _v => console.log('UPDATE value to ', _v) //trigged when press ENTER 14 | return 18 | }) 19 | .add('Loading', () => { 20 | return 22 | }) 23 | -------------------------------------------------------------------------------- /__tests__/reducers/hello.js: -------------------------------------------------------------------------------- 1 | import helloWorld from '../../src/reducers/hello.js' 2 | 3 | describe('helloWorld reducer', () => { 4 | it('should return the initial state', (done) => { 5 | expect( 6 | helloWorld([], {}) 7 | ).toEqual([]) 8 | done() 9 | }) 10 | 11 | it('should return the HELLO_WORLD state', (done) => { 12 | expect( 13 | helloWorld({hello:""}, { 14 | type: 'HELLO_WORLD' 15 | }) 16 | ).toEqual({ hello:'', message: 'Hello, World!' }) 17 | done() 18 | }) 19 | 20 | it('should return the SET_MESSAGE state', (done) => { 21 | 22 | var _message = 'I am message' 23 | 24 | expect( 25 | helloWorld({hello:""}, { 26 | type: 'SET_MESSAGE', 27 | message: _message 28 | }) 29 | ).toEqual({ hello:'', message: _message }) 30 | done() 31 | }) 32 | }) -------------------------------------------------------------------------------- /__tests__/components/Hello.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {shallow} from 'enzyme' 3 | import Hello from '../../src/components/Hello' 4 | 5 | jest.mock('../../src/components/Hello.css', () => { 6 | return jest.fn() 7 | }) 8 | 9 | var _mockMessage = 'mock message' 10 | var _mockFun = jest.fn() 11 | function setup() { 12 | const props = { 13 | onClick: _mockFun, 14 | message: _mockMessage 15 | } 16 | 17 | const enzymeWrapper = shallow() 18 | 19 | return {props, enzymeWrapper} 20 | } 21 | 22 | describe('components', () => { 23 | it('should render self and subcomponents', (done) => { 24 | const {enzymeWrapper} = setup() 25 | const newMessage = 'Hello, World!' 26 | 27 | expect(enzymeWrapper.find('h1').text()).toBe(_mockMessage) 28 | enzymeWrapper.find('button').simulate('click') 29 | expect(_mockFun).toBeCalled() 30 | 31 | done() 32 | }) 33 | }) -------------------------------------------------------------------------------- /src/containers/PreloadHelloWorld.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import Hello from './../components/Hello' 3 | import helloActionCreator from '../actions/hello' 4 | 5 | const mapStateToProps = (state, ownProps) => { 6 | return { 7 | message: state.hello.message 8 | } 9 | } 10 | 11 | const mapDispatchToProps = (dispatch, ownProps) => { 12 | return { 13 | onClick: () => { 14 | dispatch(helloActionCreator.helloWorld()); 15 | } 16 | } 17 | } 18 | 19 | const preloadHello = connect( 20 | mapStateToProps, 21 | mapDispatchToProps 22 | )(Hello) 23 | 24 | preloadHello.initState = (store, req, res) => { 25 | return (dispatch, getState) => { 26 | return new Promise((resolve, reject) => { 27 | console.log('preload ......................') 28 | dispatch(helloActionCreator.helloWorld()) 29 | resolve(1) 30 | }) 31 | } 32 | } 33 | 34 | export default preloadHello -------------------------------------------------------------------------------- /src/components/shared/EditableDiv.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import classNames from 'classnames' 3 | import style from './EditableDiv.css' 4 | 5 | class EditableDiv extends React.Component { 6 | constructor (props) { 7 | super(props) 8 | } 9 | 10 | render () { 11 | const { value, onChange, onUpdate } = this.props 12 | const _onChange = (e) => onChange(e.target.textContent) 13 | const css = classNames(style['single-line']) 14 | const handleKeyPress = (target) => { 15 | (target.charCode === 13)?onUpdate():null 16 | } 17 | return (
23 | {value} 24 |
) 25 | } 26 | } 27 | 28 | EditableDiv.propTypes = { 29 | } 30 | 31 | export default EditableDiv 32 | -------------------------------------------------------------------------------- /src/components/JsonAPI.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | class JsonAPI extends React.Component { 5 | componentDidMount() { 6 | this.props.fetchData() 7 | } 8 | 9 | render() { 10 | const { products } = this.props 11 | const _renderli = products.map((item, index) => { 12 | return ( 13 |
14 |

{item.id} 15 | {item.title} 16 |

17 |

{item.body}

18 |
19 | ) 20 | }) 21 | 22 | return ( 23 |
24 |

Response

25 | https://jsonplaceholder.typicode.com/posts 26 |
27 | {_renderli} 28 |
29 |
30 | ) 31 | } 32 | } 33 | 34 | JsonAPI.propTypes = { 35 | fetchData: PropTypes.func.isRequired, 36 | products: PropTypes.array.isRequired 37 | } 38 | 39 | export default JsonAPI 40 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | // you can use this file to add your custom webpack plugins, loaders and anything you like. 2 | // This is just the basic way to add additional webpack configurations. 3 | // For more information refer the docs: https://storybook.js.org/configurations/custom-webpack-config 4 | 5 | // IMPORTANT 6 | // When you add this file, we won't add the default configurations which is similar 7 | // to "React Create App". This only has babel loader to load JavaScript. 8 | 9 | const path = require('path'); 10 | 11 | module.exports = { 12 | plugins: [ 13 | // your custom plugins 14 | ], 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.css$/, 19 | use: [ 20 | 'style-loader', 21 | { 22 | loader: 'css-loader', 23 | options: { 24 | modules: true, 25 | importLoaders: 1, 26 | localIdentName: '[name]__[local]___[hash:base64:5]' 27 | } 28 | }, 29 | { 30 | loader: 'postcss-loader' 31 | } 32 | ] 33 | } 34 | ] 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Vaibhav Mule 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /__tests__/containers/HelloWorld.js: -------------------------------------------------------------------------------- 1 | 2 | import { Provider } from 'react-redux' 3 | import { createStore, compose, applyMiddleware } from 'redux' 4 | import { shallow, mount } from 'enzyme' 5 | import React from 'react' 6 | import thunk from 'redux-thunk' 7 | 8 | import HelloWorld from '../../src/containers/HelloWorld.js' 9 | 10 | import reducer from '../../src/reducers' 11 | 12 | jest.mock('../../src/components/Hello.css', () => { 13 | return jest.fn() 14 | }) 15 | jest.mock('../../src/styles/global.css', () => { 16 | return jest.fn() 17 | }) 18 | 19 | 20 | let store = createStore( 21 | reducer, 22 | compose(applyMiddleware(thunk)) 23 | ) 24 | 25 | const enzymeWrapper = mount( 26 | 27 | 28 | ) 29 | 30 | describe('containers', () => { 31 | it('should render self and subcomponents', (done) => { 32 | var _message1 = 'Hello' 33 | var _message2 = 'Hello, World!' 34 | 35 | expect(enzymeWrapper.find('h1').text()).toBe(_message1) 36 | enzymeWrapper.find('button').simulate('click'); 37 | expect(enzymeWrapper.find('h1').text()).toBe(_message2) 38 | done() 39 | }) 40 | }) -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | 2 | require('babel-register'); 3 | 4 | let app = new (require('express'))() 5 | const port = 3000 6 | 7 | require('css-modules-require-hook')({ 8 | generateScopedName: '[name]__[local]___[hash:base64:5]' 9 | }) 10 | 11 | 12 | // initalize webpack dev middleware if in development context 13 | if (process.env.NODE_ENV === 'development') { 14 | let webpack = require('webpack') 15 | let config = require('../webpack.config') 16 | 17 | let devMiddleware = require('webpack-dev-middleware') 18 | let hotDevMiddleware = require('webpack-hot-middleware') 19 | let compiler = webpack(config) 20 | let devMiddlewareConfig = { 21 | noInfo: true, 22 | stats: {colors: true}, 23 | publicPath: config.output.publicPath 24 | } 25 | 26 | app.use(devMiddleware(compiler, devMiddlewareConfig)) 27 | app.use(hotDevMiddleware(compiler)) 28 | } 29 | 30 | app.use(require('express').static('public')) 31 | 32 | let serverRender = require('./serverRender') 33 | 34 | app.get('*', serverRender) 35 | 36 | app.listen(port, function(error) { 37 | if (error) { 38 | console.error(error) 39 | } else { 40 | console.info('==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.', port, port) 41 | } 42 | }) -------------------------------------------------------------------------------- /src/clientRender.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | 3 | import React from 'react' 4 | import { render } from 'react-dom' 5 | import { Provider } from 'react-redux' 6 | import { 7 | createStore, 8 | compose, 9 | applyMiddleware } from 'redux' 10 | import thunk from 'redux-thunk' 11 | import _reducers from './reducers' 12 | import matchConfig from './matchConfig' 13 | import { 14 | BrowserRouter, 15 | Route, 16 | Switch 17 | } from 'react-router-dom' 18 | 19 | const composeEnhancers = process.env.NODE_ENV !== 'production' && 20 | typeof window !== 21 | 'undefined' && 22 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? 23 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : 24 | compose 25 | 26 | const initState = window.__PRELOADED_STATE__ 27 | delete window.__PRELOADED_STATE__ 28 | 29 | const store = createStore( 30 | _reducers, 31 | initState, 32 | composeEnhancers(applyMiddleware(thunk)), 33 | ) 34 | 35 | render( 36 | 37 | 38 | 39 | { 40 | matchConfig.map((route, index) => ) 41 | } 42 | 43 | 44 | , 45 | document.getElementById('root') 46 | ) -------------------------------------------------------------------------------- /__e2e__/config.js: -------------------------------------------------------------------------------- 1 | // conf.js 2 | 3 | var _sauceUser= process.env.SAUCE_USERNAME, 4 | _sauceKey= process.env.SAUCE_KEY; 5 | 6 | function genSeleniumAddress(){ 7 | return (!process.env.IS_REMOTE)? 'http://127.0.0.1:4444/wd/hub' : "http://" + _sauceUser + ":" + _sauceKey + 8 | "@ondemand.saucelabs.com:80/wd/hub" 9 | } 10 | 11 | exports.config = { 12 | sauceUser: _sauceUser, 13 | sauceKey: _sauceKey, 14 | 15 | seleniumAddress: genSeleniumAddress(), 16 | 17 | specs: ['specs/*spec.js'], 18 | 19 | // restartBrowserBetweenTests: true, 20 | 21 | onPrepare: function(){ 22 | var caps = browser.getCapabilities() 23 | }, 24 | 25 | multiCapabilities: [{ 26 | browserName: 'chrome', 27 | version: '41', 28 | platform: 'Windows 7', 29 | name: "chrome-tests", 30 | shardTestFiles: true, 31 | maxInstances: 25 32 | }], 33 | 34 | jasmineNodeOpts: { 35 | defaultTimeoutInterval: 10000 36 | }, 37 | onComplete: function() { 38 | 39 | var printSessionId = function(jobName){ 40 | browser.getSession().then(function(session) { 41 | console.log('SauceOnDemandSessionID=' + session.getId() + ' job-name=' + jobName); 42 | }); 43 | } 44 | printSessionId("Insert Job Name Here"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve, join } = require('path') 2 | const webpack = require('webpack') 3 | 4 | const config = { 5 | devtool: 'cheap-eval-source-map', 6 | entry: { 7 | home: [ 8 | 'webpack-hot-middleware/client', 9 | './src/clientRender.js' 10 | ] 11 | }, 12 | output: { 13 | path: resolve(__dirname,'public/'), 14 | filename: 'bundle.js', 15 | publicPath: '/static/' 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.js$/, 21 | use: [ 22 | 'babel-loader' 23 | ], 24 | exclude: '/node_modules/' 25 | }, 26 | { 27 | test: /node_modules\/.+\.css$/, 28 | use: [ 29 | 'style-loader', 30 | { 31 | loader: 'css-loader', 32 | options: { 33 | modules: false, 34 | importLoaders: 1 35 | } 36 | } 37 | ] 38 | }, 39 | { 40 | test: /\.css$/, 41 | use: [ 42 | 'style-loader', 43 | { 44 | loader: 'css-loader', 45 | options: { 46 | modules: true, 47 | importLoaders: 1, 48 | localIdentName: '[name]__[local]___[hash:base64:5]' 49 | } 50 | }, 51 | { 52 | loader: 'postcss-loader' 53 | } 54 | ] 55 | } 56 | ] 57 | }, 58 | plugins: [ 59 | new webpack.HotModuleReplacementPlugin(), 60 | new webpack.NoEmitOnErrorsPlugin(), 61 | new webpack.NamedModulesPlugin() 62 | ] 63 | } 64 | module.exports = config 65 | -------------------------------------------------------------------------------- /webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | 2 | const { resolve, join } = require('path') 3 | const webpack = require('webpack') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | 6 | const config = { 7 | entry: { 8 | bundle: [ 9 | './src/clientRender.js' 10 | ], 11 | 'vendor/js': [ 12 | 'react', 13 | 'react-dom', 14 | 'redux', 15 | 'react-redux' 16 | ] 17 | }, 18 | output: { 19 | path: resolve(__dirname, 'public/static'), 20 | filename: '[name].js', 21 | publicPath: '/static/' 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.js[x]?$/, 27 | enforce: 'pre', 28 | use: [{ 29 | loader: 'eslint-loader', 30 | options: { fix: true } 31 | }], 32 | exclude: '/node_modules/' 33 | }, 34 | { 35 | test: /\.js$/, 36 | use: [ 37 | 'babel-loader' 38 | ], 39 | exclude: '/node_modules/' 40 | }, 41 | { 42 | test: /\.css$/, 43 | use: ExtractTextPlugin.extract({ 44 | use: [ 45 | { 46 | loader: 'css-loader', 47 | options: { 48 | modules: true, 49 | importLoaders: 1, 50 | localIdentName: '[name]_[local]___[hash:base64:5]' 51 | } 52 | }, 53 | { 54 | loader: 'postcss-loader' 55 | } 56 | ] 57 | }) 58 | } 59 | ] 60 | }, 61 | plugins: [ 62 | new webpack.optimize.CommonsChunkPlugin({ 63 | name: 'vendor', 64 | minChunks: function (module) { 65 | // this assumes your vendor imports exist in the node_modules directory 66 | return module.context && module.context.indexOf('node_modules') !== -1 67 | } 68 | }), 69 | new ExtractTextPlugin({ 70 | filename: '[name].css', 71 | disable: false, 72 | allChunks: true 73 | }) 74 | ] 75 | } 76 | 77 | module.exports = config 78 | -------------------------------------------------------------------------------- /src/serverRender.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | 3 | import React from 'react' 4 | import ReactDOMServer from 'react-dom/server' 5 | import { createStore, compose, applyMiddleware } from 'redux' 6 | import thunk from 'redux-thunk' 7 | import _reducers from './reducers' 8 | import { Provider } from 'react-redux'; 9 | import matchConfig from './matchConfig' 10 | import { 11 | StaticRouter, 12 | Route, 13 | Switch, 14 | matchPath 15 | } from 'react-router-dom' 16 | 17 | 18 | function serverRender(req, res) { 19 | const composeEnhancers = process.env.NODE_ENV !== 'production' && 20 | typeof window !== 'undefined' && 21 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? 22 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : 23 | compose 24 | 25 | const store = createStore( 26 | _reducers, 27 | composeEnhancers(applyMiddleware(thunk)) 28 | ) 29 | 30 | let initState; 31 | matchConfig.some(route => { 32 | const match = matchPath(req.url, route) 33 | if (match) { 34 | initState = route.initState 35 | } 36 | return match 37 | }) 38 | 39 | store.dispatch(initState(store, req, res)) 40 | .then(() => { 41 | renderStoreRouter(store, req, res) 42 | }) 43 | } 44 | 45 | function renderStoreRouter(store, req, res) { 46 | const context = {} 47 | const componentStr = ReactDOMServer.renderToString( 48 | 49 | 50 | 51 | { 52 | matchConfig.map((route, index) => ) 53 | } 54 | 55 | 56 | 57 | ) 58 | res.send(renderFullPage(componentStr, store.getState())) 59 | } 60 | 61 | 62 | function renderFullPage(html, preloadedState) { 63 | let vendorJS = '' 64 | let bundleCSS = '' 65 | if (process.env.NODE_ENV === 'development') { 66 | // do something 67 | } else { 68 | bundleCSS = '/static/bundle.css' 69 | vendorJS = '/static/vendor.js' 70 | } 71 | return ` 72 | 73 | 74 | 75 | Redux Hello World 76 | 77 | 78 | 79 |
${`
${html}
`}
80 | 85 | 86 | 87 | 88 | 89 | ` 90 | } 91 | 92 | module.exports = serverRender 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-boilerplate", 3 | "version": "1.0.0", 4 | "description": "Hello, World! app in React and Redux", 5 | "main": "./src/server.js", 6 | "dependencies": { 7 | "babel-cli": "^6.24.1", 8 | "babel-polyfill": "^6.23.0", 9 | "babel-runtime": "^6.23.0", 10 | "classnames": "^2.2.5", 11 | "express": "^4.15.3", 12 | "isomorphic-fetch": "^2.2.1", 13 | "prop-types": "^15.5.10", 14 | "react": "^15.6.1", 15 | "react-dom": "^15.6.1", 16 | "react-redux": "^5.0.5", 17 | "react-router-dom": "^4.1.2", 18 | "redux": "^3.7.2", 19 | "redux-logger": "^3.0.6", 20 | "redux-mock-store": "^1.2.3", 21 | "redux-thunk": "^2.2.0" 22 | }, 23 | "devDependencies": { 24 | "@storybook/react": "^3.1.8", 25 | "babel-core": "^6.7.6", 26 | "babel-eslint": "^7.2.3", 27 | "babel-jest": "*", 28 | "babel-loader": "^6.2.4", 29 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 30 | "babel-plugin-transform-runtime": "^6.7.5", 31 | "babel-preset-es2015": "^6.6.0", 32 | "babel-preset-react": "^6.5.0", 33 | "babel-preset-react-hmre": "^1.1.1", 34 | "babel-preset-stage-0": "^6.5.0", 35 | "babel-register": "^6.7.2", 36 | "css-loader": "^0.28.4", 37 | "css-modules-require-hook": "^4.0.6", 38 | "enzyme": "^2.8.0", 39 | "eslint": "^4.2.0", 40 | "eslint-loader": "^1.9.0", 41 | "eslint-plugin-import": "^2.7.0", 42 | "eslint-plugin-react": "^7.1.0", 43 | "extract-text-webpack-plugin": "^2.1.2", 44 | "install": "^0.10.1", 45 | "jest": "^19.0.2", 46 | "jest-fetch-mock": "^1.1.1", 47 | "postcss-loader": "^2.0.5", 48 | "react-addons-test-utils": "^15.4.2", 49 | "regenerator-runtime": "*", 50 | "style-loader": "^0.18.2", 51 | "webpack": "^3.0.0", 52 | "webpack-dev-middleware": "^1.10.2", 53 | "webpack-hot-middleware": "^2.18.0" 54 | }, 55 | "scripts": { 56 | "build": "NODE_ENV=production babel src --out-dir dist --copy-files && webpack -p --progress --config webpack.prod.config.js", 57 | "start": "NODE_ENV=production node ./dist/server.js", 58 | "dev": "NODE_ENV=development babel-node ./src/server.js --presets es2015,stage-2", 59 | "test": "./node_modules/jest/bin/jest.js --coverage", 60 | "test:watch": "./node_modules/jest/bin/jest.js --watchAll #runs all tests --cache ", 61 | "storybook": "start-storybook -p 6006", 62 | "build-storybook": "build-storybook", 63 | "lint:fix": "eslint --fix .js src", 64 | "lint": "eslint --ext .js src" 65 | }, 66 | "eslintConfig": { 67 | "env": { 68 | "browser": true, 69 | "node": true 70 | } 71 | }, 72 | "repository": { 73 | "type": "git", 74 | "url": "git+https://github.com/wahengchang/react-redux-boilerplate.git" 75 | }, 76 | "keywords": [ 77 | "react", 78 | "redux", 79 | "boilerplate", 80 | "tutorial" 81 | ], 82 | "author": "waheng", 83 | "license": "ISC", 84 | "bugs": { 85 | "url": "https://github.com/wahengchang/react-redux-boilerplate/issues" 86 | }, 87 | "homepage": "https://github.com/wahengchang/react-redux-boilerplate#readme", 88 | "jest": { 89 | "testPathIgnorePatterns": [ 90 | "./__tests__/notRun.js" 91 | ], 92 | "automock": false, 93 | "setupFiles": [ 94 | "./setupJest.js" 95 | ] 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-redux-boilerplate 2 | - It is React-Redux as infrastructure boilerplate, with this developers just focus on developing component, action creator and reducer, without spending time in router, dev/pro build enviroement, wiring up file and express as HTTP server. 3 | 4 | ## Read more 5 | - [Example: Adding new page](https://github.com/wahengchang/react-redux-boilerplate/wiki/Add-new-page) 6 | - [Example: Thread upvote/downvote project](https://github.com/wahengchang/react-redux-boilerplate-example) 7 | - [Shared components Storybook](https://github.com/wahengchang/react-redux-boilerplate/tree/master/src/components) `$ npm run storybook` 8 | - EditableDiv 9 | - Loading icon 10 | 11 | ## Install 12 | 13 | ``` 14 | $ git clone https://github.com/wahengchang/react-redux-boilerplate 15 | $ npm install 16 | ``` 17 | 18 | ## Run (Develop Mode) 19 | It is runnign in Development mode, enable HMR 20 | - http://localhost:3000/ 21 | - http://localhost:3000/api , example of fetching data by action, and dispatch to update UI component 22 | - http://localhost:3000/preload , example of fetch data from [here](https://jsonplaceholder.typicode.com/posts 23 | ), before server rendering components 24 | 25 | ``` 26 | $ npm run dev 27 | open http://localhost:3000/ 28 | 29 | ``` 30 | 31 | 32 | ## Run (Production Mode) 33 | - Compiling react/redux script to browser use lib, with webpack default optimized config. 34 | - Disable unnecessary funtionality which helps to debug in development mode 35 | ``` 36 | $ npm run build 37 | $ node dist/server.js 38 | 39 | ``` 40 | 41 | ## lint 42 | 43 | #### lint fix 44 | ``` 45 | $ npm run lint:fix 46 | ``` 47 | 48 | #### lint check 49 | ``` 50 | $ npm run lint 51 | ``` 52 | 53 | 54 | ## Storybook 55 | ``` 56 | $ npm run storybook 57 | ``` 58 | 59 | 60 | ## Test 61 | [More detail](https://github.com/wahengchang/react-redux-boilerplate/wiki/Test ): about test of action creater, component, container and reducer 62 | 63 | ``` 64 | $ npm run test 65 | 66 | 67 | Test Suites: 4 passed, 4 total 68 | Tests: 6 passed, 6 total 69 | Snapshots: 0 total 70 | Time: 1.824s, estimated 2s 71 | 72 | ``` 73 | or watch mode 74 | ``` 75 | $ npm run test:watch 76 | ``` 77 | 78 | 79 | # Server Rendering Structure 80 | ![Server Rendering structure](https://cdn-images-1.medium.com/max/1500/1*uu7MvpLsU-UUzYCG42M8hA.jpeg "React Redux server rendering structure") 81 | Above is the structure of how the whole app works, the app bases on Express web framework, which serves only one route, with res.sendFile function to put index.html into the browser. Inside the scoop of the structure, what we are interested is the blue box, the interaction between react component, redux, root component, store and reducer. 82 | 83 | # Shared Components 84 | ![react-redux-universial-container-compont](https://user-images.githubusercontent.com/5538753/27771266-2af87dde-5f7e-11e7-9c7c-ec92b57643aa.jpg) 85 | 86 | 87 | # React-Redux Structure 88 | ![react-helloworld-component-5-20](https://user-images.githubusercontent.com/5538753/27132284-dfaeda12-5140-11e7-9855-7681362a00f8.jpg) 89 | _**index.js**_ , as the entry file and a high level root component, which gathers all the sub-component as the subtree of the Virtual DOM, also it is the only file entangled with many independent modules. Apart from it, different file requires independent modules, which makes clean code and work independently. 90 | 91 | 92 | ## Credit 93 | - [https://github.com/vaibhavmule/react-redux-helloworld](https://github.com/vaibhavmule/react-redux-helloworld) 94 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true, 6 | "commonjs": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:react/recommended" 11 | ], 12 | "parserOptions": { 13 | "ecmaVersion": 2017, 14 | "sourceType": "module", 15 | "ecmaFeatures": { 16 | "experimentalObjectRestSpread": true, 17 | "jsx": true, 18 | "arrowFunctions": true, 19 | "classes": true, 20 | "modules": true, 21 | "defaultParams": true 22 | } 23 | }, 24 | "parser": "babel-eslint", 25 | "plugins": [ 26 | "react" 27 | ], 28 | "rules": { 29 | "no-console": "off", 30 | "for-direction": "error", 31 | "getter-return": [ 32 | "error", 33 | { 34 | allowImplicit: false 35 | } 36 | ], 37 | "no-await-in-loop": "error", 38 | "no-compare-neg-zero": "error", 39 | "no-cond-assign": [ 40 | "error", 41 | "except-parens" 42 | ], 43 | "no-constant-condition": [ 44 | "error", 45 | { 46 | checkLoops: false 47 | } 48 | ], 49 | "no-control-regex": "error", 50 | "no-debugger": "error", 51 | "no-dupe-args": "error", 52 | "no-dupe-keys": "error", 53 | "no-duplicate-case": "error", 54 | "no-empty": [ 55 | "error", 56 | { 57 | allowEmptyCatch: false 58 | } 59 | ], 60 | "no-empty-character-class": "error", 61 | "no-ex-assign": "error", 62 | "no-extra-boolean-cast": "error", 63 | "no-extra-parens": [ 64 | "error", 65 | "all", 66 | { 67 | conditionalAssign: false, 68 | returnAssign: false, 69 | nestedBinaryExpressions: false, 70 | ignoreJSX: "multi-line", 71 | enforceForArrowConditionals: false 72 | } 73 | ], 74 | "no-extra-semi": "error", 75 | "no-func-assign": "error", 76 | "no-inner-declarations": [ 77 | "error", 78 | "both" 79 | ], 80 | "no-invalid-regexp": "error", 81 | "no-irregular-whitespace": [ 82 | "error", 83 | { 84 | skipStrings: true, 85 | skipComments: false, 86 | skipRegExps: true, 87 | skipTemplates: true 88 | } 89 | ], 90 | "no-obj-calls": "error", 91 | "no-regex-spaces": "error", 92 | "no-sparse-arrays": "error", 93 | "no-template-curly-in-string": "error", 94 | "no-unexpected-multiline": "error", 95 | "no-unreachable": "error", 96 | "no-unsafe-finally": "error", 97 | "no-unsafe-negation": "error", 98 | "use-isnan": "error", 99 | "valid-typeof": "error", 100 | "accessor-pairs": [ 101 | "error", 102 | { 103 | setWithoutGet: true, 104 | getWithoutSet: false 105 | } 106 | ], 107 | "array-callback-return": "error", 108 | "block-scoped-var": "error", 109 | "complexity": [ 110 | "error", 111 | { 112 | max: 5 113 | } 114 | ], 115 | "curly": [ 116 | "error", 117 | "all" 118 | ], 119 | "dot-location": [ 120 | "error", 121 | "property" 122 | ], 123 | "eqeqeq": [ 124 | "error", 125 | "always" 126 | ], 127 | "guard-for-in": "error", 128 | "no-caller": "error", 129 | "no-case-declarations": "error", 130 | "no-empty-function": [ 131 | "error", 132 | { 133 | allow: [ 134 | "functions", 135 | "arrowFunctions" 136 | ] 137 | } 138 | ], 139 | "no-empty-pattern": "error", 140 | "no-eq-null": "error", 141 | "no-eval": "error", 142 | "no-extend-native": "error", 143 | "no-extra-bind": "error", 144 | "no-extra-label": "error", 145 | "no-fallthrough": "error", 146 | "no-floating-decimal": "error", 147 | "no-global-assign": "error", 148 | "no-implicit-coercion": "error", 149 | "no-implicit-globals": "error", 150 | "no-implied-eval": "error", 151 | "no-invalid-this": "error", 152 | "no-iterator": "error", 153 | "no-labels": "error", 154 | "no-lone-blocks": "error", 155 | "no-loop-func": "error", 156 | "no-magic-numbers": [ 157 | "error", 158 | { 159 | ignore: [ 160 | -1, 161 | 0, 162 | 1, 163 | 2, 164 | 3, 165 | 100 166 | ], 167 | ignoreArrayIndexes: true, 168 | enforceConst: true, 169 | detectObjects: false 170 | } 171 | ], 172 | "no-multi-spaces": [ 173 | "error", 174 | { 175 | ignoreEOLComments: true, 176 | exceptions: { 177 | Property: true, 178 | BinaryExpression: false, 179 | VariableDeclarator: true, 180 | ImportDeclaration: true 181 | } 182 | } 183 | ], 184 | "no-multi-str": "error", 185 | "no-new": "error", 186 | "no-new-func": "error", 187 | "no-new-wrappers": "error", 188 | "no-octal": "error", 189 | "no-octal-escape": "error", 190 | "no-param-reassign": "error", 191 | "no-proto": "error", 192 | "no-redeclare": "error", 193 | "no-restricted-properties": "error", 194 | "no-return-assign": [ 195 | "error", 196 | "always" 197 | ], 198 | "no-return-await": "error", 199 | "no-self-assign": "error", 200 | "no-self-compare": "error", 201 | "no-sequences": "error", 202 | "no-throw-literal": "error", 203 | "no-unmodified-loop-condition": "error", 204 | "no-unused-expressions": [ 205 | "error", 206 | { 207 | allowShortCircuit: true, 208 | allowTernary: true, 209 | allowTaggedTemplates: true 210 | } 211 | ], 212 | "no-unused-labels": "error", 213 | "no-useless-call": "error", 214 | "no-useless-concat": "error", 215 | "no-useless-escape": "error", 216 | "no-void": "error", 217 | "no-with": "error", 218 | "prefer-promise-reject-errors": "error", 219 | "radix": "error", 220 | "require-await": "error", 221 | "wrap-iife": [ 222 | "error", 223 | "outside", 224 | { 225 | functionPrototypeMethods: true 226 | } 227 | ], 228 | "yoda": [ 229 | "error", 230 | "never", 231 | { 232 | exceptRange: false, 233 | onlyEquality: false 234 | } 235 | ], 236 | "strict": [ 237 | "error", 238 | "never" 239 | ], 240 | "no-delete-var": "error", 241 | "no-label-var": "error", 242 | "no-shadow": [ 243 | "error", 244 | { 245 | builtinGlobals: false, 246 | hoist: "functions" 247 | } 248 | ], 249 | "no-shadow-restricted-names": "error", 250 | "no-undef": [ 251 | "error", 252 | { 253 | typeof: false 254 | } 255 | ], 256 | "no-undef-init": "error", 257 | "no-undefined": "error", 258 | "no-unused-vars": [ 259 | "error", 260 | { 261 | varsIgnorePattern: "[S|s]tyle", 262 | vars: "all", 263 | args: "none", 264 | caughtErrors: "none" 265 | } 266 | ], 267 | "no-use-before-define": [ 268 | "error", 269 | { 270 | functions: false, 271 | classes: true, 272 | variables: true 273 | } 274 | ], 275 | // "global-require": "error", 276 | "handle-callback-err": "error", 277 | "no-buffer-constructor": "error", 278 | "no-new-require": "error", 279 | "no-path-concat": "error", 280 | "array-bracket-newline": [ 281 | "error", 282 | { 283 | multiline: true, 284 | minItems: null 285 | } 286 | ], 287 | "array-bracket-spacing": [ 288 | "error", 289 | "never" 290 | ], 291 | "block-spacing": [ 292 | "error", 293 | "always" 294 | ], 295 | "brace-style": [ 296 | "error", 297 | "1tbs", 298 | { 299 | allowSingleLine: false 300 | } 301 | ], 302 | "comma-spacing": [ 303 | "error", 304 | { 305 | "before": false, 306 | "after": true 307 | } 308 | ], 309 | "comma-style": [ 310 | "error", 311 | "last" 312 | ], 313 | "computed-property-spacing": [ 314 | "error", 315 | "never" 316 | ], 317 | "func-call-spacing": [ 318 | "error", 319 | "never" 320 | ], 321 | "func-name-matching": [ 322 | "error", 323 | "always", 324 | { 325 | includeCommonJSModuleExports: false 326 | } 327 | ], 328 | "indent": [ 329 | "error", 330 | 2, 331 | { 332 | SwitchCase: 1, 333 | VariableDeclarator: 1, 334 | outerIIFEBody: 1, 335 | MemberExpression: 1, 336 | FunctionDeclaration: { 337 | body: 1, 338 | parameters: 1 339 | }, 340 | CallExpression: { 341 | arguments: 1 342 | }, 343 | ArrayExpression: 1, 344 | ObjectExpression: 1, 345 | flatTernaryExpressions: true 346 | } 347 | ], 348 | "jsx-quotes": [ 349 | "error", 350 | "prefer-double" 351 | ], 352 | "key-spacing": [ 353 | "error", 354 | { 355 | beforeColon: false, 356 | afterColon: true, 357 | mode: "strict", 358 | } 359 | ], 360 | "keyword-spacing": [ 361 | "error", 362 | { 363 | before: true, 364 | after: true 365 | } 366 | ], 367 | "max-depth": [ 368 | "error", 369 | 5 370 | ], 371 | "max-lines": [ 372 | "error", 373 | { 374 | max: 300, 375 | skipBlankLines: true, 376 | skipComments: true 377 | } 378 | ], 379 | "max-nested-callbacks": [ 380 | "error", 381 | 3 382 | ], 383 | "max-params": [ 384 | "error", 385 | 7 386 | ], 387 | "new-cap": [ 388 | "error", 389 | { 390 | newIsCap: true, 391 | capIsNew: false, 392 | properties: true 393 | } 394 | ], 395 | "new-parens": "error", 396 | "no-array-constructor": "error", 397 | "no-mixed-spaces-and-tabs": "error", 398 | "no-multiple-empty-lines": [ 399 | "error", 400 | { 401 | max: 3, 402 | maxEOF: 1, 403 | maxBOF: 1 404 | } 405 | ], 406 | "no-new-object": "error", 407 | "no-tabs": "error", 408 | "no-trailing-spaces": "error", 409 | "no-unneeded-ternary": "error", 410 | "no-whitespace-before-property": "error", 411 | "nonblock-statement-body-position": [ 412 | "error", 413 | "beside", 414 | { 415 | overrides: { 416 | while: "below" 417 | } 418 | } 419 | ], 420 | "object-curly-newline": [ 421 | "error", 422 | { 423 | multiline: true, 424 | consistent: true 425 | } 426 | ], 427 | // "object-curly-spacing": [ 428 | // "error", 429 | // "never", 430 | // { 431 | // arraysInObjects: false, 432 | // objectsInObjects: false 433 | // } 434 | // ], 435 | "one-var": [ 436 | "error", 437 | "never" 438 | ], 439 | "one-var-declaration-per-line": [ 440 | "error", 441 | "always" 442 | ], 443 | "operator-linebreak": [ 444 | "error", 445 | "after" 446 | ], 447 | "padded-blocks": [ 448 | "error", 449 | "never" 450 | ], 451 | "quotes": [ 452 | "error", 453 | "single", 454 | { 455 | avoidEscape: true, 456 | allowTemplateLiterals: true 457 | } 458 | ], 459 | // "semi": [ 460 | // "error", 461 | // "always", 462 | // { 463 | // omitLastInOneLineBlock: true 464 | // } 465 | // ], 466 | "semi-spacing": [ 467 | "error", 468 | { 469 | before: false, 470 | after: true 471 | } 472 | ], 473 | "semi-style": [ 474 | "error", 475 | "last" 476 | ], 477 | "space-before-blocks": [ 478 | "error", 479 | "always" 480 | ], 481 | // "space-before-function-paren": [ 482 | // "error", 483 | // { 484 | // anonymous: "never", 485 | // named: "never", 486 | // asyncArrow: "always" 487 | // } 488 | // ], 489 | "space-in-parens": [ 490 | "error", 491 | "never" 492 | ], 493 | "space-infix-ops": "error", 494 | "space-unary-ops": [ 495 | "error", 496 | { 497 | words: true, 498 | nonwords: false 499 | } 500 | ], 501 | "spaced-comment": [ 502 | "error", 503 | "always", 504 | { 505 | exceptions: [ 506 | "*" 507 | ] 508 | } 509 | ], 510 | "switch-colon-spacing": [ 511 | "error", 512 | { 513 | after: true, 514 | before: false 515 | } 516 | ], 517 | "template-tag-spacing": [ 518 | "error", 519 | "never" 520 | ], 521 | "unicode-bom": [ 522 | "error", 523 | "never" 524 | ], 525 | "arrow-spacing": [ 526 | "error", 527 | { 528 | before: true, 529 | after: true 530 | } 531 | ], 532 | "constructor-super": "error", 533 | "generator-star-spacing": [ 534 | "error", 535 | { 536 | before: false, 537 | after: true 538 | } 539 | ], 540 | "no-class-assign": "error", 541 | "no-confusing-arrow": "error", 542 | "no-const-assign": "error", 543 | "no-dupe-class-members": "error", 544 | "no-duplicate-imports": "error", 545 | "no-new-symbol": "error", 546 | "no-this-before-super": "error", 547 | "no-useless-computed-key": "error", 548 | "no-useless-constructor": "error", 549 | "no-useless-rename": "error", 550 | "no-var": "error", 551 | // "object-shorthand": [ 552 | // "error", 553 | // "always", 554 | // { 555 | // avoidQuotes: true, 556 | // ignoreConstructors: false, 557 | // avoidExplicitReturnArrows: true 558 | // } 559 | // ], 560 | "prefer-numeric-literals": "error", 561 | "prefer-rest-params": "error", 562 | "prefer-spread": "error", 563 | "require-yield": "error", 564 | "rest-spread-spacing": [ 565 | "error", 566 | "never" 567 | ], 568 | "symbol-description": "error", 569 | "template-curly-spacing": [ 570 | "error", 571 | "never" 572 | ], 573 | "yield-star-spacing": [ 574 | "error", 575 | "after" 576 | ] 577 | } 578 | } --------------------------------------------------------------------------------