We're now going to use some ES6 syntax, which is a great improvement over the "old" ES5 syntax. All browsers and JS environments understand ES5 well, but not ES6. That's where a tool called Babel comes to the rescue!
💡 Babel is a compiler that transforms ES6 code (and other things like React's JSX syntax) into ES5 code. It is very modular and can be used in tons of different environments. It is by far the preferred ES5 compiler of the React community.
- Move your
index.js
into a newsrc
folder. This is where you will write your ES6 code. Alib
folder is where the compiled ES5 code will go. Babel will take care of creating it. Remove the previouscolor
-related code inindex.js
, and replace it with a simple:
const str = 'ES6';
console.log(`Hello ${str}`);
We're using a template string here, which is an ES6 feature that lets us inject variables directly inside the string without concatenation using ${}
. Note that template strings are created using backquotes.
-
Run
yarn add --dev babel-cli
to install the CLI interface for Babel. -
To run our program, we now need to execute
node lib
instead ofnode .
(index.js
is the default file Node looks for, which is why we can omitindex.js
). Add abuild
script in yourpackage.json
, and tweak yourstart
script to runbuild
beforenode lib
:
"scripts": {
"start": "yarn run build && node lib",
"build": "babel src -d lib"
},
We simply tell Babel to compile an ES6 src
directory into an ES5 lib
directory with the -d
flag.
If you try to run yarn start
now, it should print the correct output, but you can see that the generated lib/index.js
did not change much compared to src/index.js
. That's because we didn't give Babel any information about which transformations we want to apply. The only reason it prints the right output is because Node natively understands ES6 without Babel's help. Some browsers or older versions of Node would not be so successful though!
-
Run
yarn add --dev babel-preset-latest
to install a Babel preset package containing configurations for the most recent ECMAScript features supported by Babel. -
In
package.json
, add ababel
field for the Babel configuration. Make it use thelatest
Babel preset like this:
"babel": {
"presets": [
"latest"
]
},
Note: A .babelrc
file at the root of your project could also be used instead of the babel
field of package.json
. Your root folder will get more and more bloated over time, so keep the Babel config in package.json
until it grows too large.
-
Try running
yarn start
again. Thelib/index.js
file should now have been correctly transformed into ES5 code (var
everywhere!). -
Add
/lib/
to your.gitignore
.
We now have the basic compilation working. To make this environment a bit more usable, we are going to add a few more scripts
tasks:
"scripts": {
"start": "yarn run watch",
"clean": "rimraf lib",
"prebuild": "yarn run clean",
"build": "babel src -d lib",
"lint": "eslint src/**/*.js",
"watch": "watch 'yarn run main' src --interval=1",
"main": "yarn run lint && yarn run build && node lib"
},
clean
is a task that simply deletes our entire auto-generated lib
folder before every build
. This is typically useful to get rid of old compiled files after renaming or deleting some in src
, or to make sure the lib
folder is in sync with the src
folder if your build fails and you don't notice. We use rimraf
instead of a plain rm -rf
in order to support Windows environments as well.
- Run
yarn add --dev rimraf
.
Tasks that are prefixed by pre
or post
will respectively be called after and before said task. clean
is related to build
and we want to run it before every build, so it's a good candidate for a pre
task. prebuild
simply calls yarn run clean
.
main
is going to be... well the main task of our workflow. It performs every operation needed for the build (many more will be added later), and runs the program.
watch
is going to trigger main
every time a file changes in src
. We use the [watch](https://www.npmjs.com/package/watch)
package to monitor file changes, which you need to install:
- Run
yarn add --dev watch
.
Note: The interval
option is the duration in seconds between file change checks. The default value is a bit too slow to my taste, so I use 1
second, which seems reasonable. You can use decimal numbers like 0.5
as well.
Alright, we're now good to go.
- Run
yarn start
. It should clean, build, print "Hello ES6" and start watching for changes. Try modifyingsrc/index.js
to make sure the whole task flow is triggered again.
💡 ES6: The most significant improvement of the JavaScript language. There are too many ES6 features to list them here but typical ES6 code uses classes with
class
,const
andlet
, template strings, and arrow functions ((param) => { console.log('Hi'); }
).
- Create a new file,
src/dog.js
, containing the following ES6 class:
class Dog {
constructor(name) {
this.name = name;
}
bark() {
return `Wah wah, I am ${this.name}`;
}
}
module.exports = Dog;
It should not look surprising to you if you've done OOP in the past in any language. It's relatively recent for JavaScript though. The class is exposed to the outside world via the module.exports
assignment.
In src/index.js
, write the following:
const Dog = require('./dog');
const toby = new Dog('Toby');
console.log(toby.bark());
As you can see, unlike the community-made package color
that we used before, when we require one of our files, we use ./
in the require()
.
- Run
yarn start
and it should print 'Wah wah, I am Toby'.
Here we simply replace const Dog = require('./dog')
by import Dog from './dog'
, which is the newer ES6 modules syntax (as opposed to "CommonJS" modules syntax).
In dog.js
, we also replace module.exports = Dog
by export default Dog
.
Note that in dog.js
, the name Dog
is only used in the export
. Therefore it could be possible to export directly an anonymous class like this instead:
export default class {
constructor(name) {
this.name = name;
}
bark() {
return `Wah wah, I am ${this.name}`;
}
}
You might now guess that the name 'Dog' used in the import
in index.js
is actually completely up to you. This would work just fine:
import Cat from './dog'; // Don't do this
const toby = new Cat('Toby');
Obviously, most of the time you will use the same name as the class / module you're importing.
yarn start
should still print "Wah wah, I am Toby".
We're going to lint our code to catch potential issues. ESLint is the linter of choice for ES6 code. Instead of configuring the rules we want for our code ourselves, we will use the config created by Airbnb. This config uses a few plugins, so we need to install those as well to use their config.
Check out Airbnb's most recent instructions to install the config package and all its dependencies correctly. As of 2016-11-11, they recommend using the following command in your terminal:
npm info eslint-config-airbnb@latest peerDependencies --json | command sed 's/[\{\},]//g ; s/: /@/g' | xargs yarn add --dev eslint-config-airbnb@latest
It should install everything you need and add eslint-config-airbnb
, eslint-plugin-import
, eslint-plugin-jsx-a11y
, and eslint-plugin-react
to your package.json
file automatically.
Note: I've replaced npm install
by yarn add
in this command. Also, this won't work on Windows, so take a look at the package.json
file of this repository and just install all the ESLint-related dependencies manually using yarn add --dev packagename@^#.#.#
with #.#.#
being the versions given in package.json
for each package.
In package.json
, add an eslintConfig
field like so:
"eslintConfig": {
"extends": "airbnb"
},
Note: An .eslintrc.js
, .eslintrc.json
, or . eslintrc.yaml
file at the root of your project could also be used instead of the eslintConfig
field of package.json
. Just like for the Babel configuration, we try to avoid bloating the root folder with too many files, but if you have a complex ESLint config, consider this alternative.
We'll create an NPM/Yarn script to runs ESLint. Let's install the eslint
package to be able to use the eslint
CLI:
- Run
yarn add --dev eslint
Add the following script to your package.json
:
"lint": "eslint src/**/*.js"
And update your main
task:
"main": "yarn run lint && yarn run clean && yarn run build && node lib"
Here we just tell ESLint that the files we want to lint are all the .js
files under src
, pretty explicit.
-
Run
yarn start
, and you should see a warning for usingconsole.log()
inindex.js
. Let's say that we want thisconsole.log()
to be valid inindex.js
instead of triggering a warning in this example. Add/* eslint-disable no-console */
at the top of ourindex.js
file to allow the use ofconsole
in this file. -
Run
yarn start
and we are now all clear!
Note: This section sets you up with ESLint in the console. It is great for catching errors at build time / before pushing, but you also probably want it integrated to your IDE. Do NOT use your IDE's native linting for ES6. Configure it so the binary it uses for linting is the one in your node_modules
folder. This way it can use all of your project's config, the Airbnb preset, etc. Otherwise you will just get a generic ES6 linting.
💡 Flow: A static type checker by Facebook. It detects inconsistent types in your code. For instance, it will give you an error if you try to use a string where should be using a number.
-
In order for Babel to understand and remove Flow annotations during the transpilation process, install the Flow preset for Babel by running
yarn add --dev babel-preset-flow
. Then, add"flow"
underbabel.presets
in yourpackage.json
. -
Create an empty
.flowconfig
file at the root of your project -
Run
yarn add --dev flow-bin
to install Flow, and create atypecheck
task:
"typecheck": "flow"
- Add
typecheck
in the task chain of yourmain
script:
"main": "yarn run typecheck && yarn run lint && yarn run build && node lib"
Alright, we should be able to run Flow now.
- Add Flow annotations to
src/shared/dog.js
like so:
// @flow
class Dog {
name: string;
constructor(name: string) {
this.name = name;
}
bark(): string {
return `Wah wah, I am ${this.name}`;
}
barkInConsole() {
/* eslint-disable no-console */
console.log(this.bark());
/* eslint-enable no-console */
}
}
export default Dog;
The // @flow
comment tells Flow that we want this file to be typechecked. For the rest, Flow annotations are typically a colon after a function parameter or a function name. Check the documentation for more details.
Now if you run yarn start
, Flow will work fine, but ESLint is going to complain about that non-standard syntax we're using. Since Babel's parser is all up-and-running with parsing Flow content thanks to the babel-preset-flow
plugin we installed, it'd be nice if ESLint could rely on Babel's parser instead of trying to understand Flow annotations on its own. That's actually possible using the babel-eslint
package. Let's do this.
-
Run
yarn add --dev babel-eslint
-
In
package.json
, undereslintConfig
, add the following property:"parser": "babel-eslint"
yarn start
should now both lint and typecheck your code fine.
Now that ESLint and Babel are able to share a common parser, we can actually get ESLint to lint our Flow annotations via the eslint-plugin-flowtype
plugin.
- Run
yarn add --dev eslint-plugin-flowtype
and add"flowtype"
undereslintConfig.plugins
inpackage.json
, and add"plugin:flowtype/recommended"
undereslintConfig.extends
in an array next to"airbnb"
.
Now if you type name:string
as an annotation, ESLint should complain that you forgot a space after the colon for instance.
Note: The "parser": "babel-eslint"
property that I made you write in package.json
is actually included in the "plugin:flowtype/recommended"
config, so you can now remove it for a more minimal package.json
. Leaving it there is more explicit though, so that's up to your personal preference. Since this tutorial is about the most minimal setup, I removed it.
- You can now add
// @flow
in every.js
and.jsx
file undersrc
, runyarn test
oryarn start
, and add type annotations everywhere Flow asks you to do so.
One counterintuitive case is the following, for src/client/component/message.jsx
:
const Message = ({ message }: { message: string }) => <div>{message}</div>;
As you can see, when destructuring function parameters, you must annotate the extracted properties using a sort of object literal notation.
Another case you will encounter is that in src/client/reducers/dog-reducer.js
, Flow will complain about Immutable not having a default export. This issue is discussed in #863 on Immutable, which highlights 2 workarounds:
import { Map as ImmutableMap } from 'immutable';
// or
import * as Immutable from 'immutable';
Until Immutable officially adresses the issue, just pick whichever looks better to you when importing Immutable components. I'm personally going for import * as Immutable from 'immutable'
since it's shorter and won't require refactoring the code when this issue gets fixed.
Note: If Flow detects type errors in your node_modules
folder, add an [ignore]
section in your .flowconfig
to ignore the packages causing issues specifically (do not ignore the entire node_modules
directory). It could look like this:
[ignore]
.*/node_modules/gulp-flowtype/.*
In my case, the linter-flow
plugin for Atom was detecting type errors in the node_modules/gulp-flowtype
directory, which contains files annotated with // @flow
.
You now have bullet-proof code that is linted, typechecked, and tested, good job!
Back to the previous section or the table of contents.
Next section: 4 - Express Server
Back to the previous section or the table of contents.