A.

Blog

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:

npm create vite@latest -- --template react-ts

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:

Create a package.json file

Usage:
npm init <package-spec> (same as `npx <package-spec>`)
npm init <@scope> (same as `npx <@scope>/create`)

Options:
[--init-author-name <name>] [--init-author-url <url>] [--init-license <license>]
[--init-module <module>] [--init-version <version>] [-y|--yes] [-f|--force]
[--scope <@scope>]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[-ws|--workspaces] [--no-workspaces-update] [--include-workspace-root]

aliases: create, innit

Run "npm help init" for more info

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:

npm init foo -> npm exec create-foo
npm init @usr/foo -> npm exec @usr/create-foo
npm init @usr -> npm exec @usr/create
npm init @usr@2.0.0 -> npm exec @usr/create@2.0.0
npm init @usr/foo@2.0.0 -> npm exec @usr/create-foo@2.0.0

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:

  1. The latest version of create-vite is downloaded and globally installed
  2. npm exec create-vite@latest is run
  3. index.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:

{
"name": "create-example-cli",
"version": "0.0.1",
"bin": "index.js"
}

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:

#!/usr/bin/env node

console.log("Hello world!");

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:

$ npm exec .
Hello world!

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:

#!/usr/bin/env node

const { argv } = require("node:process");

const name = argv[2] || "world";

console.log(`Hello ${name}!`);

Now let’s test using an argument:

$ npm exec . -- Alex
Hello Alex!

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.

$ npm link
added 1 package, and audited 3 packages in 239ms

found 0 vulnerabilities

Now that our local package is installed, we can now use it with npm create:

$ npm create example-cli -- Alex

> create-example-cli@0.0.1 npx
> create-example-cli Alex

Hello Alex!

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).

$ npm create example-cli@latest -- Alex
Need to install the following packages:
create-example-cli@0.0.1
Ok to proceed? (y)

> npx
> create-example-cli

Hello Alex!

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!