Let's build a small CLI tool today to scaffold some JavaScript files!
We're going to be super minimal today (since I haven't ate dinner yet and still have work to do :crying:) and build out the equivalent to a "Hello, world!" project using the EJS template engine.
Update: This took too long and now I am super hungry... but about to upload and cook some dinner! Things are looking up!
Any concepts you learn today can be applied to scale.
You should have the usual suspects for a NPM/Yarn project and require a Nodejs version that supports ES6 basics like destructing, arrow functions etc (basically any).
Why templates?
There has been much debate about the usage of templates at the places I have worked.
One benefit is that you can use them to scaffold the basics for you without the need for basically any manual work - think scaffold new components, routes, SCSS files etc.
Some argue that this can be overkill or not flexible enough, however I have found recently in increasinly large codebases that have been around for a few years, templates have been a great "source of truth" for enforcing particular coding patterns in our codebase and a great way for onboarding new developers into these patterns (and avoid them from copying the old!).
Setting things up
Initialise a new project and add three dependencies that we will use:
# initalise with basic setup
yarn init -y
yarn add ejs fs-extra yargs-parser
We are going to use yargs-parser to parse through our CLI arguments, fs-extra as an extension to Node's internal FS (it comes with some fun extra tidbits that we will use) and we're going to use ejs to render out our templates!
Setting up our project to handle CLI arguments
Let's make a templates/starter
folder from the root of our project.
Once created, add a basic templates/starter/index.js
file with the following:
const fs = require("fs-extra")
const ejs = require("ejs")
const argv = require("yargs-parser")(process.argv.slice(2))
const main = () => {
try {
console.log(argv)
} catch (err) {
console.error(err)
}
}
main()
What we are basically doing is calling the main
function straight away and logging out argv
.
argv
is the result of our helper library Yargs Parser shifting through what we give it at the command line. If we run node templates/starter/index.js
, we should see the following:
{ _: [] }
We get an object with the key _
and an empty array. What's going on here? Without going to deep into things (see the docs for a better explanation), anything passed into the command line after the first two arguments ("node" and "templates/starter/index.js") will be stored in the _
array, and another other flag we pass ie --flag=3 --test friend
will be put under it's own key-value pair in the object.
Let's quickly test that now with node templates/starter/index.js hello --flag=3 --test friend
.
{ _: [ 'hello' ], flag: 3, test: 'friend' }
Perfect! We see our two flags add as their own key-value pair and anything else passed as an argument is added to the _
array!
We are going to use this to pass arguments to our simple template renderer.
Add the ability to read EJS files
Let's add the file templates/starter/main.ejs
file with the following:
const <%= fn %> = () => {
<% for (const arg of leftovers) { %>
console.log('<%= arg %>')
<% } %>
}
<%= fn %>()
Whoa, it looks like JS... but what is this funky syntax!?
That, my friends, is the EJS syntax. If we check the npm README, two of the features mentioned above are included:
- Control flow with
<% %>
- Escaped output with
<%= %>
Basically, we are running JS logic between the first feature we are using with the second feature resulting in our writing something to the template!
If this doesn't make sense just yet, do not fear. We are above to put it to good use.
Converting the template into something useful
Return to templates/starter/index.js
now and let's update our script to the following:
const fs = require("fs-extra") // note: not being used just yet
const ejs = require("ejs")
const argv = require("yargs-parser")(process.argv.slice(2))
const path = require("path")
const main = () => {
// 1. Welcome log
console.log("Generating template...")
try {
// 2. Destructure args from argv and set _ array to variable "data"
const { _: leftovers, out, fn } = argv
// 3. Add the args we want to use in the .ejs template
// to an object
const data = {
fn,
leftovers,
}
// 4. Create an empty options object to pass to the
// ejs.renderFile function (we are keeping defaults)
const options = {}
// 5. Check that the required flags are in
if (!out || !fn) {
console.error("--out and --fn flag required")
process.exit(1)
}
// 6. Set our ejs template file, nominating it to read the
// sibling "main.ejs" file sibling in the same directory
const filename = path.join(__dirname, "./main.ejs")
// 7. Run the renderFile, passing the required args
// as outlined on the package docs.
ejs.renderFile(filename, data, options, function(err, str) {
// str => Rendered HTML string
if (err) {
console.error(err)
}
console.log(str)
})
} catch (err) {
console.error(err)
}
}
main()
So now what is happening in our file? I've written them in the comments above, however here are them laid out together:
- Create a welcome log "Generating template..."
- Destructure args from
argv
and set _ array to variable "data" - Add the args we want to use in the .ejs template to an object
- Create an empty options object to pass to the ejs.renderFile function (we are keeping defaults)
- Check that the required flags are in (and exit the program if not)
- Set our ejs template file, nominating it to read the sibling "main.ejs" file sibling in the same directory
- Run the renderFile, passing the required args as outlined on the package docs.
As for seven, that argument from the EJS docs looks as follows.
ejs.renderFile(filename, data, options, function(err, str) {
// str => Rendered HTML string
})
The arguments are that we want to pass the template filename to render (it will be the file path), the data that we wish to render in that template (for us it will be the fn
and leftovers
we wronte in the main.ejs
file earlier), we are just leaving the options as an empty object and finally we get a callback that gives us an error and string as arguments.
Sweet! Let's try some calls out!
First, let's see what happens when we are missing the out
or fn
flag.
nodemon templates/starter/index.js hello world this is me --out=out.js
# Generating template...
# --out and --fn flag required
nodemon templates/starter/index.js hello world this is me --fn=main
# Generating template...
# --out and --fn flag required
Peaches, now if we add both flags in?
nodemon templates/starter/index.js hello world this is me --fn=main
We get the following logged out
Generating template...
const main = () => {
console.log('hello')
console.log('world')
console.log('this')
console.log('is')
console.log('me')
}
main()
Whoa! What's the JS? That's the str
variable being given back in our callback function from ejs.renderFile
! Super neat. Let's write that to a file!
Note: The spacing in the JS may look weird. I will address this later, but the solution will not be part of this tiny project.
Writing the file out
We are going to use our help fs-extra
module to write this out!
Note: Using
fs-extra
in the demo is actually a little OP (over-powered). It is a great library to know for sure though, so run with me on this one.
Update templates/starter/index.js
to look like the following:
#!/usr/bin/env node
const fs = require("fs-extra")
const ejs = require("ejs")
const argv = require("yargs-parser")(process.argv.slice(2))
const path = require("path")
const main = () => {
// 1. Welcome log
console.log("Generating template...")
try {
// 2. Destructure args from argv and set _ array to variable "data"
const { _: leftovers, out, fn } = argv
// 3. Add the args we want to use in the .ejs template
// to an object
const data = {
fn,
leftovers,
}
// 4. Create an empty options object to pass to the
// ejs.renderFile function (we are keeping defaults)
const options = {}
// 5. Check that the required flags are in
if (!out || !fn) {
console.error("--out and --fn flag required")
process.exit(1)
}
// 6. Set our ejs template file, nominating it to read the
// sibling "main.ejs" file sibling in the same directory
const filename = path.join(__dirname, "./main.ejs")
// 7. Run the renderFile, passing the required args
// as outlined on the package docs.
ejs.renderFile(filename, data, options, function(err, str) {
// str => Rendered HTML string
if (err) {
console.error(err)
}
// 8. Write file to --out path
const outputFile = path.join(process.cwd(), out)
fs.ensureFileSync(outputFile)
fs.outputFileSync(outputFile, str)
})
} catch (err) {
console.error(err)
}
}
main()
The only change now in the comments is at "Write file to --out path".
In those three lines, we are using path to join the current working directory (cwd) with the argument passed to our --out
flag.
Afterwards, we are using ensureFileSync
to ensure the paths to the file exists. We do this since if we passed --out=path/to/main.js
, we want to make sure the path
and to
folder exist, otherwise it will fail. Our fs-extra
function abstracts that difficulty for us!
Finally, we write that file out.
Run the following one last time:
node templates/starter/index.js hello world this is me --out=out.js --fn=main
If you now check the root directory, you will see that out.js
has been generated! Crazy stuff.
Without even doing anything, let's run node out.js
and behold the power of the output:
hello
world
this
is
me
Woo! We just generated a template that is valid JavaScript!
Checking out.js
, we see the str
that we saw before has been written to the file:
const main = () => {
console.log("hello")
console.log("world")
console.log("this")
console.log("is")
console.log("me")
}
main()
Hooray! We are the greatest!
It is important to note that the above has some quirky spacing. This can happen when outputting the templates from EJS, so I always follow up in my own work by using Prettier.io. I am very, very hungry, so I am not going to add that last tidbit in tonight, but I highly recommend checking it out and trying it for yourself!
While this has been a super, SUPER basic entry into the world of templating, you can run off with this knowledge now and go as crazy as you want! Groovy templateing projects such as Hygen have used the same methods to a larger extent. Go forth with your witchcrazy and wizardy.
Resources and Further Reading
Image credit: Karsten Würth
Originally posted on my blog. Follow me on Twitter for more hidden gems @dennisokeeffe92.
Top comments (0)