Consider a circumstance where you’re building a terminal project using Node.js. One way to parse arguments would be to use the process.argv
array, which is shown below:
var myArgs = process.argv.slice(2);
console.log("Your website name: ", myArgs[0]);
console.log("Your favorite number", myArgs[1]);
This will be the output:
While this might work, there is a minor flaw with the code. Referring to your arguments by array index is not very clean. What if the client passes in parameters in the wrong order?
This is where Yargs comes in. It allows developers to build interactive command-line tools with zero difficulties. Furthermore, the library also has support for accepting flags and building help screens.
Now that we have talked about its fruition, let’s get started!
In this article, you will build a program that will use Mozilla’s Readability API to fetch content from any website. The app will also be global; it can run from anywhere.
If the user passes in the -f
flag, it will download the HTML of the desired page.
The saved HTML will look like this:
Getting Started
Project initialization
To scaffold a project with NPM, run the following bash commands:
mkdir reader
cd reader
touch index.js
mkdir utils
npm init -y
Installation of modules
For this project, we need the following packages:
@mozilla/readability: This package will extract the text content from the desired page.
chalk: To output colored text to the terminal.
got: To get the website’s HTML code.
dompurify and jsdom: Remove malicious JavaScript from the page’s source code. As a result, this prevents XSS attacks.
yargs: To read command-line arguments.
To acquire these dependencies, run the following terminal command:
npm i @mozilla/readability chalk got dompurify jsdom yargs
When that’s done, it’s time to code our app.
Building The Project
Showing the help screen
If the user inputs the wrong argument or uses the --help
parameter, then our program should output the available options to the console.
To do so, write the following code in index.js
:
const argv = require("yargs/yargs")(process.argv.slice(2))
.option("site", {
alias: "s",
describe: "The website to fetch"
})
.option("file", {
alias: "f",
describe: "Save the HTML to disk"
})
.demandOption(["site"], "Please specify the website")
.help().argv;
console.log(argv.site, argv.file);
- Lines 2–4: Describe the
--site
option. It will have an alias of-s
. - Lines 6–8: Describe the
--file
argument. Its shorter alternative will be-f
. - Line 10: Tell Yargs the
--site
option will be mandatory. Without it, the program will not proceed. - Line 13: For debugging purposes, output the client’s arguments to the terminal.
This will be the output of the code:
Great! Our code works. Now, let’s code our utility functions.
Parsing the website
In your utils
folder, create a file called readerUtils.js
. Here, start by writing the following code:
const { Readability, isProbablyReaderable } = require("@mozilla/readability");
const got = require("got");
const chalk = require("chalk");
const fs = require("fs/promises");
const { JSDOM } = require("jsdom");
const window = new JSDOM("").window;
const DOMPurify = require("dompurify")(window);
- Line 7: Pass in our
JSDOM
instance toDOMPurify
. We will use this to sanitize incoming HTML code.
Next, add the following code in this file: async function parseURL(site) {
“const response = await got(site);
const doc = new JSDOM(DOMPurify.sanitize(response.body)); if (isProbablyReaderable(doc.window.document)) {
let reader = new Readability(doc.window.document);
let article = reader.parse();return { html: article.content, title: article.title, text: article.textContent };
} else {
return { error: “The site was not readable” };
}
}
Let’s deconstruct this code piece by piece.
- Line 2: Fetch the HTML from the desired page.
- Line 3: Sanitize this response for security reasons. This will give us clean HTML as a result.
- Lines 5–13: If the site is readable, pass this purified HTML to the Readability API and return the result.
- Lines 14–15: Otherwise, tell the client that an error occurred.
Saving HTML to disk
If the user uses the --file
option, we want our app to download the page’s HTML.
Append the following code into your utils/readerUtils.js
file:
async function saveToFile(html) {
await fs.writeFile("./content.html", html, "utf-8");
console.log("file saved to " + process.cwd() + "/content.html");
}
- Line 1: Our function will take in one argument. This is the HTML of the page.
- Line 2: Write the source code to the
content.html
file and save it. - Line 3: Finally, inform the user that the file is saved.
Sending parsed data
Now that we have parsed the data, all that remains for us is to render it to the terminal.
To do so, append this block of code in utils/readerUtils.js
:
async function sendData(site, save) {
const { html, title, text, error } = await parseSite(site);
if (!error) {
console.log(chalk.blue.underline(title));
console.log(text);
if (save) {
saveToFile(html);
console.log(chalk.red("contents saved to file"));
}
} else {
console.log(chalk.red.bold("The site could not be read"));
}
}
module.exports = sendData;
A few inferences from this code:
- Line 2: Perform object destructuring to get the
html
,title
,text
, anderror
properties from theparseSite
function.
To learn more about object destructuring, check out this article by Jim Rottinger - Line 4: Check if
error
isundefined
. In other words, see if our parsing operation has succeeded. - Lines 5 and 6: Display the
title
andtext
property to the terminal. We have used thechalk
module to render thetitle
in blue color. - Line 7: If the
save
boolean istrue
, invoke thesaveToFile
method and notify the user. - Line 12: If an error occurs, inform the client.
We’re almost done! In the next section, we will integrate our utility functions with our project.
Linking utilities with our app
Backtrack to your index.js
file and write the following code:
const sendData = require("./utils/readerUtils");
//further code removed..
(async () => {
await sendData(argv.site, argv.file);
})();
This code means that if the user runs the index.js
file, the sendData
method will run.
Let’s test this out!
Now, check if the content.html
contains the text of your chosen web page.
Great! Our code works without any flaws. In the next section, we will tell Node.js to make our app global via symlinks.
In the end, index.js
should look like this:
const sendData = require("./utils/readerUtils");
const argv = require("yargs/yargs")(process.argv.slice(2))
.option("site", {
alias: "s",
describe: "The website to fetch",
})
.option("file", {
alias: "f",
describe: "Save the HTML to disk",
})
.demandOption(["site"], "Please specify the website")
.help().argv;
(async () => {
await sendData(argv.site, argv.file);
})();
Creating symbolic links
As the first step, go to your package.json
file and change the name
field to reader
. Next, add the preferGlobal
and bin
folder like so:
{
"name": "reader", //name of the app
"main": "index.js",
"preferGlobal": true,
"bin": "./index.js",
//..further code
- Line 4: Tell Node.js that we can run this program from anywhere.
- Line 5: The
bin
property specifies that when the user runs thereader
command, the project should run theindex.js
module.
When that’s done, all that’s left is to create a symlink like so. This will tell NPM that our project can run from any location.
npm link
To check if our app is now working, try running the reader
command:
We got an error. This is because we now need to inform our operating system that this program will use Node.js as its interpreter. We can solve this by using a Shebang.
In index.js
, write the following line at the beginning:
And we’re done!
In the end, index.js
should look like this:
#!/usr/bin/env node
const sendData = require("./utils/readerUtils");
const argv = require("yargs/yargs")(process.argv.slice(2))
.option("site", {
alias: "s",
describe: "The website to fetch",
})
.option("file", {
alias: "f",
describe: "Save the HTML to disk",
})
.demandOption(["site"], "Please specify the website")
.help().argv;
(async () => {
await sendData(argv.site, argv.file);
})();
Conclusion
If you want to build powerful terminal apps with Node.js, then look no further than Yargs. It will let you get the job done with relative ease and speed.
Thank you so much for making it to the end! Happy coding!