├── .gitignore
├── src
├── assets
│ └── stylesheets
│ │ └── base.scss
├── components
│ └── App
│ │ ├── __tests__
│ │ └── index.js
│ │ └── index.js
└── index.js
├── __tests__
├── cssStub.js
└── config.js
├── .babelrc
├── dist
└── index.html
├── webpack.prod.config.js
├── server.js
├── webpack.config.js
├── package.json
├── README.md
└── .eslintrc
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | *npm-debug.log
3 | *.DS_Store
4 | dist/bundle.js
5 | coverage
6 |
--------------------------------------------------------------------------------
/src/assets/stylesheets/base.scss:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: helvetica;
3 | font-weight: 400;
4 | }
5 |
--------------------------------------------------------------------------------
/__tests__/cssStub.js:
--------------------------------------------------------------------------------
1 | // This is a stub file to keep Jest from yelling
2 | // about the CSS module imports in our components.
3 | export default {};
4 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", {
4 | targets: { node: "8.11.4" },
5 | }],
6 | "stage-0",
7 | "react"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Node + React Starter
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/__tests__/config.js:
--------------------------------------------------------------------------------
1 | // Enzyme needs this Adapter or it throws an error in the tests.
2 | // You can read more here: http://airbnb.io/enzyme/docs/installation/index.html
3 |
4 | import { configure } from 'enzyme';
5 | import Adapter from 'enzyme-adapter-react-16';
6 |
7 | configure({ adapter: new Adapter() });
8 |
--------------------------------------------------------------------------------
/webpack.prod.config.js:
--------------------------------------------------------------------------------
1 | const config = require('./webpack.config.js');
2 | const webpack = require('webpack');
3 |
4 | config.plugins.push(
5 | new webpack.DefinePlugin({
6 | "process.env": {
7 | "NODE_ENV": JSON.stringify("production")
8 | }
9 | })
10 | );
11 |
12 | config.plugins.push(
13 | new webpack.optimize.UglifyJsPlugin({
14 | compress: {
15 | warnings: false
16 | }
17 | })
18 | );
19 |
20 | module.exports = config;
21 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const express = require('express');
3 | const app = express();
4 | const PORT = process.env.PORT || 8080;
5 |
6 | app.use(express.static(path.join(__dirname, 'dist')));
7 |
8 | app.get('/', function(request, response) {
9 | response.sendFile(__dirname + '/dist/index.html');
10 | });
11 |
12 | app.listen(PORT, error => (
13 | error
14 | ? console.error(error)
15 | : console.info(`Listening on port ${PORT}. Visit http://localhost:${PORT}/ in your browser.`)
16 | ));
17 |
--------------------------------------------------------------------------------
/src/components/App/__tests__/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 |
4 | import '__tests__/config';
5 |
6 | import App from '../index';
7 |
8 | test('it should render the App component', () => {
9 | const wrapper = shallow(
10 |
11 | );
12 |
13 | expect(wrapper.find('h1').text()).toEqual('Hello, World!');
14 | });
15 |
16 | it('should run a solid smoke test', () => {
17 | // this test is intentionally failing
18 | expect(true).toEqual(false);
19 | });
20 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { ApolloClient } from 'apollo-client';
4 | import { HttpLink } from 'apollo-link-http';
5 | import { InMemoryCache } from 'apollo-cache-inmemory';
6 | import { ApolloProvider } from 'react-apollo';
7 |
8 | import App from './components/App';
9 |
10 | const client = new ApolloClient({
11 | link: new HttpLink({
12 | uri: 'https://api.github.com/graphql',
13 | headers: {
14 | authorization: 'bearer YOUR-TOKEN-HERE'
15 | }
16 | }),
17 | cache: new InMemoryCache()
18 | });
19 |
20 | render(
21 |
22 |
23 | ,
24 | document.getElementById('root')
25 | );
26 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | // React v.16 uses some newer JS functionality, so to ensure everything
4 | // works across all browsers, we're adding babel-polyfill here.
5 | require('babel-polyfill');
6 |
7 | module.exports = {
8 | entry: [
9 | './src/index'
10 | ],
11 | module: {
12 | loaders: [
13 | { test: /\.js?$/, loader: 'babel-loader', exclude: /node_modules/ },
14 | { test: /\.s?css$/, loader: 'style-loader!css-loader!sass-loader' },
15 | ]
16 | },
17 | resolve: {
18 | modules: [
19 | path.resolve('./'),
20 | path.resolve('./node_modules'),
21 | ],
22 | extensions: ['.js','.scss'],
23 | },
24 | output: {
25 | path: path.join(__dirname, '/dist'),
26 | publicPath: '/',
27 | filename: 'bundle.js'
28 | },
29 | devtool: 'cheap-eval-source-map',
30 | devServer: {
31 | contentBase: './dist',
32 | hot: true
33 | },
34 | plugins: [
35 | new webpack.optimize.OccurrenceOrderPlugin(),
36 | new webpack.HotModuleReplacementPlugin(),
37 | new webpack.NoEmitOnErrorsPlugin()
38 | ]
39 | };
40 |
--------------------------------------------------------------------------------
/src/components/App/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { withApollo } from 'react-apollo';
3 | import gql from 'graphql-tag';
4 |
5 | import 'src/assets/stylesheets/base.scss';
6 |
7 | function packageQuery(name, owner) {
8 | return gql`
9 | query {
10 | repository(name: "${name}", owner: "${owner}") {
11 | object(expression: "master:package.json") {
12 | ... on Blob {
13 | text
14 | }
15 | }
16 | }
17 | }
18 | `;
19 | }
20 |
21 | class App extends Component {
22 | state = {
23 | owner: 'sendgrid',
24 | repo: '',
25 | dependencies: {}
26 | };
27 |
28 | handleChange = (e) => {
29 | const { id, value } = e.target;
30 | return this.setState({ [id]: value });
31 | };
32 |
33 | handleSubmit = async (e) => {
34 | e.preventDefault();
35 | const response = await this.props.client.query({
36 | query: packageQuery(this.state.repo, this.state.owner)
37 | });
38 | const { repository } = response.data;
39 | const parsed = JSON.parse(repository.object.text);
40 | return this.setState({ dependencies: parsed.dependencies });
41 | };
42 |
43 | render() {
44 | return (
45 |
46 |
dep-check
47 |
58 |
59 | {Object.keys(this.state.dependencies).map((dep, i) => (
60 | -
61 | {dep}: {this.state.dependencies[dep]}
62 |
63 | ))}
64 |
65 |
66 | );
67 | }
68 | }
69 |
70 | export default withApollo(App);
71 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-node-starter",
3 | "version": "1.1.0",
4 | "description": "an example for deploying a React + NodeJS app to Heroku",
5 | "main": "server.js",
6 | "scripts": {
7 | "build": "webpack --config webpack.prod.config.js",
8 | "dev": "webpack-dev-server --hot --inline",
9 | "lint": "eslint src/**",
10 | "lint:watch": "esw -w src/**",
11 | "review": "npm run lint && npm test",
12 | "start": "npm run build && NODE_ENV=production node server.js",
13 | "test": "jest src/**",
14 | "test:watch": "jest src/** --watch",
15 | "test:watchAll": "jest src/** --watchAll",
16 | "test:coverage:report": "open coverage/lcov-report/index.html"
17 | },
18 | "author": "",
19 | "license": "ISC",
20 | "dependencies": {
21 | "apollo-cache-inmemory": "^1.2.9",
22 | "apollo-client": "^2.4.1",
23 | "apollo-link-http": "^1.5.4",
24 | "axios": "^0.18.0",
25 | "babel-core": "^6.26.0",
26 | "babel-loader": "^7.1.2",
27 | "babel-plugin-transform-regenerator": "^6.26.0",
28 | "babel-polyfill": "^6.26.0",
29 | "babel-preset-env": "^1.6.0",
30 | "babel-preset-react": "^6.24.1",
31 | "babel-preset-stage-0": "^6.24.1",
32 | "css-loader": "^0.28.7",
33 | "enzyme-adapter-react-16": "^1.0.0",
34 | "express": "^4.15.5",
35 | "graphql": "^14.0.2",
36 | "graphql-tag": "^2.9.2",
37 | "node-sass": "^4.5.3",
38 | "prop-types": "^15.6.0",
39 | "raf": "^3.3.2",
40 | "react": "^16.5.0",
41 | "react-apollo": "^2.1.11",
42 | "react-dom": "^16.5.0",
43 | "sass-loader": "^6.0.6",
44 | "style-loader": "^0.18.2",
45 | "webpack": "^3.6.0"
46 | },
47 | "devDependencies": {
48 | "babel-eslint": "^8.0.1",
49 | "enzyme": "^3.0.0",
50 | "eslint": "^4.7.2",
51 | "eslint-loader": "^1.9.0",
52 | "eslint-plugin-jsx-a11y": "^6.0.2",
53 | "eslint-plugin-react": "^7.4.0",
54 | "eslint-watch": "^3.1.2",
55 | "jest": "^21.2.0",
56 | "react-addons-test-utils": "^15.6.2",
57 | "react-test-renderer": "^16.0.0",
58 | "webpack-dev-middleware": "^1.12.0",
59 | "webpack-dev-server": "^2.8.2",
60 | "webpack-hot-middleware": "^2.19.1"
61 | },
62 | "jest": {
63 | "collectCoverage": true,
64 | "collectCoverageFrom": [
65 | "src/**",
66 | "!src/index.js"
67 | ],
68 | "coverageThreshold": {
69 | "global": {
70 | "branches": 90,
71 | "functions": 90,
72 | "lines": 90,
73 | "statements": 90
74 | }
75 | },
76 | "moduleDirectories": [
77 | "node_modules",
78 | "./"
79 | ],
80 | "moduleNameMapper": {
81 | "^.+.(css|scss|sass)$": "/__tests__/cssStub.js"
82 | },
83 | "setupFiles": [
84 | "raf/polyfill"
85 | ]
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React + Node Starter
2 | _for [Heroku](https://www.heroku.com/) deployment_
3 |
4 | ## OVERVIEW
5 | This is a simple starter to get you up and running for React projects. This is intended to provide:
6 |
7 | * a lightweight Webpack config (for development and production)
8 | * some helpful tooling for development workflow
9 | * a similar setup to what you'll see in the wild
10 | * Heroku-ready deployment setup
11 |
12 | ## UP & RUNNING
13 | Install dependencies:
14 | ```
15 | $ npm install
16 | ```
17 | _or_
18 | ```
19 | $ yarn
20 | ```
21 |
22 | Fire up a development server:
23 | ```
24 | $ yarn dev
25 | ```
26 |
27 | Once the server is running, you can visit `http://localhost:8080/`
28 |
29 | ## Linting
30 | _This assumes you have eslint and eslint-watch installed. If you don't, run the following:_
31 | ```
32 | $ npm i -g eslint eslint-watch
33 | ```
34 | or if you need permissions:
35 | ```
36 | $ sudo npm i -g eslint eslint-watch
37 | ```
38 |
39 | To run the linter once:
40 | ```
41 | $ yarn lint
42 | ```
43 |
44 | To run the watch task:
45 | ```
46 | $ yarn lint:watch
47 | ```
48 |
49 | ## Testing
50 | An initial test suite has been setup with two tests (one passing and one intentionally failing).
51 | We're using Jest and Enzyme for our test setup. The basic test setup lives in `./__tests__`.
52 | The main configuration for Jest lives at the bottom of `package.json`. I've also added a few
53 | handy scripts, which I've listed below. Jest also gives us a test coverage tool for free, so I've added that too. The setup is at the bottom of `package.json`. Everything is set to 90% coverage, but your welcome to update that to whatever you'd like.
54 |
55 | To run the tests once:
56 | ```
57 | $ yarn test
58 | ```
59 |
60 | To run the watch script (for only relevant test files)
61 | ```
62 | $ yarn test:watch
63 | ```
64 |
65 | To run the watch script (for all test files)
66 | ```
67 | $ yarn test:watchAll
68 | ```
69 |
70 | To view the coverage report:
71 | ```
72 | $ yarn test:coverage:report
73 | ```
74 |
75 | ## Review
76 | If you'd like to run the linter and tests at once (this is a nice check before pushing to Github or deploys), you can run:
77 |
78 | ```
79 | $ yarn review
80 | ```
81 |
82 | ## Production Build
83 |
84 | To build your production assets and run the server:
85 | ```
86 | $ yarn start
87 | ```
88 |
89 | ## CHANGELOG
90 | **v2.0.0**
91 | This app has been updated to use React v.16.0.0! 🎉
92 |
93 | Major Changes:
94 | * Updates React and ReactDOM to v16
95 | * Replaces Mocha with Jest
96 | * Adds `babel-polyfill` and updates Babel config
97 | * Colocates tests with components
98 |
99 | Minor Changes:
100 | * Updates all other dependencies to latest
101 | * Allows absolute import paths
102 | * Adds new test scripts
103 | * Adds test coverage
104 |
105 | **v1.1.0**
106 | This app has been updated to use React v15.6 and Webpack 3.5! 🎉
107 |
108 | Major Changes:
109 | * Updates React and ReactDOM to v15.6
110 | * Updates Webpack to v3.5
111 |
112 | Minor Changes:
113 | * Updates all other dependencies to latest
114 | * Updates App.js syntax
115 | * Updates eslint rules
116 | * Updates server.js
117 | * Updates README
118 |
119 | **v1.0.0**
120 | This app has been updated to use React v15.5 and Webpack 2.3! 🎉
121 |
122 | Major Changes:
123 | * Updates React and ReactDOM to v15.5
124 | * Updates Webpack to v2.3
125 | * Enables hot-reloading for local development
126 | * Adds initial test suite with Enzyme, Expect, and Mocha
127 |
128 | Minor Changes:
129 | * Updates all other dependencies to latest
130 | * Updates eslint rules
131 | * Updates npm scripts
132 | * Refactors server.js
133 | * Updates README
134 |
135 | **v.0.2.0**
136 | This app has been updated to use React v15 and Babel v6! I have also updated the file structure to reflect naming conventions you'll most likely see in other applications. If you'd like to go back to v.0.0.1 (which should've been named 0.1.0), you can find go back to [this commit](https://github.com/alanbsmith/react-node-example/commit/dd6d745c4b7066fd12104d5005b805afaf469d91).
137 |
138 | ## DEPLOYING TO HEROKU
139 | This app is set up for deployment to Heroku!
140 |
141 | _This assumes you have already have a Heroku account and have the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) installed_
142 | ```
143 | $ heroku login
144 | $ heroku create -a name-of-your-app
145 | $ git push heroku master
146 | $ heroku open
147 | ```
148 |
149 | Heroku will follow the `build` command in your `package.json` and compile assets with `webpack.prod.config.js`. It runs the Express web server in `server.js`.
150 |
151 | If you're unfamiliar with Heroku deployment (or just need a refresher), they have a really great walkthrough [here](https://devcenter.heroku.com/articles/getting-started-with-nodejs#introduction).
152 |
153 | ## REDUX STARTER
154 | If you're looking for a similar, minimalistic Redux starter, I would recommend Marc Garreau's [here](https://github.com/marcgarreau/redux-starter)
155 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | root: true,
3 | parser: 'babel-eslint',
4 | plugins: [/*'import', */'jsx-a11y', 'react'],
5 |
6 | env: {
7 | browser: true,
8 | commonjs: true,
9 | es6: true,
10 | jest: true,
11 | node: true
12 | },
13 |
14 | parserOptions: {
15 | ecmaVersion: 6,
16 | sourceType: 'module',
17 | ecmaFeatures: {
18 | jsx: true,
19 | generators: true,
20 | experimentalObjectRestSpread: true
21 | }
22 | },
23 |
24 | settings: {
25 | 'import/ignore': [
26 | 'node_modules',
27 | '\\.(json|css|jpg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm)$',
28 | ],
29 | 'import/extensions': ['.js'],
30 | 'import/resolver': {
31 | node: {
32 | extensions: ['.js', '.json']
33 | }
34 | }
35 | },
36 |
37 | rules: {
38 | // http://eslint.org/docs/rules/
39 | 'array-callback-return': 'warn',
40 | 'camelcase': 'warn',
41 | 'curly': 'warn',
42 | 'default-case': ['warn', { commentPattern: '^no default$' }],
43 | 'dot-location': ['warn', 'property'],
44 | 'eol-last': 'warn',
45 | 'eqeqeq': ['warn', 'always'],
46 | 'indent': ['warn', 2, { "SwitchCase": 1 }],
47 | 'guard-for-in': 'warn',
48 | 'keyword-spacing': 'warn',
49 | 'new-parens': 'warn',
50 | 'no-array-constructor': 'warn',
51 | 'no-caller': 'warn',
52 | 'no-cond-assign': ['warn', 'always'],
53 | 'no-const-assign': 'warn',
54 | 'no-control-regex': 'warn',
55 | 'no-delete-var': 'warn',
56 | 'no-dupe-args': 'warn',
57 | 'no-dupe-class-members': 'warn',
58 | 'no-dupe-keys': 'warn',
59 | 'no-duplicate-case': 'warn',
60 | 'no-empty-character-class': 'warn',
61 | 'no-empty-pattern': 'warn',
62 | 'no-eval': 'warn',
63 | 'no-ex-assign': 'warn',
64 | 'no-extend-native': 'warn',
65 | 'no-extra-bind': 'warn',
66 | 'no-extra-label': 'warn',
67 | 'no-fallthrough': 'warn',
68 | 'no-func-assign': 'warn',
69 | 'no-global-assign': 'warn',
70 | 'no-implied-eval': 'warn',
71 | 'no-invalid-regexp': 'warn',
72 | 'no-iterator': 'warn',
73 | 'no-label-var': 'warn',
74 | 'no-labels': ['warn', { allowLoop: false, allowSwitch: false }],
75 | 'no-lone-blocks': 'warn',
76 | 'no-loop-func': 'warn',
77 | 'no-mixed-operators': ['warn', {
78 | groups: [
79 | ['&', '|', '^', '~', '<<', '>>', '>>>'],
80 | ['==', '!=', '===', '!==', '>', '>=', '<', '<='],
81 | ['&&', '||'],
82 | ['in', 'instanceof']
83 | ],
84 | allowSamePrecedence: false
85 | }],
86 | 'no-multi-str': 'warn',
87 | 'no-new-func': 'warn',
88 | 'no-new-object': 'warn',
89 | 'no-new-symbol': 'warn',
90 | 'no-new-wrappers': 'warn',
91 | 'no-obj-calls': 'warn',
92 | 'no-octal': 'warn',
93 | 'no-octal-escape': 'warn',
94 | 'no-redeclare': 'warn',
95 | 'no-regex-spaces': 'warn',
96 | 'no-restricted-syntax': [
97 | 'warn',
98 | 'LabeledStatement',
99 | 'WithStatement',
100 | ],
101 | 'no-script-url': 'warn',
102 | 'no-self-assign': 'warn',
103 | 'no-self-compare': 'warn',
104 | 'no-sequences': 'warn',
105 | 'no-shadow-restricted-names': 'warn',
106 | 'no-sparse-arrays': 'warn',
107 | 'no-template-curly-in-string': 'warn',
108 | 'no-this-before-super': 'warn',
109 | 'no-throw-literal': 'warn',
110 | 'no-undef': 'warn',
111 | 'no-unexpected-multiline': 'warn',
112 | 'no-unreachable': 'warn',
113 | 'no-unsafe-negation': 'warn',
114 | 'no-unused-expressions': 'warn',
115 | 'no-unused-labels': 'warn',
116 | 'no-unused-vars': ['warn', { vars: 'local', args: 'none' }],
117 | 'no-use-before-define': ['warn', 'nofunc'],
118 | 'no-useless-computed-key': 'warn',
119 | 'no-useless-concat': 'warn',
120 | 'no-useless-constructor': 'warn',
121 | 'no-useless-escape': 'warn',
122 | 'no-useless-rename': ['warn', {
123 | ignoreDestructuring: false,
124 | ignoreImport: false,
125 | ignoreExport: false,
126 | }],
127 | 'no-with': 'warn',
128 | 'no-whitespace-before-property': 'warn',
129 | 'object-curly-spacing': ['warn', 'always'],
130 | 'operator-assignment': ['warn', 'always'],
131 | radix: 'warn',
132 | 'require-yield': 'warn',
133 | 'rest-spread-spacing': ['warn', 'never'],
134 | 'semi': 'warn',
135 | strict: ['warn', 'never'],
136 | 'unicode-bom': ['warn', 'never'],
137 | 'use-isnan': 'warn',
138 | 'valid-typeof': 'warn',
139 |
140 | 'react/jsx-boolean-value': 'warn',
141 | 'react/jsx-closing-bracket-location': 'warn',
142 | 'react/jsx-curly-spacing': 'warn',
143 | 'react/jsx-equals-spacing': ['warn', 'never'],
144 | 'react/jsx-first-prop-new-line': ['warn', 'multiline'],
145 | 'react/jsx-handler-names': 'warn',
146 | 'react/jsx-indent': ['warn', 2],
147 | 'react/jsx-indent-props': ['warn', 2],
148 | 'react/jsx-key': 'warn',
149 | 'react/jsx-max-props-per-line': 'warn',
150 | 'react/jsx-no-bind': ['warn', {'allowArrowFunctions': true}],
151 | 'react/jsx-no-comment-textnodes': 'warn',
152 | 'react/jsx-no-duplicate-props': ['warn', { ignoreCase: true }],
153 | 'react/jsx-no-undef': 'warn',
154 | 'react/jsx-pascal-case': ['warn', {
155 | allowAllCaps: true,
156 | ignore: [],
157 | }],
158 | 'react/jsx-sort-props': 'warn',
159 | 'react/jsx-tag-spacing': 'warn',
160 | 'react/jsx-uses-react': 'warn',
161 | 'react/jsx-uses-vars': 'warn',
162 | 'react/jsx-wrap-multilines': 'warn',
163 | 'react/no-deprecated': 'warn',
164 | 'react/no-did-mount-set-state': 'warn',
165 | 'react/no-did-update-set-state': 'warn',
166 | 'react/no-direct-mutation-state': 'warn',
167 | 'react/no-is-mounted': 'warn',
168 | 'react/no-unused-prop-types': 'warn',
169 | 'react/prefer-es6-class': 'warn',
170 | 'react/prefer-stateless-function': 'warn',
171 | 'react/prop-types': 'warn',
172 | 'react/react-in-jsx-scope': 'warn',
173 | 'react/require-render-return': 'warn',
174 | 'react/self-closing-comp': 'warn',
175 | 'react/sort-comp': 'warn',
176 | 'react/sort-prop-types': 'warn',
177 | 'react/style-prop-object': 'warn',
178 | 'react/void-dom-elements-no-children': 'warn',
179 |
180 | // https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules
181 | 'jsx-a11y/aria-role': 'warn',
182 | 'jsx-a11y/alt-text': 'warn',
183 | 'jsx-a11y/img-redundant-alt': 'warn',
184 | 'jsx-a11y/no-access-key': 'warn'
185 | }
186 | }
187 |
--------------------------------------------------------------------------------