iTranslated by AI
npm Project Setup for Building CLI Tools with TypeScript
Target Audience
- Familiar with TypeScript
- Writing mostly web apps, but wanting to write command-line tools as well
- Not knowing where to put personal tools and just putting them in
~/bin, etc.
Where to Put Personal Scripts
Writing a quick shell script and putting it in ~/bin—this is about me. Those are hard to maintain. You should stop doing that.
Recently, I've been organizing CLI tools in a different way. To get straight to the point, you likely have a folder for your project list in your home directory; create a new project folder for CLI commands there. Put them in there.
$ tree ~
myProjects
├── project1
├── project2
└── project3
$ mkdir myProjects/my-utils
How to Create a Package
You probably know that commands become available when you install an npm package. We'll use that. "Installing is a hassle, I don't want to do it." Exactly. Actually, you don't need to install it. If you npm link, you can directly execute scripts from that project from anywhere in the system.
Here, I'm using tsx to execute TypeScript scripts directly. I don't bother with building because it's a hassle. Having fewer steps is the right way to go.
$ cd myProjects/my-utils
$ hx package.json
$ cat package.json
{
"name": "my-utils",
"version": "1.0.0",
"bin": {
"mytool": "./cli.js"
}
}
$ cat ./cli.js
#!/usr/bin/env tsx
console.log('Hello world!)
$ chmod +x ./cli.js
$ npm link
$ mytool
Hello world!
$ ls -l $(realpath $(which mytool))
-rwxr-xr-x 1 my-name staff 48 4 9 17:03 /Users/my-name/myProjects/myutils/cli.js
# Files in the project can now be executed directly
What if you want to delete that folder? You should be managing it with GitHub before being told, so you can install it from there.
$ cd ~/myProjects/my-utils
$ npm unlink -g my-utils
$ which mytool
$ npm install -g git+https://github.com/my-name/my-utils.git
$ mytool
Hello world!
Since you can install it this way, you can delete the local folder at any time.
If you want to do maintenance, just run npm uninstall -g once, clone it, and run npm link again to return to the previous state.
npm uninstall -g my-utils
gh repo clone my-name/my-utils
cd my-utils
npm link
Folder Structure So Far
The minimal folder structure is as follows:
myProjects
├── myutils
│ ├── cli.js
│ ├── package-lock.json
│ └── package.json
Q&A
- Do I need
maininpackage.json?- No, you don't. That is for specifying which file to load when this package is
imported. This package won't beimported, so it's unnecessary.
- No, you don't. That is for specifying which file to load when this package is
- What about
authors,repository,homepage, etc.?- If the warnings are annoying, feel free to fill them in.
Folder Structure of a Recently Created CLI Tool
I wrote a tool that reads data from a Google Sheet and writes it to a Google Calendar. This is what its folder structure looks like.
It runs with tsx. However, I wanted to experiment and see if it would also work if bundled. It did.
my-tool-latest
├── README.md
├── package.json # mtl(./bin/index.ts) and my-tool-latest(./dist/index.js) in bin
├── pnpm-lock.yaml
├── bin
│ ├── index.ts # Entry point for direct tsx execution, imports src/index.ts
│ └── my-tool-latest.js # Entry point for bundle execution, imports dist/index.js
├── src # ✍🏻 TypeScript source directory. This is the workspace.
│ ├── index.ts # The actual entry point
│ ├── consola.ts
│ ├── fetchSpreadsheet.ts
│ ├── getFlatEvents.ts
│ └── registerEvents.ts
├── dist # Directory for bundled files
│ └── index.js
├── .env # Environment variable settings
├── .gitignore
├── .prettierrc
├── .vscode
│ └── tasks.json
├── client_secret_5xxxxxxxx8-vxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com.json # A key-like file for accessing Google services
└── tsup.config.ts
Conclusion
The npm command installation feature is fantastic. Say goodbye to brew Formulae.
Discussion