Minimalistic boilerplate to power your next React app.
See what's provided, what to do next, and why we bothered.
Just clone or fork this repo, rm -rf .git
inside the repo, and git init
to start your new
project using this boilerplate as a base.
More literally:
# If you'd like, fork js-reactor and clone your fork locally
$ git clone [email protected]:<forked-by-user>/js-reactor.git your-project
# Or, just straight up clone this repo
$ git clone [email protected]:bigchaindb/js-reactor.git your-project
# And reinitialize the repo
$ cd your-project
$ rm -rf .git && git init
$ git add . && git commit -m "Initial commit"
This will clone this repository into your-project/
and initialize a new git repository for you to
start working in.
Open src/app.js
to start working on the app. A demo server with hot reloading is provided out of
the box, so you can immediately see what you're building.
$ npm install
$ npm start # By default runs on localhost:3000
$ vim src/app.js
Some Good Things~~TM~~ you may be interested in doing:
- Replace some of the values in package.json, specifically:
name
- Your project's nameversion
- Your project's versiondescription
- A description of your projecthomepage
- Your project's homepagebugs
- Where people should go for helpauthors
- Authors of your projectrepository
- Your project's online repository
- Replace this README.md with your own project's
- If desired, select your own license (replace LICENSE.md and package.json's
license
) - Use a more appropriate element to render your app into (see app.js and the demo index.html)
- Add tests (and add npm scripts for testing)
- Look at extending the initial Webpack features
Requires Node 5+. Install nvm if you haven't already.
- Babel 6, with ES2017 support
- Webpack 2
- See webpack.config.js config
- JS loader with Babel
- CSS loader
- Source maps
- Production build settings
- Dev server
- Note: This project is currently pegged to Webpack 2.1.0-beta.17. Significant
changes have since been made to the betas (e.g.
loaders
renamed torules
, etc) and the provided configuration and notes will likely not immediately work with the most recent betas. As Webpack2 begins to stabilize, we'll upgrade accordingly (pull requests are always welcome though 😉!).
- See webpack.config.js config
- Autoprefixer
- ESLint + ESLint-plugin-react
- React hot reloading
A small barebones app is provided in src/app.js
. The initial Webpack config expects this to be the
root of the app and uses it as its entry point.
The demo app (demo/app.js
) just imports the app at src/app.js
, which, when run, will be
rendered into demo/index.html
through the #js-reactor-app
element.
Pretty basic stuff:
npm start
: Start Webpack dev server with hot reloadingnpm run build
: Build project intobuild/
npm run build:watch
: Build project intobuild/
and watch for changesnpm run build:dist
: Build project with production settings intodist/
npm run lint
: Lint entire project
Some standard fare:
- Use
NODE_ENV=PRODUCTION
environment variable for building - Bundle, uglify, and minimize JS + CSS
- Extract CSS out from JS
- Extract source maps from bundles
In the interest of Keeping Things Simple~~TM~~, the initial webpack
config was kept rather barebones. It can only understand Javascript files
(.js
or .jsx
) and CSS files (.css
).
Here are some other nice things you can include, if desired:
- Build systems
- Using environment variables
- Browser support
- Sass / SCSS
- LESS
- PostCSS
- CSS Modules
- HTML generation
- Assets
- Bootstrap
- Developing with
npm link
- Developing libraries
We've kept it simple with npm scripts, but if you like Gulp / Grunt / etc, feel free to integrate Webpack with them.
Webpack provides the define plugin to inject free variables into your builds.
Initially, NODE_ENV
is already defined and passed into your builds, so you can use it by
referencing process.env.NODE_ENV
. This is a great way to handle logic surrounding debug and
production modes, as Webpack can do dead code elimination during production builds
(see the docs). To make things
more streamline, you may also consider using warning and
babel-plugin-dev-expression.
To inject more variables like this, add definitions to the DEFINITIONS object.
See the browserlist for the default list of supported browsers (used by Autoprefixer).
If you'd like to include support for IE8 and lower, you should also disable UglifyJSPlugin's
screw_ie8
flag in the webpack configuration.
Install some of the Sass utilities:
npm install -D node-sass sass-loader
And add sass-loader to your Webpack loaders, with the
test /\.s[ac]ss$/
, using these settings:
{
loader: 'sass',
query: {
precision: '8', // If you use bootstrap, must be >= 8. See https://github.com/twbs/bootstrap-sass#sass-number-precision
outputStyle: 'expanded',
sourceMap: true
},
},
Assuming you're not going to use plain CSS anymore, you can edit your webpack.config.js
's
CSS_LOADERS
to look something like:
const CSS_LOADER = combineLoaders([
{
loader: 'css',
// ...
},
{ loader: 'postcss' },
{
loader: 'sass',
query: {
precision: '8', // See https://github.com/twbs/bootstrap-sass#sass-number-precision
outputStyle: 'expanded',
sourceMap: true
}
},
// ...
]);
And now replace the css
loader specification's test (test: /\.css$/
), with test: /\.s[ac]ss$/
.
See less-loader, with a set up that is likely very similar to sass-loader.
Install the PostCSS plugins via npm, and add them to the postcss
array in the Webpack
config. When in doubt, refer back to the plugin's documentation.
Modify the css-loader with something like:
{
loader: 'css',
query: {
modules: true,
importLoaders: 2, // NOTE: This must be the number of loaders after this (ie. 2 if you have postcss-loader and sass-loader chained after)
localIdentName: '[path]__[name]__[local]_[hash:base64:5]',
sourceMap: true
},
},
Assuming you're not going to use CSS without CSS Modules anymore, you can edit your
webpack.config.js
's CSS_LOADERS
to look something like:
const CSS_LOADER = combineLoaders([
{
loader: 'css',
query: {
modules: true,
importLoaders: 2, // 2 for the chained postcss-loader and sass-loader
localIdentName: '[path]__[name]__[local]_[hash:base64:5]',
sourceMap: true
},
},
{ loader: 'postcss' },
{
loader: 'sass',
// ...
},
// ...
]);
Note the comments above on importLoaders
: this value should always match the number of loaders
chained after the css-loader
in order for imported files to be processed by the later loaders.
Using plugins, you can have Webpack generate the HTML file that will render your app -- allowing you to avoid hard coding the inclusion of any built files. We've had success with html-webpack-plugin.
If you do this, you may also be interested in setting a more appropriate [output.publicPath
] in your
Webpack configuration, as well as modifying the default publicPath
that's set by the demo server.
There are a number of ways to load SVGs, but we've had success with loading them as React components that we can manipulate.
Install svg-react-loader and image-webpack-loader:
npm install -D react-svg-loader image-webpack-loader
Add a SVG loader configuration to your Webpack configuration:
const SVG_LOADER = combineLoaders([
{ loader: 'babel' },
{ loader: 'svg-react' },
// Can't supply the query using the query object as json formats aren't supported
{ loader: 'image-webpack?{ svgo: { plugins: [{ removeTitle: true }, { cleanupIDs: false }] } }' },
]);
And add the SVG loader to your Webpack configuration:
module: {
loaders: [
{
test: /\.svg$/,
exclude: [PATHS.NODE_MODULES],
loader: SVG_LOADER
},
// ...
],
// ...
}
Install image-webpack-loader and url-loader:
npm install -D file-loader url-loader image-webpack-loader
Add an image loader configuration to your Webpack configuration:
// Define a PNG loader that will be inlined as a Data Url if it's under 100kb
const PNG_LOADER = combineLoaders([
{
loader: 'url',
query: {
limit: 100000,
mimetype: 'image/png',
},
},
// Can't supply the query using the query object as json formats aren't supported
// NOTE: These are super awesome optimization levels, you should dial them down if the build is slow
{ loader: 'image-webpack?{ optimizationLevel: 7, pngquant: { quality: "65-90", speed: 1 } }' },
]);
And add the image loader to your Webpack configuration:
module: {
loaders: [
{
test: /\.png$/,
exclude: [PATHS.NODE_MODULES],
loader: PNG_LOADER
},
// ...
],
// ...
}
See the image-webpack-loader docs to see how to define loaders for other formats as well as control their qualities.
Our preferred way of loading Bootstrap is to use bootstrap-loader so that bootstrap can be safely loaded into the global scope without more configuration when using CSS Modules.
Install it, with bootstrap-sass:
npm install -D bootstrap-loader
npm install -S bootstrap-sass
Create a .bootstraprc
:
---
bootstrapVersion: 3
# Make sure to load bootstrap through Sass and Autoprefixer
styleLoaders:
- style
- css?sourceMap
- postcss
- sass?sourceMap&output=expanded&precision=8
styles:
# Always needs to be included
mixins: true
# Any other stylesheets you'd like
And add it to your Webpack config and demo server's entry:
// webpack.config.js
const config = {
entry: [
PRODUCTION || EXTRACT ? 'bootstrap-loader/extractStyles' : 'bootstrap-loader',
PATHS.APP,
],
// ...
}
// server.demo.js
config.entry = ['bootstrap-loader', path.resolve(__dirname, 'demo/app')];
See the usage and configuration docs for more information.
Due to the way npm link
works, it can cause your bundles to have duplications (for example, if
both your project and the link
ed library use React, it'll be included twice). Add aliases using
config.resolve.alias to have all
imports matching the alias to resolve to the path you specify.
This is especially important if you plan to keep the link
s for publishing, but useful while
developing to make builds faster.
Although this boilerplate wasn't set up with library creation in mind, much of it can still be recycled if you're developing something reusable across multiple projects. While there are no hard rules, there are typically a few nice-to-haves in terms of generated outputs:
- A transpiled version - for older build systems
- A transpiled version with ES6 import syntax - for newer build systems that understand the import syntax
- A minified bundle, in UMD format - for drop-in script tags and initial ease of use
Note: Users will always have the option to directly include your un-built version if they use a module bundler and configure it to build your package properly. Whenever possible, this should be the recommended approach as it gives users full control over what they import.
The rest of this section will assume you want to generate all three of these outputs.
Starting with the default .babelrc, remove the transform-runtime
plugin from the
plugins
array. This is done to keep the unbundled builds decoupled from babel, as
transform-runtime
adds babel-runtime
imports in the transpiled code and thereby forces your
users to also depend on babel-runtime
.
Then, install babel-plugin-transform-es2015-modules-commonjs
(to add back the CommonJS
transformation we've turned from the default babel-preset-latest
) and add a new environment in
your .babelrc
like so:
{
...
'env': {
...
'cjs': {
'plugins': ['transform-es2015-modules-commonjs']
},
...
},
...
}
Finally, to actually build the code:
cross-env BABEL_ENV=cjs babel <code-dir> -d cjs
This will transpile the javascript files in <code-dir>
into cjs/
using the cjs
babel
environment you just set up. If you plan to be doing this regularly, it's probably worth putting
into your package.json
's scripts
section.
If you've kept the default [['latest', { es2015: { modules: false } }]]
preset, generating the ES6
version doesn't require any further configuration and can be done using just:
babel <code-dir> -d es6
This version relies on using webpack to bundle everything together into a single file, similar to
how the boilerplate is set up initially. You should be careful about including everything, making
sure that the necessary polyfills (through babel-runtime
, not babel-polyfill
!) are bundled as
you can't assume anything about the user's environment if they just drop the bundled script into a
script tag.
Again, set up a new babel environment that will re-add the transform-runtime
plugin (since we
can now bundle the babel-runtime
dependencies in):
{
...
'env': {
...
'bundle': {
'plugins': [
['transform-runtime', {
'polyfill': true
} ]
]
},
...
},
...
}
Note: Make sure babel-runtime
is installed as a devDependency!
Now you'll need to change a few things in your webpack configuration. Assuming your entry point and
output paths are OK, you should add a few things to config.output
to let webpack know you want a
library:
const config = {
...
output: {
...,
library: 'your-library-name',
libraryTarget: 'umd',
...
},
...
}
And assuming you don't want to bundle React and other shared
peer-dependencies, move react
, react-dom
,
and etc to be dev and peer dependencies in your package.json
and add an externals
pattern
to config
to tell webpack to avoid adding these packages in the final bundle:
const config = {
...
externals: PRODUCTION ? [/^react(-dom|-addons.*)?$/] : null,
...
}
Finally, the build script is just:
cross-env NODE_ENV=production BABEL_ENV=bundle webpack -p
Note the use of NODE_ENV=production
here since the bundled package will usually be used without a
module bundler and therefore needs to have any development checks dependent on NODE_ENV
be
stripped away before usage.
If you have scss, css, or other assets, it's best to process them into directly usable forms (ie. already preprocessed css, optimized assets) and then output them into either a single directory or with each output separately. If your javascript directly requires some of these assets, for example a component requiring its own stylesheet, you'll probably want to structure the built assets in a similar fashion as the pre-built assets.
Oh boy.
Well, for the most part, your CommonJS export should be enough unless you are using any browser-specific APIs (ie. the DOM, browser globals, etc). Node users should never have to import from your bundled version and should just use your package's default CommonJS export.
However, if you are building something that uses browser-specific APIs but would like to strip those
away depending on the target, then you'll probably want to specify what you'd like to include by
using different root entry points for each target. Remember to set your package.json
's main
field to point to your built node output, and the browser
field to point to the web output to let
the user's module bundler pick the right version accordingly.
Note: If you'd like to support Node's import convention of allowing a single export to be
immediately usable by an importer, ie. var foo = require('foo')()
where foo
exports a single
function, you'll need to add babel-plugin-add-module-exports
to your .babelrc
's CommonJS build
(before the transform-es2015-modules-commonjs
). If you've been following this document, your
.bablerc
's cjs
entry should then look like:
{
...
'env': {
...
'cjs': {
'plugins': [
'add-module-exports',
'transform-es2015-modules-commonjs'
]
},
...
},
...
}
As a final touch, you may also want to change your webpack configuration to have target: 'node'
(if you have multiple targets, you can pass back an array of configurations each with their own
target, ie. web and node, and output location). This seems to cause build errors for some packages
though, so your leverage with this may vary; usually just telling Node users to use your CommonJS
export is enough.
If you've set up your package.json
with your own settings, you're mostly good
to go. There's just a few settings left to include in there:
- main - Your main, CommonJS, export. If you've
been following this section, this will be
lib/index.js
. - jsnext:main - Your ES6 export. If you've been
following this section, this will be
es6/index.js
. Note that this isn't official in terms of NPM yet, but most modern module bundlers already accept this field or can be configured to. - files - Files to be included for NPM. Make sure
to include the folders that hold your built versions; if you've been following this section,
you'll want to include
bundle
,es6
, andlib
. - directories - Package directories; the
most important one here is the
lib
entry, which, if you've been following this section, will belib/
. - keywords - Keywords to find your package on NPM. You're probably the expert here ;).
At the moment, we're pegged to a backwards-compatible Webpack2 beta (2.1.0-beta.17) because Webpack 2 comes with a lot of great features, especially tree-shaking. Although it's in beta, we've been using it for a while and have had no stability issues. Although there are some package warnings, most 1.x plugins and loaders will work seamlessly with no changes required (read as: we have yet to encounter any plugin requiring more than just a version bump on their Webpack peer-dependency to support Webpack 2, aside from the 1.x extract-text plugin on Webpack 2.1.0-beta.16+). Just make sure to use the most recent plugin version. Note that since beta.17, Webpack2 has rethought a number of pieces in its configuration and our findings may not be true for the most recent betas; we'll upgrade the provided configuration and additional notes as things stabilize.
We include our own ESLint config, based off our Javascript Styleguide, which is itself based off of the Airbnb Javascript Styleguide. If you find you don't like the settings, you can change them through the .eslintrc.json config.
There are a TON of plugins and loaders, so go exploring!
Yep.
But before you go back to your boilerplate-finder and pick out the most complicated boilerplate of all time (just joking; you should check it out as a resource for things to include on top / later), think about all the effort you'll spend either understanding everything in that boilerplate or, perhaps more anti-climatically, ripping out the features you don't need.
We built this because we like starting small and simple; we like understanding our tools and why we use them. And also because we make tons of demos.
Special thanks to the BigchainDB/ascribe.io team for their insights and code contributions:
@diminator, @r-marques, @vrde, @ttmc, @rhsimplex, @sbellem, @TimDaub