A Simple Node Checklist

Gleb Bahmutov, PhD

March 7, 2018 @ 14:00
Mont-Royal

@bahmutov   @confooca   #confoo

C / C++ / C# / Java / CoffeeScript / JavaScript / Node / Angular / Vue / Cycle.js / functional

JavaScript ninja, image processing expert, software quality fanatic, Microsoft MVP

@bahmutov   @confooca   #confoo

Cypress.io open source E2E test runner

JavaScript ninja, image processing expert, software quality fanatic, Microsoft MVP

@bahmutov   @confooca   #confoo

70 blog posts about Node

160 blog posts about JavaScript

  • Roman Empire

  • JavaScript

  • NPM

  • package.json and .npmrc

  • Checklists

  • How to automate

@bahmutov   @confooca   #confoo

tip: reload page to get new funny definition of "NPM"

I ❤️ publishing NPM modules

2000 years ago ....

@bahmutov   @confooca   #confoo

"Divide and Rule (Conquer)" 

@bahmutov   @confooca   #confoo

@bahmutov   @confooca   #confoo

@bahmutov   @confooca   #confoo

Factoring out a piece of code

  • Creates clear boundary (API)

  • Forces documenting it

  • Allows iteration behind versions

@bahmutov   @confooca   #confoo

README.md

Name and description

CI and other badges

Requirements and install

Example

Links and license

@bahmutov   @confooca   #confoo

About 8 years ago

Happily coding in C++ / C#

I need a library to do X...

@bahmutov   @confooca   #confoo

Finding a cross-platform C++ library is ... hard

Qt, GTK, .... autoconf, Makefiles

@bahmutov   @confooca   #confoo

Incredibly fragile

@bahmutov   @confooca   #confoo

About 5 years ago

Back to Java and ... JS?

Java Server

Swing UI

Client Browser

JS UI

@bahmutov   @confooca   #confoo

undefined is not a function

@bahmutov   @confooca   #confoo

JavaScript has a bad reputation

ES5 got wide adoption

= browsers are mostly the same!

@bahmutov   @confooca   #confoo

+ Node.js for the server

About 5 years ago

More info on how an idea becomes an ES feature

@bahmutov   @confooca   #confoo

ES standard is improving every year

@bahmutov   @confooca   #confoo

@bahmutov   @confooca   #confoo

JavaScript around us

  • Browser

  • Server

  • Desktop

  • Mobile

  • Gray areas:

    • IoT, robots, code snippets, build scripts, sw

@bahmutov   @confooca   #confoo

NPM Registry

aka OSS heaven

@bahmutov   @confooca   #confoo

@bahmutov   @confooca   #confoo

@bahmutov   @confooca   #confoo

source: https://npms.io

Let's write and share some JS code!

@bahmutov   @confooca   #confoo

mkdir foo-bar && cd foo-bar
npm init
name: (foo-bar) 
version: (1.0.0) 
description: Example module
...

@bahmutov   @confooca   #confoo

$ cat package.json 
{
  "name": "foo-bar",
  "version": "1.0.0",
  "description": "Example module",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "example",
    "node",
    "npm"
  ],
  "author": "Gleb Bahmutov <gleb.bahmutov@gmail.com>",
  "license": "MIT"
}
$ npm init -y
$ cat package.json
{
  "name": "foo-bar",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

@bahmutov   @confooca   #confoo

Something is missing ...

$ npm home debug

Something is missing ...

$ npm issues debug

package.json

{
  "name": "foo-bar",
  "version": "1.0.0",
  "description": "Example module",
  "author": "Gleb Bahmutov <gleb.bahmutov@gmail.com>",
  "bugs": "https://github.com/bahmutov/foo-bar/issues",
  "homepage": "https://github.com/bahmutov/foo-bar#readme",
  "repository": {
    "type": "git",
    "url": "https://github.com/bahmutov/foo-bar.git"
  },
  "license": "MIT"
}

and many, many more!

@bahmutov   @confooca   #confoo

{
  "name": "stop-build",
  "version": "0.0.0-development",
  "description": "Exits with non-zero code if there are modified Git files",
  "author": "Gleb Bahmutov <gleb.bahmutov@gmail.com>",
  "bugs": "https://github.com/bahmutov/stop-build/issues",
  "config": {
    "next-update": {
      "commands": {
        "pre-git": "./.git/hooks/pre-commit"
      }
    },
    "pre-git": {
      "commit-msg": "simple",
      "pre-commit": [
        "npm run pretest",
        "npm run deps",
        "npm run ban",
        "git add ."
      ],
      "pre-push": [
        "npm test",
        "npm run secure",
        "npm run license",
        "npm run ban -- --all",
        "npm run size"
      ],
      "post-commit": [],
      "post-merge": []
    }
  },
  "engines": {
    "node": ">=6"
  },
  "files": [
    "src/*.js",
    "!src/*-spec.js"
  ],
  "bin": {
    "stop-build": "src/index.js"
  },
  "homepage": "https://github.com/bahmutov/stop-build#readme",
  "keywords": [
    "build",
    "ci",
    "exit",
    "git",
    "utility"
  ],
  "license": "MIT",
  "main": "src/",
  "publishConfig": {
    "registry": "http://registry.npmjs.org/"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/bahmutov/stop-build.git"
  },
  "scripts": {
    "ban": "ban",
    "deps": "deps-ok",
    "issues": "git-issues",
    "license": "license-checker --production --onlyunknown --csv",
    "pretty": "prettier-standard 'src/*.js'",
    "prelint": "npm run pretty",
    "lint": "standard --verbose --fix src/*.js",
    "pretest": "npm run lint",
    "secure": "nsp check",
    "size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";",
    "test": "npm run unit",
    "unit": "mocha src/*-spec.js",
    "semantic-release": "semantic-release pre && npm publish && semantic-release post"
  },
  "devDependencies": {
    "ban-sensitive-files": "1.9.0",
    "deps-ok": "1.2.0",
    "git-issues": "1.3.1",
    "license-checker": "11.0.0",
    "mocha": "3.4.2",
    "nsp": "2.6.3",
    "pre-git": "3.15.0",
    "prettier-standard": "5.1.0",
    "semantic-release": "^6.3.6",
    "standard": "10.0.2"
  },
  "dependencies": {
    "ggit": "1.15.1",
    "pluralize": "5.0.0"
  }
}

@bahmutov   @confooca   #confoo

What are these?!

{
  "bin": {
    "foo": "src/index.js"
  }
}
$ ./node_modules/.bin/foo
$ $(npm bin)/foo

@bahmutov   @confooca   #confoo

Creating executable scripts

{
  "scripts": {
    "start": "foo"
  }
}

@bahmutov   @confooca   #confoo

{
  "files": [
    "src/*.ts",
    "!src/spec*.js",
    "dist"
  ],
  "scripts": {
    "size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";",
  }
}
$ ls src
banner.ts index.ts register.ts spec1.js	spec2.js
$ npm run size

> mocha-banner@0.0.0-development size /Users/gleb/git/mocha-banner
> t="$(npm pack .)"; wc -c "${t}"; tar tvf "${t}"; rm "${t}";

    3088 mocha-banner-0.0.0-development.tgz
-rw-r--r--  0 0      0        2518 Jan 17 11:00 package/package.json
-rw-r--r--  0 0      0        3164 Jan 17 11:03 package/README.md
-rw-r--r--  0 0      0         181 Jan 17 11:03 package/dist/banner.js
-rw-r--r--  0 0      0         242 Jan 17 11:03 package/dist/index.js
-rw-r--r--  0 0      0         370 Jan 17 11:03 package/dist/register.js
-rw-r--r--  0 0      0          80 Jan 17 11:02 package/src/banner.ts
-rw-r--r--  0 0      0         129 Jan 17 10:43 package/src/index.ts
-rw-r--r--  0 0      0         286 Jan 17 10:43 package/src/register.ts

Node is a moving target ...

@bahmutov   @confooca   #confoo

{
  "engines": {
    "node": ">=6",
    "npm": ">=3"
  }
}

Generates warning

$ node -v
v4.2.0
$ npm i
WARN: engine foo@1.0.0: wanted ...

@bahmutov   @confooca   #confoo

{
  "engines": {
    "node": ">=6",
    "npm": ">=3"
  }
}
engine-strict=true

.npmrc

Stops npm install

@bahmutov   @confooca   #confoo

package.json vs .npmrc

For the users of the package

For development

local only

global + profile + local

@bahmutov   @confooca   #confoo

$ cat .npmrc
registry=http://registry.npmjs.org/
save-exact=true
progress=false
package-lock=false
$ npm config ls -l

shows all defaults

@bahmutov   @confooca   #confoo

$ npm config ls -l
; cli configs
long = true
metrics-registry = "http://registry.npmjs.org/"
scope = ""
user-agent = "npm/5.6.0 node/v8.9.4 darwin x64"

; project config /Users/gleb/git/mocha-banner/.npmrc
package-lock = false
progress = false
registry = "http://registry.npmjs.org/"
save-exact = true

; userconfig /Users/gleb/.npmrc
@cypress:registry = "https://registry.npmjs.org/"
package-lock = false
progress = false
save-exact = true

; default values
access = null
allow-same-version = false
also = null
always-auth = false
auth-type = "legacy"
bin-links = true
browser = null
ca = null
cache = "/Users/gleb/.npm"
cache-lock-retries = 10
cache-lock-stale = 60000
cache-lock-wait = 10000
cache-max = null
cache-min = 10
cafile = undefined
cert = null
cidr = null
color = true
commit-hooks = true
depth = null
description = true
dev = false
dry-run = false
editor = "vi"
engine-strict = false
fetch-retries = 2
fetch-retry-factor = 10
fetch-retry-maxtimeout = 60000
fetch-retry-mintimeout = 10000
force = false
git = "git"
git-tag-version = true
global = false
global-style = false
globalconfig = "/Users/gleb/.nvm/versions/node/v8.9.4/etc/npmrc"
globalignorefile = "/Users/gleb/.nvm/versions/node/v8.9.4/etc/npmignore"
group = 20
ham-it-up = false
heading = "npm"
https-proxy = null
if-present = false
ignore-prepublish = false
ignore-scripts = false
init-author-email = ""
init-author-name = ""
init-author-url = ""
init-license = "ISC"
init-module = "/Users/gleb/.npm-init.js"
init-version = "1.0.0"
json = false
key = null
legacy-bundling = false
link = false
local-address = undefined
loglevel = "notice"
logs-max = 10
; long = false (overridden)
maxsockets = 50
message = "%s"
; metrics-registry = null (overridden)
node-options = null
node-version = "8.9.4"
offline = false
onload-script = null
only = null
optional = true
otp = 0
; package-lock = true (overridden)
package-lock-only = false
parseable = false
prefer-offline = false
prefer-online = false
prefix = "/Users/gleb/.nvm/versions/node/v8.9.4"
production = false
; progress = true (overridden)
proxy = null
read-only = false
rebuild-bundle = true
; registry = "https://registry.npmjs.org/" (overridden)
rollback = true
save = true
save-bundle = false
save-dev = false
; save-exact = false (overridden)
save-optional = false
save-prefix = "^"
save-prod = false
scope = ""
script-shell = null
scripts-prepend-node-path = "warn-only"
searchexclude = null
searchlimit = 20
searchopts = ""
searchstaleness = 900
send-metrics = false
shell = "/bin/bash"
shrinkwrap = true
sign-git-tag = false
sso-poll-frequency = 500
sso-type = "oauth"
strict-ssl = true
tag = "latest"
tag-version-prefix = "v"
timing = false
tmp = "/var/folders/bf/cgb7wvb905q3n6hgtjc1lqrm0000gn/T"
umask = 18
unicode = true
unsafe-perm = true
usage = false
user = 501
; user-agent = "npm/{npm-version} node/{node-version} {platform} {arch}" (overridden)
userconfig = "/Users/gleb/.npmrc"
version = false
versions = false
viewer = "man"

Solid project

  • good README

  • complete package.json

    • urls, files, scripts

  • opinionated .npmrc

I'm forgetting something ...

@bahmutov   @confooca   #confoo

@bahmutov   @confooca   #confoo

Checklists

Human brain "glazes" over routine details

Going through a list helps

@bahmutov   @confooca   #confoo

✅ fill all package.json fields

@bahmutov   @confooca   #confoo

  • description
  • keywords
  • author
  • bugs
  • homepage
  • license (!)
  • ...

✅ catch simple errors using a linter

ESLint, Standard, Xo

@bahmutov   @confooca   #confoo

✅ stop arguing about style

Good auto-formatting

Prettier

Prettier is an opinionated code formatter

Static source linter
Standard

One JavaScript Style to Rule Them All

@bahmutov   @confooca   #confoo

$ npm home prettier
$ npm home standard

@bahmutov   @confooca   #confoo

{
  "scripts": {
    "prelint": "prettier-standard '*.js'",
    "lint": "standard --fix *.js",
    "pretest": "npm run lint",
    "test": "exit 1"
  }
}

package.json

$ npm run test
--- runs prelint
--- runs lint
--- runs test

@bahmutov   @confooca   #confoo

// index.js
const add = (a, b) => a + b
const sub = (a, b) => {
  return a + b
}
module.exports.add = ad
module.exports.sub = sub

@bahmutov   @confooca   #confoo

> standard --fix *.js

  my-app/index.js:1:7: 
   'add' is assigned a value but never used.
  my-app/index.js:5:22: 
   'ad' is not defined.

@bahmutov   @confooca   #confoo

✅ start unit testing right away

Tape, Mocha, Ava, Jest, ...

Delightful JavaScript Testing

@bahmutov   @confooca   #confoo

$ npm install --save-dev jest
$ node_modules/.bin/jest
/* eslint-env jest */
describe('add', () => {
  const add = require('.').add
  it('adds numbers', () => {
    expect(add(1, 2)).toBe(3)
  })
})

@bahmutov   @confooca   #confoo

$ jest
 PASS  ./test.js
  add
    ✓ adds numbers (4ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.763s, estimated 1s
Ran all test suites.

@bahmutov   @confooca   #confoo

Never "npm publish" yourself

@bahmutov   @confooca   #confoo

✅ CI + semantic release

✅ git hooks

✅ commit message convention

✅ use logging library

✅ crash reporting

✅ secure dependencies

✅ check missing dependencies

@bahmutov   @confooca   #confoo

Automate

@bahmutov   @confooca   #confoo

$ npm install -g yo generator-node-bahmutov
git init
git remote add origin <remote git>
yo node-bahmutov

Answer 3 questions: name, description, keywords

@bahmutov   @confooca   #confoo

@bahmutov   @confooca   #confoo

const Generator = require('yeoman-generator')
const g = Generator.extend({
  copyNpmrc () {
    this.fs.copy(this.templatePath('npmrc'), 
      this.destinationPath('.npmrc'))
  },
  gitOrigin () {
    const done = this.async()
    originUrl()
      .then(url => {
        // remember url
        done()
      })
  }
})
module.exports = g

Optimize for iteration

Final Thoughts

Solid project means remembering a LOT of options

Node & NPM are great

@bahmutov   @confooca   #confoo

Final Thoughts

Checklists help

@bahmutov   @confooca   #confoo

Final Thoughts

Automate it 🤖

Thank you 👏

A Simple Node Checklist

Gleb Bahmutov, PhD