Building a JavaScript Development Environment
How to setup a JS development environment in 2018
This blog post is a summary of the excellent Pluralsight Course by Cory House.
Editor and Configuration
First of all, editor of choice here is surprise surprise VS Code. I’m actually happly surprised that Erich Gamma is behind this.
Use EditorConfig to manage, well, editor configurations. Tabs VS spaces, etc. Note that VS Code need to install a plugin for it to work.
The example .editorconfig
file:
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
[*.md]
trim_trailing_whitespace = true
Package Management
Package Manager
The choice of our package manager is npm.
The package.json
has all the packages that will be used in the course.
{
"name": "javascript-development-environment",
"version": "1.0.0",
"description": "JavaScript development environment Pluralsight course by Cory House",
"scripts": {},
"author": "Cory House",
"license": "MIT",
"dependencies": {
"npm": "^6.0.1",
"whatwg-fetch": "1.0.0"
},
"devDependencies": {
"babel-cli": "6.16.0",
"babel-core": "6.17.0",
"babel-loader": "6.2.5",
"babel-preset-latest": "6.16.0",
"babel-register": "6.16.3",
"chai": "3.5.0",
"chalk": "1.1.3",
"cheerio": "0.22.0",
"compression": "1.6.2",
"cross-env": "3.1.3",
"css-loader": "0.25.0",
"eslint": "3.8.1",
"eslint-plugin-import": "2.0.1",
"eslint-watch": "2.1.14",
"express": "4.14.0",
"extract-text-webpack-plugin": "1.0.1",
"html-webpack-plugin": "2.22.0",
"jsdom": "9.8.0",
"json-schema-faker": "0.3.6",
"json-server": "0.8.22",
"localtunnel": "1.8.1",
"mocha": "3.1.2",
"nock": "8.1.0",
"npm-run-all": "3.1.1",
"nsp": "2.6.2",
"numeral": "1.5.3",
"open": "0.0.5",
"rimraf": "2.5.4",
"style-loader": "0.13.1",
"webpack": "1.13.2",
"webpack-dev-middleware": "1.8.4",
"webpack-hot-middleware": "2.13.0",
"webpack-md5-hash": "0.0.5"
}
}
To install the packages, do npm install
.
Package Security
Since everyone can submit to npm, it is better to check for any vulnerabilities before using packages.
To install:
npm install -g nsp
To run:
nsp check
Development Web Server
We are gonna use Express as our development web server.
Create the /buildScript/srcServer.js
:
var express = require('express');
var path = require('path');
var open = require('open');
var port = 3000;
var app = express();
app.get('/', function(req, res) {
res.sendFile(path.join(__dirname, '../src/index.html'));
});
app.listen(port, function(err) {
if (err) {
console.log(err);
} else{
open('http://localhost:' + port);
}
});
Then create the /src/index.html
:
<html>
<body>
<h1>Hello World!!</h1>
</body>
</html>
Sharing Work In Progress
For expose your WIP to the world, let’s use localtunnel.
npm install -g localtunnel
Start your app.
lt --port 8000
Automation
We choose npm script here. Let’s try it out.
npm scripts
First, write the script to start the dev server, in package.json
:
...
"scripts": {
"start": "node buildScript/srcServer.js"
},
...
Now we can start the server by just npm start
Pre/Post Hooks
Create buildScript/startMessage.js
:
var chalk = require('chalk');
console.log(chalk.green('Starting app in dev mode...'));
Then add prestart
to the scripts:
...
"scripts": {
"prestart": "node buildScript/startMessage.js",
"start": "node buildScript/srcServer.js"
},
...
Now if we run npm start
, the prestart script will be ran before the start script.
Add Security Check and Share
We now create a script to run security check and share:
...
"scripts": {
"prestart": "node buildScript/startMessage.js",
"start": "node buildScript/srcServer.js",
"security-check": "nsp check;exit 0",
"share": "lt --port 3000"
},
...
Notice that in this way, nsp
doesn’t need to be installed globally.
One thing that is not mentioned in the video is that if nsp check
finds some vulnerabilities, it will fail npm. To fix this, we can just force it to always return 0.
Concurrent Tasks
Run mutiple tasks in parallel:
...
"start": "npm-run-all --parallel security-check open:src",
"open:src": "node buildScripts/srcServer.js",
...
Transpiling
We choose Babel to compile newer javascript code to older one.
Let’s first add the .babelrc
to the root of our project:
{
"presets": [
"latest"
]
}
This basically says use the latest JS.
Let’s try to use the module feature from ES6 in startMessage.js
:
import chalk from 'chalk';
Now in package.json
we use babel-node
instead of ´node´.
...
"prestart": "babel-node buildScripts/startMessage.js",
...
Bundling
We choose ES6 Modules to be the module format and webpack as the bundler.
Configure Webpack with Express
Now create a webpack.config.dev.js
in the root:
import path from 'path';
export default {
debug: true,
devtool: 'inline-source-map',
noInfo: false,
entry: [
path.resolve(__dirname, 'src/index')
],
target: 'web',
output: {
path: path.resolve(__dirname, 'src'),
publicPath: '/',
filename: 'bundle.js'
},
plugins: [],
module: {
loaders: [
{test: /\.js$/, exclude: /node_modules/, loaders: ['babel']},
{test: /\.css$/, loaders: ['style','css']}
]
}
}
Then we need to configure Express to use it.
In /buildScripts/srcServer.js
:
...
import webpack from 'webpack'
import config from '../webpack.config.dev'
const port = 3000
const app = express()
const compiler = webpack(config)
app.use(require('webpack-dev-middleware')(compiler, {
noInfo: true,
publicPath: config.output.publicPath
}))
...
Create an App Entry Point
First create /src/index.js
:
import numeral from 'numeral'
const courseValue = numeral(1000).format('$0,0.00')
console.log(`I would pay ${courseValue} for this course!`)
Now start the app, you should see that bundle.js
from the browser, it is not really a physical file but in memory.
Handling CSS with Webpack
First let’s create /src/index.css
:
body {
font-family: sans-serif;
}
table th {
padding: 5px
}
Now we can import this css into our index.html
through index.js
.
In index.js
add one line:
import './index.js'
Sourcemap
Sourcemap allows us to see the original code in the browser console. We have already configured it in our webpack config
devtool: 'inline-source-map'
.
Now if you try to set a debugger
break point in the index.js
, you can see the original code in the browser console.
The cool thing is that this sourcemap will only be downloaded when the console is open, neat.
Linting
We choose ESLint.
Create /.eslintrc.json
:
{
"root": true,
"extends": [
"eslint:recommended",
"plugin:import/errors",
"plugin:import/warnings"
],
"parserOptions": {
"ecmaVersion": 7,
"sourceType": "module"
},
"env": {
"browser": true,
"node": true,
"mocha": true
},
"rules": {
"no-console": 1
}
}
Next, create a lint script in the package.json
:
...
"scripts": {
"lint": "esw webpack.config.* src buildScripts --color; exit 0"
},
...
Watch files
To watch for file changes and run eslint, we create another script, and add it to the start
script.
...
"scripts": {
...
"start": "npm-run-all --parallel security-check open:src lint:watch",
"lint:watch": "npm run lint -- --watch",
...
},
...
Testing and Continuous Integration
First of all, let’s frame the scope here, we are talking about unit testing, not UI testing.
OK, now we have to make 6 decisions:
- Testing Framework: Mocha
- Assertion Library: Chai
- Helper Library: JSDOM, Cheerio
- Where to Run Tests: Here we choose JSDOM again.
- Where do Test Files Belong? Alongside the file being tested.
- When should tests run? Unit Tests Should Run When You Hit Save.
Now we have made the decisions, let’s it all setup.
First let’s add a script to configure the tests /buildScripts/testSetup.js
:
// This file is't transpiled, so must use CommonJS and ES5
// Register babel to transpile before our tests run.
require('babel-register')();
// Disable webpack features that Mocha doesn't understand
require.extensions['.css'] = function() {};
Now create a task to run tests:
"scripts": {
...
"test": "mocha --reporter progress buildScripts/testSetup.js \"src/**/*.test.js\"",
...
},
...
Now let’s write some simple tests to try it out.
Create /src/index.test.js
:
import {expect} from 'chai';
import jsdom from 'jsdom';
import fs from 'fs';
describe('Our first test', () => {
it('should pass', () => {
expect(true).to.equal(true);
});
});
describe('index.html', () => {
it('should say hello', (done) => {
const index = fs.readFileSync("./src/index.html", "utf-8");
jsdom.env(index, function(err, window) {
const h1 = window.document.getElementsByTagName('h1')[0];
expect(h1.innerHTML).to.equal("Hello World!");
done();
window.close();
});
})
})
Now if you run npm test
, you will see 2 tests pass.
OK, let’s next configure the tests to be ran on every save.
Add the following task in package.json
and add it in start
:
"scripts": {
...
"start": "npm-run-all --parallel security-check open:src lint:watch test:watch",
"test:watch": "npm run test -- --watch"
...
},
Setup Travis CI
Go to Travis and login with github account, go to profile and enable the js-dev-env project.
Now create /.travis.yml
:
language: node_js
node_js:
- "6"
That’s all you need to setup Travis, now just wait and see the result.
HTTP Calls
Let’s first create a dummy api endpoint localhost:3000/users
which just returns a list of user objects.
In /buildScripts/srcServer.js
, add the following code:
app.get('/users', function(req, res) {
res.json([
{"id": 1, "firstName": "Bob", "lastName": "Smith", "email": "bob@gmail.com"},
{"id": 2, "firstName": "Tammy", "lastName": "Norton", "email": "tammy@gmail.com"},
{"id": 3, "firstName": "Tinna", "lastName": "Lee", "email": "tina@yahoo.com"}
]);
});
Next, let’s create /src/api/userApi.js
:
import 'whatwg-fetch';
export function getUsers() {
return get('users');
}
function get(url) {
return fetch(url).then(onSuccess, onError);
}
function onSuccess(response) {
return response.json();
}
function onError(error) {
console.log(error); // eslint-disable-line no-console
}
Then, put a table in the /src/index.html
:
<h1>Users</h1>
<table>
<thead>
<th> </th>
<th>Id</th>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
</thead>
<tbody id="users"></tbody>
</table>
And in /src/index.js
add:
import {getUsers} from './api/userApi';
// Populate table of users via API call
getUsers().then(result => {
let usersBody = "";
result.forEach(user => {
usersBody += `<tr>
<td><a href="#" data-id="${user.id}" class="deleteUser">Delete</a></td>
<td>${user.id}</td>
<td>${user.firstName}</td>
<td>${user.lastName}</td>
<td>${user.email}</td>
</tr>`
});
window.document.getElementById('users').innerHTML = usersBody;
});
Now we can see a list of users displayed in the table, great.
Our Plan for Mocking HTTP
- Declare our schema:
- Generate Random Data:
- Serve Data via API
Now let’s glue them together to generate some fake data!
First create /buildScripts/mockDataSchema.js
:
export const schema = {
"type": "object",
"properties": {
"users": {
"type": "array",
"minItems": 3,
"maxItems": 5,
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"unique": true,
"minimum": 1
},
"firstName": {
"type": "string",
"faker": "name.firstName"
},
"lastName": {
"type": "string",
"faker": "name.lastName"
},
"email": {
"type": "string",
"faker": "internet.email"
}
},
"required": ["id", "firstName", "lastName", "email"]
}
}
},
"required": ["users"]
};
Next, write a script /buildScripts/generateMockData.js
to generate fake data:
/* This script generate mock data for local development. */
/* eslint-disable no-console */
import jsf from 'json-schema-faker';
import {schema} from './mockDataSchema';
import fs from 'fs';
import chalk from 'chalk';
import faker from "faker"
// This is a fix from github which is not in the video
jsf.extend("faker", function() {
return faker
})
const json = JSON.stringify(jsf(schema));
fs.writeFile("./src/api/db.json", json, function (err) {
if (err) {
return console.log(chalk.red(err));
} else {
console.log(chalk.green("Mock data generated."));
}
});
Finally, configure package.json
to include it and also use json server serve the data.
"scripts": {
...
"start": "npm-run-all --parallel security-check open:src lint:watch test:watch start-mockapi",
"generate-mock-data": "babel-node buildScripts/generateMockData",
"prestart-mockapi": "npm run generate-mock-data",
"start-mockapi": "json-server --watch src/api/db.json --port 3001"
...
},
We would like to use the fake api for development. How to do that?
Well first let’s create /src/api/baseUrl.js
:
export default function getBaseUrl() {
const inDevelopment = window.location.hostname === 'localhost';
return inDevelopment ? 'http://localhost:3001' : '/';
}
Now in /api/userApi.js
make the following changes:
import getBaseUrl from './baseUrl';
function get(url) {
return fetch(baseUrl + url).then(onSuccess, onError);
}
Now we have the get users api, let’s also add delete users api.
First add the delete user api in /src/api/userApi.js
:
export function deleteUser(id){
return del(`users/${id}`)
}
function del(url) {
const request = new Request(baseUrl + url, {
method: 'DELETE'
});
return fetch(request).then(onSuccess, onError);
}
Then in /src/index.js
add the following code:
// Populate table of users via API call
getUsers().then(result => {
...
const deleteLinks = window.document.getElementsByClassName('deleteUser');
// Must use array.from to create a real array from a DOM collection
// getElementsByClassName only returns an "array like" object
Array.from(deleteLinks, link => {
link.onclick = function(event) {
const element = event.target;
event.preventDefault();
deleteUser(element.attributes["data-id"].value);
const row = element.parentNode.parentNode;
row.parentNode.removeChild(row);
};
});
});
Now if you click the delete button, that user will be removed from the screen, and also from the fake json data db!
Project Structure
Some useful tips on structuring JS projects:
- JS Belongs in a
.js
File - Organize by Feature instead of File Type
- Extract logic into POJOs.
Production Build
Minification and Sourcemaps
First create a webpack.config.prod.js
for production:
import path from 'path';
import webpack from 'webpack';
export default {
debug: true,
devtool: 'source-map',
noInfo: false,
entry: [
path.resolve(__dirname, 'src/index')
],
target: 'web',
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: '/',
filename: 'bundle.js'
},
plugins: [
// Eliminate duplicate packages when generating bundle
new webpack.optimize.DedupePlugin(),
// Minify JS
new webpack.optimize.UglifyJsPlugin()
],
module: {
loaders: [
{test: /\.js$/, exclude: /node_modules/, loaders: ['babel']},
{test: /\.css$/, loaders: ['style','css']}
]
}
}
Note that we changed the output to dist
and also added some plugins for minifications.
Next, let’s create a /buildScripts/distServer.js
to serve the files in dist
:
import express from 'express';
import path from 'path';
import open from 'open';
import compression from 'compression';
/* eslint-disable no-console */
const port = 3000;
const app = express();
app.use(compression());
app.use(express.static('dist'));
app.get('/', function(req, res) {
res.sendFile(path.join(__dirname, '../dist/index.html'));
});
app.get('/users', function(req, res) {
res.json([
{"id": 1, "firstName": "Bob", "lastName": "Smith", "email": "bob@gmail.com"},
{"id": 2, "firstName": "Tammy", "lastName": "Norton", "email": "tammy@gmail.com"},
{"id": 3, "firstName": "Tinna", "lastName": "Lee", "email": "tina@yahoo.com"}
]);
});
app.listen(port, function(err) {
if (err) {
console.log(err);
} else{
open('http://localhost:' + port);
}
});
Toggle Mock Api
Then, let’s put in a better way to toggle between real data and mock data, modify /src/api/baseUrl.js
:
export default function getBaseUrl() {
return getQueryStringParameterByName('useMockApi') ? 'http://localhost:3001/' : '/';
}
function getQueryStringParameterByName(name, url) {
if (!url) url = window.location.href;
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
Now to use mock data, we just append ?useMockApi=true
to the url in the browser.
Now it’s time to write some npm script to build all of these.
Add these new tasks:
"clean-dist": "rimraf ./dist && mkdir dist",
"prebuild": "npm-run-all clean-dist test lint",
"build": "babel-node buildScripts/build.js",
"postbuild": "babel-node buildScripts/distServer.js"
Now if you run npm run build -s
, it will show that no index.html found, which is what we are going to tackle next.
Dynamic HTML Generation
Our solution to this is to use webpack to dynamically generate html files.
Add the following code to /webpack.config.prod.js
:
import HtmlWebpackPlugin from 'html-webpack-plugin';
plugins: [
// Create HTML file that includes reference to bundled JS.
new HtmlWebpackPlugin({
template: 'src/index.html',
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true
},
inject: true
}),
],
Now we can remove the javascript tag from index.html
since it will be injected by webpack. Also copy the same to /webpack.config.dev.js
.
Bundle Splitting
We are going to put all the vendor JS libraries in a separate bundle so that the client won’t have to donwload those libraries everytime when our application code changes.
First create a /src/vendor.js
:
/* This file contains references to the vendor libraries
we're using in this project. This is used by webpack
in the production build only*. A separate bundle for vendor
code is useful since it's unlikely to change as often
as the application's code. So all the libraries we reference
here will be written to vendor.js so they can be
cached until one of them change. So basically, this avoids
customers having to download a huge JS file anytime a line
of code changes. They only have to download vendor.js when
a vendor library changes which should be less frequent.
Any files that aren't referenced here will be bundled into
main.js for the production build.
*/
/* eslint-disable no-unused-vars */
import fetch from 'whatwg-fetch';
Then modify the webpack.config.prod.js
as follows:
entry: {
vendor: path.resolve(__dirname, 'src/vendor'),
main: path.resolve(__dirname, 'src/index')
},
output: {
...
filename: '[name].js'
},
plugins: [
// Use CommonsChunkPlugin to create a separate bundle
// of vendor libraries so that they're cached separatetly.
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor'
}),
...
],
Cache Busting
If we attach a hash number at the end of every files, then the client can cache the files forever until we make a new release.
Let’s do it.
In webpack.config.prod.js
:
import WebpackMd5Hash from 'webpack-md5-hash';
output: {
...
filename: '[name].[chunkhash].js'
},
plugins: [
// Hash the files using MD5 so that their names change when the content changes.
new WebpackMd5Hash(),
...
]
Extract and Minify CSS
The goal is to extract out the css file and put a hash number in its name.
So in webpack.config.prod.js
:
import ExtractTextPlugin from 'extract-text-webpack-plugin';
plugins: [
// Generate an external css file with a hash in the filename
new ExtractTextPlugin('[name].[contenthash].css'),
...
]
module: {
loaders: [
...
{test: /\.css$/, loader: ExtractTextPlugin.extract('css?sourceMap')}
]
Error Logging
The author uses TrackJS, since it is not free, I will omit this part all together.
Production Deploy
We will deploy the api backend to Heroku and the frontend to Surge.
Deploy API to Heroku
First, take a look at the guide on how to deploy nodejs app on Heroku.
OK, now let’s try to split up our project.
First separate out the api part. The author created a repo here already so we can just use that. Remember to run npm install
after clone it.
Then follow the steps:
- Login to Heroku.
heroku login
- Deploy the app.
heroku create
git push heroku master
heroku ps:scale web=1
heroku open
OK, now the api site is deployed!
Now we go back to our project, open /src/api/baseUrl.js
and update the following code:
export default function getBaseUrl() {
return getQueryStringParameterByName('useMockApi') ? 'http://localhost:3001/' : 'https://sheltered-wildwood-50048.herokuapp.com/';
}
Deploy UI to Surge
Add one line to scripts
in package.json
:
"deploy": "surge ./dist"
Then run npm run build
to build once.
Now we can deploy to surge npm run deploy
.
The complete source code can be found here
P.S. Now we are at the end of this course, I have to say this is totally INSANE! I have to now take a 6 hour course to setup my development environment?!?!?!?!? What kind of obsurdity is this! And what is worse? This is only the beginning. What if I want to use Angular? React? Remember this is only the “starter kit”. I really hope this blog post will become obsolete soon.
~THE END~
Share this post
Twitter
Google+
Facebook
Reddit
LinkedIn
StumbleUpon
Email