Andrew Welch · Insights · #frontend #gulp #package.json

Published , updated · 5 min read ·


For more tools, technologies, and techniques, check out the devMode.fm podcast!

A Better package.json for the Frontend

Fron­tend work­flow tools like gulp are almost manda­to­ry for web­dev these days; here’s what I con­sid­er to be a bet­ter package.json setup

Package

If you’re doing web­dev, odds are good you’re using a work­flow tool like Gulp to auto­mate your build process. Whether you’re using Gulp, Gruntweb­pack, or npm scripts, it doesn’t real­ly mat­ter. Use what­ev­er tool works for you.

If you’re not using a workflow automation tool yet, you should be. It’s worth investing the upfront time to get it working, for the time it’ll save you on every project going forward.

I’m cur­rent­ly using Gulp on most projects, so that’s what I’ll be pre­sent­ing here, but the method­olo­gies described work for any of the fron­tend work­flow tools. Check out the A Gulp Work­flow for Fron­tend Devel­op­ment Automa­tion arti­cle for more on Gulp, and how it can use the package.json described in this article.

The package.json file is the pack­ag­ing for­mat for node.js appli­ca­tions, and Gulp is a node.js appli­ca­tion. But we can also lever­age the package.json file to encom­pass our entire website.

In the past, peo­ple were using Bow­er as a fron­tend pack­age man­ag­er for the third par­ty Javscript/​CSS used on their web­sites. The advan­tage of using a pack­age man­ag­er is threefold:

  1. You have a cen­tral­ized man­i­fest of all of the third par­ty pack­ages your web­site uses
  2. You can eas­i­ly update to more recent ver­sions of the third par­ty pack­ages your web­site uses
  3. You are always obtain­ing the pack­ages from their canon­i­cal source

In the bad old days before Bow­er, peo­ple man­u­al­ly down­loaded their fron­tend pack­ages, which made updat­ing them a pain. So most of the time, it nev­er happened.

I’ve used Bow­er in the past, but since we’re already using npm as a pack­age man­ag­er for our node.js pack­ages used by Gulp, why not use it for the fron­tend pack­ages as well?

Using more than one package manager for our website seems silly. I’ve never run into a case where a package is in Bower, but is not in NPM

Tan­gent: While I talk about npm through­out the rest of this arti­cle, I’m actu­al­ly using yarn these days to man­age my npm pack­ages. It uses the exact same npmjs​.com pack­age reg­istry that npm uses, it just does it bet­ter and faster. And it uses a yarn.lock file to ensure that the pack­age installs are deter­min­is­tic. I urge you to check it!

Instead of hav­ing to do npm install and bower install to get our project boot­strapped, and then remem­ber­ing to do both npm update and bower update to update our pack­ages, we just do npm install or npm update. Or if we’re using yarn, we can just do yarn! Great!

If you’re not famil­iar with the package.json for­mat, check out this package.json Inter­ac­tive Guide. While there are many keys in the package.json that have spe­cial mean­ing, it’s instruc­tive to keep in mind that it’s just JSON. We can add what­ev­er we want to it. So we will! More on that later.

Link Package All the Things!

For our web­sites, we’ll use the devDependencies key in our package.json for node.js pack­ages that Gulp uses via our gulpfile.js for automat­ing our fron­tend work­flow. Here are the devDependencies for this very web­site you’re read­ing now:

"devDependencies": {
        "babel-plugin-transform-runtime": "^6.15.0",
        "babel-preset-es2015": "^6.16.0",
        "chalk": "^1.1.3",
        "critical": "^1.1.0",
        "fancy-log": "^1.2.0",
        "git-rev-sync": "^1.7.1",
        "gulp": "^3.9.0",
        "gulp-autoprefixer": "^3.1.0",
        "gulp-babel": "^6.1.2",
        "gulp-cached": "^1.1.1",
        "gulp-concat": "^2.6.0",
        "gulp-cssnano": "^2.1.2",
        "gulp-debug": "^2.1.2",
        "gulp-download": "^0.0.1",
        "gulp-favicons": "^2.2.6",
        "gulp-filter": "^5.0.1",
        "gulp-fontello": "^0.4.6",
        "gulp-header": "^1.8.7",
        "gulp-if": "^2.0.1",
        "gulp-imagemin": "^3.1.1",
        "gulp-livereload": "^3.8.1",
        "gulp-load-plugins": "^1.3.0",
        "gulp-newer": "^1.2.0",
        "gulp-plumber": "^1.1.0",
        "gulp-print": "^2.0.1",
        "gulp-rename": "^1.2.2",
        "gulp-replace": "0.5.4",
        "gulp-rev": "^7.1.0",
        "gulp-sass": "^2.1.0",
        "gulp-size": "^2.1.0",
        "gulp-sourcemaps": "^2.2.1",
        "gulp-streamify": "1.0.2",
        "gulp-uglify": "^1.5.4",
        "moment": "^2.14.1",
        "pa11y": "^4.11.0",
        "vinyl-source-stream": "^1.1.0",
        "vueify": "^8.7.0"
    },

It’s not impor­tant that you know what all of these things are; rather just that you see they are all node.js pack­ages that we use for automat­ing our fron­tend workflow.

Since we con­sid­er the web­site we’re build­ing the app”, we’ll use the dependencies key in our package.json for the fron­tend pack­ages (JavaScript/​CSS) that we use in-brows­er. Here are the dependencies for this very web­site you’re read­ing now:

"dependencies": {
        "fg-loadcss": "^1.2.0",
        "flexboxgrid": "^6.3.1",
        "fontfaceobserver": "^2.0.5",
        "lazysizes": "^2.0.6",
        "loadjs": "^3.3.1",
        "normalize.css": "^5.0.0",
        "picturefill": "^3.0.2",
        "prismjs": "^1.5.1",
        "tiny-cookie": "^1.0.1",
        "vue": "^2.2.0",
        "vue-resource": "^1.0.3",
        "vue2-autocomplete": "git://github.com/nystudio107/vue2-autocomplete.git#master"
    },

Boom, that’s it! We do an npm install or yarn and all of the third par­ty pack­ages we use to make our web­site — both in-brows­er and for our work­flow automa­tion — are installed! 

You can find the pack­ages on npmjs​.com, and install them as dependencies via npm install -S <package-name>, or install them as devDependencies via npm install -D <package-name>.

If you’re using yarn instead of npm (which you should be!), you can find the pack­ages on npmjs​.com, and install them as dependencies via yarn add <package-name>, or install them as devDependencies via yarn add --dev <package-name>.

Or if you pre­fer, you can just edit your package.json file your­self, and then do npm install or yarn.

If for some crazy rea­son you run into a sit­u­a­tion where an npm pack­age doesn’t exist for some­thing you want to use, or you’re installing some­thing from a pri­vate git repo, you can always just do this in your package.json:

"private-package": "git+ssh://git@github.com/yourhandle/private-package.git#master",

One thing that’s fair­ly crit­i­cal to under­stand­ing how to use npm is the pack­age ver­sion, it’s what appears after the pack­age name in our package.json file. The ver­sion is expressed as a semver which is just a way to express the ver­sion you want installed, and the thresh­old for new­er ver­sion that it can be updat­ed to.

For instance, we might want all bug fix ver­sions of of a par­tic­u­lar pack­age, but we don’t want any break­ing changes. Using semver, we can rest easy that when we type npm update or yarn, we’ll get only the non-break­ing bug fix or patch ver­sions, and our project won’t blow up.

I typ­i­cal­ly do '^1.0.0' for the pack­ages I install, which says I want all updates up to the next major ver­sion”, 2.0.0 in this case); a more com­mon pat­tern is to do '~1.0.0' (which will give you only patch updates, in this case up to 1.1.0). A great way to learn semver is to check out the npm semver cal­cu­la­tor and play around with it a bit. It’s real­ly not hard to learn, and it’s time well-spent.

Tan­gent: We should con­sid­er any­thing we’ve got­ten from a pack­age man­ag­er (whether it is Bow­er or npm or what­ev­er) as read-only. Use your gulpfile.js to copy or trans­form things from these third-par­ty pack­ages into your web­site. Don’t use them direct­ly, or — gasp — mod­i­fy them.

Link Cleaning up our gulpfile.js

With all of these npm pack­ages (aka node mod­ules), our gulp­files tend to end up with a ton of cruft at the top, where we can’t even see our code until we scroll down past a few pages of things like this:

var gulp        = require('gulp'),
    sass        = require('gulp-sass'),
    rename      = require('gulp-rename'),
    cssmin      = require('gulp-minify-css'),
    concat      = require('gulp-concat'),
    uglify      = require('gulp-uglify'),
    jshint      = require('gulp-jshint'),
    scsslint    = require('gulp-scss-lint'),
    cache       = require('gulp-cached'),
    prefix      = require('gulp-autoprefixer'),
    browserSync = require('browser-sync'),
    reload      = browserSync.reload,
    size        = require('gulp-size'),
    imagemin    = require('gulp-imagemin'),
    pngquant    = require('imagemin-pngquant'),
    plumber     = require('gulp-plumber'),
    deploy      = require('gulp-gh-pages'),
    notify      = require('gulp-notify'),
    changed     = require('gulp-changed'),
    favicons    = require('gulp-favicons'),
    fontello    = require('gulp-fontello'),
    print       = require('gulp-print'),
    replace     = require('gulp-replace'),
    csslint     = require('gulp-csslint'),
    newer       = require('gulp-newer'),
    log         = require('fancy-log'),
    chalk       = require('chalk'),
    jsonminify = require('gulp-jsonminify'),
    critical    = require('critical');

This real­ly is fair­ly point­less, we’re already declar­ing what we want in our devDependencies.

Instead, let’s clean it up a bit by using the gulp-load-plu­g­ins Gulp plu­g­in. What it does is it’ll load in your node.js mod­ules name­spaced under the $ vari­able (hel­lo, jQuery!). Since we know we’re using every­thing in devDependencies for our work­flow automa­tion via Gulp, we can change that raft of require()s above into just:

// load all plugins in 'devDependencies' into the variable $
const $ = require('gulp-load-plugins')({
        pattern: ['*'],
        scope: ['devDependencies']
    });

Then to use our node.js mod­ules in our gulpfile.js, we just access them via the $ vari­able (well, real­ly a const in this case, but what­ev­er) like this:

$.fancyLog("-> Compiling scss");

Ahhh, so neat and tidy! Now let’s keep on a roll clean­ing things up!

Link Separating your data from your code

A term all of the cool kids are using these days is DRY. They don’t mean that your code is bor­ing, they mean that you Don’t Repeat Your­self. Back in the day, when we’d walk 2 miles uphill in the snow to get to our bus stop, we just called this mod­u­lar­i­ty or reusability.

What­ev­er you call it, it’s a good thing. Essen­tial­ly, we want to take the bits of code we write, and re-use them as much as pos­si­ble from project to project. As a step towards this when build­ing web­sites, there’s what I do in my gulpfile.js:

// package vars
const pkg = require('./package.json');

Now we have access to the entire package.json data struc­ture in our gulpfile.js. So what we can do is we can sep­a­rate the code in our gulpfile.js from the data that it oper­ates on.

Want to start a new project? Great! All you will have to change is your package.json to reflect what’s dif­fer­ent about this project… since your gulpfile.js just oper­ates on the data that’s fed into it via the package.json, this all just works. And you end up with clean­er, more read­able code, too.

For instance, we change this:

// scss - build the scss to the build folder, including the required paths, and writing out a sourcemap
gulp.task('scss', () => {
    $.fancyLog("-> Compiling scss:");
    return gulp.src("./src/scss/style.scss")
        .pipe($.plumber({ errorHandler: onError }))
        .pipe($.sourcemaps.init({loadMaps: true}))
        .pipe($.sass({
                includePaths: "./src/scss/"
            })
            .on('error', $.sass.logError))
        .pipe($.cached('sass_compile'))
        .pipe($.autoprefixer())
        .pipe($.sourcemaps.write('./'))
        .pipe($.size({ gzip: true, showFiles: true }))
        .pipe(gulp.dest("./build/css/"));
});

Into this:

// scss - build the scss to the build folder, including the required paths, and writing out a sourcemap
gulp.task('scss', () => {
    $.fancyLog("-> Compiling scss: " + pkg.paths.build.css + pkg.vars.scssName);
    return gulp.src(pkg.paths.src.scss + pkg.vars.scssName)
        .pipe($.plumber({ errorHandler: onError }))
        .pipe($.sourcemaps.init({loadMaps: true}))
        .pipe($.sass({
                includePaths: pkg.paths.scss
            })
            .on('error', $.sass.logError))
        .pipe($.cached('sass_compile'))
        .pipe($.autoprefixer())
        .pipe($.sourcemaps.write('./'))
        .pipe($.size({ gzip: true, showFiles: true }))
        .pipe(gulp.dest(pkg.paths.build.css));
});

Not only are we not hard-cod­ing the file names and paths inline in our gulpfile.js, we’re not even includ­ing them in our gulpfile.js at all — they are pulled from our package.json which defines every­thing that’s unique about this par­tic­u­lar project.

Pret­ty cool, right? While this isn’t the be-all and end-all of how to do things, it’s worked out well for me. The code is more reusable, and it’s clean­er and eas­i­er to read.

That’s it folks, hope you found some of this useful!

Link The Full Monty

I’ll include the full package.json used for this web­site here, to help you get a bet­ter idea how things work under the hood:

{
    "name": "nystudio107",
    "version": "1.0.0",
    "description": "Website for nystudio107.com",
    "main": "gulpfile.js",
    "author": "Andrew Welch, nystudio107 <andrew@nystudio107.com>",
    "copyright": "nystudio107",
    "license": "UNLICENSED",
    "private": true,
    "paths": {
        "src": {
            "base": "./src/",
            "css": "./src/css/",
            "fontello": "./src/fontello/",
            "fonts": "./src/fonts/",
            "json": "./src/json/",
            "js": "./src/js/",
            "img": "./src/img/",
            "scss": "./src/scss/"
        },
        "dist": {
            "base": "./public/",
            "css": "./public/css/",
            "js": "./public/js/",
            "fonts": "./public/fonts/",
            "img": "./public/img/"
        },
        "build": {
            "base": "./build/",
            "css": "./build/css/",
            "fontello": "./build/fonts/fontello/",
            "fonts": "./build/fonts/",
            "js": "./build/js/",
            "html": "./build/html/",
            "img": "./build/img/"
        },
        "favicon": {
            "src": "./src/img/favicon_src.png",
            "dest": "./public/img/site/",
            "path": "/img/site/"
        },
        "scss": [],
        "templates": "./craft/templates/"
    },
    "urls": {
        "live": "https://nystudio107.com/",
        "local": "https://nystudio107.dev/",
        "critical": "https://nystudio107.com/"
    },
    "vars": {
        "siteCssName": "site.combined.min.css",
        "scssName": "style.scss",
        "cssName": "style.css"
    },
    "globs": {
        "distCss": [
            "./node_modules/normalize.css/normalize.css",
            "./node_modules/flexboxgrid/dist/flexboxgrid.min.css",
            "./node_modules/vue2-autocomplete/dist/style/vue2-autocomplete.css",
            "./src/css/prism-theme.css",
            "./build/fonts/fontello/css/fontello-codes.css",
            "./build/css/*.css",
            "./src/css/*.css"
        ],
        "img": [
            "./public/img/"
        ],
        "components": [
            "./src/components/**/*.vue"
        ],
        "fonts": [
            "./build/fonts/fontello/font/*.{eot,ttf,woff,woff2}",
            "./src/fonts/*.{eot,ttf,woff,woff2}"
        ],
        "critical": [
            {
                "url": "",
                "template": "index"
            },
            {
                "url": "blog/stop-using-htaccess-files-no-really",
                "template": "blog/_entry"
            },
            {
                "url": "blog/stop-using-htaccess-files-no-really",
                "template": "blog/_amp_entry"
            },
            {
                "url": "blog",
                "template": "blog/index"
            },
            {
                "url": "blog",
                "template": "blog/amp_index"
            },
            {
                "url": "offline",
                "template": "offline"
            },
            {
                "url": "wordpress",
                "template": "wordpress"
            },
            {
                "url": "404",
                "template": "404"
            }
        ],
        "download": [
            {
                "url": "https://www.google-analytics.com/analytics.js",
                "dest": "./public/js/"
            },
            {
                "url": "https://static.small.chat/messenger.css",
                "dest": "./public/css/"
            },
            {
                "url": "https://static.small.chat/messenger.js",
                "dest": "./public/js/"
            }
        ],
        "distJs": [
            "./build/js/*.js",
            "./node_modules/lazysizes/lazysizes.min.js",
            "./node_modules/lazysizes/plugins/bgset/ls.bgset.min.js",
            "./node_modules/picturefill/dist/picturefill.min.js",
            "./node_modules/vue/dist/vue.min.js",
            "./node_modules/vue2-autocomplete/dist/vue2-autocomplete.js",
            "./node_modules/vue-resource/dist/vue-resource.min.js"
        ],
        "prismJs": [
            "./node_modules/prismjs/prism.js",
            "./node_modules/prismjs/components/prism-markup.js",
            "./node_modules/prismjs/components/prism-apacheconf.js",
            "./node_modules/prismjs/components/prism-css.js",
            "./node_modules/prismjs/components/prism-json.js",
            "./node_modules/prismjs/components/prism-twig.js",
            "./node_modules/prismjs/components/prism-php.js",
            "./node_modules/prismjs/components/prism-bash.js",
            "./node_modules/prismjs/components/prism-javascript.js",
            "./node_modules/prismjs/plugins/line-numbers/prism-line-numbers.min.js"
        ],
        "babelJs": [
            "./src/js/*.js"
        ],
        "inlineJs": [
            "./node_modules/fg-loadcss/src/loadCSS.js",
            "./node_modules/fg-loadcss/src/cssrelpreload.js",
            "./node_modules/fontfaceobserver/fontfaceobserver.js",
            "./node_modules/loadjs/dist/loadjs.min.js",
            "./node_modules/tiny-cookie/tiny-cookie.min.js",
            "./src/js/register-service-worker.js",
            "./src/js/asyncload-blog-fonts.js",
            "./src/js/asyncload-site-fonts.js"
        ],
        "siteIcon": "./public/img/site/favicon.*"
    },
    "dependencies": {
        "fg-loadcss": "^1.2.0",
        "flexboxgrid": "^6.3.1",
        "fontfaceobserver": "^2.0.5",
        "lazysizes": "^2.0.6",
        "loadjs": "^3.3.1",
        "normalize.css": "^5.0.0",
        "picturefill": "^3.0.2",
        "prismjs": "^1.5.1",
        "tiny-cookie": "^1.0.1",
        "vue": "^2.2.0",
        "vue-resource": "^1.0.3",
        "vue2-autocomplete": "git://github.com/nystudio107/vue2-autocomplete.git#master"
    },
    "devDependencies": {
        "babel-plugin-transform-runtime": "^6.15.0",
        "babel-preset-es2015": "^6.16.0",
        "chalk": "^1.1.3",
        "critical": "^1.1.0",
        "fancy-log": "^1.2.0",
        "git-rev-sync": "^1.7.1",
        "gulp": "^3.9.0",
        "gulp-autoprefixer": "^3.1.0",
        "gulp-babel": "^6.1.2",
        "gulp-cached": "^1.1.1",
        "gulp-concat": "^2.6.0",
        "gulp-cssnano": "^2.1.2",
        "gulp-debug": "^2.1.2",
        "gulp-download": "^0.0.1",
        "gulp-favicons": "^2.2.6",
        "gulp-filter": "^5.0.1",
        "gulp-fontello": "^0.4.6",
        "gulp-header": "^1.8.7",
        "gulp-if": "^2.0.1",
        "gulp-imagemin": "^3.1.1",
        "gulp-livereload": "^3.8.1",
        "gulp-load-plugins": "^1.3.0",
        "gulp-newer": "^1.2.0",
        "gulp-plumber": "^1.1.0",
        "gulp-print": "^2.0.1",
        "gulp-rename": "^1.2.2",
        "gulp-replace": "0.5.4",
        "gulp-rev": "^7.1.0",
        "gulp-sass": "^2.1.0",
        "gulp-size": "^2.1.0",
        "gulp-sourcemaps": "^2.2.1",
        "gulp-streamify": "1.0.2",
        "gulp-uglify": "^1.5.4",
        "moment": "^2.14.1",
        "pa11y": "^4.11.0",
        "vinyl-source-stream": "^1.1.0",
        "vueify": "^8.7.0"
    },
    "scripts": {
        "start": "gulp",
        "build": "gulp build"
    },
    "repository": {
        "type": "git",
        "url": "git@codewhore.com:nystudio107.git"
    },
    "bugs": {
        "email": "andrew@nystudio107.com"
    }
}