About a year ago, I started the rxjs-experiments project. Aside of rxjs, it is all vanilla JS. I needed a simple frontend router with at least a deferred mounting feature (only mount a route when a promise is resolved). After some research on npm and github, I choose to write it myself.
The purpose of this article is not the router itself, but the whole workflow around it to get it to a published package that will be:
- maintainable (you should have a linter, unit tests and a CI like in any other of your projects)
- format unopinionated – as in whatever way you choose to consume the package – be it using:
- webpack/browserify/rollup (or any other module bundler) in CommonJS or ES2015 module mode
- directly in the browser (via a umd build)
- providing some documentation and example
You can apply some of the following concepts to any regular project by the way …
Source code available at topheman/lite-router.
Getting started
git init
npm init -y
I’ll be using yarn in the examples. You can use npm as well of course.
.gitignore
Since you will be publishing your package on the npm registry and example on github pages, your workspace will contain artefacts (files that aren’t from your source code but were generated from a build task) that shouldn’t be versioned in git (to avoid noise and problems in merge conflicts).
Your .gitignore
file should look like something like that:
.DS_Store *.log node_modules .idea dist lib es coverage build
That way, any generated file won’t be versioned in git – though, they will be part of your published package as we’ll see bellow.
.editorconfig
An .editorconfig
is just good practice (especially when you work with a team), to enforce things such as:
- Indent style: tabs or space
- Encoding
- EOL
Example of .editorconfig:
# http://editorconfig.org root = true [*] # change these settings to your own preference indent_style = space indent_size = 2 # it's recommend to keep these unchanged end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md] trim_trailing_whitespace = false [{package,bower}.json] indent_style = space indent_size = 2
Source code
Put your source code in the src
folder.
Setup Babel
Run
yarn add babel-cli babel-core babel-preset-env cross-env --dev
The .babelrc
file lets you describe how you want babel to behave:
- The
env
option will let you use a specific config according toBABEL_ENV
(Jest will override it usingNODE_ENV
) - The
babel-preset-env
is a preset that will determine which babel plugins you need, based on the options you pass
{ "env": { // jest doesn't take account of BABEL_ENV, you need to set NODE_ENV - https://facebook.github.io/jest/docs/getting-started.html#using-babel "commonjs": { "presets": [ ["env", { "useBuiltIns": false }] ] }, "es": { "presets": [ ["env", { "useBuiltIns": false, "modules": false }] ] } } }
useBuiltIns
means that we don’t want to ship any useless polyfills (leave that choice to the final user)."modules": false
means that you don’t want the modules to be transform to CommonJS (will be used when building ES2015 modules)
Setup jest, eslint and pre-commit hook
You can totally skip this section if you don’t bother about unit tests, code quality …
jest
Run
yarn add babel-jest jest --dev
Add the following to your package.json
:
"scripts": { "jest": "cross-env NODE_ENV=commonjs ./node_modules/.bin/jest", "jest:watch": "npm run jest -- --watch" }, "jest": { "testRegex": "(/tests/.*\\.spec.js)$" }
jest.testRegex
describes the pattern of your unit tests filenames. So create a unit test file like tests/index.spec.js
:
describe('foo', () => {
it('bar', () => {
expect(true).toBe(true)
})
})
Now, you can run:
npm run jest
: one shot unit-testnpm run jest:watch
: runs unit-tests in watch mode
Jest sets the
NODE_ENV
to “test” if it isn’t provided and otherwise let’s you use a custom override. It doesn’t useBABEL_ENV
at all.
eslint
Run
yarn add eslint eslint-plugin-import eslint-config-airbnb-base babel-eslint --dev
Create a .eslintrc
file:
{
"parser": "babel-eslint",
"rules": {
"max-len": 0,
"comma-dangle": 0,
"brace-style": [2, "stroustrup"],
"no-console": 0,
"padded-blocks": 0,
"indent": [2, 2, {"SwitchCase": 1}],
"spaced-comment": 1,
"quotes": ["error", "single", { "allowTemplateLiterals": true }],
"import/prefer-default-export": "off",
"arrow-parens": 0,
"consistent-return": 0,
"no-useless-escape": 0,
"no-underscore-dangle": 0
},
"extends": "airbnb-base",
"env": {
"browser": true,
"jest": true
}
}
What did you just install ? What does this .eslintrc
file contains ?
- the package
eslint
will let you lint your files - the package
eslint-plugin-import
is necessary when you lint ES2015+ (ES6+) based source code - the package
babel-eslint
will be used as a parser by eslint, because eslint itself might not support all babel features - the package
eslint-config-airbnb-base
is an extensible set of rules shared by airbnb (you could use an other preset) – those rules are overridable in therule
section.
Add the following in the scripts
section of your package.json
:
"lint": "./node_modules/.bin/eslint src", "lint-fix": "./node_modules/.bin/eslint --fix src --ext .js", "test": "npm run lint && npm run jest"
Now, running npm test
will both lint your source code and run your unit tests.
Pre-commit hook
To make sure you don’t commit broken code, setup a pre-commit hook that will lint your source code and run your tests before each of your commits.
Run
yarn add pre-commit --dev
Add the following section to your package.json
:
"pre-commit": [ "test" ],
Setup build steps
We will build and distribute our package in 3 different formats, that way the end user will be able to use the one that fits the most his use / build tools.
Tools like webpack are able to use any of the 3 formats bellow, though, your end user might want to use ES2015 aware tools (like rollup) that can take advantage of there feature (such as importing only what you use in your bundle or even tree shaking)
And if your end user doesn’t want to use tools like webpack or browserify, but use AMD (or even use the package defined in global namespace), you will provide the UMD build (useful on platforms like codepen).
This is why, as a package author, it is interesting to publish in those different formats for your final users.
To install rollup and rimraf, run
yarn add rimraf rollup rollup-plugin-babel rollup-plugin-commonjs rollup-plugin-node-resolve rollup-plugin-replace rollup-plugin-uglify rollup-watch --dev
Add the following in the scripts
section of your package.json
:
"clean": "rimraf lib dist es", "build": "npm run build:commonjs && npm run build:umd && npm run build:umd:min && npm run build:es", "build:watch": "echo 'build && watch the COMMONJS version of the package - for other version, run specific tasks' && npm run build:commonjs:watch", "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", "build:commonjs:watch": "npm run build:commonjs -- --watch", "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", "build:es:watch": "npm run build:es -- --watch", "build:umd": "cross-env BABEL_ENV=es NODE_ENV=development node_modules/.bin/rollup src/index.js --config --sourcemap --output dist/lite-router.js", "build:umd:watch": "npm run build:umd -- --watch", "build:umd:min": "cross-env BABEL_ENV=es NODE_ENV=production rollup src/index.js --config --output dist/lite-router.min.js",
Each build:*
task will build your package in a specific folder.
Each build:*:watch
task will build your package in that specific folder in watch mode.
You can npm run
the following tasks:
build:commonjs
: will build the CommonJS version in thelib
folderbuild:es
: will build the ES2015+ modules version in thees
folderbuild:umd
: will build the UMD version atdist/lite-router.js
(with sourcemaps)build:umd:min
: will build the minified UMD version atdist/lite-router.min.js
npm run clean
will cleanup the directories created by those build tasks.
Setup example
I will go deeper on how to manage github pages (git orphan branch as a deployment channel) in an other post – in the mean time, you can check out the README of the lite-router project (actually, most of my projects hold the gh-pages branch in a build/dist
directory).
Test in local with npm link
In your development workflow, you might want to both work on your package and use it (without re-publishing a new version to npm at each change) on an other project. For that, you can use npm link
.
Say you work on my-package
(this the name
attribute in your package.json
) and you want to be able to test it directly in my-project
:
cd my-package
npm link
cd ../my-project
npm link my-package
From there you will be able to import { myFeature } from 'my-package'
in your project (as if you had npm installed your my-package
).
Just run the correct build:*:watch
task in your my-package
so that the build stays up to date with the changes you might apply to its source code.
Upgrade the package.json file
All the following explanations were applied to this package.json file.You can also checkout the npm doc about the package.json.
Information related
In your package.json
, make sure you have:
- a
name
, aversion
, adescription
and anauthor
section - a
license
, ahomepage
and akeywords
section - a
repository
andbugs
section
Specify endpoints
So that your build files will be part of your final published package, you will have to declare them. Add the following to your package.json
:
"main": "lib/index.js", "module": "es/index.js", "jsnext:main": "es/index.js", "files": [ "dist", "lib", "es", "src" ],
- The
files
section tells npm to package those folders when publishing (otherwise, they would be ignored, since they are listed in the.gitignore
file) main
defines the endpoint of the CommonJS buildjsnext:main
andmodule
define the endpoint of the ES2015 build (we define twice the endpoint becausejsnext:main
was the first to be in use but it’s more likely thatmodule
will be standardized)
Add prepare script
Add the following in the scripts
section of your package.json
:
"prepare": "npm run clean && npm test && npm run build",
This will make sure the build files (which aren’t part of the git repo) are generated when your contributers (NOT users) run npm install
after forking and cloning your repo.
Travis CI
Don’t bother about this section if you don’t use Travis CI. If you use an other CI tool, well, it’s pretty much the same.
The following .travis.yml
file will test your builds, run the linter and the unit tests:
language: node_js node_js: - "6" script: - npm test
Note: Since I added npm test
in the prepare
script, the tests will run twice (this is also why there is no mention of npm run build
, since it’s tested after the install). There are ways to avoid it I wont talk about it here.
Checkout an example of that kind of travis test.
Publish your package
Don’t forget to add a README.md
file with:
- Why you made the package
- How to install it
- A short example
- Describe the API
- In a subsection, explain how to contribute (your git workflow, how to install, run, test, build …)
- Add some license
If you have your npm account setup on your computer, you are ready to publish your package. Just run:
npm run build
npm publish
Once published, you can
- npm install it somewhere else
- access the different builds through unpkg.com (useful platform made by Michael Jackson, one of the author of React Router) – example.
Resources / Credits
Source code available at topheman/lite-router.