« Scott Nonnenberg


Better changelogs, strings, and paths

2016 Jul 19

I’m always on the lookout for ways to do Node.js and Javascript development better, but I haven’t found a good vehicle for these kinds of discoveries yet. I briefly mentioned a few in a recent post, and covered ESLint quite deeply in three full posts.

Let’s channel Goldilocks and see if we can hit the sweet spot with this one. Here are a few nice stack improvements from my last couple months…

standard-version + validate-commit-msg

Far too many package owners out there direct their users to a raw commit log to figure out what’s been happening lately. Yes, commits do track all changes in the project. But an explicit CHANGELOG.md in your repository is so much clearer.

What if we could make changelog generation automatic?

standard-version does just that. It will automatically update CHANGELOG.md based on your recent commits, bump version numbers automatically based on what kinds of changes were made, and even add a git tag.

What’s the catch? You have to format your commit messages in a certain ‘conventional’ way, taken from the Angular commit guidelines. For example, this is a new ‘parameters’ feature, which will bump the minor version by itself, but the the breaking changes annotation in the body will cause this commit to force a major version bump instead:

feat(parameters): cb parameter first, must be fn; new: justNotate()

BREAKING CHANGE: now `notate(cb, err, data)` instead of
`notate(err, cb, data`. Also, cb is required unless you are using
`justNotate(err, data)`.

How will you remember to follow this format? How will you ensure everyone working in a repository follows it?

Have no fear! The ghooks node module allows you to set up package.json-defined git hooks (husky works for this too), installed in a project at npm install time. And the validate-commit-msg node module is designed for the commit-msg hook, ensuring that commit messages are formatted properly! In your package.json:

{
  "config": {
    "ghooks": {
      "commit-msg": "validate-commit-msg"
    },
    "validate-commit-msg": {
      "maxSubjectLength": 72
    }
  },
}

(I have no idea why the default is 100 characters - any more than 72 characters wraps on Github!)

You have two options to handle pull requests:

  1. Every commit inside of each pull request follows commit guidelines and can appear in your changelog.
  2. By using GitHub’s squash merge for pull requests, the changelog will point to the pull request for that item. Each pull request should address just one thing, since it will be able to have just one entry in the changelog. I prefer this approach.

You can even go further and use the commitizen project to generate commits of the right format. You might consider it to help out folks new to the syntax.

I’m a huge fan! I use it all of my projects now.

Tagged template literals

Do you use the new backtick-delimited strings introduced in ES2015, now known as template literals? I like them a lot. I turned on ESLint’s prefer-template rule to make me use them more!

But I did discover a frustration with them: they would quickly get too long for my 90-character lines. And unlike normal string concatenation, I can’t split up the line without getting both newlines and extra spaces (due to indentation) in the final string!

It turns out that we have a release valve. The ECMAScript spec for template literals allows for something called a tag function to be prepended. And this function can do all sorts of interesting processing with the contents of that literal.

For example, removing newlines and indentation from a multiline template literal, very much needed in my code:

const oldIndented =
  'one\n' +
  '  two\n' +
  'Three';
const newIndented = removeIndentTag`
  one
    two
  three
`;

const oldSingleLine =
  'one' +
  ' two' +
  ' three';
const newSingleLine = removeNewlinesTag`
  one
  two
  three
`;

I think we can agree that the old is a lot more painful to deal with than the new: remembering to include those spaces on the second and third lines of oldSingleLine, remembering to include \n and + in the right parts of oldIndented. The new is clean and simple.

And these tag functions are not hard to write yourself! The key is the (strings, ...values) function prototype, and the reduce() call to build the final string from the provided pieces:

function assembleTag(strings, ...values) {
  return strings.reduce((result, item, i) => result + values[i - 1] + item);
}

After that, you can do whatever you want with the resultant string. Here’s a simple method of composing serial ‘processor` methods which each take and return a string:

function composeTag(...processors) {
  return function tag(...args) {
    const value = assembleTag(...args);
    console.log('raw:');
    console.log(`'${value}'`);
    return processors.reduce((result, item) => item(result), value);
  };
}

We can now build the two tags show above, removeIndentTag and removeNewlinesTag:

function trimNewlines(value) {
  return value
    .replace(/^\r\n|\n|\r/, '')
    .replace(/\r\n|\n|\r$/, '');
}

function removeNewlines(value) {
  return value
    .replace(/\r\n|\n|\r/g, ' ');
}

function removeIndent(value) {
  const matches = value.match(/^[ \t]*(?=\S)/gm);
  const lengths = matches.map(match => match.length);
  const minLength = Math.min(...lengths);

  if (!minLength) {
    return value;
  }

  const regexp = new RegExp(`^[ \\t]{${minLength}}`, 'gm');
  return value.replace(regexp, '');
}

const removeIndentTag = composeTag(trimNewlines, removeIndent);
const removeNewlinesTag = composeTag(trimNewlines, removeIndent, removeNewlines);

You have all the pieces now - you can do much more interesting things than this with your tag functions. Get creative! Return something other than a string! Do interesting transforms on the data before you assemble the final string!

And use this code, in my blog-code project on GitHub, as your starting point!

app-module-path

I touched on app-module-path in my second ESLint article, but it’s worth talking about it a bit further. Why? I strongly believe that we should completely eliminate ../ in all require() and import statements.

The importance of this becomes clear once a project grows to more than five or so directories. And certainly if you routinely need to pop up more than a level or two. Once I’m up to about four levels, I almost never get it right the first time. And most of my tests have that many levels to traverse when pulling in product code. It’s a problem.

app-module-path fixes all that by letting you add more search paths to be used by your Node.js require() or import statements. By default in Node.js any path that doesn’t start with .. or ./ searches files and directories under your node_modules directory. Now you can make a path like src/views/login look at your own project’s files!

Setting it up is extremely simple, with three options:

  1. require('app-module-path').addPath(targetDir); to add targetDir as a new base path.
  2. require('app-module-path/register') to add the directory of the requiring file as a new base path.
  3. require('app-module-path/cwd') to add the current working directory as a new base path. Given that most lookups are from the root of the project and most apps don’t change their working directory, this is a great option.

It should be noted, sadly, that node modules probably shouldn’t do this. Sure, use it during testing to make the require() statements in your tests nicer, but don’t force this behavior on your clients. Using something like this is a process-level decision. And it might make it hard to Webpack or Browserify your module.

And finally, do be aware that you can easily shadow installed node modules if you have files or directories in your own project with the same name. Your files will be loaded first in the case of a conflict.

Always improving!

Well, those are the big ones. As you might expect, there lots of other recent small wins:

Given that I’m always learning new things and refining techniques, I should be able to make a series out of these kinds of posts. Keep an eye out!

And of course, let me know if you come across anything interesting! :0)


There is a library out there that provides some useful tag functions like what I describe above, but I wouldn’t recommend it.

I won't share your email with anyone. See previous emails.

NEXT:

Hands-on with CircleCI and Node.js 2016 Jul 26

If you’ve been watching my @scottnonnenberg/notate repo on Github, you might have noticed quite a bit of churn related to setting up CircleCI. I learned quite a lot, and I’m passing all of that on... Read more »

PREVIOUS:

Private Node.js modules: A journey 2016 Jul 12

One of the best benefits of Node.js is the ease of extracting code into its own new project. But you probably won’t want to make that code fully public. It took me quite a while to get to a... Read more »


Hi, I'm Scott. I've written both server and client code in many languages for many employers and clients. I've also got a bit of an unusual perspective, since I've spent time in roles outside the pure 'software developer.' You can find me on Mastodon.