How to use a compiled bin in a TypeScript monorepo with pnpm
Today’s scrap has a very long title and is about pnpm workspaces that contain a compiled executable in a TypeScript monorepo.
Problem
When running pnpm install
in a monorepo, the local bin
file of a workspace
may not exist yet. This happens when that file needs to be generated first (e.g.
when using TypeScript). Then pnpm
is unable to link the missing file. This
also results in errors when trying to execute the bin
from another workspace.
tl/dr; Make sure the referenced file in the bin
field of package.json
exists, and import the generated file from there.
Solution
So how to safely use a compiled bin? Let’s assume this situation:
- The entry script for the CLI tool is at
src/cli.ts
- This source file is compiled to
lib/cli.js
- Compiled by the
build
script that runstsc
Here are some relevant bits in the package.json
file of the workspace that
wants to expose the bin
:
Use "type": "module"
to publish as ESM in package.json
. Import the generated
file from bin/my-command.js
:
Publishing as CommonJS? Then use require
:
Make sure to include the shebang (that first line starting with #!
), or
consumers of your package will see errors like this:
Publishing
In case the package is supposed to be published, use the prepublishOnly
script
and make sure to include both the bin
and lib
folders in the files
field
(like in the example above).
A note about postinstall
scripts
Using a postinstall
script to create the file works since pnpm v8.6.6,
but postinstall
scripts should be avoided when possible:
- Can perform malicious acts (security scanners don’t like them)
- Can be disabled by the consumer using
--ignore-scripts
- Can be disabled if the consumer uses
pnpm.onlyBuiltDependencies
Bun does not execute arbitrary lifecycle scripts for installed dependencies.
That’s why this little guide doesn’t promote it, and this scrap got longer than I wanted!
Additional notes
- This scrap is based on this GitHub comment in the pnpm repository.
- I’ve seen and tried workarounds to (
mkdir
and)touch
the file frompostinstall
scripts, but that’s flaky at best and not portable. - The same issue might occur when using npm, Bun and/or Yarn. True or not, it’s better to be safe than sorry.
- If you are using only JavaScript (or JavaScript with TypeScript in JSDoc) then
you can target the
src/cli.js
file directly from thebin
field.