├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE ├── .travis.yml ├── .gitignore ├── img ├── js.png ├── npm.png ├── pm2.png ├── chai.png ├── eslint.png ├── flow.png ├── jest.png ├── mocha.png ├── react.png ├── redux.png ├── yarn.png ├── webpack.png ├── flow-padded.png ├── jest-padded.png ├── js-padded.png ├── pm2-padded.png ├── yarn-padded.png ├── eslint-padded.png ├── pm2-padded-90.png ├── react-padded.png ├── react-router.png ├── redux-padded.png ├── bootstrap-padded.png ├── eslint-padded-90.png ├── flow-padded-90.png ├── jest-padded-90.png ├── react-padded-90.png ├── redux-padded-90.png ├── webpack-padded.png ├── yarn-padded-90.png ├── webpack-padded-90.png ├── bootstrap-padded-90.png ├── react-router-padded.png └── react-router-padded-90.png ├── package.json ├── mdlint.js ├── LICENSE.md ├── CHANGELOG.md ├── how-to-translate.md ├── yarn.lock ├── README.md └── tutorial ├── 09-travis-coveralls-heroku.md ├── 07-socket-io.md ├── 01-node-yarn-package-json.md ├── 03-express-nodemon-pm2.md ├── 04-webpack-react-hmr.md ├── 08-bootstrap-jss.md ├── 02-babel-es6-eslint-flow-jest-husky.md ├── 06-react-router-ssr-helmet.md └── 05-redux-immutable-fetch.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: verekia 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: node 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | 5 | -------------------------------------------------------------------------------- /img/js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/js.png -------------------------------------------------------------------------------- /img/npm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/npm.png -------------------------------------------------------------------------------- /img/pm2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/pm2.png -------------------------------------------------------------------------------- /img/chai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/chai.png -------------------------------------------------------------------------------- /img/eslint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/eslint.png -------------------------------------------------------------------------------- /img/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/flow.png -------------------------------------------------------------------------------- /img/jest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/jest.png -------------------------------------------------------------------------------- /img/mocha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/mocha.png -------------------------------------------------------------------------------- /img/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/react.png -------------------------------------------------------------------------------- /img/redux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/redux.png -------------------------------------------------------------------------------- /img/yarn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/yarn.png -------------------------------------------------------------------------------- /img/webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/webpack.png -------------------------------------------------------------------------------- /img/flow-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/flow-padded.png -------------------------------------------------------------------------------- /img/jest-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/jest-padded.png -------------------------------------------------------------------------------- /img/js-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/js-padded.png -------------------------------------------------------------------------------- /img/pm2-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/pm2-padded.png -------------------------------------------------------------------------------- /img/yarn-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/yarn-padded.png -------------------------------------------------------------------------------- /img/eslint-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/eslint-padded.png -------------------------------------------------------------------------------- /img/pm2-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/pm2-padded-90.png -------------------------------------------------------------------------------- /img/react-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/react-padded.png -------------------------------------------------------------------------------- /img/react-router.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/react-router.png -------------------------------------------------------------------------------- /img/redux-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/redux-padded.png -------------------------------------------------------------------------------- /img/bootstrap-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/bootstrap-padded.png -------------------------------------------------------------------------------- /img/eslint-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/eslint-padded-90.png -------------------------------------------------------------------------------- /img/flow-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/flow-padded-90.png -------------------------------------------------------------------------------- /img/jest-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/jest-padded-90.png -------------------------------------------------------------------------------- /img/react-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/react-padded-90.png -------------------------------------------------------------------------------- /img/redux-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/redux-padded-90.png -------------------------------------------------------------------------------- /img/webpack-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/webpack-padded.png -------------------------------------------------------------------------------- /img/yarn-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/yarn-padded-90.png -------------------------------------------------------------------------------- /img/webpack-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/webpack-padded-90.png -------------------------------------------------------------------------------- /img/bootstrap-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/bootstrap-padded-90.png -------------------------------------------------------------------------------- /img/react-router-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/react-router-padded.png -------------------------------------------------------------------------------- /img/react-router-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verekia/js-stack-from-scratch/HEAD/img/react-router-padded-90.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "2.4.5", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "test": "node mdlint.js" 7 | }, 8 | "devDependencies": { 9 | "glob": "^7.1.1", 10 | "markdownlint": "^0.4.0" 11 | }, 12 | "repository": "verekia/js-stack-from-scratch", 13 | "author": "Jonathan Verrecchia - @verekia", 14 | "license": "MIT" 15 | } 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE: -------------------------------------------------------------------------------- 1 | ### Type of issue: (feature suggestion, bug?) 2 | 3 | ### Chapter: 4 | 5 | ### If it's a bug: 6 | 7 | Please try using the code provided instead of your own to see if that solves the issue. If it does, compare the content of relevant files to see if anything is missing in your version. Every chapter is automatically tested, so issues are likely coming from missing an instruction in the tutorial or a typo. Feel free to open an issue if there is a problem with instructions though. 8 | -------------------------------------------------------------------------------- /mdlint.js: -------------------------------------------------------------------------------- 1 | const glob = require('glob') 2 | const markdownlint = require('markdownlint') 3 | 4 | const config = { 5 | 'default': true, 6 | 'line_length': false, 7 | 'no-emphasis-as-header': false, 8 | } 9 | 10 | const files = glob.sync('**/*.md', { ignore: '**/node_modules/**' }) 11 | 12 | markdownlint({ files, config }, (err, result) => { 13 | if (!err) { 14 | const resultString = result.toString() 15 | console.log('== Linting Markdown Files...') 16 | if (resultString) { 17 | console.log(resultString) 18 | process.exit(1) 19 | } else { 20 | console.log('== OK!') 21 | } 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2017 Jonathan Verrecchia 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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## v2.4.5 4 | 5 | - Add `babel-plugin-flow-react-proptypes`. 6 | - Add `eslint-plugin-compat`. 7 | - Add JSS `composes` example. 8 | 9 | ## v2.4.4 10 | 11 | - Update Immutable to remove the `import * as Immutable from 'immutable'` syntax. 12 | - Declare Flow types outside of function params for React components. 13 | - Improve Webpack `publicPath`. 14 | 15 | ## V2, up to v2.4.3 16 | 17 | - Gulp is gone, replaced by NPM (Yarn) scripts. 18 | - Express has been added, with template strings for static HTML. Gzip compression enabled. 19 | - Support for development environment with Nodemon and production environment with PM2. 20 | - Minification or sourcemaps depending on the environment via Webpack. 21 | - Add Webpack Dev Server, with Hot Module Replacement and `react-hot-loader`. 22 | - Add an asynchronous call example with `redux-thunk`. 23 | - Linting / type checking / testing is not launched at every file change anymore, but triggered by Git Hooks via Husky. 24 | - Some chapters have been combined to make it easier to maintain the tutorial. 25 | - Replace Chai and Mocha by Jest. 26 | - Add React-Router, Server-Side rendering, `react-helmet`. 27 | - Rename all "dog" things and replaced it by "hello" things. It's a Hello World app after all. 28 | - Add Twitter Bootstrap, JSS, and `react-jss` for styling. 29 | - Add a Websocket example with Socket.IO. 30 | - Add optional Heroku, Travis, and Coveralls integrations. 31 | -------------------------------------------------------------------------------- /how-to-translate.md: -------------------------------------------------------------------------------- 1 | # How to translate this tutorial 2 | 3 | Thank you for your interest in translating my tutorial! Here are a few recommendations to get started. 4 | 5 | This tutorial is in constant evolution to provide the best learning experience to readers. Both the code and `README.md` files will change over time. It is great if you do a one-shot translation that won't evolve, but it would be even better if you could try to keep up with the original English version as it changes! 6 | 7 | Here is what I think is a good workflow: 8 | 9 | - Check if there is already an [ongoing translation](https://github.com/verekia/js-stack-from-scratch/issues/147) for your language. If that's the case, get in touch with the folks who opened it and consider collaborating. All maintainers will be mentioned on the English repo, so team work is encouraged! You can open issues on their translation fork project to offer your help on certain chapters for instance. 10 | 11 | - Join the [Translations Gitter room](https://gitter.im/js-stack-from-scratch/Translations) if you're feeling chatty. 12 | 13 | - Fork the main [English repository](https://github.com/verekia/js-stack-from-scratch). 14 | 15 | - Post in [this issue](https://github.com/verekia/js-stack-from-scratch/issues/147) the language and URL of your forked repo. 16 | 17 | - Translate the `README.md` files. 18 | 19 | - Add a note somewhere explaining on the main `README.md` that this is a translation, with a link to the English repository. If you don't plan to make the translation evolve over time, you can maybe add a little note saying to refer to the English one for an up-to-date version of the tutorial. I'll leave that up to your preference. 20 | 21 | - Submit a Pull Request to the English repo to add a link to your forked repository under the Translations section of the main `README.md`. It could look like this: 22 | 23 | ```md 24 | ## Translations 25 | 26 | - [Language](http://github.com/yourprofile/your-fork) by [You](http://yourwebsite.com) 27 | or 28 | - [Language](http://github.com/yourprofile/your-fork) by [@You](http://twitter.com/yourprofile) 29 | or 30 | - [Language](http://github.com/yourprofile/your-fork) by [@You](http://github.com/yourprofile) 31 | ``` 32 | 33 | Since I want to reward you for your good work as much as possible, you can put any link you like on your name (to your personal website, Twitter profile, or Github profile for instance). 34 | 35 | - After your original one-shot translation, if you want to update your repo with the latest change from the main English repo, [sync your fork](https://help.github.com/articles/syncing-a-fork/) with my repo. To make it easy to see what changed since your initial translation, you can use Github's feature to [compare commits](https://help.github.com/articles/comparing-commits-across-time/#comparing-commits). Set the **base** to the last commit from the English repo you used to translate, and compare it to **master**, like so: 36 | 37 | 38 | https://github.com/verekia/js-stack-from-scratch/compare/c65dfa65d02c21063d94f0955de90947ba5273ad...master 39 | 40 | 41 | That should give you a easy-to-read diff to see exactly what changed in `README.md` files since your translation! 42 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | argparse@^1.0.7: 6 | version "1.0.9" 7 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" 8 | dependencies: 9 | sprintf-js "~1.0.2" 10 | 11 | balanced-match@^0.4.1: 12 | version "0.4.2" 13 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" 14 | 15 | brace-expansion@^1.0.0: 16 | version "1.1.6" 17 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9" 18 | dependencies: 19 | balanced-match "^0.4.1" 20 | concat-map "0.0.1" 21 | 22 | concat-map@0.0.1: 23 | version "0.0.1" 24 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 25 | 26 | entities@~1.1.1: 27 | version "1.1.1" 28 | resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" 29 | 30 | fs.realpath@^1.0.0: 31 | version "1.0.0" 32 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 33 | 34 | glob@^7.1.1: 35 | version "7.1.1" 36 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" 37 | dependencies: 38 | fs.realpath "^1.0.0" 39 | inflight "^1.0.4" 40 | inherits "2" 41 | minimatch "^3.0.2" 42 | once "^1.3.0" 43 | path-is-absolute "^1.0.0" 44 | 45 | inflight@^1.0.4: 46 | version "1.0.6" 47 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 48 | dependencies: 49 | once "^1.3.0" 50 | wrappy "1" 51 | 52 | inherits@2: 53 | version "2.0.3" 54 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 55 | 56 | linkify-it@^2.0.0: 57 | version "2.0.3" 58 | resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.0.3.tgz#d94a4648f9b1c179d64fa97291268bdb6ce9434f" 59 | dependencies: 60 | uc.micro "^1.0.1" 61 | 62 | markdown-it@^8.3.0: 63 | version "8.3.1" 64 | resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.3.1.tgz#2f4b622948ccdc193d66f3ca2d43125ac4ac7323" 65 | dependencies: 66 | argparse "^1.0.7" 67 | entities "~1.1.1" 68 | linkify-it "^2.0.0" 69 | mdurl "^1.0.1" 70 | uc.micro "^1.0.3" 71 | 72 | markdownlint@^0.4.0: 73 | version "0.4.0" 74 | resolved "https://registry.yarnpkg.com/markdownlint/-/markdownlint-0.4.0.tgz#5be8a342c4ab4d5c6cfe8702db30c63bbed01589" 75 | dependencies: 76 | markdown-it "^8.3.0" 77 | 78 | mdurl@^1.0.1: 79 | version "1.0.1" 80 | resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" 81 | 82 | minimatch@^3.0.2: 83 | version "3.0.3" 84 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" 85 | dependencies: 86 | brace-expansion "^1.0.0" 87 | 88 | once@^1.3.0: 89 | version "1.4.0" 90 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 91 | dependencies: 92 | wrappy "1" 93 | 94 | path-is-absolute@^1.0.0: 95 | version "1.0.1" 96 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 97 | 98 | sprintf-js@~1.0.2: 99 | version "1.0.3" 100 | resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" 101 | 102 | uc.micro@^1.0.1, uc.micro@^1.0.3: 103 | version "1.0.3" 104 | resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192" 105 | 106 | wrappy@1: 107 | version "1.0.2" 108 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaScript Stack from Scratch 2 | 3 | [![Build Status](https://travis-ci.org/verekia/js-stack-from-scratch.svg?branch=master)](https://travis-ci.org/verekia/js-stack-from-scratch) 4 | [![Release](https://img.shields.io/github/release/verekia/js-stack-from-scratch.svg?style=flat-square)](https://github.com/verekia/js-stack-from-scratch/releases) 5 | [![Gitter](https://img.shields.io/gitter/room/js-stack-from-scratch/Lobby.svg?style=flat-square)](https://gitter.im/js-stack-from-scratch/) 6 | 7 | [![React](/img/react-padded-90.png)](https://facebook.github.io/react/) 8 | [![Redux](/img/redux-padded-90.png)](http://redux.js.org/) 9 | [![React Router](/img/react-router-padded-90.png)](https://github.com/ReactTraining/react-router) 10 | [![Flow](/img/flow-padded-90.png)](https://flowtype.org/) 11 | [![ESLint](/img/eslint-padded-90.png)](http://eslint.org/) 12 | [![Jest](/img/jest-padded-90.png)](https://facebook.github.io/jest/) 13 | [![Yarn](/img/yarn-padded-90.png)](https://yarnpkg.com/) 14 | [![Webpack](/img/webpack-padded-90.png)](https://webpack.github.io/) 15 | 16 | Welcome to my modern JavaScript stack tutorial: **JavaScript Stack from Scratch**. 17 | 18 | > 🎉 **This is the V2 of the tutorial, major changes happened since the 2016 release. Check the [Change Log](/CHANGELOG.md)!** 19 | 20 | This is a straight-to-the-point guide to assembling a JavaScript stack. It requires some general programming knowledge, and JavaScript basics. **It focuses on wiring tools together** and giving you the **simplest possible example** for each tool. You can see this tutorial as *a way to write your own boilerplate from scratch*. Since the goal of this tutorial is to assemble various tools, I do not go into details about how these tools work individually. Refer to their documentation or find other tutorials if you want to acquire deeper knowledge in them. 21 | 22 | You don't need to use this entire stack if you build a simple web page with a few JS interactions of course (a combination of Browserify/Webpack + Babel + jQuery is enough to be able to write ES6 code in different files), but if you want to build a web app that scales, and need help setting things up, this tutorial will work great for you. 23 | 24 | A big chunk of the stack described in this tutorial uses React. If you are beginning and just want to learn React, [create-react-app](https://github.com/facebookincubator/create-react-app) will get you up and running with a React environment very quickly with a pre-made configuration. I would for instance recommend this approach to someone who arrives in a team that's using React and needs to catch up with a learning playground. In this tutorial you won't use a pre-made configuration, because I want you to understand everything that's happening under the hood. 25 | 26 | Code examples are available for each chapter, and you can run them all with `yarn && yarn start`. I recommend writing everything from scratch yourself by following the **step-by-step instructions** though. 27 | 28 | Final code available in the [JS-Stack-Boilerplate repository](https://github.com/verekia/js-stack-boilerplate), and in the [releases](https://github.com/verekia/js-stack-from-scratch/releases). There is a [live demo](https://js-stack.herokuapp.com/) too. 29 | 30 | Works on Linux, macOS, and Windows. 31 | 32 | > **Note**: Since the tutorial was last edited in May 2017, a few libraries have slightly changed their APIs. 95% of the tutorial is still perfectly valid, but if you run into something weird, make sure to check out the [open issues](https://github.com/verekia/js-stack-from-scratch/issues?q=is%3Aopen+is%3Aissue+label%3Abug). 33 | 34 | ## Table of contents 35 | 36 | [01 - Node, Yarn, `package.json`](/tutorial/01-node-yarn-package-json.md#readme) 37 | 38 | [02 - Babel, ES6, ESLint, Flow, Jest, Husky](/tutorial/02-babel-es6-eslint-flow-jest-husky.md#readme) 39 | 40 | [03 - Express, Nodemon, PM2](/tutorial/03-express-nodemon-pm2.md#readme) 41 | 42 | [04 - Webpack, React, HMR](/tutorial/04-webpack-react-hmr.md#readme) 43 | 44 | [05 - Redux, Immutable, Fetch](/tutorial/05-redux-immutable-fetch.md#readme) 45 | 46 | [06 - React Router, Server-Side Rendering, Helmet](/tutorial/06-react-router-ssr-helmet.md#readme) 47 | 48 | [07 - Socket.IO](/tutorial/07-socket-io.md#readme) 49 | 50 | [08 - Bootstrap, JSS](/tutorial/08-bootstrap-jss.md#readme) 51 | 52 | [09 - Travis, Coveralls, Heroku](/tutorial/09-travis-coveralls-heroku.md#readme) 53 | 54 | ## Coming up next 55 | 56 | Setting up your editor (Atom first), MongoDB, Progressive Web App, E2E testing. 57 | 58 | ## Translations 59 | 60 | If you want to add your translation, please read the [translation recommendations](/how-to-translate.md) to get started! 61 | 62 | ### V2 63 | 64 | - [Bulgarian](https://github.com/mihailgaberov/js-stack-from-scratch) by [mihailgaberov](http://github.com/mihailgaberov) 65 | - [Chinese (simplified)](https://github.com/yepbug/js-stack-from-scratch/) by [@yepbug](https://github.com/yepbug) 66 | - [French](https://github.com/naomihauret/js-stack-from-scratch/) by [Naomi Hauret](https://twitter.com/naomihauret) 67 | - [Italian](https://github.com/fbertone/guida-javascript-moderno) by [Fabrizio Bertone](https://github.com/fbertone) - [fbertone.it](http://fbertone.it) 68 | - [Polish](https://github.com/mbiesiad/js-stack-from-scratch) by [@mbiesiad](https://github.com/mbiesiad) 69 | 70 | Check out the [ongoing translations](https://github.com/verekia/js-stack-from-scratch/issues/147). 71 | 72 | ### V1 73 | 74 | - [Chinese (simplified)](https://github.com/pd4d10/js-stack-from-scratch) by [@pd4d10](http://github.com/pd4d10) 75 | - [Italian](https://github.com/fbertone/js-stack-from-scratch) by [Fabrizio Bertone](https://github.com/fbertone) 76 | - [Japanese](https://github.com/takahashim/js-stack-from-scratch) by [@takahashim](https://github.com/takahashim) 77 | - [Russian](https://github.com/UsulPro/js-stack-from-scratch) by [React Theming](https://github.com/sm-react/react-theming) 78 | - [Thai](https://github.com/MicroBenz/js-stack-from-scratch) by [MicroBenz](https://github.com/MicroBenz) 79 | 80 | ## Credits 81 | 82 | Created by [@verekia](https://twitter.com/verekia) – [verekia.com](http://verekia.com/). 83 | 84 | License: MIT 85 | -------------------------------------------------------------------------------- /tutorial/09-travis-coveralls-heroku.md: -------------------------------------------------------------------------------- 1 | # 09 - Travis, Coveralls, and Heroku 2 | 3 | Code for this chapter available in the `master` branch of the [JS-Stack-Boilerplate repository](https://github.com/verekia/js-stack-boilerplate). 4 | 5 | In this chapter, I integrate our app with third-party services. These services offer free and paid plans. It is a bit controversial to use such services in a tutorial that relies exclusively on community-driven and free open source tools, which is why I maintain 2 different branches of the [JS-Stack-Boilerplate repository](https://github.com/verekia/js-stack-boilerplate), `master` and `master-no-services`. 6 | 7 | ## Travis 8 | 9 | > 💡 **[Travis CI](https://travis-ci.org/)** is a popular continuous integration platform, free for open source projects. 10 | 11 | If your project is hosted publicly on Github, integrating Travis is very simple. First, authenticate with your Github account on Travis, and add your repository. 12 | 13 | - Then, create a `.travis.yml` file containing: 14 | 15 | ```yaml 16 | language: node_js 17 | node_js: node 18 | script: yarn test && yarn prod:build 19 | ``` 20 | 21 | Travis will detect automatically that you use Yarn because you have a `yarn.lock` file. Every time you push code to your Github repository, it will run `yarn test && yarn prod:build`. If nothing goes wrong, you should get a green build. 22 | 23 | ## Coveralls 24 | 25 | > 💡 **[Coveralls](https://coveralls.io)** is a service that gives you a history and statistics of your test coverage. 26 | 27 | If your project is open-source on Github and compatible with Coveralls' supported Continuous Integration services, the only thing you need to do is to pipe the coverage file generated by Jest to the `coveralls` binary. 28 | 29 | - Run `yarn add --dev coveralls` 30 | 31 | - Edit your `.travis.yml`'s `script` instruction like so: 32 | 33 | ```yaml 34 | script: yarn test && yarn prod:build && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 35 | ``` 36 | 37 | Now, every time Travis will build, it will automatically submit your Jest test coverage data to Coveralls. 38 | 39 | ## Badges 40 | 41 | Is everything green on Travis and Coveralls? Great, show it off to the world with shiny badges! 42 | 43 | You can use the code provided by Travis or Coveralls directly, or use [shields.io](http://shields.io/) to homogenize or customize your badges. Let's use shields.io here. 44 | 45 | - Create or edit your `README.md` like so: 46 | 47 | ```md 48 | [![Build Status](https://img.shields.io/travis/GITHUB-USERNAME/GITHUB-REPO.svg?style=flat-square)](https://travis-ci.org/GITHUB-USERNAME/GITHUB-REPO) 49 | [![Coverage Status](https://img.shields.io/coveralls/GITHUB-USERNAME/GITHUB-REPO.svg?style=flat-square)](https://coveralls.io/github/GITHUB-USERNAME/GITHUB-REPO?branch=master) 50 | ``` 51 | 52 | Of course, replace `GITHUB-USERNAME/GITHUB-REPO` by your actual Github username and repository name. 53 | 54 | ## Heroku 55 | 56 | > 💡 **[Heroku](https://www.heroku.com/)** is a [PaaS](https://en.wikipedia.org/wiki/Platform_as_a_service) to deploy to. It takes care of infrastructure details, so you can focus on developing your app without worrying about what happens behind the scenes. 57 | 58 | This tutorial is not sponsored in any way by Heroku, but Heroku being one hell of a platform, I am going to show you how to deploy your app to it. Yep, that's the kind of free love you get when you build a great product. 59 | 60 | **Note**: I might add an AWS section in this chapter later, but one thing at a time. 61 | 62 | ### Web setup 63 | 64 | - If that's not done yet, install the [Heroku CLI](https://devcenter.heroku.com/articles/getting-started-with-nodejs) and log in. 65 | 66 | - Go to your [Heroku Dashboard](https://dashboard.heroku.com/) and create 2 apps, one called `your-project` and one called `your-project-staging` for instance. 67 | 68 | We are going to let Heroku take care of transpiling our ES6/Flow code with Babel, and generate client bundles with Webpack. But since these are `devDependencies`, Yarn won't install them in a production environment like Heroku. Let's change this behavior with the `NPM_CONFIG_PRODUCTION` env variable. 69 | 70 | - In both apps, under Settings > Config Variables, add `NPM_CONFIG_PRODUCTION` set to `false`. 71 | 72 | - Create a Pipeline, and grant Heroku access to your Github. 73 | 74 | - Add both apps to the pipeline, make the staging one auto-deploy on changes in `master`, and enable Review Apps. 75 | 76 | Alright, let's prepare our project for a deployment to Heroku. 77 | 78 | ### Running in production mode locally 79 | 80 | - Create a `.env` file containing: 81 | 82 | ```.env 83 | NODE_ENV='production' 84 | PORT='8000' 85 | ``` 86 | 87 | That's in this file that you should put your local-only variables and secret variables. Don't commit it to a public repository if your project is private. 88 | 89 | - Add `/.env` to your `.gitignore` 90 | 91 | - Create a `Procfile` file containing: 92 | 93 | ```Procfile 94 | web: node lib/server 95 | ``` 96 | 97 | That's where we specify the entry point of our server. 98 | 99 | We are not going to use PM2 anymore, we'll use `heroku local` instead to run in production mode locally. 100 | 101 | - Run `yarn remove pm2` 102 | 103 | - Edit your `prod:start` script in `package.json`: 104 | 105 | ```json 106 | "prod:start": "heroku local", 107 | ``` 108 | 109 | - Remove `prod:stop` from `package.json`. We don't need it anymore since `heroku local` is a hanging process that we can kill with Ctrl+C, unlike `pm2 start`. 110 | 111 | 🏁 Run `yarn prod:build` and `yarn prod:start`. It should start your server and show you the logs. 112 | 113 | ### Deploying to production 114 | 115 | - Add the following line to your `scripts` in `package.json`: 116 | 117 | ```json 118 | "heroku-postbuild": "yarn prod:build", 119 | ``` 120 | 121 | `heroku-postbuild` is a task that will be run every time you deploy an app to Heroku. 122 | 123 | You will also probably want to specify a specific version of Node or Yarn for Heroku to use. 124 | 125 | - Add this to your `package.json`: 126 | 127 | ```json 128 | "engines": { 129 | "node": "7.x", 130 | "yarn": "0.20.3" 131 | }, 132 | ``` 133 | 134 | - Create an `app.json` file containing: 135 | 136 | ```json 137 | { 138 | "env": { 139 | "NPM_CONFIG_PRODUCTION": "false" 140 | } 141 | } 142 | ``` 143 | 144 | This is for your Review Apps to use. 145 | 146 | You should now be all set to use Heroku Pipeline deployments. 147 | 148 | 🏁 Create a new git branch, make changes and open a Github Pull Request to instantiate a Review App. Check your changes on the Review App URL, and if everything looks good, merge your Pull Request with `master` on Github. A few minutes later, your staging app should have been automatically deployed. Check your changes on the staging app URL, and if everything still looks good, promote staging to production. 149 | 150 | You are done! Congratulations if you finished this entire tutorial starting from scratch. 151 | 152 | You deserve this emoji medal: 🏅 153 | 154 | Back to the [previous section](08-bootstrap-jss.md#readme) or the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents). 155 | -------------------------------------------------------------------------------- /tutorial/07-socket-io.md: -------------------------------------------------------------------------------- 1 | # 07 - Socket.IO 2 | 3 | Code for this chapter available [here](https://github.com/verekia/js-stack-walkthrough/tree/master/07-socket-io). 4 | 5 | > 💡 **[Socket.IO](https://github.com/socketio/socket.io)** is a library to easily deal with Websockets. It provides a convenient API and fallback for browsers that don't support Websockets. 6 | 7 | In this chapter, we are going to set up a basic message exchange between the client and the server. In order to not add more pages and components – which would be unrelated to the core feature we're interested in here – we are going to make this exchange happen in the browser console. No UI stuff in this chapter. 8 | 9 | - Run `yarn add socket.io socket.io-client` 10 | 11 | ## Server-side 12 | 13 | - Edit your `src/server/index.js` like so: 14 | 15 | ```js 16 | // @flow 17 | 18 | import compression from 'compression' 19 | import express from 'express' 20 | import { Server } from 'http' 21 | import socketIO from 'socket.io' 22 | 23 | import routing from './routing' 24 | import { WEB_PORT, STATIC_PATH } from '../shared/config' 25 | import { isProd } from '../shared/util' 26 | import setUpSocket from './socket' 27 | 28 | const app = express() 29 | // flow-disable-next-line 30 | const http = Server(app) 31 | const io = socketIO(http) 32 | setUpSocket(io) 33 | 34 | app.use(compression()) 35 | app.use(STATIC_PATH, express.static('dist')) 36 | app.use(STATIC_PATH, express.static('public')) 37 | 38 | routing(app) 39 | 40 | http.listen(WEB_PORT, () => { 41 | // eslint-disable-next-line no-console 42 | console.log(`Server running on port ${WEB_PORT} ${isProd ? '(production)' : 43 | '(development).\nKeep "yarn dev:wds" running in an other terminal'}.`) 44 | }) 45 | ``` 46 | 47 | Note that in order for Socket.IO to work, you need to use `Server` from `http` to `listen` to incoming requests, and not the Express `app`. Fortunately, that doesn't change much of the code. All the Websocket details are externalized in a different file, called with `setUpSocket`. 48 | 49 | - Add the following constants to `src/shared/config.js`: 50 | 51 | ```js 52 | export const IO_CONNECT = 'connect' 53 | export const IO_DISCONNECT = 'disconnect' 54 | export const IO_CLIENT_HELLO = 'IO_CLIENT_HELLO' 55 | export const IO_CLIENT_JOIN_ROOM = 'IO_CLIENT_JOIN_ROOM' 56 | export const IO_SERVER_HELLO = 'IO_SERVER_HELLO' 57 | ``` 58 | 59 | These are the *type of messages* your client and your server will exchange. I suggest prefixing them with either `IO_CLIENT` or `IO_SERVER` to make it clearer *who* is sending the message. Otherwise, things can get pretty confusing when you have a lot of message types. 60 | 61 | As you can see, we have a `IO_CLIENT_JOIN_ROOM`, because for the sake of demonstration, we are going to make clients join a room (like a chatroom). Rooms are useful to broadcast messages to specific groups of users. 62 | 63 | - Create a `src/server/socket.js` file containing: 64 | 65 | ```js 66 | // @flow 67 | 68 | import { 69 | IO_CONNECT, 70 | IO_DISCONNECT, 71 | IO_CLIENT_JOIN_ROOM, 72 | IO_CLIENT_HELLO, 73 | IO_SERVER_HELLO, 74 | } from '../shared/config' 75 | 76 | /* eslint-disable no-console */ 77 | const setUpSocket = (io: Object) => { 78 | io.on(IO_CONNECT, (socket) => { 79 | console.log('[socket.io] A client connected.') 80 | 81 | socket.on(IO_CLIENT_JOIN_ROOM, (room) => { 82 | socket.join(room) 83 | console.log(`[socket.io] A client joined room ${room}.`) 84 | 85 | io.emit(IO_SERVER_HELLO, 'Hello everyone!') 86 | io.to(room).emit(IO_SERVER_HELLO, `Hello clients of room ${room}!`) 87 | socket.emit(IO_SERVER_HELLO, 'Hello you!') 88 | }) 89 | 90 | socket.on(IO_CLIENT_HELLO, (clientMessage) => { 91 | console.log(`[socket.io] Client: ${clientMessage}`) 92 | }) 93 | 94 | socket.on(IO_DISCONNECT, () => { 95 | console.log('[socket.io] A client disconnected.') 96 | }) 97 | }) 98 | } 99 | /* eslint-enable no-console */ 100 | 101 | export default setUpSocket 102 | ``` 103 | 104 | Okay, so in this file, we implement *how our server should react when clients connect and send messages to it*: 105 | 106 | - When the client connects, we log it in the server console, and get access to the `socket` object, which we can use to communicate back with that client. 107 | - When a client sends `IO_CLIENT_JOIN_ROOM`, we make it join the `room` it wants. Once it has joined a room, we send 3 demo messages: 1 message to every user, 1 message to users in that room, 1 message to that client only. 108 | - When the client sends `IO_CLIENT_HELLO`, we log its message in the server console. 109 | - When the client disconnects, we log it as well. 110 | 111 | ## Client-side 112 | 113 | The client-side of things is going to look very similar. 114 | 115 | - Edit `src/client/index.jsx` like so: 116 | 117 | ```js 118 | // [...] 119 | import setUpSocket from './socket' 120 | 121 | // [at the very end of the file] 122 | setUpSocket(store) 123 | ``` 124 | 125 | As you can see, we pass the Redux store to `setUpSocket`. This way whenever a Websocket message coming from the server should alter the client's Redux state, we can `dispatch` actions. We are not going to `dispatch` anything in this example though. 126 | 127 | - Create a `src/client/socket.js` file containing: 128 | 129 | ```js 130 | // @flow 131 | 132 | import socketIOClient from 'socket.io-client' 133 | 134 | import { 135 | IO_CONNECT, 136 | IO_DISCONNECT, 137 | IO_CLIENT_HELLO, 138 | IO_CLIENT_JOIN_ROOM, 139 | IO_SERVER_HELLO, 140 | } from '../shared/config' 141 | 142 | const socket = socketIOClient(window.location.host) 143 | 144 | /* eslint-disable no-console */ 145 | // eslint-disable-next-line no-unused-vars 146 | const setUpSocket = (store: Object) => { 147 | socket.on(IO_CONNECT, () => { 148 | console.log('[socket.io] Connected.') 149 | socket.emit(IO_CLIENT_JOIN_ROOM, 'hello-1234') 150 | socket.emit(IO_CLIENT_HELLO, 'Hello!') 151 | }) 152 | 153 | socket.on(IO_SERVER_HELLO, (serverMessage) => { 154 | console.log(`[socket.io] Server: ${serverMessage}`) 155 | }) 156 | 157 | socket.on(IO_DISCONNECT, () => { 158 | console.log('[socket.io] Disconnected.') 159 | }) 160 | } 161 | /* eslint-enable no-console */ 162 | 163 | export default setUpSocket 164 | ``` 165 | 166 | What happens here should not be surprising if you understood well what we did on the server: 167 | 168 | - As soon as the client is connected, we log it in the browser console and join the room `hello-1234` with a `IO_CLIENT_JOIN_ROOM` message. 169 | - We then send `Hello!` with a `IO_CLIENT_HELLO` message. 170 | - If the server sends us a `IO_SERVER_HELLO` message, we log it in the browser console. 171 | - We also log any disconnection. 172 | 173 | 🏁 Run `yarn start` and `yarn dev:wds`, open `http://localhost:8000`. Then, open your browser console, and also look at the terminal of your Express server. You should see the Websocket communication between your client and server. 174 | 175 | Next section: [08 - Bootstrap, JSS](08-bootstrap-jss.md#readme) 176 | 177 | Back to the [previous section](06-react-router-ssr-helmet.md#readme) or the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents). 178 | -------------------------------------------------------------------------------- /tutorial/01-node-yarn-package-json.md: -------------------------------------------------------------------------------- 1 | # 01 - Node, Yarn, and `package.json` 2 | 3 | Code for this chapter available [here](https://github.com/verekia/js-stack-walkthrough/tree/master/01-node-yarn-package-json). 4 | 5 | In this section we will set up Node, Yarn, a basic `package.json` file, and try a package. 6 | 7 | ## Node 8 | 9 | > 💡 **[Node.js](https://nodejs.org/)** is a JavaScript runtime environment. It is mostly used for Back-End development, but also for general scripting. In the context of Front-End development, it can be used to perform a whole bunch of tasks like linting, testing, and assembling files. 10 | 11 | We will use Node for basically everything in this tutorial, so you're going to need it. Head to the [download page](https://nodejs.org/en/download/current/) for **macOS** or **Windows** binaries, or the [package manager installations page](https://nodejs.org/en/download/package-manager/) for Linux distributions. 12 | 13 | For instance, on **Ubuntu / Debian**, you would run the following commands to install Node: 14 | 15 | ```sh 16 | curl -sL https://deb.nodesource.com/setup_7.x | sudo -E bash - 17 | sudo apt-get install -y nodejs 18 | ``` 19 | 20 | You want any version of Node > 6.5.0. 21 | 22 | ## Node Version Management Tools 23 | 24 | If you need the flexibility to use multiple versions of Node, check out [NVM](https://github.com/creationix/nvm) or [tj/n](https://github.com/tj/n). 25 | 26 | ## NPM 27 | 28 | NPM is the default package manager for Node. It is automatically installed alongside with Node. Package managers are used to install and manage packages (modules of code that you or someone else wrote). We are going to use a lot of packages in this tutorial, but we'll use Yarn, another package manager. 29 | 30 | ## Yarn 31 | 32 | > 💡 **[Yarn](https://yarnpkg.com/)** is a Node.js package manager which is much faster than NPM, has offline support, and fetches dependencies [more predictably](https://yarnpkg.com/en/docs/yarn-lock). 33 | 34 | Since it [came out](https://code.facebook.com/posts/1840075619545360) in October 2016, it received a very quick adoption and may soon become the package manager of choice of the JavaScript community. If you want to stick to NPM you can simply replace all `yarn add` and `yarn add --dev` commands of this tutorial by `npm install --save` and `npm install --save-dev`. 35 | 36 | Install Yarn by following the [instructions](https://yarnpkg.com/en/docs/install) for your OS. I would recommend using the **Installation Script** from the *Alternatives* tab if you are on macOS or Unix, to [avoid](https://github.com/yarnpkg/yarn/issues/1505) relying on other package managers: 37 | 38 | ```sh 39 | curl -o- -L https://yarnpkg.com/install.sh | bash 40 | ``` 41 | 42 | ## `package.json` 43 | 44 | > 💡 **[package.json](https://yarnpkg.com/en/docs/package-json)** is the file used to describe and configure your JavaScript project. It contains general information (your project name, version, contributors, license, etc), configuration options for tools you use, and even a section to run *tasks*. 45 | 46 | - Create a new folder to work in, and `cd` in it. 47 | - Run `yarn init` and answer the questions (`yarn init -y` to skip all questions), to generate a `package.json` file automatically. 48 | 49 | Here is the basic `package.json` I'll use in this tutorial: 50 | 51 | ```json 52 | { 53 | "name": "your-project", 54 | "version": "1.0.0", 55 | "license": "MIT" 56 | } 57 | ``` 58 | 59 | ## Hello World 60 | 61 | - Create an `index.js` file containing `console.log('Hello world')` 62 | 63 | 🏁 Run `node .` in this folder (`index.js` is the default file Node looks for in a folder). It should print "Hello world". 64 | 65 | **Note**: See that 🏁 racing flag emoji? I will use it every time you reach a **checkpoint**. We are sometimes going to make a lot of changes in a row, and your code may not work until you reach the next checkpoint. 66 | 67 | ## `start` script 68 | 69 | Running `node .` to execute our program is a bit too low-level. We are going to use an NPM/Yarn script to trigger the execution of that code instead. That will give us a nice abstraction to be able to always use `yarn start`, even when our program gets more complicated. 70 | 71 | - In `package.json`, add a `scripts` object like so: 72 | 73 | ```json 74 | { 75 | "name": "your-project", 76 | "version": "1.0.0", 77 | "license": "MIT", 78 | "scripts": { 79 | "start": "node ." 80 | } 81 | } 82 | ``` 83 | 84 | `start` is the name we give to the *task* that will run our program. We are going to create a lot of different tasks in this `scripts` object throughout this tutorial. `start` is typically the name given to the default task of an application. Some other standard task names are `stop` and `test`. 85 | 86 | `package.json` must be a valid JSON file, which means that you cannot have trailing commas. So be careful when editing manually your `package.json` file. 87 | 88 | 🏁 Run `yarn start`. It should print `Hello world`. 89 | 90 | ## Git and `.gitignore` 91 | 92 | - Initialize a Git repository with `git init` 93 | 94 | - Create a `.gitignore` file and add the following to it: 95 | 96 | ```gitignore 97 | .DS_Store 98 | /*.log 99 | ``` 100 | 101 | `.DS_Store` files are auto-generated macOS files that you should never have in your repository. 102 | 103 | `npm-debug.log` and `yarn-error.log` are files that are created when your package manager encounters an error, we don't want them versioned in our repository. 104 | 105 | ## Installing and using a package 106 | 107 | In this section we will install and use a package. A "package" is simply a piece of code that someone else wrote, and that you can use in your own code. It can be anything. Here, we're going to try a package that helps you manipulate colors for instance. 108 | 109 | - Install the community-made package called `color` by running `yarn add color` 110 | 111 | Open `package.json` to see how Yarn automatically added `color` in `dependencies`. 112 | 113 | A `node_modules` folder has been created to store the package. 114 | 115 | - Add `node_modules/` to your `.gitignore` 116 | 117 | You will also notice that a `yarn.lock` file got generated by Yarn. You should commit this file to your repository, as it will ensure that everyone in your team uses the same version of your packages. If you're sticking to NPM instead of Yarn, the equivalent of this file is the *shrinkwrap*. 118 | 119 | - Write the following to your `index.js` file: 120 | 121 | ```js 122 | const color = require('color') 123 | 124 | const redHexa = color({ r: 255, g: 0, b: 0 }).hex() 125 | 126 | console.log(redHexa) 127 | ``` 128 | 129 | 🏁 Run `yarn start`. It should print `#FF0000`. 130 | 131 | Congratulations, you installed and used a package! 132 | 133 | `color` is just used in this section to teach you how to use a simple package. We won't need it anymore, so you can uninstall it: 134 | 135 | - Run `yarn remove color` 136 | 137 | ## Two kinds of dependencies 138 | 139 | There are two kinds of package dependencies, `"dependencies"` and `"devDependencies"`: 140 | 141 | **Dependencies** are libraries you need for your application to function (React, Redux, Lodash, jQuery, etc). You install them with `yarn add [package]`. 142 | 143 | **Dev Dependencies** are libraries used during development or to build your application (Webpack, SASS, linters, testing frameworks, etc). You install those with `yarn add --dev [package]`. 144 | 145 | Next section: [02 - Babel, ES6, ESLint, Flow, Jest, Husky](02-babel-es6-eslint-flow-jest-husky.md#readme) 146 | 147 | Back to the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents). 148 | -------------------------------------------------------------------------------- /tutorial/03-express-nodemon-pm2.md: -------------------------------------------------------------------------------- 1 | # 03 - Express, Nodemon, and PM2 2 | 3 | Code for this chapter available [here](https://github.com/verekia/js-stack-walkthrough/tree/master/03-express-nodemon-pm2). 4 | 5 | In this section we are going to create the server that will render our web app. We will also set up a development mode and a production mode for this server. 6 | 7 | ## Express 8 | 9 | > 💡 **[Express](http://expressjs.com/)** is by far the most popular web application framework for Node. It provides a very simple and minimal API, and its features can be extended with *middleware*. 10 | 11 | Let's set up a minimal Express server to serve an HTML page with some CSS. 12 | 13 | - Delete everything inside `src` 14 | 15 | Create the following files and folders: 16 | 17 | - Create a `public/css/style.css` file containing: 18 | 19 | ```css 20 | body { 21 | width: 960px; 22 | margin: auto; 23 | font-family: sans-serif; 24 | } 25 | 26 | h1 { 27 | color: limegreen; 28 | } 29 | ``` 30 | 31 | - Create an empty `src/client/` folder. 32 | 33 | - Create an empty `src/shared/` folder. 34 | 35 | This folder is where we put *isomorphic / universal* JavaScript code – files that are used by both the client and the server. A great use case of shared code is *routes*, as you will see later in this tutorial when we'll make an asynchronous call. Here we simply have some configuration constants as an example for now. 36 | 37 | - Create a `src/shared/config.js` file, containing: 38 | 39 | ```js 40 | // @flow 41 | 42 | export const WEB_PORT = process.env.PORT || 8000 43 | export const STATIC_PATH = '/static' 44 | export const APP_NAME = 'Hello App' 45 | ``` 46 | 47 | If the Node process used to run your app has a `process.env.PORT` environment variable set (that's the case when you deploy to Heroku for instance), it will use this for the port. If there is none, we default to `8000`. 48 | 49 | - Create a `src/shared/util.js` file containing: 50 | 51 | ```js 52 | // @flow 53 | 54 | // eslint-disable-next-line import/prefer-default-export 55 | export const isProd = process.env.NODE_ENV === 'production' 56 | ``` 57 | 58 | That's a simple util to test if we are running in production mode or not. The `// eslint-disable-next-line import/prefer-default-export` comment is because we only have one named export here. You can remove it as you add other exports in this file. 59 | 60 | - Run `yarn add express compression` 61 | 62 | `compression` is an Express middleware to activate Gzip compression on the server. 63 | 64 | - Create a `src/server/index.js` file containing: 65 | 66 | ```js 67 | // @flow 68 | 69 | import compression from 'compression' 70 | import express from 'express' 71 | 72 | import { APP_NAME, STATIC_PATH, WEB_PORT } from '../shared/config' 73 | import { isProd } from '../shared/util' 74 | import renderApp from './render-app' 75 | 76 | const app = express() 77 | 78 | app.use(compression()) 79 | app.use(STATIC_PATH, express.static('dist')) 80 | app.use(STATIC_PATH, express.static('public')) 81 | 82 | app.get('/', (req, res) => { 83 | res.send(renderApp(APP_NAME)) 84 | }) 85 | 86 | app.listen(WEB_PORT, () => { 87 | // eslint-disable-next-line no-console 88 | console.log(`Server running on port ${WEB_PORT} ${isProd ? '(production)' : '(development)'}.`) 89 | }) 90 | ``` 91 | 92 | Nothing fancy here, it's almost Express' Hello World tutorial with a few additional imports. We're using 2 different static file directories here. `dist` for generated files, `public` for declarative ones. 93 | 94 | - Create a `src/server/render-app.js` file containing: 95 | 96 | ```js 97 | // @flow 98 | 99 | import { STATIC_PATH } from '../shared/config' 100 | 101 | const renderApp = (title: string) => 102 | ` 103 | 104 | 105 | ${title} 106 | 107 | 108 | 109 |

${title}

110 | 111 | 112 | ` 113 | 114 | export default renderApp 115 | ``` 116 | 117 | You know how you typically have *templating engines* on the back-end? Well these are pretty much obsolete now that JavaScript supports template strings. Here we create a function that takes a `title` as a parameter and injects it in both the `title` and `h1` tags of the page, returning the complete HTML string. We also use a `STATIC_PATH` constant as the base path for all our static assets. 118 | 119 | ### HTML template strings syntax highlighting in Atom (optional) 120 | 121 | It might be possible to get syntax highlighting working for HTML code inside template strings depending on your editor. In Atom, if you prefix your template string with an `html` tag (or any tag that *ends* with `html`, like `ilovehtml`), it will automatically highlight the content of that string. I sometimes use the `html` tag of the `common-tags` library to take advantage of this: 122 | 123 | ```js 124 | import { html } from `common-tags` 125 | 126 | const template = html` 127 |
Wow, colors!
128 | ` 129 | ``` 130 | 131 | I did not include this trick in the boilerplate of this tutorial, since it seems to only work in Atom, and it's less than ideal. Some of you Atom users might find it useful though. 132 | 133 | Anyway, back to business! 134 | 135 | - In `package.json` change your `start` script like so: `"start": "babel-node src/server",` 136 | 137 | 🏁 Run `yarn start`, and hit `localhost:8000` in your browser. If everything works as expected you should see a blank page with "Hello App" written both on the tab title and as a green heading on the page. 138 | 139 | **Note**: Some processes – typically processes that wait for things to happen, like a server for instance – will prevent you from entering commands in your terminal until they're done. To interrupt such processes and get your prompt back, press **Ctrl+C**. You can alternatively open a new terminal tab if you want to keep them running while being able to enter commands. You can also make these processes run in the background but that's out of the scope of this tutorial. 140 | 141 | ## Nodemon 142 | 143 | > 💡 **[Nodemon](https://nodemon.io/)** is a utility to automatically restart your Node server when file changes happen in the directory. 144 | 145 | We are going to use Nodemon whenever we are in **development** mode. 146 | 147 | - Run `yarn add --dev nodemon` 148 | 149 | - Change your `scripts` like so: 150 | 151 | ```json 152 | "start": "yarn dev:start", 153 | "dev:start": "nodemon --ignore lib --exec babel-node src/server", 154 | ``` 155 | 156 | `start` is now just a pointer to an other task, `dev:start`. That gives us a layer of abstraction to tweak what the default task is. 157 | 158 | In `dev:start`, the `--ignore lib` flag is to *not* restart the server when changes happen in the `lib` directory. You don't have this directory yet, but we're going to generate it in the next section of this chapter, so it will soon make sense. Nodemon typically runs the `node` binary. In our case, since we're using Babel, we can tell Nodemon to use the `babel-node` binary instead. This way it will understand all the ES6/Flow code. 159 | 160 | 🏁 Run `yarn start` and open `localhost:8000`. Go ahead and change the `APP_NAME` constant in `src/shared/config.js`, which should trigger a restart of your server in the terminal. Refresh the page to see the updated title. Note that this automatic restart of the server is different from *Hot Module Replacement*, which is when components on the page update in real-time. Here we still need a manual refresh, but at least we don't need to kill the process and restart it manually to see changes. Hot Module Replacement will be introduced in the next chapter. 161 | 162 | ## PM2 163 | 164 | > 💡 **[PM2](http://pm2.keymetrics.io/)** is a Process Manager for Node. It keeps your processes alive in production, and offers tons of features to manage them and monitor them. 165 | 166 | We are going to use PM2 whenever we are in **production** mode. 167 | 168 | - Run `yarn add --dev pm2` 169 | 170 | In production, you want your server to be as performant as possible. `babel-node` triggers the whole Babel transpilation process for your files at each execution, which is not something you want in production. We need Babel to do all this work beforehand, and have our server serve plain old pre-compiled ES5 files. 171 | 172 | One of the main features of Babel is to take a folder of ES6 code (usually named `src`) and transpile it into a folder of ES5 code (usually named `lib`). 173 | 174 | This `lib` folder being auto-generated, it's a good practice to clean it up before a new build, since it may contain unwanted old files. A neat simple package to delete files with cross platform support is `rimraf`. 175 | 176 | - Run `yarn add --dev rimraf` 177 | 178 | Let's add the following `prod:build` task to our `scripts`: 179 | 180 | ```json 181 | "prod:build": "rimraf lib && babel src -d lib --ignore .test.js", 182 | ``` 183 | 184 | - Run `yarn prod:build`, and it should generate a `lib` folder containing the transpiled code, except for files ending in `.test.js` (note that `.test.jsx` files are also ignored by this parameter). 185 | 186 | - Add `/lib/` to your `.gitignore` 187 | 188 | One last thing: We are going to pass a `NODE_ENV` environment variable to our PM2 binary. With Unix, you would do this by running `NODE_ENV=production pm2`, but Windows uses a different syntax. We're going to use a small package called `cross-env` to make this syntax work on Windows as well. 189 | 190 | - Run `yarn add --dev cross-env` 191 | 192 | Let's update our `package.json` like so: 193 | 194 | ```json 195 | "scripts": { 196 | "start": "yarn dev:start", 197 | "dev:start": "nodemon --ignore lib --exec babel-node src/server", 198 | "prod:build": "rimraf lib && babel src -d lib --ignore .test.js", 199 | "prod:start": "cross-env NODE_ENV=production pm2 start lib/server && pm2 logs", 200 | "prod:stop": "pm2 delete server", 201 | "test": "eslint src && flow && jest --coverage", 202 | "precommit": "yarn test", 203 | "prepush": "yarn test" 204 | }, 205 | ``` 206 | 207 | 🏁 Run `yarn prod:build`, then run `yarn prod:start`. PM2 should show an active process. Go to `http://localhost:8000/` in your browser and you should see your app. Your terminal should show the logs, which should be "Server running on port 8000 (production).". Note that with PM2, your processes are run in the background. If you press Ctrl+C, it will kill the `pm2 logs` command, which was the last command our our `prod:start` chain, but the server should still render the page. If you want to stop the server, run `yarn prod:stop` 208 | 209 | Now that we have a `prod:build` task, it would be neat to make sure it works fine before pushing code to the repository. Since it is probably unnecessary to run it for every commit, I suggest adding it to the `prepush` task: 210 | 211 | ```json 212 | "prepush": "yarn test && yarn prod:build" 213 | ``` 214 | 215 | 🏁 Run `yarn prepush` or just push your files to trigger the process. 216 | 217 | **Note**: We don't have any test here, so Jest will complain a bit. Ignore it for now. 218 | 219 | Next section: [04 - Webpack, React, HMR](04-webpack-react-hmr.md#readme) 220 | 221 | Back to the [previous section](02-babel-es6-eslint-flow-jest-husky.md#readme) or the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents). 222 | -------------------------------------------------------------------------------- /tutorial/04-webpack-react-hmr.md: -------------------------------------------------------------------------------- 1 | # 04 - Webpack, React, and Hot Module Replacement 2 | 3 | Code for this chapter available [here](https://github.com/verekia/js-stack-walkthrough/tree/master/04-webpack-react-hmr). 4 | 5 | ## Webpack 6 | 7 | > 💡 **[Webpack](https://webpack.js.org/)** is a *module bundler*. It takes a whole bunch of various source files, processes them, and assembles them into one (usually) JavaScript file called a bundle, which is the only file your client will execute. 8 | 9 | Let's create some very basic *hello world* and bundle it with Webpack. 10 | 11 | - In `src/shared/config.js`, add the following constants: 12 | 13 | ```js 14 | export const WDS_PORT = 7000 15 | 16 | export const APP_CONTAINER_CLASS = 'js-app' 17 | export const APP_CONTAINER_SELECTOR = `.${APP_CONTAINER_CLASS}` 18 | ``` 19 | 20 | - Create an `src/client/index.js` file containing: 21 | 22 | ```js 23 | import 'babel-polyfill' 24 | 25 | import { APP_CONTAINER_SELECTOR } from '../shared/config' 26 | 27 | document.querySelector(APP_CONTAINER_SELECTOR).innerHTML = '

Hello Webpack!

' 28 | ``` 29 | 30 | If you want to use some of the most recent ES features in your client code, like `Promise`s, you need to include the [Babel Polyfill](https://babeljs.io/docs/usage/polyfill/) before anything else in your bundle. 31 | 32 | - Run `yarn add babel-polyfill` 33 | 34 | If you run ESLint on this file, it will complain about `document` being undefined. 35 | 36 | - Add the following to `env` in your `.eslintrc.json` to allow the use of `window` and `document`: 37 | 38 | ```json 39 | "env": { 40 | "browser": true, 41 | "jest": true 42 | } 43 | ``` 44 | 45 | Alright, we now need to bundle this ES6 client app into an ES5 bundle. 46 | 47 | - Create a `webpack.config.babel.js` file containing: 48 | 49 | ```js 50 | // @flow 51 | 52 | import path from 'path' 53 | 54 | import { WDS_PORT } from './src/shared/config' 55 | import { isProd } from './src/shared/util' 56 | 57 | export default { 58 | entry: [ 59 | './src/client', 60 | ], 61 | output: { 62 | filename: 'js/bundle.js', 63 | path: path.resolve(__dirname, 'dist'), 64 | publicPath: isProd ? '/static/' : `http://localhost:${WDS_PORT}/dist/`, 65 | }, 66 | module: { 67 | rules: [ 68 | { test: /\.(js|jsx)$/, use: 'babel-loader', exclude: /node_modules/ }, 69 | ], 70 | }, 71 | devtool: isProd ? false : 'source-map', 72 | resolve: { 73 | extensions: ['.js', '.jsx'], 74 | }, 75 | devServer: { 76 | port: WDS_PORT, 77 | }, 78 | } 79 | ``` 80 | 81 | This file is used to describe how our bundle should be assembled: `entry` is the starting point of our app, `output.filename` is the name of the bundle to generate, `output.path` and `output.publicPath` describe the destination folder and URL. We put the bundle in a `dist` folder, which will contain things that are generated automatically (unlike the declarative CSS we created earlier which lives in `public`). `module.rules` is where you tell Webpack to apply some treatment to some type of files. Here we say that we want all `.js` and `.jsx` (for React) files except the ones in `node_modules` to go through something called `babel-loader`. We also want these two extensions to be used to `resolve` modules when we `import` them. Finally, we declare a port for Webpack Dev Server. 82 | 83 | **Note**: The `.babel.js` extension is a Webpack feature to apply our Babel transformations to this config file. 84 | 85 | `babel-loader` is a plugin for Webpack that transpiles your code just like we've been doing since the beginning of this tutorial. The only difference is that this time, the code will end up running in the browser instead of your server. 86 | 87 | - Run `yarn add --dev webpack webpack-dev-server babel-core babel-loader` 88 | 89 | `babel-core` is a peer-dependency of `babel-loader`, so we installed it as well. 90 | 91 | - Add `/dist/` to your `.gitignore` 92 | 93 | ### Tasks update 94 | 95 | In development mode, we are going to use `webpack-dev-server` to take advantage of Hot Module Reloading (later in this chapter), and in production we'll simply use `webpack` to generate bundles. In both cases, the `--progress` flag is useful to display additional information when Webpack is compiling your files. In production, we'll also pass the `-p` flag to `webpack` to minify our code, and the `NODE_ENV` variable set to `production`. 96 | 97 | Let's update our `scripts` to implement all this, and improve some other tasks as well: 98 | 99 | ```json 100 | "scripts": { 101 | "start": "yarn dev:start", 102 | "dev:start": "nodemon -e js,jsx --ignore lib --ignore dist --exec babel-node src/server", 103 | "dev:wds": "webpack-dev-server --progress", 104 | "prod:build": "rimraf lib dist && babel src -d lib --ignore .test.js && cross-env NODE_ENV=production webpack -p --progress", 105 | "prod:start": "cross-env NODE_ENV=production pm2 start lib/server && pm2 logs", 106 | "prod:stop": "pm2 delete server", 107 | "lint": "eslint src webpack.config.babel.js --ext .js,.jsx", 108 | "test": "yarn lint && flow && jest --coverage", 109 | "precommit": "yarn test", 110 | "prepush": "yarn test && yarn prod:build" 111 | }, 112 | ``` 113 | 114 | In `dev:start` we explicitly declare file extensions to monitor, `.js` and `.jsx`, and add `dist` in the ignored directories. 115 | 116 | We created a separate `lint` task and added `webpack.config.babel.js` to the files to lint. 117 | 118 | - Next, let's create the container for our app in `src/server/render-app.js`, and include the bundle that will be generated: 119 | 120 | ```js 121 | // @flow 122 | 123 | import { APP_CONTAINER_CLASS, STATIC_PATH, WDS_PORT } from '../shared/config' 124 | import { isProd } from '../shared/util' 125 | 126 | const renderApp = (title: string) => 127 | ` 128 | 129 | 130 | ${title} 131 | 132 | 133 | 134 |
135 | 136 | 137 | 138 | ` 139 | 140 | export default renderApp 141 | ``` 142 | 143 | Depending on the environment we're in, we'll include either the Webpack Dev Server bundle, or the production bundle. Note that the path to Webpack Dev Server's bundle is *virtual*, `dist/js/bundle.js` is not actually read from your hard drive in development mode. It's also necessary to give Webpack Dev Server a different port than your main web port. 144 | 145 | - Finally, in `src/server/index.js`, tweak your `console.log` message like so: 146 | 147 | ```js 148 | console.log(`Server running on port ${WEB_PORT} ${isProd ? '(production)' : 149 | '(development).\nKeep "yarn dev:wds" running in an other terminal'}.`) 150 | ``` 151 | 152 | That will give other developers a hint about what to do if they try to just run `yarn start` without Webpack Dev Server. 153 | 154 | Alright that was a lot of changes, let's see if everything works as expected: 155 | 156 | 🏁 Run `yarn start` in a terminal. Open an other terminal tab or window, and run `yarn dev:wds` in it. Once Webpack Dev Server is done generating the bundle and its sourcemaps (which should both be ~600kB files) and both processes hang in your terminals, open `http://localhost:8000/` and you should see "Hello Webpack!". Open your Chrome console, and under the Source tab, check which files are included. You should only see `static/css/style.css` under `localhost:8000/`, and have all your ES6 source files under `webpack://./src`. That means sourcemaps are working. In your editor, in `src/client/index.js`, try changing `Hello Webpack!` into any other string. As you save the file, Webpack Dev Server in your terminal should generate a new bundle and the Chrome tab should reload automatically. 157 | 158 | - Kill the previous processes in your terminals with Ctrl+C, then run `yarn prod:build`, and then `yarn prod:start`. Open `http://localhost:8000/` and you should still see "Hello Webpack!". In the Source tab of the Chrome console, you should this time find `static/js/bundle.js` under `localhost:8000/`, but no `webpack://` sources. Click on `bundle.js` to make sure it is minified. Run `yarn prod:stop`. 159 | 160 | Good job, I know this was quite dense. You deserve a break! The next section is easier. 161 | 162 | **Note**: I would recommend to have at least 3 terminals open, one for your Express server, one for the Webpack Dev Server, and one for Git, tests, and general commands like installing packages with `yarn`. Ideally, you should split your terminal screen in multiple panes to see them all. 163 | 164 | ## React 165 | 166 | > 💡 **[React](https://facebook.github.io/react/)** is a library for building user interfaces by Facebook. It uses the **[JSX](https://facebook.github.io/react/docs/jsx-in-depth.html)** syntax to represent HTML elements and components while leveraging the power of JavaScript. 167 | 168 | In this section we are going to render some text using React and JSX. 169 | 170 | First, let's install React and ReactDOM: 171 | 172 | - Run `yarn add react react-dom` 173 | 174 | Rename your `src/client/index.js` file into `src/client/index.jsx` and write some React code in it: 175 | 176 | ```js 177 | // @flow 178 | 179 | import 'babel-polyfill' 180 | 181 | import React from 'react' 182 | import ReactDOM from 'react-dom' 183 | 184 | import App from './app' 185 | import { APP_CONTAINER_SELECTOR } from '../shared/config' 186 | 187 | ReactDOM.render(, document.querySelector(APP_CONTAINER_SELECTOR)) 188 | ``` 189 | 190 | - Create a `src/client/app.jsx` file containing: 191 | 192 | ```js 193 | // @flow 194 | 195 | import React from 'react' 196 | 197 | const App = () =>

Hello React!

198 | 199 | export default App 200 | ``` 201 | 202 | Since we use the JSX syntax here, we have to tell Babel that it needs to transform it with the `babel-preset-react` preset. And while we're at it, we're also going to add a Babel plugin called `flow-react-proptypes` which automatically generates PropTypes from Flow annotations for your React components. 203 | 204 | - Run `yarn add --dev babel-preset-react babel-plugin-flow-react-proptypes` and edit your `.babelrc` file like so: 205 | 206 | ```json 207 | { 208 | "presets": [ 209 | "env", 210 | "flow", 211 | "react" 212 | ], 213 | "plugins": [ 214 | "flow-react-proptypes" 215 | ] 216 | } 217 | ``` 218 | 219 | 🏁 Run `yarn start` and `yarn dev:wds` and hit `http://localhost:8000`. You should see "Hello React!". 220 | 221 | Now try changing the text in `src/client/app.jsx` to something else. Webpack Dev Server should reload the page automatically, which is pretty neat, but we are going to make it even better. 222 | 223 | ## Hot Module Replacement 224 | 225 | > 💡 **[Hot Module Replacement](https://webpack.js.org/concepts/hot-module-replacement/)** (*HMR*) is a powerful Webpack feature to replace a module on the fly without reloading the entire page. 226 | 227 | To make HMR work with React, we are going to need to tweak a few things. 228 | 229 | - Run `yarn add react-hot-loader@next` 230 | 231 | - Edit your `webpack.config.babel.js` like so: 232 | 233 | ```js 234 | import webpack from 'webpack' 235 | // [...] 236 | entry: [ 237 | 'react-hot-loader/patch', 238 | './src/client', 239 | ], 240 | // [...] 241 | devServer: { 242 | port: WDS_PORT, 243 | hot: true, 244 | headers: { 245 | 'Access-Control-Allow-Origin': '*', 246 | }, 247 | }, 248 | plugins: [ 249 | new webpack.optimize.OccurrenceOrderPlugin(), 250 | new webpack.HotModuleReplacementPlugin(), 251 | new webpack.NamedModulesPlugin(), 252 | new webpack.NoEmitOnErrorsPlugin(), 253 | ], 254 | ``` 255 | 256 | The `headers` bit is to allow Cross-Origin Resource Sharing which is necessary for HMR. 257 | 258 | - Edit your `src/client/index.jsx` file: 259 | 260 | ```js 261 | // @flow 262 | 263 | import 'babel-polyfill' 264 | 265 | import React from 'react' 266 | import ReactDOM from 'react-dom' 267 | import { AppContainer } from 'react-hot-loader' 268 | 269 | import App from './app' 270 | import { APP_CONTAINER_SELECTOR } from '../shared/config' 271 | 272 | const rootEl = document.querySelector(APP_CONTAINER_SELECTOR) 273 | 274 | const wrapApp = AppComponent => 275 | 276 | 277 | 278 | 279 | ReactDOM.render(wrapApp(App), rootEl) 280 | 281 | if (module.hot) { 282 | // flow-disable-next-line 283 | module.hot.accept('./app', () => { 284 | // eslint-disable-next-line global-require 285 | const NextApp = require('./app').default 286 | ReactDOM.render(wrapApp(NextApp), rootEl) 287 | }) 288 | } 289 | ``` 290 | 291 | We need to make our `App` a child of `react-hot-loader`'s `AppContainer`, and we need to `require` the next version of our `App` when hot-reloading. To make this process clean and DRY, we create a little `wrapApp` function that we use in both places it needs to render `App`. Feel free to move the `eslint-disable global-require` to the top of the file to make this more readable. 292 | 293 | 🏁 Restart your `yarn dev:wds` process if it was still running. Open `localhost:8000`. In the Console tab, you should see some logs about HMR. Go ahead and change something in `src/client/app.jsx` and your changes should be reflected in your browser after a few seconds, without any full-page reload! 294 | 295 | Next section: [05 - Redux, Immutable, Fetch](05-redux-immutable-fetch.md#readme) 296 | 297 | Back to the [previous section](03-express-nodemon-pm2.md#readme) or the [table of contents](https://github.com/verekia/js-stack-from-scratch#table-of-contents). 298 | -------------------------------------------------------------------------------- /tutorial/08-bootstrap-jss.md: -------------------------------------------------------------------------------- 1 | # 08 - Bootstrap and JSS 2 | 3 | Code for this chapter available in the [`master-no-services`](https://github.com/verekia/js-stack-boilerplate/tree/master-no-services) branch of the [JS-Stack-Boilerplate repository](https://github.com/verekia/js-stack-boilerplate). 4 | 5 | Alright! It's time to give our ugly app a facelift. We are going to use Twitter Bootstrap to give it some base styles. We'll then add a CSS-in-JS library to add some custom styles. 6 | 7 | ## Twitter Bootstrap 8 | 9 | > 💡 **[Twitter Bootstrap](http://getbootstrap.com/)** is a library of UI components. 10 | 11 | There are 2 options to integrate Bootstrap in a React app. Both have their pros and cons: 12 | 13 | - Using the official release, **which uses jQuery and Tether** for the behavior of its components. 14 | - Using a third-party library that re-implements all of Bootstrap's components in React, like [React-Bootstrap](https://react-bootstrap.github.io/) or [Reactstrap](https://reactstrap.github.io/). 15 | 16 | Third-party libraries provide very convenient React components that dramatically reduce the code bloat compared to the official HTML components, and integrate greatly with your React codebase. That being said, I must say that I am quite reluctant to use them, because they will always be *behind* the official releases (sometimes potentially far behind). They also won't work with Bootstrap themes that implement their own JS. That's a pretty tough drawback considering that one major strength of Bootstrap is its huge community of designers who make beautiful themes. 17 | 18 | For this reason, I'm going to make the tradeoff of integrating the official release, alongside with jQuery and Tether. One of the concerns of this approach is the file size of our bundle of course. For your information, the bundle weighs about 200KB (Gzipped) with jQuery, Tether, and Bootstrap's JS included. I think that's reasonable, but if that's too much for you, you should probably consider an other option for Bootstrap, or even not using Bootstrap at all. 19 | 20 | ### Bootstrap's CSS 21 | 22 | - Delete `public/css/style.css` 23 | 24 | - Run `yarn add bootstrap@4.0.0-alpha.6` 25 | 26 | - Copy `bootstrap.min.css` and `bootstrap.min.css.map` from `node_modules/bootstrap/dist/css` to your `public/css` folder. 27 | 28 | - Edit `src/server/render-app.jsx` like so: 29 | 30 | ```html 31 | 32 | ``` 33 | 34 | ### Bootstrap's JS with jQuery and Tether 35 | 36 | Now that we have Bootstrap's styles loaded on our page, we need the JavaScript behavior for the components. 37 | 38 | - Run `yarn add jquery tether` 39 | 40 | - Edit `src/client/index.jsx` like so: 41 | 42 | ```js 43 | import $ from 'jquery' 44 | import Tether from 'tether' 45 | 46 | // [right after all your imports] 47 | 48 | window.jQuery = $ 49 | window.Tether = Tether 50 | require('bootstrap') 51 | ``` 52 | 53 | That will load Bootstrap's JavaScript code. 54 | 55 | ### Bootstrap Components 56 | 57 | Alright, it's time for you to copy-paste a whole bunch of files. 58 | 59 | - Edit `src/shared/component/page/hello-async.jsx` like so: 60 | 61 | ```js 62 | // @flow 63 | 64 | import React from 'react' 65 | import Helmet from 'react-helmet' 66 | 67 | import MessageAsync from '../../container/message-async' 68 | import HelloAsyncButton from '../../container/hello-async-button' 69 | 70 | const title = 'Async Hello Page' 71 | 72 | const HelloAsyncPage = () => 73 |
74 | 81 |
82 |
83 |

{title}

84 | 85 | 86 |
87 |
88 |
89 | 90 | export default HelloAsyncPage 91 | ``` 92 | 93 | - Edit `src/shared/component/page/hello.jsx` like so: 94 | 95 | ```js 96 | // @flow 97 | 98 | import React from 'react' 99 | import Helmet from 'react-helmet' 100 | 101 | import Message from '../../container/message' 102 | import HelloButton from '../../container/hello-button' 103 | 104 | const title = 'Hello Page' 105 | 106 | const HelloPage = () => 107 |
108 | 115 |
116 |
117 |

{title}

118 | 119 | 120 |
121 |
122 |
123 | 124 | export default HelloPage 125 | ``` 126 | 127 | - Edit `src/shared/component/page/home.jsx` like so: 128 | 129 | ```js 130 | // @flow 131 | 132 | import React from 'react' 133 | import Helmet from 'react-helmet' 134 | 135 | import ModalExample from '../modal-example' 136 | import { APP_NAME } from '../../config' 137 | 138 | const HomePage = () => 139 |
140 | 146 |
147 |
148 |

{APP_NAME}

149 |
150 |
151 |
152 |
153 |
154 |

Bootstrap

155 |

156 | 157 |

158 |
159 |
160 |

JSS (soon)

161 |
162 |
163 |

Websockets

164 |

Open your browser console.

165 |
166 |
167 |
168 | 169 |
170 | 171 | export default HomePage 172 | ``` 173 | 174 | - Edit `src/shared/component/page/not-found.jsx` like so: 175 | 176 | ```js 177 | // @flow 178 | 179 | import React from 'react' 180 | import Helmet from 'react-helmet' 181 | import { Link } from 'react-router-dom' 182 | import { HOME_PAGE_ROUTE } from '../../routes' 183 | 184 | const title = 'Page Not Found!' 185 | 186 | const NotFoundPage = () => 187 |
188 | 189 |
190 |
191 |

{title}

192 |
Go to the homepage.
193 |
194 |
195 |
196 | 197 | export default NotFoundPage 198 | ``` 199 | 200 | - Edit `src/shared/component/button.jsx` like so: 201 | 202 | ```js 203 | // [...] 204 | 210 | // [...] 211 | ``` 212 | 213 | - Create a `src/shared/component/footer.jsx` file containing: 214 | 215 | ```js 216 | // @flow 217 | 218 | import React from 'react' 219 | import { APP_NAME } from '../config' 220 | 221 | const Footer = () => 222 |
223 |
224 |
225 |

© {APP_NAME} 2017

226 |
227 |
228 | 229 | export default Footer 230 | ``` 231 | 232 | - Create a `src/shared/component/modal-example.jsx` containing: 233 | 234 | ```js 235 | // @flow 236 | 237 | import React from 'react' 238 | 239 | const ModalExample = () => 240 |
241 |
242 |
243 |
244 |
Modal title
245 | 246 |
247 |
248 | This is a Bootstrap modal. It uses jQuery. 249 |
250 |
251 | 252 |
253 |
254 |
255 |
256 | 257 | export default ModalExample 258 | ``` 259 | 260 | - Edit `src/shared/app.jsx` like so: 261 | 262 | ```js 263 | const App = () => 264 |
265 | ``` 266 | 267 | This is an example of a *React inline style*. 268 | 269 | This will translate into: `
` in your DOM. We need this style to push the content under the navigation bar, but that's what's important here. [React inline styles](https://speakerdeck.com/vjeux/react-css-in-js) are a great way to isolate your component's styles from the global CSS namespace, but it comes at a price: You cannot use some native CSS features like `:hover`, Media Queries, animations, or `font-face`. That's [one of the reasons](https://github.com/cssinjs/jss/blob/master/docs/benefits.md#compared-to-inline-styles) we're going to integrate a CSS-in-JS library, JSS, later in this chapter. 270 | 271 | - Edit `src/shared/component/nav.jsx` like so: 272 | 273 | ```js 274 | // @flow 275 | 276 | import $ from 'jquery' 277 | import React from 'react' 278 | import { Link, NavLink } from 'react-router-dom' 279 | import { APP_NAME } from '../config' 280 | import { 281 | HOME_PAGE_ROUTE, 282 | HELLO_PAGE_ROUTE, 283 | HELLO_ASYNC_PAGE_ROUTE, 284 | NOT_FOUND_DEMO_PAGE_ROUTE, 285 | } from '../routes' 286 | 287 | const handleNavLinkClick = () => { 288 | $('body').scrollTop(0) 289 | $('.js-navbar-collapse').collapse('hide') 290 | } 291 | 292 | const Nav = () => 293 | 313 | 314 | export default Nav 315 | ``` 316 | 317 | There is something new here, `handleNavLinkClick`. One issue I encountered using Bootstrap's `navbar` in an SPA is that clicking on a link on mobile does not collapse the menu, and does not scroll back to the top of the page. This is a great opportunity to show you an example of how you would integrate some jQuery / Bootstrap-specific code in your app: 318 | 319 | ```js 320 | import $ from 'jquery' 321 | // [...] 322 | 323 | const handleNavLinkClick = () => { 324 | $('body').scrollTop(0) 325 | $('.js-navbar-collapse').collapse('hide') 326 | } 327 | 328 | 329 | ``` 330 | 331 | **Note**: I've removed accessibility-related attributes (like `aria` attributes) to make the code more readable *in the context of this tutorial*. **You should absolutely put them back**. Refer to Bootstrap's documentation and code samples to see how to use them. 332 | 333 | 🏁 Your app should now be entirely styled with Bootstrap. 334 | 335 | ## The current state of CSS 336 | 337 | In 2016, the typical modern JavaScript stack settled. The different libraries and tools this tutorial made you set up are pretty much the *cutting-edge industry standard* (*cough – even though it could become completely outdated in a year from now – cough*). Yes, that's a complex stack to set up, but at least, most front-end devs agree that React-Redux-Webpack is the way to go. Now regarding CSS, I have some pretty bad news. Nothing settled, there is no standard way to go, no standard stack. 338 | 339 | SASS, BEM, SMACSS, SUIT, Bass CSS, React Inline Styles, LESS, Styled Components, CSSX, JSS, Radium, Web Components, CSS Modules, OOCSS, Tachyons, Stylus, Atomic CSS, PostCSS, Aphrodite, React Native for Web, and many more that I forget are all different approaches or tools to get the job done. They all do it well, which is the problem, there is no clear winner, it's a big mess. 340 | 341 | The cool React kids tend to favor React inline styles, CSS-in-JS, or CSS Modules approaches though, since they integrate really well with React and solve programmatically many [issues](https://speakerdeck.com/vjeux/react-css-in-js) that regular CSS approaches struggle with. 342 | 343 | CSS Modules work well, but they don't leverage the power of JavaScript and its many features over CSS. They just provide encapsulation, which is fine, but React inline styles and CSS-in-JS take styling to an other level in my opinion. My personal suggestion would be to use React inline styles for common styles (that's also what you have to use for React Native), and use a CSS-in-JS library for things like `:hover` and media queries. 344 | 345 | There are [tons of CSS-in-JS libraries](https://github.com/MicheleBertoli/css-in-js). JSS is a full-featured, well-rounded, and [performant](https://github.com/cssinjs/jss/blob/master/docs/performance.md) one. 346 | 347 | ## JSS 348 | 349 | > 💡 **[JSS](http://cssinjs.org/)** is a CSS-in-JS library to write your styles in JavaScript and inject them into your app. 350 | 351 | Now that we have some base template with Bootstrap, let's write some custom CSS. I mentioned earlier that React inline styles could not handle `:hover` and media queries, so we'll show a simple example of this on the homepage using JSS. JSS can be used via `react-jss`, a library that is convenient to use with React components. 352 | 353 | - Run `yarn add react-jss` 354 | 355 | Add the following to your `.flowconfig` file, as there is currently a Flow [issue](https://github.com/cssinjs/jss/issues/411) with JSS: 356 | 357 | ```flowconfig 358 | [ignore] 359 | .*/node_modules/jss/.* 360 | ``` 361 | 362 | ### Server-side 363 | 364 | JSS can render styles on the server for the initial rendering. 365 | 366 | - Add the following constants to `src/shared/config.js`: 367 | 368 | ```js 369 | export const JSS_SSR_CLASS = 'jss-ssr' 370 | export const JSS_SSR_SELECTOR = `.${JSS_SSR_CLASS}` 371 | ``` 372 | 373 | - Edit `src/server/render-app.jsx` like so: 374 | 375 | ```js 376 | import { SheetsRegistry, SheetsRegistryProvider } from 'react-jss' 377 | // [...] 378 | import { APP_CONTAINER_CLASS, JSS_SSR_CLASS, STATIC_PATH, WDS_PORT } from '../shared/config' 379 | // [...] 380 | const renderApp = (location: string, plainPartialState: ?Object, routerContext: ?Object = {}) => { 381 | const store = initStore(plainPartialState) 382 | const sheets = new SheetsRegistry() 383 | const appHtml = ReactDOMServer.renderToString( 384 | 385 | 386 | 387 | 388 | 389 | 390 | ) 391 | // [...] 392 | 393 | 394 | // [...] 395 | ``` 396 | 397 | ## Client-side 398 | 399 | The first thing the client should do after rendering the app client-side, is to get rid of the server-generated JSS styles. 400 | 401 | - Add the following to `src/client/index.jsx` after the `ReactDOM.render` calls (before `setUpSocket(store)` for instance): 402 | 403 | ```js 404 | import { APP_CONTAINER_SELECTOR, JSS_SSR_SELECTOR } from '../shared/config' 405 | // [...] 406 | 407 | const jssServerSide = document.querySelector(JSS_SSR_SELECTOR) 408 | // flow-disable-next-line 409 | jssServerSide.parentNode.removeChild(jssServerSide) 410 | 411 | setUpSocket(store) 412 | ``` 413 | 414 | Edit `src/shared/component/page/home.jsx` like so: 415 | 416 | ```js 417 | import injectSheet from 'react-jss' 418 | // [...] 419 | const styles = { 420 | hoverMe: { 421 | '&:hover': { 422 | color: 'red', 423 | }, 424 | }, 425 | '@media (max-width: 800px)': { 426 | resizeMe: { 427 | color: 'red', 428 | }, 429 | }, 430 | specialButton: { 431 | composes: ['btn', 'btn-primary'], 432 | backgroundColor: 'limegreen', 433 | }, 434 | } 435 | 436 | const HomePage = ({ classes }: { classes: Object }) => 437 | // [...] 438 |
439 |

JSS

440 |

Hover me.

441 |

Resize the window.

442 | 443 |
444 | // [...] 445 | 446 | export default injectSheet(styles)(HomePage) 447 | ``` 448 | 449 | Unlike React inline styles, JSS uses classes. You pass styles to `injectSheet` and the CSS classes end up in the props of your component. 450 | 451 | 🏁 Run `yarn start` and `yarn dev:wds`. Open the homepage. Show the source of the page (not in the inspector) to see that the JSS styles are present in the DOM at the initial render in the `