Writing
One library, many opinions
October 21, 2026
It started with a date formatter. One function, maybe eight lines, copied into three Angular apps because it was faster than setting anything up. A year later there were three date formatters. One handled time zones, one had a bug the other two had fixed, and one had grown an optional argument nobody could explain. The shared library was supposed to end that. It did, and then it handed me a different set of problems I hadn’t known to worry about.
The first surprise is that you can’t just publish an Angular folder. The templates and decorators need compiling into something a consumer’s build can understand, and that consumer may be on a slightly different Angular version than you. That’s what ng-packagr is for. It produces the Angular Package Format, which is a fancy way of saying “the shape the CLI expects,” and it compiles partially, leaving the final step to whoever installs it. I spent a while treating it as an annoying extra build. It’s the whole reason the thing works across apps at all.
The real product, it turned out, wasn’t the components. It was one file
called public-api.ts. Every line in it is a promise. Export a component and
its inputs are now something you can never rename without a version bump and
a conversation. Export a helper “just in case” and someone will build on it by
Thursday. The best decision I made was being stingy there. A lot of the
library is deliberately unreachable, and that’s what lets me rewrite it on a
quiet afternoon without anyone noticing.
Then we hit the bundle size. Importing one button pulled in the forms module,
the date helpers and a chunk of stuff nobody asked for, because it was all one
entry point and tree-shaking can only do so much with Angular metadata. The
fix was secondary entry points: @scope/ui/buttons, @scope/ui/forms, each
its own little package with its own ng-package.json. It’s more boilerplate
than I’d like. It also means an app that wants a button gets a button and
nothing else, and the boundaries between pieces are now enforced by the
import path instead of by good intentions.
The afternoon I lost to peer dependencies deserves its own paragraph. Angular
was listed as a plain dependency. One of the apps installed the library and
got a second copy of Angular, so a service provided in the app was being
looked up by a library that had its own idea of what the injection token was.
The error said “no provider,” and the provider was right there, and I read
that line a lot. Angular, RxJS, anything the consumer already has, goes in
peerDependencies with an honest version range. Nothing else taught me the
difference between the two fields quite as thoroughly.
What I didn’t expect was how much the newer Angular made this easier. A
standalone component carries its own imports, so there’s no module to export
alongside it and no module to forget. Signal inputs give each component a
flat, readable surface, and computed() keeps the derived state inside where
it belongs. The library got smaller on the outside while doing more on the
inside, which is exactly the direction you want a shared thing to grow.
The job of a library, I’ve decided, is to be boring. Nobody wants a clever button. They want the same button they had yesterday, with the fix they asked for, and a changelog they can read in thirty seconds. A library is a contract with people who will never open the source, including you in six months. Fewer exports, honest versions, and one date formatter. That last one is still only eight lines.
— Adi