13-Fingerprinting

When deploying updates to your new shiny app, you're going to want the browser to fetch the latest version if there has been an update, rather than using the cached version the browser may store. The simplest way we can do this is by appending a content hash to the filename, to ensure that when you push a new update, a new hash will be created, thus invalidating the previous cached versions.

To do this, we can use a simple plugin called broccoli-asset-rev:

1
yarn add --dev broccoli-asset-rev@^2.7.0

Now, update your Brocfile.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
// Brocfile.js
const funnel = require('broccoli-funnel');
const merge = require('broccoli-merge-trees');
const compileSass = require('broccoli-sass-source-maps')(require('sass'));
const esLint = require("broccoli-lint-eslint");
const sassLint = require("broccoli-sass-lint");
const Rollup = require("broccoli-rollup");
const LiveReload = require('broccoli-livereload');
const CleanCss = require('broccoli-clean-css');
const log = require('broccoli-stew').log;
const assetRev = require('broccoli-asset-rev');
const babel = require('rollup-plugin-babel');
const nodeResolve = require('rollup-plugin-node-resolve');
const commonjs = require('rollup-plugin-commonjs');
const uglify = require('rollup-plugin-uglify').uglify;
const env = require('broccoli-env').getEnv() || 'development';
const isProduction = env === 'production';

// Status
console.log('Environment: ' + env);

const appRoot = "app";

// Copy HTML file from app root to destination
const html = funnel(appRoot, {
files: ["index.html"],
annotation: "Index file",
});

// Lint js files
let js = esLint(appRoot, {
persist: true
});

// Compile JS through rollup
const rollupPlugins = [
nodeResolve({
jsnext: true,
browser: true,
}),
commonjs({
include: 'node_modules/**',
}),
babel({
exclude: 'node_modules/**',
}),
];

// Uglify the output for production
if (isProduction) {
rollupPlugins.push(uglify());
}

js = new Rollup(js, {
inputFiles: ['**/*.js'],
rollup: {
input: 'app.js',
output: {
file: 'assets/app.js',
format: 'es',
sourcemap: !isProduction,
},
plugins: rollupPlugins,
}
});

// Lint css files
let css = sassLint(appRoot + '/styles', {
disableTestGenerator: true,
});

// Copy CSS file into assets
css = compileSass(
[appRoot],
'styles/app.scss',
'assets/app.css',
{
sourceMap: !isProduction,
sourceMapContents: true,
annotation: "Sass files"
}
);

// Compress our CSS
if (isProduction) {
css = new CleanCss(css);
}

// Copy public files into destination
const public = funnel('public', {
annotation: "Public files",
});

// Remove the existing module.exports and replace with:
let tree = merge([html, js, css, public], {annotation: "Final output"});

// Include asset hashes
if (isProduction) {
tree = assetRev(tree);
} else {
// Log the output tree
tree = log(tree, {
output: 'tree',
});

tree = new LiveReload(tree, {
target: 'index.html',
});
}

module.exports = tree;

What we've done here, is add the AssetRev plugin as the last plugin to the tree, for production builds only. It will auto-hash js, css, png, jpg, gif and map files, and will update html, css and js files with any references to the original un-hashed file, with the new hashed file. There are additional options to specify a prefix so that files can be hosted on a CDN if you wish, see the plugin github page.

Run yarn build-prod and checkout the dist directory:

1
2
3
4
5
6
└── assets/
├── assets/app-4e1b4144662735eb546985bf69c03889.js
└── assets/app-75fd3e0f1a1ab573158459ad50aa232f.css
└── images/
├── images/broccoli-logo-4b34a6c5a21fbbd16639b21ec2e78902.png
└── index.html

Completed Branch: 13-fingerprints

Next: 14-complete