← All writing
Craft · · 8 min

Only mine was broken

Tooling JavaScript

A client’s nav collapsed into the hamburger about 200px too early on my machine, and on nobody else’s.

I’d been at the new agency three weeks. When you’re three weeks in and something doesn’t work, the first explanation you reach for is yourself, because it usually is yourself. You’re on the wrong branch. You skipped a line in the setup instructions. You’re looking at a stale build in a tab you forgot you had open. Every one of those has been true of me before, and none of them was true here. But I hadn’t earned the right to skip past them yet.

Of course it was me

So I went through the list. Right branch, clean working directory, same Node version as the setup notes asked for. Hard refresh, then a different browser, then a private window in case something in my profile was doing it. I deleted node_modules and installed again, which is the move you make when you’ve stopped having ideas, and got the identical wrong nav back out.

I did not ask anyone. Asking meant walking up to someone in week three and saying that I couldn’t run the build, which is a sentence with a subtext, and the subtext is the whole reason people don’t say it. There’s a version of the next 12 months where I’m the person who’s good at CSS and a version where I’m the hire nobody can quite explain. It felt like this was deciding which.

By the second afternoon the plan I was seriously considering was to wipe Node off the machine and start over.

Four of us, four different builds

I gave up and asked. One of the other front end developers pulled a chair over, and inside a few minutes we had both compiled stylesheets open side by side, same source files, different output. Different vendor prefixes on the flex declarations, and a couple of rules in a different order.

Which is not a thing your source code can do to you. Something between the SCSS and the CSS was different, and the only things in there are the packages.

terminal
# mine, installed three weeks ago
$ npm ls autoprefixer
└── autoprefixer@6.5.1

# theirs, installed in the spring
$ npm ls autoprefixer
└── autoprefixer@6.3.6
Shell

The project asks for it like this, and so does every other project in the building.

package.json
"devDependencies": {
  "autoprefixer": "^6.3.1",
  "gulp-sass": "^2.3.2"
}
JSON

The caret means anything below 7.0, decided fresh every time somebody installs. Autoprefixer exists to write prefixes based on which browsers still need them, so a newer copy with newer browser data writes different prefixes on purpose. That’s the feature. My install was three weeks old and theirs was a fossil of whatever the registry was serving in the spring, and the fossil is what the client site had been built from all year.

And then the thing I thought I’d found turned over on me. It wasn’t that my install was broken. It’s that all four of us had a different node_modules, and the only reason nobody had run into it is that nobody had deleted the folder in months. We had never once run the same code. The repository we were all handing back and forth was the same, and the thing that actually produced the site was four separate piles of packages that happened to be sitting on four laptops.

Nobody had noticed because noticing requires somebody to install from scratch, and the person who installs from scratch is whoever joined most recently.

Lock, stock and one committed file

Facebook put out a package manager two weeks ago, which I’d read about and filed under interesting. It turns out to be the exact thing.

It reads the same package.json, pulls from the same registry, and puts the same shape of folder on disk. It also writes down what it resolved. Every package, every version, including all the packages your packages wanted, in a file you commit.

terminal
# resolves everything, writes yarn.lock
yarn
# this is the part that matters
git add yarn.lock
# instead of npm install --save-dev
yarn add --dev autoprefixer
Shell

That’s the migration. One command and a commit.

There was already npm shrinkwrap, which does roughly this. I have never in my life worked on a project that used it. It’s opt-in, it had a reputation for being fussy, and opt-in plus fussy means the person who’d have to go first never does. A lockfile that gets written for you and just sits in the repo is the same idea with the friction taken out. I’m fairly sure the friction was doing all of the damage.

Installs are also much faster. I don’t want to be dismissive about that, since I do it several times a day and the parallel fetching is real. But the speed is the part that got written about and the lockfile is the part that changed anything.

I’ve committed one to the project I was handed and put two sentences in its README. Whether the rest of the shop moves over is not a week-three decision and I’m not going to pretend otherwise.

The lockfile is not a warehouse

Worth being clear about, because the pitch goes vague right here. A lockfile guarantees you get the same thing. It does not guarantee that you get anything.

It’s the same registry it was before. If a package gets unpublished, or the registry has a bad morning, the lockfile can’t conjure the tarball, it can only tell you precisely which tarball you’re not getting. There’s an offline cache that’s supposed to help and I haven’t tested it properly yet.

Which is the March thing, really. A package went away, a lot of builds stopped working, everybody was amused for about a day and then went straight back to what they were doing. That was the week to take reproducibility seriously and most of us didn’t. I don’t think it was because we disagreed. There was nothing convenient to do about it. Convenient turns out to be most of the variable.

It also does nothing whatsoever about the part that should probably bother me more, which is that a five-page brochure site now arrives with several hundred packages in it that not one of us has read a line of. Installing that faster, and identically on four machines, genuinely helps a situation I’d rather not look at directly.

Did I think about this at all?

Now the less flattering read, because I wrote almost exactly this post at the end of last year about moving a build from Grunt to Gulp, and the shape is identical, and the shape is what worries me.

I have a test for this now: did the migration cost less than it saved. This one did, comfortably. Half an hour against a problem that ate two of my days and had presumably been quietly eating other people’s for a year. By that measure Grunt to Gulp was a bad call and this was a good one.

Except I ran the test afterward. I read the announcement, I was interested, and the lockfile was in the repo that same evening, days before I could have told you what problem it solved for us specifically. The evaluation is a thing I built later to explain a decision I was always going to make. That’s a less impressive account of how I pick tools than the one I’d give in an interview, where I’d say something about assessing tradeoffs. I suspect what I mostly do is be available lol

The one genuine difference I can point to is that this is cheap to walk back out of. Grunt to Gulp was a rewrite, a week of it, every plugin swapped for a different plugin. Yarn is a command and a file, and npm install still works the whole time, so if it turns out to be a mistake nothing has been thrown away. A migration you can reverse is a different kind of risk from one you can’t, and I gave that no weight at all in 2015. Although noticing that a decision was reversible is also a very comfortable thing to notice about a decision you already made, so take it for what it’s worth.

The two days I don’t really mind. It gets me that four of us were building the same site from the same repository and believing it. There was nothing in the setup that could have told us otherwise, because the only way to find out is for somebody to start clean. That’s the new person, always. So the tax lands on whoever has the least context, the least standing to say something looks wrong, and the strongest reason to assume the problem is them. I sat with that for two days before I asked, and the thing that would have saved me is a generated file nobody will ever open, sitting in the repo doing nothing at all until the day it does.

Read similar posts
10 min

It's never the prompt

The prompting turns out to be the least interesting part of my Claude Code setup, and most of what's actually doing the work is a handful of files I wrote once and then stopped thinking about.

17 min

Exit 0 means allow

Porting an internal Claude Code safety tool from Linux to macOS took a couple months, and nearly every failure I found on the new platform failed in the permissive direction while printing a checkmark on the way out.