Are you trying or interested in creating your own global NPM module? If you are, you've come to the right place! If not, I suggest you to stick around anyway.
A couple of weeks ago, I had this idea about making my own CLI tool for managing my AWS profiles instead of using the one that the AWS CLI provides. I know, why reinvent the wheel? I didn't had the intention of releasing this tool, but I just wanted to know it would be done. And so I did.
In this tutorial, we're going to be creating a simple global NPM module that tells hilarious Chuck Norris jokes using Chuck Norris Jokes API. We're going include some cool stuff like async code and user prompts so you can see how everything works in a typical CLI tool.
Setting up your project
For this tutorial, you're going to need to have Node installed on your computer. You can do so by going to Node's official website and following the instructions there.
After you're done with that, you're going to do the following:
- Open up your terminal
- Create a new directory called
chuck-me-a-joke
- Navigate to that directory
- Run
npm init
and setting up your Node package (you can just leave everything as it is or modify whatever you want)
Cool! Now that you have created your project, we can move up to the next step which is installing everything you need.
Packages installation
Typescript
As you might already figured out by the title, we're going to use Typescript for this project. If you're not already familiarized with Typescript, don't worry, you can take a look at this post that will get you started.
We're going to install Typescript as a dev dependency. We can install it by running the following commands:
npm install typescript --save-dev
npm install @types/node --save-dev
Next, we're going to set up Typescript. First, let's run the following command:
npx tsc --init
This will generate a tsconfig.json
file on the root of your project that will contain the configuration that Typescript will use for building our project. Open this file and replace it's content with the following:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"removeComments": false,
"noImplicitAny": false,
"outDir": "dist"
},
"exclude": [
"node_modules"
]
}
Finally, we need to add our build
script so that our Typescript project can be compiled to JavaScript. We can do so by adding this line to the scripts
on our package.json
:
"build": "tsc"
Prompts
Now that we've got Typescript set up, we're going to install Prompts. This a really cool library for managing user interaction with the command line. You can write text, select from a list, add colors, and much more.
You can install it by running the following commands:
npm i prompts
npm i @types/prompts --save-dev
Axios
Axios is a pretty simple but powerful library that allows you to easily make http requests. We're going to use this for fetching jokes from the API. Run the following command for installing Axios:
npm i axios
Folder structure
All right, we now have everything we need to start making our NPM module.
First, let's create our (simple) folder structure. For this, just create a new folder called src
in the root of your project, and create a new file inside it called index.ts
which will serve as the entry point for the application. This means that we need to specify this on our package.json
. For this, just open the package.json
file and replace this:
"main": "index.js"
with this:
"main": "dist/src/index.js"
But hold on, what's this dist
folder doing there? This is the folder that we told Typescript to build our application in. We added this in the tsconfig.json
file when we set up Typescript.
Code time!
Ok! Let's code!
First, we're going to create a service that fetches jokes from the API. For this, we're going to create a folder called services
inside our src
folder. Then, we're going to create a jokes.service.ts
file with the following content:
import axios from 'axios';
const CHUCK_JOKES_URL = 'https://api.chucknorris.io/jokes/random';
type Joke = {
value: string;
}
export async function getJoke(): Promise<Joke> {
try {
const response = await axios.get(CHUCK_JOKES_URL);
return response.data;
} catch (err) {
throw new Error('Failed to fetch Chuck joke.');
}
}
Neat! We've got ourselves a service that gets random jokes. Now we need to call it from our index.ts
file.
Copy the following code into our index.ts
:
#!/usr/bin/env node
import { getJoke } from "./services/jokes.service";
(async function chuckJokes() {
const joke = await getJoke();
console.info(joke.value);
})();
The first line in our index.ts
is called a shebang line. This is used by Unix based systems to determine what interpreter should run this file. Windows doesn't allow shebangs because they rely on the file extension, so it just ignores this line.
Then, we have an immediately-invoked async function called chuck-me-a-joke
that fetches a random joke from the API and then it just prints it out. This can also be done using top-level await in newer Node versions.
Now, you can run npm run build && node dist/src/index.ts
and it will print a random joke. Cool. But we want to just run a single command on our terminal. For this, we need to define the bin
property on our package.json
which maps the name of the package (chuck-me-a-joke) to our executable file. Being that said, add the following line to our package.json
:
"bin": "dist/src/index.js"
Then, we can run npm run build && npm i -g .
to install the package globally (remember to be placed on the root of the project).
Now, if you run chuck-me-a-joke
... BOOM! We're prompted with a hilarious Chuck Norris joke on our terminal.
That looks cool, but you've probably seen some CLI tools that allow parameters to be passed to the function. Let's make our function accept the following two parameters:
-
random
which gives the user a random Chuck Norris joke. -
category
which gives you a list of categories to pick from and returns a Chuck Norris joke from that category.
First of all, let's modify our getJokes
function in the jokes.service.ts
so that it accepts an optional category parameter:
import axios from 'axios';
const CHUCK_JOKES_URL = 'https://api.chucknorris.io/jokes/random';
type Joke = {
value: string;
}
export async function getJoke(category?: string): Promise<Joke> {
try {
const response = await axios.get(category ? `${CHUCK_JOKES_URL}?category=${category}` : CHUCK_JOKES_URL);
return response.data;
} catch (err) {
throw new Error('Failed to fetch Chuck joke.');
}
}
Ok! Now, let's modify our index.ts
to accept the parameters that we've just discussed to be passed in the command line:
#!/usr/bin/env node
import prompts = require("prompts");
import { getJoke } from "./services/jokes.service";
const chuckJokesCategories = [
{ title: "Animal", value: "animal" },
{ title: "Career", value: "career" },
{ title: "Celebrity", value: "celebrity" },
{ title: "Developer", value: "dev" },
{ title: "Explicit", value: "explicit" },
{ title: "Fashion", value: "fashion" },
{ title: "Food", value: "food" },
{ title: "History", value: "history" },
{ title: "Money", value: "money" },
{ title: "Movie", value: "movie" },
{ title: "Music", value: "music" },
{ title: "Political", value: "political" },
{ title: "Religion", value: "religion" },
{ title: "Science", value: "science" },
{ title: "Sport", value: "sport" },
{ title: "Travel", value: "travel" }
];
(async function chuckJokes() {
const args = process.argv.splice(2);
const arg = args[0];
if (args.length > 1) {
return console.info("You can only pass one argument; `random` or `category`");
}
if (!arg) {
return console.info("You need to pass one of the following arguments: `random` or `category`.");
}
let joke;
if (arg === 'random') {
joke = await getJoke();
} else if (arg === 'category') {
const category = await prompts({
type: 'select',
name: 'value',
message: 'Pick a category',
choices: chuckJokesCategories,
initial: 1
});
joke = await getJoke(category.value);
} else {
return console.log(`Sorry, ${arg} is not a valid argument.`);
}
console.info(joke.value);
})();
process.argv is an array that contains the list of parameters passed in the command line. We're removing the first two parameters that are node
and the path to the script because we don't really care about them.
Then, if chuck-me-a-joke
is called without any params or with more than one, we'll get an error saying that we need to pass the random
or category
params.
Finally, if the parameter is random
, we'll return a random Chuck Norris joke. If the parameter is category
, we will ask the user to select one of the categories using the Prompts library and we'll show a joke that belongs to the chosen category.
Remember that you need to rebuild and reinstall the module every time you make changes. Otherwise chuck-me-a-joke
will not be updated. You can do this by running npm run build && npm i -g .
Publishing
Before publishing our module, we need to add a README.md file to the root of the project containing information about what it does, how it works and all of that stuff. I won't add it here because I don't think is necessary but feel free to look at other npm packages to have an idea of what this README file should have.
Now, before publishing it, we need to create an account in NPM. After signing up and confirming our email, go to your terminal and in your project root run npm adduser
.
This will ask for your username and password to validate that is really you who is trying to publish the package.
After that just run npm publish --access public
and that's it! We have our own global NPM module published and for the people to use and enjoy.
Outro
I hope this post was somewhat useful for you.
As with every code, you should probably add tests to it. I didn't do it on this post because I believe it's out of scope, but here's a link to the GitHub repository that contains the final version which includes tests.
You can check the module in NPM as well.
Special thanks to Mathias for letting me use his awesome Chuck Norris Jokes API!
Also, special thanks to Chuck Norris for existing. This wouldn't be possible without you.
See you in the next one!
Top comments (0)