Although the wacky world of JavaScript is still very active (read: volatile), the community appears to have unified around a set of standard-ish tools and practices that make working on projects much easier. It’s worthwhile to learn these tenets of modern JavaScript development and bring existing projects in-line with them:

  1. Installing
  2. Task running
  3. Building
  4. Developing and debugging locally
  5. Versioning and releasing

Note: Parts 3 and 4 of this article apply only to client-side development, but parts 1,2, and 5 apply to both client and server development.

Primer

npm. Say it with me, people: npm. npm is the beginning, middle, and end of any JavaScript project. Yes, even purely front-end projects. Bower is dead, JSPM doesn’t matter, and Yarn isn’t ubiquitous yet. npm is the premier JavaScript package manager — not only because it is a really good package manager, but because it provides a common project interface that has been adopted by a critical mass of JavaScript developers. You can think of this article as a modern npm user’s guide.

Installing

npm install, amirite? Well, let’s take a minute to review a few basics to make sure we’re all on the same page:
This installs a package to be used by your app at runtime and updates the package.json file to list it as a dependency.

npm install --save [package-name]

This installs a package to be used as a development tool by you, the developer, during development and updates the package.json file to list it as a dependency. Use this for things like webpack (which we’ll get to shortly!), testing and deployment tools, etc.

npm install --save-dev [package-name]

And then there’s this:

npm install -g [package-name]

DON’T DO THIS. No, really, in most cases it’s an AntiPattern — you should only be installing packages globally when you don’t care whether or not other developers have it installed on their machines at all (you usually do).

If your project requires a package to be installed in order to work on it, do not use npm install -g. That’s where local binaries come in to save the day! Here’s the tl;dr version of what that means: Any npm package that can be installed globally (with -g) can also be installed locally into a project. This is important because when other developers clone and npm install your project, they are guaranteed to have all of the necessary dependencies, including the CLI tools that your project might depend on. Local npm binaries are magical and will make your projects much easier to “just pull down” from a repo and get to work on.

“But my project requires all sorts of weird system changes and preflight tasks to be run before anyone can start working on it”

Well, that speaks to a larger problem with your project, but sometimes we all have to make concessions in the name of pragmatism. Never fear, postinstall is here! Rather than walk developers through a complicated manual setup process, you can likely automate most installation steps with a script. If that’s possible for your project, you can transparently automate the invocation of that script at installation-time:

package.json
{
  ...
  "scripts": {
    "postinstall": "sh setup.sh"
  }
}

npm automatically runs the postinstall script once npm install successfully completes. There are all sorts of other “special” scripts that npm supports, so take a gander at the documentation. We’ll be covering a few of the more common ones in the rest of this article.

Task running

It’s time to stop Grunting and Gulping and start npm running. Minimally, npm run can and should be the primary interface for running project tasks, regardless of how they’re implemented. That is, you can keep tasks that are already written as Gulp or Grunt tasks (or whatever else), and simply paper over them with npm scripts:

package.json
{
  ...
  "scripts": {
    "build": "grunt build",
    "test": "gulp test",
    "lint": "eslint"
  }
}

The goal here is to achieve one task interface to rule them all. This makes integration with build systems (like Jenkins) easier because there’s only one API to worry about. And, if you have your Node binaries installed locally as explained above, you don’t need to worry about having system binaries installed in whatever environment your project might happen to find itself in.

As an added bonus, npm helpfully puts local binaries in the execution PATH when running scripts, so it knows to default to the local binary automatically. In other words, the build script above will attempt to use the local grunt binary before it tries the system-installed grunt binary, which helps keep your scripts tidy and readable. Thanks, npm!

Building

Any project of consequence needs to be “built,” or “compiled,” before it can be made available to users. That means, minimally, that source code is transformed into more optimized, less human-readable code. This is done for the sake of application loading performance (among other things), but the automated nature of compiling code affords all sorts of interesting opportunities for developers to improve both their authoring experience and the stability of the applications that they create.

The JavaScript community has gone through various phases of preferred build tools and systems, but generally seems to be converging on webpack right now. And that’s great, because webpack works as well with actually-standard JavaScript syntaxes as it does with standard-ish syntaxes. This is something that caused a lot of problems in years past for JavaScript development. Webpack is probably the first truly reliable and flexible source-code-goes-in-and-compiled-code-comes-out solution for the vast majority of projects. Even better, it’s ubiquitous in the JavaScript community and has great community support and adoption.

npm install --save-dev webpack

Actually configuring webpack can be a fairly complicated task and varies wildly from project to project, so we’re not going to really dive into it in this article. Flexible, generic build tools like this are inherently complex, but the payoff is always worth it in the end. Here’s a minimal webpack configuration, taken straight from the project’s documentation:

webpack.config.js
{
  context: __dirname + "/app",
  entry: "./entry",
  output: {
    path: __dirname + "/dist",
    filename: "bundle.js"
  }
}

You’ll need to do your own research and experimentation to fit webpack into your project, but for the moment I suggest that you take it on faith that webpack is the build tool that you should be using in your project. Aside from ubiquity, one of webpack’s greatest strengths comes from its ability to work with pretty much any JavaScript module format (most tools are tied to one or two), JavaScript-based language (ES5, ES6, JSX, TypeScript, etc.), and even non-JavaScript assets (like images, CSS, and Sass). Basically, if something goes on the web, this tool can pack it. See what I did there?

Once configured, actually running a build on the command line is really easy:

webpack

But of course, we want to use the locally-installed webpack binary and have a common interface over it, so let’s just add a script for it:

package.json
{
  ...
  "scripts": {
    "build": "webpack"
  }
}

And now we can just run npm run build from the command line and trust that our code will be built!

Developing and debugging locally

Back in the day, all you needed was a local static web server to test your front end code. For a long time, developers using Macs thought they were really clever because they could just run python -m SimpleHTTPServer 8080 and carry on… but things aren’t that simple anymore. We are in a weird time in JavaScript history where you have to compile your code before you can run it in the browser, which is conceptually a little backwards, but at least our tools have finally matured to the point where this isn’t a total pain. Enter webpack-dev-server! Assuming that your project is successfully building with webpack, setting up the dev server is really easy:

npm install --save-dev webpack-dev-server

And then just paper over it with an npm script

{
  ...
  "scripts": {
    "start": "webpack-dev-server"
  }
}

And then add a bit of configuration to webpack.config.js:

webpack.config.js
{
  context: __dirname + "/app",
  entry: "./entry",
  output: {
    path: __dirname + "/dist",
    filename: "bundle.js"
  },
  devServer: {
    port: 9000
  }
}

Once that’s all hooked up, all that’s left is to run npm start, navigate to http://localhost:9000/webpack-dev-server/, and you’re off and running! You will notice a little status bar at the top of the page that gives you live information about what webpack is doing (such as recompiling), or if there was a problem with the build. On top of all this, webpack-dev-server automatically reloads, so every time you make a code change, the webpack will recompile and reload the browser for you. It’s an incredible convenience that makes debugging your code a breeze.

Leveraging the super flexible magic of the webpack build system discussed earlier, webpack-dev-server affords us the ability to work with any number of languages in any sort of configuration and debug it efficiently in the browser. Rather than building your entire project into a production-ready binary — which can take some time, depending on how much code you’re working with — webpack-dev-server runs faster, unoptimized builds and keeps you in your edit-test-repeat mental flow. And, thanks to webpack’s excellent support for source maps, you can even use your browser debugger as though you were working with raw source code.

Building on our minimal webpack.config.js file from above, we can just add a line to tell webpack to create the source map file for us by setting the devtool property:

webpack.config.js
{
  context: __dirname + "/app",
  entry: "./entry",
  output: {
    path: __dirname + "/dist",
    filename: "bundle.js"
  },
  devServer: {
    port: 9005
  },
  devtool: 'source-map'
}

Versioning and releasing

SemVer provides a clear and flexible approach to versioning software, and it’s become the de facto standard for many open source projects. Semver in a nutshell (as taken from their site):

Given a version number MAJOR.MINOR.PATCH, increment the:

  1. MAJOR version when you make incompatible API changes,
  2. MINOR version when you add functionality in a backwards-compatible manner, and
  3. PATCH version when you make backwards-compatible bug fixes.

Luckily, npm has first-class support for SemVer.  Because of this, making project releases is as simple as:

npm version [patch|minor|major]

By default, this will increment the relevant part of the version field in your package.json file, commit the change, and then make a Git tag named after the new version. But, like the handy postinstall script from before, we can add in our own custom functionality around npm version:

package.json
{
  ...
  "scripts": {
    "build": "webpack",
    "postversion": "git push && git push --tags && npm run build && npm run deploy",
    "deploy": "gh-pages -d dist"
  }
}

This will make it so that, upon versioning the project, the new Git commits and tags will be pushed to the remote, and then the build and deploy scripts will be run. In this case, the deploy script just calls the handy gh-pages package. Your deploy script will likely look like something else.

The beauty of this setup is that you just need to run npm version patch (or whatever SemVer level you need) to take care of the entire versioning and deployment process. If you’d like, you can even set up a preversion script to run your tests, and prevent a release if any tests are failing. This is a powerful way to ensure a high-quality codebase and painless iterations.

The present is a gift

It took the JavaScript community a while to get here, but we finally a have a sane and flexible set of tools to integrate into our projects. And, due to the fact that everything we just went over is abstracted by npm’s simple and un-opinionated package.json format, we are relatively future-friendly should a new tool come along to replace part of our infrastructure.

Much of what has been discussed in this article can be seen in action in https://github.com/jeremyckahn/modern-js-project. That project also has a few extra features (like a test watcher and ES6 support) to make your life easier. You can fork that project and tweak it as needed to build new projects out of.

Less is more, and community standards are a wonderful thing. This is a powerful model to build new projects on top of, and a great way to breathe new life into legacy codebases!

 

Photo by Dmitry Baranovskiy. Used under CC BY 2.0.




Tagged with: