Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

react-isomorphic-boilerplate

Package Overview
Dependencies
Maintainers
1
Versions
24
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

react-isomorphic-boilerplate - npm Package Compare versions

Comparing version 0.1.0 to 0.1.1

src/__mocks__/configureStore.js

18

package.json
{
"name": "react-isomorphic-boilerplate",
"version": "0.1.0",
"main": "index.js",
"version": "0.1.1",
"main": "src/server/index.js",
"license": "MIT",
"scripts": {
"start": "DEBUG=*,-nodemon*,-express*,-send nodemon --inspect dist/server.js",
"build:client:dev": "rm -f dist/*-client.js && webpack --env=dev --progress --profile --colors",
"clean": "find dist -type f -not -name 'server.js' -delete",
"build:client:dev": "npm run clean && webpack --env=dev --progress --profile --colors",
"build:server:dev": "webpack --env=dev --config=webpack.server.js --progress --profile --colors",
"build:client:dev:w": "rm -f dist/*-client.js && webpack -w --env=dev --progress --profile --colors",
"build:client:dev:w": "npm run clean && webpack -w --env=dev --progress --profile --colors",
"build:server:dev:w": "webpack -w --env=dev --config=webpack.server.js --progress --profile --colors",

@@ -16,3 +17,4 @@ "build:client:prod": "webpack --env=prod --progress --profile --colors",

"test": "npm run eslint && cross-env NODE_ENV=test nyc ava --verbose",
"report": "cross-env NODE_ENV=test nyc ava && nyc report --reporter=lcov"
"coverage": "nyc report --reporter=lcov",
"report": "cross-env NODE_ENV=test nyc ava && yarn run coverage"
},

@@ -31,2 +33,3 @@ "devDependencies": {

"babel-preset-stage-2": "^6.24.1",
"codecov": "^3.0.0",
"cross-env": "^5.1.1",

@@ -43,3 +46,3 @@ "css-loader": "^0.28.7",

"ignore-styles": "^5.0.1",
"immutability-helper": "^2.4.0",
"immutability-helper": "^2.5.0",
"lodash": "^4.17.4",

@@ -73,2 +76,4 @@ "mock-require": "^2.0.2",

"express": "^4.16.2",
"moment": "^2.19.2",
"normalizr": "^3.2.4",
"react": "^16.1.1",

@@ -97,2 +102,3 @@ "react-dom": "^16.1.1",

"src/**/__tests__/**/*.js",
"src/**/*.spec.js",
"!test/fixtures/**/*",

@@ -99,0 +105,0 @@ "!test/helpers/**/*"

# react-isomorphic-boilerplate
[![npm Version](https://img.shields.io/npm/v/react-isomorphic-boilerplate.svg?style=flat-square)](https://www.npmjs.org/package/react-isomorphic-boilerplate)
[![Build Status](https://img.shields.io/travis/ddhp/react-isomorphic-boilerplate/master.svg?style=flat-square)](https://travis-ci.org/ddhp/react-isomorphic-boilerplate)
[![codecov.io](https://codecov.io/github/ddhp/react-isomorphic-boilerplate/coverage.svg?branch=master)](https://codecov.io/github/ddhp/react-isomorphic-boilerplate?branch=master)
[![Dependency Status](https://dependencyci.com/github/ddhp/react-isomorphic-boilerplate/badge)](https://dependencyci.com/github/ddhp/react-isomorphic-boilerplate)
This boilerplate would help you build a react/redux/react-router isomorphic/universal web app
## Feature
- isomorphic: same code runs on server and browser
- SEO: information benefits to search engine would be rendered on server side
- easy to start
- production ready
- isomorphic: same code runs on server and browser.
- SEO: information benefits to search engine would be rendered on server side.
- fully testable - shows how to test react containers / redux actions and reducers / also your server app.
- easy to start.
- production ready.
## Concept
### Development
0. `yarn` and run 3 processes to start developing your app:
### Getting Started
Execute `yarn` to install.
Run 3 processes to start developing your app:
1. `yarn run build:client:dev:w`: build client side code and watch file change.

@@ -22,18 +28,28 @@ 2. `yarn run build:server:dev:w`: build server side conde and watch file change.

then you can visit `localhost:3333`.
All development code are built with [source map](http://blog.teamtreehouse.com/introduction-source-maps).
### Log
import stdout and define namespace ([example](https://github.com/ddhp/react-isomorphic-boilerplate/blob/master/src/server/pages.js)), then turn on debug message depends on platform:
- browser: allow debug log by type `localStorage.debug = '*'` in console
- server: run node with `DEBUG=*`, see `package.json.scripts.start`.
Import `stdout.js` and define namespace ([example](https://github.com/ddhp/react-isomorphic-boilerplate/blob/master/src/server/pages.js)), then turn on debug message depends on platform:
- browser: allow debug log by type `localStorage.debug = '*'` in console.
- nodejs: run node with `DEBUG=*`, see `package.json.scripts.start`.
### Packing code
- Fonts: font face are set in `src/client/global.scss`
- Images: set src relative to your js or scss file,
In production build, server side log would stay untouched to easily debug by checking log file,
and on browser side, **all debug message would be removed** by [remove-debug-loader](https://github.com/ddhp/remove-debug-loader).
[extract-text-webpack-plugin](https://github.com/webpack-contrib/extract-text-webpack-plugin) would extract them (font, image) into static assets and handle url transform.
### Static Files
- Put your fonts, images, etc. in `src/assets`.
- Fonts: set your font face in [src/entries/global.scss](https://github.com/ddhp/react-isomorphic-boilerplate/blob/master/src/entries/global.scss) and set src points to the font in assets folder.
- Images: set src relative to your js([example](https://github.com/ddhp/react-isomorphic-boilerplate/blob/master/src/containers/Demo/index.js)) or scss ([example](https://github.com/ddhp/react-isomorphic-boilerplate/blob/master/src/containers/Demo/style.scss)) file.
[extract-text-webpack-plugin](https://github.com/webpack-contrib/extract-text-webpack-plugin) would extract them (font, image) into `/dist` with hash key and handle url transform. (so you don't have to worry about cache issue)
On the other hand, node server **only** serves static files in `/dist` which means **/src/assets/ files not imported to your code base are not accessible from your web server.**
### Style
- [reset.css](https://www.npmjs.com/package/reset-css) reseting default style imported in [global.scss](https://github.com/ddhp/react-isomorphic-boilerplate/blob/master/src/client/global.scss).
- [reset.css](https://www.npmjs.com/package/reset-css) resets default style and is imported in [global.scss](https://github.com/ddhp/react-isomorphic-boilerplate/blob/master/src/client/global.scss).
- Import `global.scss` in your entry component, or define your own styles for specific entry then import them.
- `style.scss` in containers folder only set styles for react component in the folder of same level, and starts with most root class name of that component. (see [src/containers/Home/style.scss](https://github.com/ddhp/react-isomorphic-boilerplate/blob/master/src/containers/Home/style.scss))

@@ -43,4 +59,4 @@ ### SEO

- [react-helmet](https://github.com/nfl/react-helmet) help us set head (or specific property) in container and overwrites setting from parent, very handy.
- Define your basic helmet setting in each route file, see [src/routers/main.js](https://github.com/ddhp/react-isomorphic-boilerplate/blob/master/src/routes/main.js),
my idea is - head can be different for different entry of app.
- Define your basic helmet setting in each route file, see [src/routers/main.js](https://github.com/ddhp/react-isomorphic-boilerplate/blob/master/src/routes/main.js).
 My idea is - basic head meta can be different for different entries of app.
- Overwrites head info in containers. ([example](https://github.com/ddhp/react-isomorphic-boilerplate/blob/master/src/containers/About/index.js))

@@ -50,27 +66,17 @@

- [AVA](https://github.com/avajs/ava) as test runner.
- Don't use [webpack alias](https://webpack.js.org/configuration/resolve/#resolve-alias) in code base
- We use [mock-require](https://github.com/boblauer/mock-require) to mock dependencies to make test as independent as possible.
As it's name says, it only support `require` not import, so if your importing module has some dependencies needs to be mocked,
remember to `require` instead of import them in your test code.
Also append `.default` to get the right reference if your module is defined in es6 way. (see [server test](https://github.com/ddhp/react-isomorphic-boilerplate/blob/master/src/server/__tests__/index.js) for example)
- **Don't use** [webpack alias](https://webpack.js.org/configuration/resolve/#resolve-alias) in code base.
- [mock-require](https://github.com/boblauer/mock-require) mocks dependencies to make test as independent as possible.
As it's name says, it only support `require`, so in your test file, remember you have to **require** the target testing module, `import` does not work.
Also if your testing module is defined in es6 way (`export default`), remember to append `.default` to get the default export. (see [server test](https://github.com/ddhp/react-isomorphic-boilerplate/blob/master/src/server/__tests__/index.spec.js) for example)
### Production build
Build your code with:
1. `yarn run build:client:prod`
2. `yarn run build:server:prod`
## TODOS:
1. ~hash key~
2. ~production build~
3. ~style loader~
4. ~font / img loader~
5. ~test on server~
6. ~source map~
7. ~test on react component~
7-1. ~coverage report~
8. ~apply react router~
9. ~apply logic base on path(seo optimized)~
10. ~set head info~
11. ~fetch data from and submit to local api~
and your app is ready to go.
## LICENSE
MIT

@@ -15,7 +15,6 @@ // import { get as _get } from 'lodash';

export const UPDATE_ME_ID = 'UPDATE_ME_ID';
export function updateMeID(id) {
export const DUMMY_ACTION = 'DUMMY_ACTION';
export function dummyAction() {
return {
type: UPDATE_ME_ID,
payload: id
type: DUMMY_ACTION
};

@@ -54,13 +53,30 @@ }

.send(post)
.end((err, res) => {
if (err) {
debug(err);
} else {
dispatch({
type: ADD_POST,
payload: JSON.parse(res.text)
});
}
.then((res) => {
debug(res.text);
dispatch({
type: ADD_POST,
payload: JSON.parse(res.text)
});
}, (err) => {
debug(err);
});
};
}
export const VOTE = 'VOTE';
export function vote(info) {
return function (dispatch) {
return request
.post('/api/post/vote')
.send(info)
.then((res) => {
dispatch({
type: VOTE,
payload: JSON.parse(res.text)
});
}, (err) => {
debug(err);
});
};
}
import React from 'react';
import { Component } from 'react';
import { Helmet } from 'react-helmet';
import { Link } from 'react-router-dom';

@@ -17,3 +16,2 @@ export default class About extends Component {

About page
<Link to="/">Home</Link>
</div>

@@ -20,0 +18,0 @@ );

@@ -6,87 +6,25 @@ import React from 'react';

import { connect } from 'react-redux';
import { get as _get } from 'lodash';
import { Link } from 'react-router-dom';
import { accumulateCount, updateMeID, updateMe, addPost } from '../../actions';
import { get as _get } from 'lodash';
import { dummyAction } from '../../actions';
import FormPost from './FormPost';
import Postlist from './Postlist';
import stdout from '../../stdout';
const debug = stdout('container/home/index');
const debug = stdout('container/Home');
import './style.scss';
import logoImg from '../../assets/images/react-logo.png';
debug(logoImg);
export class Home extends Component {
static propTypes = {
count: PropTypes.number,
accumulateCount: PropTypes.func,
updateMeID: PropTypes.func,
updateMe: PropTypes.func,
addPost: PropTypes.func,
me: PropTypes.object,
dummyAction: PropTypes.func,
posts: PropTypes.array
}
constructor(props) {
super(props);
this.state = {
value: '',
postText: ''
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.onClick = this.onClick.bind(this);
this.onPostTextChanged = this.onPostTextChanged.bind(this);
this.onPostSubmit = this.onPostSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
debug('A name was submitted: ' + this.state.value);
event.preventDefault();
this.props.updateMeID(this.state.value);
}
onClick() {
debug('onclick');
this.props.updateMe({
id: Math.random().toString()
});
}
onPostTextChanged(e) {
this.setState({postText: e.target.value});
}
onPostSubmit(e) {
e.preventDefault();
const { postText } = this.state,
payload = {
text: postText
};
this.props.addPost(payload);
}
componentDidMount() {
this.props.accumulateCount();
this.props.dummyAction();
}
// shouldComponentUpdate(nextProps, nextState) {
// const { name: thisName, sex: thisSex } = this.props.me,
// { name: nextName, sex: nextSex } = nextProps.me;
// if (thisName !== nextName ||
// thisSex !== nextSex ||
// this.state !== nextState) {
// return true;
// } else {
// return false;
// }
// }
render() {
debug('render method');
const { name, sex } = this.props.me,
{ posts } = this.props;
const { posts } = this.props;
return (

@@ -99,40 +37,10 @@ <div className="page--home">

</Helmet>
<h1 className="demo--font">
Title in Spectral SC
A red flair silhouetted the jagged edge of a wing
</h1>
<Link to="/about">To About</Link>
<div className="demo--bg"></div>
<img className="demo--img-src" src={logoImg} />
<FormPost />
<ul className="posts">
<ul className="list--posts">
{posts.map((p) => {
return (
<li key={p.id}>
{p.id}, {p.text}
</li>
);
return <Postlist post={p} key={p.id} />;
})}
</ul>
<form className="form--post" onSubmit={this.onPostSubmit}>
<label>
TEXT:
<input className="input--post-text" type="text" value={this.state.postText} onChange={this.onPostTextChanged} />
</label>
<input type="submit" value="Submit" />
</form>
counter: {this.props.count}
<div>name: {name}</div>
<div>sex: {sex}</div>
<form className="form--me" onSubmit={this.handleSubmit}>
<label>
ID:
<input className="input--id" type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
<p onClick={this.onClick}>Set random user id by update whole user object</p>
</div>

@@ -144,13 +52,11 @@ );

function mapStateToProps(state) {
const count = _get(state, 'pages.home.count', 0),
entities = _get(state, 'entities'),
me = _get(entities, 'me'),
post = _get(entities, 'post'),
posts = Object.keys(post.byId).map((k) => {
return post.byId[k];
});
const postEntity = _get(state, 'entities.post'),
postIds = _get(state, 'pages.home.posts');
let posts = postIds.map((id) => {
return postEntity[id] || {};
});
posts = posts.slice(0, 20);
debug(posts);
return {
count,
me,
posts

@@ -162,13 +68,4 @@ };

return {
accumulateCount: () => {
return dispatch(accumulateCount());
},
updateMeID: (id) => {
return dispatch(updateMeID(id));
},
updateMe: (me) => {
return dispatch(updateMe(me));
},
addPost: (post) => {
return dispatch(addPost(post));
dummyAction: () => {
return dispatch(dummyAction());
}

@@ -175,0 +72,0 @@ };

const initialState = {
id: 'myid',
name: 'jesse',
sex: 'male'
id: 'anonymous',
name: 'anonymous'
};

@@ -20,7 +19,2 @@

case 'UPDATE_ME_SEX':
return Object.assign({}, state, {
sex: payload
});
case 'UPDATE_ME':

@@ -27,0 +21,0 @@ return Object.assign({}, state, payload);

@@ -0,8 +1,17 @@

import update from 'immutability-helper';
import { get as _get } from 'lodash';
import stdout from '../../stdout';
const debug = stdout('reducer:post');
const initialState = {
byId: {},
allIds: []
};
/**
* keys:
* - id
* - arthur
* - createdAt
* - text
* - upvote
* - downvote
*
*/
const initialState = {};

@@ -13,17 +22,41 @@ export default function postReducer(state = initialState, action) {

case 'FETCH_POSTS': {
return Object.assign({}, state, payload);
if (payload.entities.posts) {
return update(state, {
$merge: payload.entities.posts
});
} else {
return state;
}
}
case 'ADD_POST': {
let allIds = Array.prototype.slice.call(state.allIds);
let byId = Object.assign({}, state.byId);
const postEntities = _get(payload, 'entities.posts', {});
const response = postEntities[payload.result];
debug(response);
if (response) {
return update(state, {
$merge: {
[payload.result]: response
}
});
} else {
return state;
}
}
allIds.push(payload.id);
byId[payload.id] = payload;
debug(allIds, byId);
return Object.assign({}, state, {
byId,
allIds
});
case 'VOTE': {
const postEntities = _get(payload, 'entities.posts', {});
const response = postEntities[payload.result];
if (response) {
return update(state, {
[payload.result]: {
$merge: {
upvote: response.upvote,
downvote: response.downvote
}
}
});
} else {
return state;
}
}

@@ -30,0 +63,0 @@

@@ -0,7 +1,13 @@

import update from 'immutability-helper';
import { get as _get } from 'lodash';
import stdout from '../../stdout';
const debug = stdout('reducer:home');
const initialState = {
count: 0
count: 0,
posts: []
};
export default function homeReducer(state = initialState, action) {
// const payload = action.payload;
const payload = action.payload;
switch (action.type) {

@@ -16,2 +22,25 @@ case 'ACCUMULATE_COUNT': {

case 'FETCH_POSTS': {
const result = _get(payload, 'result', []);
return update(state, {
posts: {
$set: result
}
});
}
case 'ADD_POST': {
const result = _get(payload, 'result');
debug(result);
if (result) {
return update(state, {
posts: {
$unshift: [result]
}
});
} else {
return state;
}
}
default:

@@ -18,0 +47,0 @@ return state;

@@ -8,4 +8,7 @@ import React, { Component } from 'react';

import { withRouter } from 'react-router-dom';
import Nav from '../containers/Nav';
import Home from '../containers/Home';
import About from '../containers/About';
import Demo from '../containers/Demo';
import Footer from '../containers/Footer';
import { fetchPosts } from '../actions';

@@ -38,2 +41,6 @@

redirect: false
}, {
path: '/demo',
key: 'demo',
component: Demo
}

@@ -74,2 +81,3 @@ ];

</Helmet>
<Nav />
<Switch>

@@ -91,2 +99,3 @@ {redirect ? <Redirect to={redirect} /> : null}

</Switch>
<Footer />
</div>

@@ -93,0 +102,0 @@ );

@@ -5,33 +5,97 @@ import express from 'express';

import configureStore from '../configureStore';
import moment from 'moment';
import { normalize } from 'normalizr';
import update from 'immutability-helper';
import stdout from '../stdout';
const debug = stdout('app-server');
const debug = stdout('server/api');
// store representing db
const store = configureStore({});
const router = express.Router();
router.get('/post', (req, res) => {
// don't know why import doesn't work
const schemas = require('../schemas');
export const postcb = (req, res) => {
const post = _get(store.getState(), 'entities.post');
debug(post);
res.send(post);
});
let posts = Object.keys(post).map((k) => {
return post[k];
});
posts = posts.sort((a, b) => {
const scoreA = a.upvote - a.downvote;
const scoreB = b.upvote - b.downvote;
return scoreB - scoreA;
});
const response = normalize(posts, [schemas.post]);
debug(response);
res.send(response);
};
router.get('/post', postcb);
export const postvotecb = (req, res) => {
const { id, isUp } = req.body;
// get post from db
const post = _get(store.getState(), `entities.post.${id}`, {});
debug('post', post);
let upvote = post.upvote;
let downvote = post.downvote;
debug('updownvote before', upvote, downvote);
if (isUp) {
upvote ++;
} else {
downvote ++;
}
debug('updownvote', upvote, downvote);
let response = update(post, {
$merge: {
downvote,
upvote
}
});
debug(response);
response = normalize(response, schemas.post);
// this is the time you notice db
store.dispatch({
type: 'VOTE',
payload: response
});
res.send(response);
};
router.post('/post/vote', bodyParser.json(), postvotecb);
router.post('/post', bodyParser.json(), (req, res) => {
const { text } = req.body,
allIds = _get(store.getState(), 'entities.post.allIds', []);
const { text, arthur } = req.body,
postEntities = _get(store.getState(), 'entities.post', {}),
allIds = Object.keys(postEntities);
let lastId = allIds[allIds.length -1] || 0;
const response = {
id: ++lastId,
text: text || ''
text: text || '',
arthur: arthur || 'anonymous',
createdAt: moment().valueOf(),
upvote: 0,
downvote: 0
};
const normalized = normalize(response, schemas.post);
debug(normalized);
// save to local memory store
store.dispatch({
type: 'ADD_POST',
payload: response
});
debug(_get(store.getState(), 'entities.post', []));
res.send(response);
payload: normalized
});
debug(_get(store.getState(), 'entities.post', {}));
res.send(normalized);
});
module.exports = (app) => {
export default (app) => {
app.use('/api', router);
};

@@ -1,16 +0,7 @@

import Express from 'express';
import stdout from '../stdout';
import pagesMiddleware from './pages';
import apiMiddleware from './api';
const debug = stdout('app-server');
const app = Express(),
port = 3333;
//Serve static files
app.use('/assets', Express.static('dist'));
import app from './app';
const port = 3333;
apiMiddleware(app);
pagesMiddleware(app);
const server = app.listen(port, function () {

@@ -22,3 +13,1 @@ const host = server.address().address,

});
export default app;

@@ -27,2 +27,4 @@ import React from 'react';

<link rel="apple-touch-icon" href="/assets/images/icon.png" />
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet" />
<link href={assetsJSON.main.css} rel="stylesheet" />

@@ -29,0 +31,0 @@

@@ -31,6 +31,6 @@ const webpack = require('webpack');

loader: 'remove-debug-loader',
// options: {
// methodName: ['mylog'],
// moduleName: ['myModule']
// }
options: {
// methodName: ['myLog'],
moduleName: ['stdout']
}
}, {

@@ -37,0 +37,0 @@ loader: 'eslint-loader',

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc