Building an npm create package
Jul 28, 2024
If you’ve been working in the JavaScript ecosystem for the last few years, you’ve probably noticed that there has been a prevalent usage of the command npm create
. This pattern is commonly used by libraries/frameworks to quickly bootstrap projects.
For instance, Vite allows you to scaffold your project with npm create vite@latest
. The CLI that runs then prompts you for information on how you’d like your project scaffolded. Alternatively, you can also pass in arguments to the npm create
command. For example, if you wanted to scaffold a React TypeScript project, you would run:
You can also see this in use with Next.js, Vue.js, and Playwright.
In this post, we’ll go over what’s happening when you run npm create
and how you can build your own package to do this.
Behind the scenes
So what exactly is going on when you run npm create
? Well, first, let’s take a look at what npm create
actually does. If you run npm create -h
, you’ll see the following:
So the first thing to notice is that npm create
is actually just an alias for npm init
. For those who have created projects with npm before, you’ll remember that npm init
is the command that you run to create your package.json
file (as it says in the help description above). So how does running something like npm create vite@latest
launch a CLI?
If you dive a little deeper into the help docs for npm init
, you’ll see that if you pass in an argument, it will use that as an “initializer” which is essentially a reference to some package published on npm. The package will then be installed and then have its main bin executed via npm exec
.
Initializers are actually transformed so they don’t 100% match their published name. Here are the transformations that are made:
So in the case of npm create vite@latest
the package that is actually being installed and executed is create-vite.
Let’s walk through step by step for what happens when you run npm create vite@latest
:
- The latest version of create-vite is downloaded and globally installed
npm exec create-vite@latest
is runindex.js
in create-vite is executed with node
Definitely a bit of magic, but quite simple once you break things down. Next, let’s start building our own CLI to use with npm create
!
Building the CLI
First, we need to create a package.json
file. You can do this with npm init
or manually:
The important parts here are the name
and bin
. Since we’re following the initializer naming conventions, we’ve added “create-” to the beginning of the package name so users will run npm create example-cli
to execute our package. The bin
field points to where the executed script lives.
Let’s create the index.js
script now:
It is important that you have #!/usr/bin/env node
as the first line, otherwise your script will run without node.
We can now test our package.json
and script to verify that it works:
Let’s modify index.js
to accept an argument to make this a little more CLI-ish. We’ll use process.arvg
from node to access the arguments:
Now let’s test using an argument:
Neat! Now we have our CLI ready to go. Let’s test running this with npm create
so we can be sure everything is working before we publish our package to npm. Before we do that, we need to install our local package globally. There are a few ways to do this, but the easiest way is to use npm link
. This will symlink your package in the global node_modules
folder which makes it very easy to make iterative changes to the CLI and test it.
Now that our local package is installed, we can now use it with npm create
:
Publishing to npm
Now that our CLI package is all set and ready, we can take the next step and publish it to npm. If you’re not already authenticated to npm locally, you’ll need to first run npm adduser
and follow the prompts.
Now that we’re logged in, we can run npm publish
. You may be asked to authenticate one more time, but after you’ve done so, your package should be successfully published and accessible via npm.
Running the CLI
Let’s take the CLI for a full end-to-end test drive. You can also try this out as I’ve published the example package onto npm (you can also find the source on GitHub).
You’ll notice that npm prompted us to download the create-example-cli package before it executed it. Subsequent runs won’t require this prompt or download.
And there we go! We’ve successfully created a CLI that is executed when you run npm create
. Our example CLI here is an extremely simple case. For most create-*
packages out there, you’ll find that they will actually go and generate files and folders, but there are also a ton of other use cases you can accomplish with this pattern that’s not just limited to scaffolding projects. For instance, at Dopt, I built a CLI that helped our users onboard through our SDKs.
There’s also a dark side though. This pattern highlights some of the security drawbacks of running third party code from npm on your local machine. You’re trusting the author of the CLI to execute a node script on your computer so make sure you actually trust the source. Stay safe and build cool things!