Add metadatas to your build files

Have you ever wondered if the file you’re been served from the server is up to date ? Why would you ?…

Not so long ago, you might have been programming in php and when something went wrong, the first thing you would say was “Try to clear your browser’s cache”.

Now that our assets filenames are hashed by default by bundlers like webpack, you could think that we shouldn’t run into such problems anymore. Since the filename of js/html (or any other files) will change based on its content, it will force the browser to retrieve the freshest version.

But between you and the server, there could be proxies / caching systems (like varnish / memcached). And more recently, on the client-side, we have ServiceWorkers that let us enable caching strategies.

So, to be 100% sure of which version of the site I’m running, for a few years now, I’ve been using a little routine that adds metadatas into the main generated files, based on:

  • infos inside the package.json (name, description, version, author, license)
  • git hash
  • date of the generation of the build

Example on an index.html:

<html>...</html><!--!
 * 
 * my-react-app-starter
 * 
 * create-react-app based project with a few pre-configured / installed features such as eslint or prettier
 * 
 * @version v1.0.0 - 2018-06-14T17:28:30+02:00
 * @revision #640ba4d - https://github.com/topheman/my-react-app-starter/tree/640ba4df20e3452acd2dd10b0f746a8300af0749
 * @author Christophe Rosset <tophe@topheman.com> (http://labs.topheman.com/)
 * @copyright 2018(c) Christophe Rosset <tophe@topheman.com> (http://labs.topheman.com/)
 * @license MIT
 * 
-->

That way, you can be sure of date / version (and other infos) of the generated file you’re been served with a simple curl.

Implementation

Whether you use create-react-app or have direct access to the webpack.config, first, copy/paste this common.js file to the root of your project, then:

npm install --save-dev moment git-rev-sync

Using create-react-app

I’ll explain bellow the implementation that you can see in this diff.

1) Copy/paste the following in a bin/expand-metadatas.js file in your project:

const { getBanner, getInfos } = require("../common");

process.env.REACT_APP_METADATAS_BANNER_HTML = getBanner("formatted");

process.env.REACT_APP_METADATAS_VERSION = getInfos().pkg.version;

2) Add --require ./bin/expand-metadatas.js to your package.json, that way, react-scripts will require bin/expand-metadatas.js before running and inject REACT_APP_METADATAS_BANNER_HTML and REACT_APP_METADATAS_VERSION as env vars:

"scripts": {
  "start": "react-scripts --require ./bin/expand-metadatas.js start",
  "build": "react-scripts --require ./bin/expand-metadatas.js build",
  "test": "react-scripts --require ./bin/expand-metadatas.js test --env=jsdom",
  "eject": "react-scripts eject"
}

3) At the end of public/index.html, append:

<!--!%REACT_APP_METADATAS_BANNER_HTML%
-->

You can also access process.env.REACT_APP_METADATAS_VERSION in the code of your app (to show the version number for example).

Using the webpack.config

Like in topheman/webpack-babel-starter, you can require common.js from your webpack.config and:

– access getBanner / getBannerHtml / getInfos
– use the return values with the HtmlWebpackPlugin

Conclusion

Next time your build is deployed on a server and you’re being told that your release doesn’t work, a simple curl will settle it … The front is not the problem ! 😉

Resources:

Pourquoi réaliser topheman/npm-registry-browser ? vidéo talk (fr)

Dans un post précédent, j’ai présenté la démarche que j’ai suivie pour mon dernier projet en React topheman/npm-registry-browser.

Il y a quelques jours, j’ai fait un talk à ce propos au meetup ReactJS chez Toucan Toco. Le meetup était enregistré, vous pouvez visionner mon talk (30min) en première partie de la vidéo :

Ressources :

A project to help getting into making React apps

Why I made topheman/npm-registry-browser

In software development, a lot of great quality resources are available, often for free. I’ve been getting feedbacks from developers – at work, online, at meetups – who shared that the hard part is not finding the knowledge but picking one library over an other or putting them all together.

Tutorials explaining a specific problem are all over there, what’s missing is projects examples / courses with a wider point of view.

This is what I decided to do in my latest project: topheman/npm-registry-browser. I respect some constraints that you would get, developing a real-world application, such as:

  • external API calls
  • using external libraries (UI kits, router, http clients …)
  • project setup for development with teams
  • code quality (linter, code formatting)
  • tests (unit / end to end)
  • automation / dev pipeline

The project itself is a Single Page Application that lets you search for packages in the npm registry and show details for each one of them such as the readme, the versions, the stats … In fact, this project is just an excuse to expose how to put together all those technologies I mentioned above.

The source code is available on github. You can test a demo online. I will be adding more features in the next weeks.

TRY IT

PS: This project is based on create-react-app and remains unejected. It was a constraint I imposed myself from the start. I never used CRA before (I have my own webpack starter-kit), so I wanted to test it to be able to tell what’s possible to do with it and what is not.

PPS: I chose not to use Redux, at least, not in that first version because … You might not need Redux (explanation) …

📺 Watch video of the talk (fr)

Faire cohabiter React et D3 – vidéo talk Best of Web 2017

En juin dernier, j’ai présenté un talk à la conférence Best of Web 2017 sur le sujet “Faire cohabiter React et d3”.

Voici la vidéo du talk, publiée par l’organisation de @bestofwebconf:

Retrouvez les autres talks sur cette playlist “Best of Web 2017”.

Ressources:

PS: Pour ceux qui se poseraient la question, la librairie que j’utilise pour faire mes slides est FormidableLabs/spectacle (les auteurs de victory), en adaptant un peu leur boilerplate, mais surtout, en utilisant l’extension thejameskyle/spectacle-code-slide qui permet une présentation de code très efficace.

Tophe @bestofweb 2017 Tophe @bestofweb 2017

Crédits:

Package a module for npm in CommonJS/ES2015/UMD with babel and rollup

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 to BABEL_ENV (Jest will override it using NODE_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-test
  • npm run jest:watch: runs unit-tests in watch mode

How jest manages BABEL_ENV:

Jest sets the NODE_ENV to “test” if it isn’t provided and otherwise let’s you use a custom override. It doesn’t use BABEL_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 the rule 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 the lib folder
  • build:es: will build the ES2015+ modules version in the es folder
  • build:umd: will build the UMD version at dist/lite-router.js (with sourcemaps)
  • build:umd:min: will build the minified UMD version at dist/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).

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, a version, a description and an author section
  • a license, a homepage and a keywords section
  • a repository and bugs 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 build
  • jsnext:main and module define the endpoint of the ES2015 build (we define twice the endpoint because jsnext:main was the first to be in use but it’s more likely that module 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.

More on prepare script.

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.