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
buildscript that runstsc
Here are some relevant bits in the package.json file of the workspace that
wants to expose the bin:
{ "name": "@org/my-cli-tool", "bin": { "my-command": "bin/my-command.js" }, "scripts": { "build": "tsc", "prepack": "pnpm run build" }, "files": ["bin", "lib"]}Use "type": "module" to publish as ESM in package.json. Import the generated
file from bin/my-command.js:
#!/usr/bin/env nodeimport '../lib/cli.js';Publishing as CommonJS? Then use require:
#!/usr/bin/env noderequire('../lib/cli.js');Make sure to include the shebang (that first line starting with #!), or
consumers of your package will see errors like this:
bin/my-command: line 1: syntax error near unexpected token `'../lib/index.js''Publishing
In case the package is supposed to be published, use the prepack (or
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 (
mkdirand)touchthe file frompostinstallscripts, 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.jsfile directly from thebinfield.