How to use a compiled bin in a TypeScript monorepo with pnpm

Published by on (last update: )

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:

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",
"prepublishOnly": "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 node
import '../lib/cli.js';

Publishing as CommonJS? Then use require:

#!/usr/bin/env node
require('../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 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:

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