Inlining package version into ES6 npm packages using sed
Here’s how I’m using sed
to inline the version
field from an npm
package.json
into my code in an ES6 package. It’s not very exciting, which is probably clear from the exhilarating
title.
Lately, I’ve been writing most of my JS in ES6. It’s just a nicer language than ES5. It’s not as small, which is a
shame; but it’s clean. It has a built-in module system,
only half a century after the concept was invented. And
by using Babel and a prepublish
script, it’s trivial to distribute a library without having to
worry about compatibility. People have been doing this for years with CoffeeScript, so it’s
nothing new.
Here is an excellent
blog post by James Allardice about how to write npm packages using ES6. So
if you’re looking for a place to get started, read that first. Just keep in mind that if you’re writing a library that
will be used in many environments, you should use the
Babel runtime
transformer and make babel-runtime
one of your package
dependencies.
The problem
I want the client library I’m writing to facilitate using a REST service to include its version number in a user agent string so that it will be easy to see from server logs which version of the library is making a request.
N.b., the
User-Agent
header cannot be set in browser-based Ajax requests, but if your library is meant to work both client-side and server-side, you may find it useful to add that header in server-based requests.
A first pass at a solution
My immediate thought was to simply import the package.json
file into my code and use it directly:
// File: src/client.js
import library from '../package.json';
const agent = library.name + '/' + library.version;
export default class Client() {
// Client code goes here.
}
And this is sometimes fine if you know all the potential environments for your library. But I found that when I
imported my client library into a project that uses webpack to bundle modules, that it
chokes trying to import a .json
file. This can be resolved in webpack.config.js
, but it just doesn’t feel right to
impose a burden on the consumer of a library. The issue arises in the compilation of ES6 import
statements into some
variety of require
function based on the module bundling tool that might be employed.
A better way
So I stepped back and thought about what I am actually trying to accomplish: I want to compile ES6 code down to ES5
and I want some basic string substitution, the sort of a thing a preprocessor might do. If the situation were very
complex, I might be tempted to use something like grunt
or gulp
or even make
. No, really!
Sometimes make
is a fine tool for JS projects.
But my needs are vastly simpler. I’m already running my code through a compile phase before publishing
it to the npm registry, so there is an obvious place where I can further transform the code: the prepublish
script.
In this particular case, preprocessing my original source before compiling it means I can replace the string literal
AGENT
with the actual user agent information. And because the preprocessing happens before compilation, my source
map will contain the correct user agent information as well.
So the change to the client source code is:
// File: src/client.js
const agent = 'AGENT'; // This gets replaced.
export default class Client() {
// Client code goes here.
}
And the scripts
section of my package.json
looks like this:
{
"babel": "babel --optional runtime --source-maps --out-dir lib/ src/",
"compile": "npm run preprocess && npm run babel && npm run postprocess",
"preprocess": "sed -i.b \"s|AGENT|$(node scripts/agent)|g\" src/client.js",
"postprocess": "mv src/client.js.b src/client.js",
"prepublish": "npm run compile"
}
Let’s consider the three compilation stages (preprocess
=> babel
=> postprocess
) in closer detail.
Stage (1 of 3): preprocess
This stage needs to generate the correct user agent string and substitute the AGENT
token in the original source file
with the actual user agent. To do this, it’s running this sed
command:
$ sed -i.b "s|AGENT|$(node ./scripts/agent)|g" src/client.js
There’s a lot happening here. The s
command in sed
is the substitution command, which replaces the first
argument with the second. Customarily, a string that looks like regular expressions is used. As with other regular
expressions, the g
flag at the end means that the substitution should be applied globally:
$ sed "s/REPLACE-THIS/WITH-THIS/g" src/client.js
But because the user agent string I want contains a /
character (e.g. MyClient/1.0.0
), I’m using a different
delimiter string, |
. So the example command above could be written as:
$ sed "s|REPLACE-THIS|WITH-THIS|g" src/client.js
I also want to replace the original source file before feeding it to the Babel compiler, so I’m using the -i
flag
to indicate that I want an inline replacement and adding a .b
modifier to that flag to indicate that the original
file should be backed up with the original file name plus .b
, i.e. client.js.b
:
$ sed -i.b "s|REPLACE-THIS|WITH-THIS|g" src/client.js
And finally, I’m using
shell command substitution
to take the output of a helper script (which, crucially, can require
the package.json
file because it is
running in node
) and using that as my substitution value. So here is the final command again:
$ sed -i.b "s|AGENT|$(node scripts/agent)|g" src/client.js
And my helper script looks like this:
// File: scripts/agent.js
var library = require(__dirname + '/../package.json');
console.log(library.name + '/' + library.version);
Since it’s a trivial script, it’s plain old ES5, but I could have used the
babel-node
command if I wanted to write it in ES6.
Stage (2 of 3): babel
By contrast, the actual compilation stage is pretty simple:
$ babel --optional runtime --source-maps --out-dir lib/ src/
It adds babel-runtime
to the library so that any ES6 primitives that don’t exist in ES5 get polyfilled and also
generates a source map for my library. It compiles the source files in src
and writes the output into lib
.
Stage (3 of 3): postprocess
And finally, this step just replaces the modified source file with the original:
$ mv src/client.js.b src/client.js
Conclusion
This is all pretty straightforward command-line usage, but it’s good to remember that utilities like sed
have existed
for decades. And sometimes the simple, built-in toolchains that are available are better options than the complex build
tools that are commonly used in the JS ecosystem.