Sunteți pe pagina 1din 191

AUTOMATING

WITH NODE.JS
By Shaun Michael Stone

© First Edition UK - 2018


AUTOMATING WITH NODE.JS
1. AUTOMATING WITH NODE.JS
2. Introduction
1. Preface
2. End Goal
3. Structure
1. Part 1
2. Part 2
3. Book Coding Style
4. Code snippets
5. Prerequisites
6. Assumptions
7. Suggestions
3. Technical Overview
1. Technical Terms
1. CLI
2. Bash
3. Node.js
4. npm
5. npm dependency
6. Node Modules
7. ES6
8. Chrome V8
9. ESLint
10. JavaScript Promise
4. Part 1 - Examples
5. 001 - Arguments
1. Comparison
1. Bash Example
2. Node Example
2. Summary
6. 002 - Process
1. Summary
7. 003 – Query String
1. Summary
8. 004 – URL
1. Summary
9. 005 - OS
1. Summary
10. 006 - Open
1. Summary
11. 007 - File System
1. Summary
12. 008 - Zip
1. Summary
13. 009 - Colours
1. Summary
14. 010 - Question
1. Summary
15. 011 – Cloning
1. Summary
16. 012 – Branching
1. Summary
17. 013 – Retain State
1. Summary
18. 014 – Choose Template
1. Summary
19. 015 - Email
1. Summary
20. 016 - SMS
1. Summary
21. 017 - CSV
1. Streams
2. Summary
22. 018 - Shorten URL
1. Link Shorteners
1. Long URL
2. Short URL
2. Summary
23. 019 - Minimist
1. Summary
24. 020 - Build
1. Summary
25. Part 2 - Build Tool
26. Scenario
1. Development Meeting
1. Required Tech
2. Required Repositories
27. Build Tool Planning
1. repositories
2. src
1. commands
2. constants
3. creators
4. helpers
5. setup
6. nobot.js
7. Games List
28. Commander
29. Configuration
30. Constants
31. Helpers
32. Setup
33. Command - Setup
34. Command - Template
35. Command - Game
36. Creator - Rock Paper Scissors
37. End to end
38. Wrap up
AUTOMATING WITH NODE.JS
All rights reserved.

Every precaution was taken in preparation for this book. However, the author
assumes no responsibility for errors or omissions, or for damages that may result
from the use of information.

No part of this publication may be reproduced, stored in a retrieval system, or


transmitted, in any form, or by any means, electronic, mechanical, photocopying,
recording, or otherwise without the consent of the author.
Introduction
“I will always choose a lazy person to do a difficult job because a lazy
person will find an easy way to do it.”

– Anonymous
Preface
Being in the technical field presents itself with some fun… and some not so fun
tasks, we can all agree upon that. For the not so fun bits I try to filter out all of
the repetitive administrative work with something that I’m better known for as a
developer; writing code. Yes, I can be a bit lazy, but that’s because I value my
time. You’d be surprised at how much time goes into updating comments on Jira,
zipping files and emailing them to colleagues, updating configuration files, and
copying and pasting directories. Arghh, I need to stop listing these before I fall
asleep.

At a previous job I found myself doing things that could’ve easily been
automated by a script. Anything that feels repetitive should ring alarm bells for
you. If it doesn’t, then you should change your mindset now. Look at what you
do on a daily basis, think about it, read this book, and see if it changes your
perspective. Ideally it should.

On one weekend, after noticing repetitive tasks at work, I took note of the steps
involved in these tasks from start to finish, and set out to build a suite of
commands that would automate them. It proved to be efficient for both me and
some of my teammates, and it gave me more time to concentrate on Reddit… I
mean, my work.

I remember reading a funny story about a programmer who automated anything


that took longer than ninety seconds. His coffee machine was connected to the
local network, and he sent commands to it and timed how long it took for him to
walk over to pick up his freshly brewed cup. He even programmed it to send a
text message via Twilio to his wife if his machine was logged in fifteen minutes
after the end of the working day, saying he was working late that night.
Being fairly accustomed to using Bash scripting in the past on a Linux virtual
machine, I decided initially it was the right tool for what I wanted to achieve. I’d
need access to the file system. I could make use of the powerful searching
commands, store variables, read in standard input from the user, and use
conditional statements to decide on how to proceed. Perfect! But then I thought,
I wonder if I can achieve the same with Node JS?

I created the bash version initially, but digging further, I learned I could create
the project with npm directly. So I rewrote the project and presented it to the
team. The great news was that me and my team were allocated time to work on
the project during work hours, and one of the technical architects was keen to
integrate this tool into our workflow. Winning!

There are two ways we can implement the code you will be learning in this
book. You can treat it as a global list of commands that behave in the same way
as an alias on a terminal, or you can create a build tool that deploys your project,
taking care of all the tedious tasks you are used to doing.

This book will help you build something along the lines of what I have, but it’s
obvious to point out that every company’s workflow follows a different path and
set of business rules. Don’t worry though, section two of this book explains a
good way of identifying and detailing your workflow. Once you have identified
this path and the associated workflow, it should be pretty straightforward to
apply the knowledge acquired from this book.
End Goal
Let’s not beat around the bush. Once you’ve finished reading this book, you
should be able to create global commands and a working bespoke Node build
tool that allows you to automate the repetitive tasks you hate doing. This build
tool will be shaped around your company’s goals, or your own. Either way, the
intention is to make your life easier. Because life is hard enough as it is, right?
Structure
The book is structured into two parts:

Part 1

The first part is a collection of recipes, or building blocks that behave as


individual global commands. These can be used as you go about your day, and
can be called at any time to speed up your workflow or just for pure
convenience.

It begins with simple examples so you can get to know more about Node’s
standard library, then moves into more practical implementations. Each recipe
corresponds with the ‘examples’ directory found in the repository. All of the
examples can be found here: https://github.com/smks/nobot-examples

Part 2

The second part is a walkthrough of creating a cross-platform build tool from the
ground up. Each script that achieves a certain task will be its own command,
with a main umbrella command – usually the name of your project –
encapsulating them all.

Instead of using Gulp or Grunt, we will be using npm directly. The plan is to
keep this tool as lightweight as possible. I will be calling the project Nobot,
because I love naming projects, it’s an obsession. The implementation can be
found here: https://github.com/smks/nobot
Above shows a high level overview of the repositories we will make use of in
part 1 and part 2 of this book.

Book Coding Style

This book uses examples when working on a Mac and sometimes Windows. You
may occasionally see different output.

Some of the code examples may wrap onto the next line due to spacing
limitations.

The coding style follows AirBnb coding standards with ESLint. A few rules
have been overridden.

Code snippets

The book will have a lot of code snippets, as you’d expect.


Below is how I would demonstrate a code example. It begins with the name of
the script, followed by code snippets, and sections of content in between to
explain what is happening.

my-script.js

This is where I introduce you to what on earth is going on.

// start of script
console.log('this is part 1 of my-script.js');

Above is the first bit of code. This is where I bore you of the details of what’s
going on, or what will happen next.

console.log('this is part 2 of my-script.js');


// end of script

Below is the output of the script ‘my-script.js’

$ node my-script.js
this is part 1 of my-script.js
this is part 2 of my-script.js

Did you know

When I’m feeling a bit generous, I provide some explanations to relevant areas
associated with the code that we write.
Immutability in the context of programming - an immutable object is an
object whose state cannot be changed once created. This can be useful
because when you pass references of that object around, you can be rest
assured other procedures will not be cheeky and modify it.

Coding time

When you see this pencil icon, get ready, because it’s time to roll up your sleeves
and get coding!

Running a terminal command

When I need to use the CLI, it may show as a single line.


node index.js
For multi-line, I will prefix the first line with a dollar sign.
$ npm install fs-extra
fetching fs-extra...

Prerequisites

1. A Laptop or Desktop.
2. Internet access.
3. A GitHub account with SSH set up correctly.
4. Ensure you are using the latest version of git to avoid legacy issues.
5. Make sure you have Node installed. This can be downloaded here for your
Mac or Windows machine: https://nodejs.org/en. This book uses a
minimum version of: 6.9.1. At the time of writing, it should be fine to use
any version above this.
6. Motivation. Please stick with it. The time you invest now will pay off in the
long run.

Assumptions

It’s assumed you have a simple understanding of JavaScript and GitHub. A basic
idea of the CLI, and minimal - or no - experience of Node JS. All third party
implementations are correct at the time of writing. Node throughout the book
may be referenced as: Node, Node JS or Node.js but all references refer to the
same technology.

Suggestions

Please feel free to suggest or contribute on GitHub (Raise a pull request) to the
code examples as you see fit, or any possible typos in this book. You can also
contact me via any of the social networks.
GitHub - https://github.com/smks
Twitter - https://twitter.com/shaunmstone
Facebook - https://www.facebook.com/automatingwithnodejs
YouTube - http://www.youtube.com/c/OpenCanvas

Or connect with me on LinkedIn for business-related requests.

LinkedIn - https://www.linkedin.com/in/shaunmstone
Technical Overview
Just to make sure we’re all on the same page, here are some of the terms in this
book that you should understand before proceeding. Feel free to skip past them if
they’re already familiar to you.
Technical Terms
CLI

Command Line Interface - is a textual interface for interacting with your


computer. It is essentially a text-based application which takes in text input,
processes it, and returns an output. When interacting with the examples in this
book, you will need to open up a CLI and type in commands to make things
happen, rather than clicking buttons and tabs with a mouse. If you are on
Windows, this will be the Command Prompt (CMD) or PowerShell. If on Mac or
Unix like systems, it will be the Terminal.

Bash

Bash is a shell command processor that runs in a CLI. You can write Bash
scripts, and run them to execute a sequence of commands. You might first clone
a repository, create a branch, add a text file with content, stage the file, commit
it, and then push back to the remote repository all in one go. This would mean
you wouldn’t have to type out each command separately and is handy for
automation. The reason this book does not use Bash is because – at the time of
this writing – Windows does not fully support it, and we want our project to be
cross platform. So we will be writing JavaScript with Node so our scripts will
run on Windows as well.

Here is an example of a Bash script.

new-branch.sh

#!/bin/bash
# 0.0.1
git checkout master
git pull origin master
git checkout -b $1

Node.js

When you open up the CLI and type node , you are interacting with the node
executable installed on your machine. When you pass a JavaScript file to it, the
node executable executes the file. Node is an Event-driven I/O server-side
JavaScript environment based on Google’s V8 engine. It was designed to build
scalable network applications. It processes incoming requests in a loop, known
as the Event Loop, and operates on a single thread, using non-blocking I/O calls.
This allows it to support a high volume of concurrent connections.

Node has two versions available on their website to download:

LTS

It stands for Long Term Support, and is the version of Node offering support and
maintenance for at least 18 months. If you have a complex Node app and want
stability, this would be the choice for you. Support and maintenance is correct at
the time of writing.

Stable

Will have support for approximately 8 months, with more up-to-date features
that are released more often. Use this version if you don’t mind having to keep
updating your application so you can keep in line with ‘on the edge’ technology.

I have opted to use the LTS version so that companies who are tied down with
their version of Node will more likely be able to run the code examples and
implement the build tool demonstrated in this book.

npm
When you download Node, it optionally gets bundled with a package manager
called npm. It stands for Node Package Manager, and is the de facto for
managing your external dependencies. If you wanted to use a library such as
React or Angular, all you need to do is run npm install [package name] , npm would then
download/install the package into your project’s node_modules directory, so it’s
ready to be used in your app.

But this is not the only thing npm does after running this command. It also adds
a record of this package to your project’s dependencies list in your package.json .
This is very handy, as it means that your project keeps track of all its
dependencies. But it gets much better.

Please note: As of npm 5.0.0, installed modules are added as a dependency to


your package.json file by default. Before this version, you would have to add the
option --save to do this.

Any developer wanting to use your app (including yourself from another
machine) can install all dependencies with just one command: npm install . When
running this command, npm goes through your dependency list in your project’s
package.json file, and downloads them one by one into the node_modules directory.

To be able to use this dependency management goodness in a freshly created


project, all you need to do is run npm init . This command will take you through a
series of questions, and create an initial package.json file for you. This file, aside
from keeping track of your project’s dependencies, also has other information
about your project, such as: project name, project version, repository details,
author name, license, etc.

npm dependency

{organisation}/{package}
# examples
facebook/react
apache/cordova-cli
expressjs/express

Each dependency in the npm ecosystem has to have a unique identifier on the
public registry, otherwise this would cause conflicts. Think of it like checking
into a hotel, if you wanted room number seven because it’s lucky, but someone
else is already in there eating bread and olives, it means you’ll have to settle for
a different room. Same applies to package names. Anyone can create their own
package and publish it to the registry, just make sure the package name you
decide to use is available.

When I try to install the ‘express’ package, it will use the one created by the
Express organisation. I can’t publish a package called ‘express’ anymore as this
is already taken.

Node Modules

When we want to break bits of code into separate files, we treat them as
‘modules’. These modules can be imported into other modules. In this example,
I want to use code from the file b.js in my current file called a.js . Both files sit
in the same directory for the following example.

a.js

const b = require('./b.js');

console.log('From a.js: running code in the file b.js');

b();

So above, we are importing the code from the file below:


b.js

const arsenalFanChant = () => {


console.log('We love you Arsenal, we do!');
}
module.exports = arsenalFanChant;

Above shows module.exports . Whatever is assigned to this object from your


JavaScript file, can be retrieved by doing a require from another JavaScript file.

Now, when we run script a.js .

$ node a.js
From a.js: running code in the file b.js
We love you Arsenal, we do!

The require function does the following:

1. It finds the path to the file you pass in.


2. It determines the type of the file (JavaScript/JSON etc.).
3. Applies wrapping of the code to give file private scope (variables inside this
file are limited to the file only making the variables private, unless
exported).
4. Evaluates the loaded code.
5. Caches the import so that we don’t have to repeat these steps when
requiring the same file somewhere else.

The way require resolves a path is very smart:

If you specified a relative or absolute path, it will load the module from that
path. You don’t even have to write the module’s file extension, as the
require method will add it for you. You can even write a directory name,
and it will look for a file named index.js in that directory.
If you just passed a module name without a path, the require method will
use a searching algorithm to find your module. It will look through its core
modules, then traverse up the directory tree looking through node_modules
directories, and finally, if it still hasn’t found your module, it will look for it
in the directories specified in its directory paths array.

If we try and require a file, but it does not have anything exported, then its value
will be undefined . To expose the function arsenalFanChant we assign it to module.exports .

If you’ve used a language like Java, you would have come across a similar idea
of importing packages.

These modules are used to avoid scope conflicts and break our code up so it’s
more maintainable. Ain’t nobody got time for thousands of lines of code in one
file!

ES6

ECMAScript 6 or ECMAScript 2015 is a significant update to the JavaScript


language, incorporating many useful features and syntactic sugar. We will be
using some of these features in the scripts we write. A list of these features and
examples can be found on this website. http://es6-features.org. Node JS supports
most of the new standard (not all) at the time of writing, so there won’t be a need
to transpile our JavaScript with Babel in this book.

// examples of syntactic sugar from ES6


class Homer {
static speak() {
console.log("doh!");
}
}
const obj = { x, y };
const odds = evens.map(v => v + 1);

Chrome V8
Chrome V8 is a JavaScript engine developed by Google, which is used by the
Chrome browser and Node.js (amongst other applications). Powered by C++, it
compiles JavaScript to native machine code (supporting many CPU
architectures), and then executes it. This means that it benefits from a much
faster execution compared with traditional techniques such as real-time
interpretation. V8 also handles memory allocation for objects, and if an object in
memory is no longer needed, an operation known as garbage collection is
applied to remove it.

ESLint

The JavaScript we write should follow a certain standard: Spacing between if

statements and blocks, indentation, variables and functions should all remain
consistent. It’s something I think is very important as it alleviates my OCD. In
this book we’ll be following AirBnb coding standards with a few overrides
declared in a .eslintrc file. ESLint will flag up anything that doesn’t abide by
these standards. This will be installed as a dependency via npm. The file below
will be incorporated into our projects.

.eslintrc

{
"extends": "airbnb",
"rules": {
"no-console": 0,
"linebreak-style": ["error", "unix"],
"no-use-before-define": ["off"],
"comma-dangle": ["error", "never"],
"global-require": ["off"],
"import/no-dynamic-require": ["off"]
},
"env": {
"browser": false,
"node": true
}
}
In our projects, we can run the following command to check the rules are being
followed.
npm run lint

JavaScript Promise

You pass your friend ten pounds and say, ‘Hey buddy! Can you get me a pizza?’.
Because he is such a good friend, he says, ‘Yes. I promise.’

function friendGetsPizza() {
return new Promise((resolve, reject) => {
// ... do things to get pizza

// Scenario 1 - He got the pizza


resolve({ name: 'Margherita' });

// Scenario 2 - He did not get the pizza


reject('cannot be trusted');
});
}

Then, when he has successfully got it, I want to eat it.

friendGetsPizza()
.then((pizza) => {
console.log('now eating pizza', pizza.name);
});

But… what if he is a terrible friend and doesn’t come back, and eats the pizza
himself? He did NOT fulfil his promise.

friendGetsPizza()
.then((pizza) => {
console.log('now eating pizza', pizza.name);
})
.catch((e) => {
console.log('take friend out of life and move on because he', e);
});

In this case, the catch function will be called rather than the then function,
because the promise was rejected.
Part 1 - Examples
In GitHub, make sure you have created an account if you want to write these
scripts from scratch. Alternatively, you can browse the completed scripts sitting
on the master branch of the repository mentioned below.

1. Fork the repository under your own name. This can be done by clicking the
Fork button on the GitHub page here https://github.com/smks/nobot-
examples. So rather than the repository being under my own name ‘smks’ it
will be under yours instead.
2. Clone your forked repository to your own machine using
git clone [your-forked-repository-url]

3. Change into the root of the nobot-examples repository you’ve just cloned.
cd nobot-examples .
4. Switch to branch develop by running the command git checkout develop .
5. Run npm install .
6. Follow along with following examples 001-020 by writing code.
7. Happy coding!

Please note: Whenever you see a file called config.example.json you need to make a
copy of it and rename to config.json . This can be done automatically for all
examples by running npm run setup in the root of the repository.
001 - Arguments
Because we will be interacting with the CLI, there is the requirement to pass our
own input. With Bash, arguments are passed by number and prefixed with a
dollar sign.

Please note: There is no need to write out the Bash examples, they are used for
demonstration only.
Comparison
Bash Example

Before we try this out in Node, let’s see how this would look in Bash. In this
example, we are running the Bash script and passing a name to it as an argument.
Since this is the first argument, we can access it in the script using $1 .

$ bash my-bash-script.sh 'Fred Flintstone'


... running script

my-bash-script.sh

name="$1"
# name is now - 'Fred Flintstone'

Node Example

We can’t do this with Node as conveniently. Instead we can use a native object
called process , which includes – as the name would imply – all the values related
to the process of the script. For the time being we just want to obtain the
arguments.

Let’s see what happens when we do this using Node. Suppose we have a script
named my-node-script.js

node my-node-script.js 'Fred Flintstone'

When running the above, we are initiating a new process. So what is in this
process object? An array called argv .
my-node-script.js

const args = process.argv;


/*
args array is now
[
'/Users/shaun/.nvm/versions/node/v8.7.0/bin/node',
'/Users/shaun/Workspace/nobot-examples/examples/001/hello.js',
'Shaun'
]
*/

As you can see above, we have three elements in the array. The first one is the
full path of the node executable, the second one is the full path of the script we
are executing, and the third one is the first argument we passed from the CLI. It’s
a common misconception to think the first argument you pass ‘Fred Flintstone’
is referenced as the first element of the array. So remember that your arguments
will start from the third index of the process array onwards. Usually there is no
need for the first two elements of the process array, so let’s remove them.

const args = process.argv.slice(2);


/*
args is now
[
'Fred Flintstone'
]
*/

Now that we have removed the array elements we don’t need, we are left with
the arguments we passed to the script.
Now it is time to code. Write out the following.

examples/001/hello.js

const args = process.argv.slice(2);


const [name] = args;

if (name === undefined) {


console.error('Please pass a name, e.g. node hello.js Shaun');
process.exit(0);
}

console.log(`Good day to you, ${name}`);

As discussed earlier, we are removing two elements that we don’t need from the
process array. Using some ES6 syntactic sugar, we can grab the first element and
assign it to a constant called name . This is the same as doing this:

const name = args[0];

The if statement is to make sure the user has actually passed an argument to
our script. If it’s undefined it means the user called the script without passing
any arguments.

If this is the case, we want to exit the script by calling exit on the process object.
This means that it won’t reach the final console log saying Good day to you, ${name} .
Instead, it will print the following and then terminate the script.
Please pass a name, e.g. node hello.js 'Shaun'

Now let’s see what happens when the user runs our script and passes an
argument.
$ node examples/001/hello.js 'Shaun'
Good day to you, Shaun
Summary
We created our first Node script, which takes the user’s name as an argument and
prints it to the CLI.

The reason we can do node filename.js is because once we’ve installed Node, our
operating system will have an executable identified as node, stored globally so
you can call it from any location in the CLI. The JavaScript file hello.js , which
contains our script, gets passed to the Node executable along with the string
‘Shaun’. The string is treated as an argument, and it will be passed to the
process.argv array. The script will then use the string to greet the user by name.
002 - Process
It’s important to understand that when you run a script, it’s being treated as its
own process. You may have many processes running at the same time on your
machine, and each has their own unique identification called a Process ID (pid).
We’ll look at how we can pass over responsibility to a separate process later on,
but for now, here is how we can output the Process ID.

examples/002/process.js

console.log(`This process is pid ${process.pid}`);

Here is the output.

This process is pid 5436

How about adding a callback when the current process has ended? So when the
script exits, this will fire.

process.on('exit', (code) => {


console.log(`The process has now finished, exiting with code: ${code}`);
});

Here is the output.

The process has now finished, exiting with code: 0


Now let’s looks into standard input (stdin) and standard output (stdout) streams.

stdin & stdout The standard input (stdin) refers to when the user types into
the terminal and then submits the data - or chunk to be processed. So the
process is reading information from you. The standard output (stdout) is
what is returned back to you, the user.

To output something using standard output, we use process.stdout.write . This method


is a simpler version of console.log . When using process.stdout.write , we have to
remember to add \n at the end of the string to signify a line break, as unlike
console.log , it does not do that for us.

Let’s make use of some process object methods. We’ll also compare it to using
console.log . We’ll begin with process.stdout .

process.stdout.write('Hello, I am writing to standard output\n');

process.stdout.write(`Current working directory: ${process.cwd()}\n`);

console.log(`This script has been running for ${process.uptime()} seconds`);


Here is the output.
$ node examples/002/process.js
Hello, I am writing to standard output
Current working directory: /Users/shaun/Workspace/nobot-examples
This script has been running for 0.064 seconds

Alright, it’s time to use standard input. We’ll start by asking the user - using the
standard output - to type something. We will then apply UTF-8 so it applies the
correct character encoding.

To read input from the user, we start by listening to the stdin readable event
listener. This event fires when the user presses enter. When the event fires, in our
event listener, we can use process.stdin.read to read the chunk of input that the user
has typed.

Finally, we check if the input chunk is not null, and in that case we output it to
the user and exit the process.

process.stdout.write('Type something then hit enter: \n');

process.stdin.setEncoding('utf8');

process.stdin.on('readable', () => {
const chunk = process.stdin.read();
if (chunk !== null) {
process.stdout.write(`You wrote: ${chunk}`);
process.exit(0);
}
});

Here is the output.


$ node examples/002/process.js
Type something then hit enter:
Hello!
You wrote: Hello!
The process has now finished, exiting with code: 0
Summary
In this section we’ve learned how to use standard input and standard output. We
were able to do this using the global object process , which provides information
about and control over the current process, such as reading standard input,
writing output, and retrieving metrics like uptime and the Process ID.
003 – Query String
I’m going to assume that your company makes use of a project management tool
such as Jira or Target Process. Our application will want to interact with the
APIs of these tools when it wants to inspect a ticket, or add a comment based on
certain business rules. Web based APIs (such as Jira’s JQL search feature) may
require passing GET parameters. These parameters are appended to the endpoint
we are targeting, in what is called the Query String. They are formatted as
follows: ?param1=value1&param2=value2 etc.

We could write a function that would take an object with the key-value pairs for
the Query String and build it ourselves, but lucky for us, Node’s standard library
has a module called querystring , which does just that. All we need to do is pass an
object of key-value pairs (representing the Query String parameters) to the
querystring.stringify method, and it will return our arguments formatted as a query
string.
API is an abbreviation for Application Programming Interface. Company
(A) may want to share their product items with another company (B).
Company (A) decide to use an API to which Company (B) can make a
request to retrieve the product details. This API would allow Company (B)
access only if they are authorised. It is down to the API implementation to
set these rules. One of the ways to access ticket information in Jira is to use
Basic Authentication, which combines your username and password and
then encodes it with base64. There are alternatives, but that’s outside the
scope of this book.

examples/003/build-querystring.js

const querystring = require('querystring');

// https://jira.my-company.com/rest/api/latest/search?jql="assignee=shaun.stone&startAt=2&maxResults=2"

const apiHost = 'https://jira.my-company.com/rest/api/latest/search?jql=';

const jqlParams = {
assignee: 'shaun.stone',
startAt: 2,
maxResults: 2
};

const apiUrl = `${apiHost}"${querystring.stringify(jqlParams)}"`;

console.log(`My JQL api call is: ${apiUrl}`);

Here is the output.

$ node examples/003/build-querystring.js
My JQL api call is: https://jira.my-company.com/rest/api/latest/search?
jql="assignee=shaun.stone&startAt=2&maxResults=2"

You can alternatively do the reverse and create an object from a query string, as
the following example illustrates.

examples/003/parse-querystring.js

const querystring = require('querystring');

const url = 'http://www.opencanvas.co.uk?myName=Shaun&myAge=28&comment=Yes+I+am+getting+old';


const parsedUrl = querystring.parse(url.substring(url.indexOf('?') + 1));

console.log(`Hi my name is ${parsedUrl.myName}`);


console.log(`I am ${parsedUrl.myAge}`);
console.log(`Oh and... ${parsedUrl.comment}`);

Here is the output.


$ node examples/003/parse-querystring.js
Hi my name is Shaun
I am 28
Oh and... Yes I am getting old
Summary
Dealing with a variety of APIs will give us a lot of control with automating
tasks. When constructing a URL with a query string, this will help us with
formatting.
004 – URL
If you need to break down a URL, consider using the url module.

examples/004/url.js

const url = require('url');

const args = process.argv.slice(2);


const [urlEntered] = args;

if (urlEntered === undefined) {


console.error('Please pass a URL e.g. https://www.google.co.uk/search?q=stranger+things');
process.exit(0);
}

const {
protocol, slashes, host, query, href
} = url.parse(urlEntered);

console.log(`Using protocol: ${protocol}`);


console.log(`Using slashes: ${slashes}`);
console.log(`Host: ${host}`);
console.log(`Query: ${query}`);
console.log(`HREF: ${href}`);

Here is the output.


$ node examples/004/url.js "https://www.google.co.uk/search?q=stranger+things"
Using protocol: https:
Using slashes: true
Host: www.google.co.uk
Query: q=stranger+things
HREF: https://www.google.co.uk/search?q=stranger+things

At this point, once you have parsed the URL, you could use the querystring
module explained in the previous chapter to parse the query string down from a
string to an object of key value pairs.
Summary
When we want to break down URLs and extract specific segments, the url

module can help us do that with ease.


005 - OS
Node provides us with an os module that allows us to dig into the hardware
specifications of our machine. When building my tool, I defaulted to installing it
in the home directory, and the good thing about the os module, is that you can
get a reference to the home directory path for Windows, Mac or Linux by doing
the following.

examples/005/os.js

const os = require('os');

const homeDirectory = os.homedir();


console.log(`Your home directory is: ${homeDirectory}`);

Here is the output:

Your home directory is: C:\Users\shaun

If you need to identify your OS platform to perform separate tasks for Windows
and Mac respectively, you can do so by calling the platform function.

const osPlatform = os.platform();


console.log(`The OS platform is: ${osPlatform}`);

Here is the output:


The OS platform is: win32

If you’d like to identify the CPU installed, this could be a useful tool for high
performance computing when trying to distribute computation between more
than one core. Or… you could just show off your specs.

examples/005/os.js

const cpuCores = os.cpus();


const coreCount = cpuCores.length;
const cpuModel = cpuCores[0].model;

console.log(`I can see your ${cpuModel} has ${coreCount} cores.`);

Here is the output:

I can see your Intel(R) Core(TM) i7-6700K CPU @ 4.00GHz has 8 cores

Yes, I did recently get an upgrade (cheeky wink). No, I didn’t include this
example so you would know… okay I did, I’m not ashamed.

The example output below is from a Mac.

$ node examples/005/os.js
Your home directory is: /Users/shaun
The OS platform is: darwin
I can see your Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz has 4 cores.

Nope… not as impressive.


Summary
Mac and Windows file systems are different, and this could be problematic when
you want to build a cross-platform tool. Fortunately, there is a new package that
can handle this more elegantly. We will make use of it in future examples.
006 - Open
Rumour has it that not everyone uses the same operating system. Different CLIs
have different commands. For example, the command to list the content of a
directory on Unix-based operating systems is ls , whereas on Windows it’s dir .
In this example I will show you how we can support both Windows and Mac
when opening a browser with a URL using the os module shown in the
previous recipe.

This time though we need to spawn a new child process to handle the execution
of opening the browser. Creating a new sub process requires enough memory in
which both the child process and the current program can be executed.

Child Processes A process is an instance of a computer program running on


your operating system. It contains the program code and its activity. A child
process is a process created by a parent process. Node allows you to create a
child process in four different ways: spawn , fork , exec and execFile .
The spawn function launches a command in a new process, and we can pass
it command arguments. It’s good to note that spawn does not create a shell to
execute the command we pass it, making it more efficient than exec . Also
exec buffers the command’s generated output and passes all of the output to
a callback function. Use exec if you know the data returned back is small as
opposed to spawn that streams back data in chunks. Use execFile if you want
to execute a file without using a shell (apart from Windows where some files
cannot be executed). Lastly fork is a variation of spawn, the difference is
that it invokes a specified module with an established communication
channel that allows messages to be passed back and forth between parent
and child.

examples/006/open.js

const { platform } = require('os');


const { exec } = require('child_process');
const WINDOWS_PLATFORM = 'win32';

const osPlatform = platform();


const args = process.argv.slice(2);
const [url] = args;

By identifying the platform, we can alternate which command to execute. The


user can pass the URL they want to visit. Once we have done some conditional
logic to set the command, the sub process can be executed. If a URL is not
passed, the script gets aborted by exiting the process.

Please note: Ensure you have installed Google Chrome for this script to work as
expected.

if (url === undefined) {


console.error('Please enter a URL, e.g. "http://www.opencanvas.co.uk"');
process.exit(0);
}

let command;

if (osPlatform === WINDOWS_PLATFORM) {


command = `start microsoft-edge:${url}`;
} else {
command = `open -a "Google Chrome" ${url}`;
}

console.log(`executing command: ${command}`);

exec(command);
// On windows machine, Edge browser opens URL
// On mac machines, Chrome browser opens URL

Here is the output.


$ node examples/006/open.js "http://www.opencanvas.co.uk"
executing command: open -a "Google Chrome" http://www.opencanvas.co.uk
Summary
This script could be extended to support other platforms if necessary. It could be
useful for opening URLs once you have committed your work and pushed to
GitHub for example. The tool could provide you with the pull request link and
ask if you want to open your browser to see the file changes before merging your
pull request.
007 - File System
Using the file system to handle files will be one of the key aspects of our build
tool. Luckily this comes out of the box with Node. You’ll most likely have to
read in data from configuration files, write new or update existing files. With the
projects I worked on, there was a configuration file in JSON format, and some of
the values needed to be replaced. I needed to update the ID, the date range, what
service requests to call and so on. Here is a watered-down example of a
configuration file for a game campaign.

examples/007/data/example-config.json

{
"projectId": 234789,
"game": "january-2019-wheres-wallis",
"startDate": "2019-01-01T00:00:00",
"endDate": "2019-01-08T00:00:00"
}

When you are working on some form of template that you want to be
configurable to the scope of your project, using a separate JSON file that
contains data separated from the source code can be a good approach to take. We
want to obtain these values in a script, and we could do something like the
following:
Please note: This is a unrecommended example of reading JSON.

examples/007/bad-example/read-json.js

const fs = require('fs');

const readJson = file => new Promise((resolve, reject) => {


fs.readFile(file, { encoding: 'UTF-8' }, (err, data) => {
if (err) {
reject(err);
}
resolve(JSON.parse(data));
});
});

// Usage
readJson(`${__dirname}/../data/example-config.json`)
.then(config => console.log(config.projectId));

Here is the output.

$ node examples/007/bad-example/read-json.js
234789

But… there is no need. In the same way that we use the require function to
import a JavaScript file, it’s possible to require JSON files as well. And as an
added bonus, it’s automatically parsed as well. It’s as straightforward as this
code:

examples/007/read-json.js

const { projectId, startDate, endDate } = require('./data/example-config');

console.log(`The ID of the project is: ${projectId}`);


console.log(`The start date of the project is: ${startDate}`);
console.log(`The end date of the project is: ${endDate}`);

Here is the resulting output.


$ node examples/007/read-json.js
The ID of the project is: 234789
The start date of the project is: 2019-01-01T00:00:00
The end date of the project is: 2019-01-08T00:00:00

Okay, so here’s the scenario. The values you need to place into the configuration
file actually exists in a Jira ticket. So instead of updating the values manually,
you could pass a ticket/issue number to the script, and it would go and fetch the
values and then use them to overwrite the values in the configuration file.

Please note: In this example we will use mock data from


examples/007/data/mock-jira-data.json for brevity. In a real world example, we would do
something like a HTTP request to an API. This will be done later on in part 2.

examples/007/helpers/get-jira-data.js

const jiraData = require('../data/mock-jira-data');

// Imagine this data being retrieved from Jira and transformed


const fetchDataFromJira = ticketNumber => jiraData[ticketNumber];

module.exports = fetchDataFromJira;

When creating an object and writing to a file, we first have to convert it to string
form by using JSON.stringify .

This helper will deal with writing to configuration files.


examples/007/helpers/write-json.js

const fs = require('fs');

const JSON_WHITESPACE = 4;

const writeJson = (file, contents) => new Promise((resolve, reject) => {


fs.writeFile(file, JSON.stringify(contents, null, JSON_WHITESPACE), (err) => {
if (err) {
reject(err);
}
resolve(`${file} written`);
});
});

module.exports = writeJson;

Now using the Jira data, let’s create a configuration file.

examples/007/write-config.js

const path = require('path');


// helpers
const writeJson = require('./helpers/write-json');
const getJiraData = require('./helpers/get-jira-data');

const args = process.argv.slice(2);


const [ticket] = args;

const CONFIG_FILE = 'config.json';


const jiraTicket = ticket || 'GS-1000';
const jiraData = getJiraData(jiraTicket);

if (jiraData === undefined) {


console.log(`JIRA ticket ${jiraTicket} not found`);
process.exit(0);
}
const newConfigFile = path.join(__dirname, 'data', CONFIG_FILE);

writeJson(newConfigFile, jiraData)
.then(msg => console.log(msg))
.catch((err) => { throw err; });

You should notice that the Jira ticket number can be passed by the user.
Hopefully this is building up a picture for you.
$ node examples/007/write-config.js "GS-1000"
/Users/shaun/Workspace/nobot-examples/examples/007/data/config.json written

Try it for yourself! The available mock Jira ticket numbers defined in
mock-jira-data.json are: ‘GS-1000’, ‘GS-1005’, ‘GS-1007’ and ‘GS-1020’.

path module allows you to work with file paths in Node. Each operating
system uses a different file path separator. If you are on a Windows machine,
you’ll find that it uses backslashes, whereas Unix-like Operating systems
like Mac use forward slashes. To avoid paths not resolving, we can make use
of the path.join function to join segments of a URL. To use this function, we
simply pass to it the segments of the path, and it returns to us the built path
appropriate to our operating system.
Making use of path is good practice, but what if we want to know the current
directory path dynamically?

__dirname This is a global variable holding the full path to the current
directory. When used in conjunction with path.join , it allows us to create new
files and/or directories in our current directory.
Summary
Writing and updating configuration files should be as automated as possible. If
you have the data sitting on a ticket somewhere and it needs to be pulled into
your project, why do it manually? It can also minimise the risk of data entry
mistakes. You just need to make sure the person entering the data in the ticket
knows what he is doing.
008 - Zip
Up until now we have been making use of Node’s standard API library such as
the process , path and os modules. If we want to extend our choice to use more
libraries that provide further functionality, we can turn to npm. The npm
ecosystem is huge, and if you do a search for a library with certain functionality,
chances are you’ll find it.

Here you can see the most used packages


https://www.npmjs.com/browse/depended. Feel free to explore what is available!

The use case here is as follows: a member of your team has requested that you
zip up a text file and an image. To accomplish this, we will use an external
dependency identified as archiver .

Please note: As you might recall from the Technical Overview section, to install
this dependency, you’d normally need to run npm install archiver in the terminal.
This command will install the dependency into node_modules and add it to the
project’s dependencies in its package.json file. However, you will not need to do
this now, as the nobot-examples project that you are using already has these
dependencies listed in its package.json , and you have run npm install before you
started (and if you haven’t, there is no time like the present). As a reminder,
npm install looks at your project’s package.json file and installs all the dependencies
listed in it.

To use the archiver npm module, after it’s installed, all we need to do is require it,
just like we’ve been doing with the Node’s standard library modules. The require
method will look in the node_modules and find a directory called archiver and
use the source code found inside this directory.
Streams are a pattern that makes a conversion of huge operations and breaks
it down into manageable chunks. If you were eating a big birthday cake, you
wouldn’t try to scoff the entire thing into your mouth, you would instead cut
small slices. Or pick at it with your fingers without anyone noticing you’ve
had so much already. Yes, Vijay. I did see you do it… many times.
examples/008/zip.js

const archiver = require('archiver');

So just to recap, if we didn’t have this package installed, Node would throw an
error saying that this module does not exist; but we have, so it won’t give us an
earful.

For this example we’ll also need to use the fs and path modules from Node’s
standard library, so let’s require these as well.

const fs = require('fs');
const path = require('path');

First off, there has to be a write stream to a filename of our choice. Then we set
up our archiver, declaring it will be a zip file, and the compression method will
be zlib - a library used for data compression.

Please note: The idea of streams will be covered in a later chapter.

const ZLIB_BEST_COMPRESSION = 9;
// create a file to stream archive data to.
const zipPath = path.join(__dirname, 'files.zip');
const output = fs.createWriteStream(zipPath);
const archive = archiver('zip', {
zlib: { level: Z_BEST_COMPRESSION }
});

When the file stream has closed, let’s run a callback that logs out the total bytes
and informs the user that the archiving has completed.

// listen for all archive data to be written


output.on('close', () => {
console.log(`Total bytes: ${archive.pointer()}`);
console.log('archiving has now finished.');
});
If an error is to occur, we’ll need to catch it in another callback and throw the
error so we can identify what went wrong.

// good practice to catch this error explicitly


archive.on('error', (err) => {
throw err;
});

Finally, we pipe the file stream to the archiver, and append the files we would
like to add, then finalise the operation. The append function is taking in a read
stream from both the text file and our image, so that when we open the zip, we
should see the files inside.

archive.pipe(output);

// add files (read the copy.txt and logo.jpg and output with different names)
const textPath = path.join(__dirname, 'copy.txt');
const logoPath = path.join(__dirname, 'logo.jpg');
archive.append(fs.createReadStream(textPath), { name: 'content.txt' });
archive.append(fs.createReadStream(logoPath), { name: 'nobot.jpg' });

// finalize the archive (ie we are done appending files but streams have to finish yet)
archive.finalize();

Here is the output.

$ node examples/008/zip.js
Total bytes: 105156
archiving has now finished.
Summary
I managed to reduce the file size by about a half. How awesome is that? If you
need to send large files over email or need to transfer to external media, then it
would be ideal to have a script to zip it up for you beforehand.
009 - Colours
Yes, I am from the UK, and that is why I have to be so blooming awkward and
spell colours like I do. When we log our output, it can be useful to differentiate
what type of message is being displayed to the user. We can do that using the
npm colors package, by giving our output a different colour based on the type of
message we are displaying. If something went wrong; use red, if something went
right; use green. If the user needs to be warned about something; use yellow.
Let’s give it a try.

examples/009/log-color.js

require('colors');

You will see when I require colors at the top, there is no need to assign it to a
variable or constant, because once it’s loaded in, the library takes effect - it
extends String.prototype .

console.log('This is a success message'.green);


console.log('This is a warning message'.yellow);
console.log('This is a error message'.red);

The colors package gives us a handful of colours that can be applied to any
string. This allows us to give semantic feedback to the user.
To take this even further, it would be good to have a helper function which
outputs messages in a specific format and colour based on the type of message.
To do that, let’s start by creating a module with constants for the message types.

examples/009/constants/message-types.js

const MESSAGE_TYPES = {
SUCCESS: 'success',
WARNING: 'warning',
ERROR: 'error'
};

module.exports = MESSAGE_TYPES;

Now let’s create the module with the logging helper function. Our helper
function will accept two arguments: the message and the message type
(optional). It will construct the formatted and coloured message and invoke
console.log to output the message to the screen.

We need to include the MESSAGE_TYPES constants in the helper as they will be needed
for two script files.
examples/009/helpers/log.js

require('colors');
const { ERROR, WARNING, SUCCESS } = require('../constants/message-types');

module.exports = function log(message, type) {


let colorMessage;
switch (type) {
case ERROR:
colorMessage = `[ERROR] ${message.red}`;
break;
case WARNING:
colorMessage = `[WARNING] ${message.yellow}`;
break;
case SUCCESS:
colorMessage = `[SUCCESS] ${message.green}`;
break;
default:
colorMessage = `[INFO] ${message}`;
}
console.log(colorMessage);
};

Now we can use the log function by requiring it at the top and calling it with the
message and its type.

examples/009/colors.js

const log = require('./helpers/log');


const { ERROR, WARNING, SUCCESS } = require('./constants/message-types');

log('This is a success message', SUCCESS);


log('This is a warning message', WARNING);
log('This is a error message', ERROR);
log('This is an info message');
The output demonstrated below will not show the colours used, but when you
run it yourself, you will see it.
$ node examples/009/colors.js
[SUCCESS] This is a success message
[WARNING] This is a warning message
[ERROR] This is a error message
[INFO] This is an info message
Summary
It goes without saying that using this in your application gives clearer feedback,
thus makes it more user-friendly.
010 - Question
Node has a native module known as readline , which builds upon the stdin and
stdout, and provides an interface – or wrapper - between them. It allows us to
read one line at a time, which means we can easily prompt and save answers
provided by the user. Let’s see if we can make use of this module to create a
project directory based on the user’s input.

For this project we’ll need the file system module, the readline module, the
standard input & output from the process object, and the path module. All native
modules to Node.

examples/010/question.js

const fs = require('fs');
const readline = require('readline');
const { stdin, stdout } = require('process');
const path = require('path');

First, let’s set up the interface that links standard input and output.

const interfaceInstance = readline.createInterface(stdin, stdout);

Next, we’ll use the interface’s question method to ask the user a question
(output) and link the callback function which will deal with the user’s answer
(input).

interfaceInstance.question('What is the name of your project? ', onProjectInput);

Now we need to define our callback function.

Let’s add it at the top, right after the require statements. We’ll make it a concise
function. As it’s always good practice to clean up after yourself, the function will
close the interface and destroy the standard input so nothing else can be inputted.
Finally we call another function with the user input (the project name), which
will deal with creating the project directory and handling errors.

const onProjectInput = (name) => {


interfaceInstance.close();
stdin.destroy();
createProjectDirectory(name);
};

Right after this function, let’s define the createProjectDirectory function which we are
calling. This function receives the user’s input; the desired project name. It starts
by trimming the input to get rid of leading and trailing spaces. It then does some
error handling, to prevent attempting to create a directory without a name or for
a directory that already exists. Of course in a real-world situation, this would
need to be stricter, which a regular expression could help with. For our toy
example, we’ll stick with our naive error handling.

Once we’re happy with the input, we go ahead and create the directory using
fs.mkdirSync . This function will create a directory synchronously.

const createProjectDirectory = (enteredName) => {


const name = enteredName.trim();
if (name === '') {
console.log('Cannot create a project without a name');
process.exit(0);
}
const projectPath = path.join(__dirname, name);
if (fs.existsSync(projectPath)) {
console.log(`${name} already exists`);
process.exit(0);
}
console.log(`creating a new project called ${name}`);
fs.mkdirSync(projectPath);
};

What does this look like in action?


$ node examples/010/question.js
What is the name of your project? yogi-bear
creating a new project called yogi-bear
Summary
The readline module is fantastic for creating step-by-step feedback for the user.
When creating our CLI application, we want to make it interactive, like a
questionnaire in case the user didn’t provide the initial arguments. It makes it
more user-friendly. Because it’s good to be friendly!
011 – Cloning
In this chapter we’re going to learn how to programmatically clone repositories.

Here it comes, a new dependency. This one is called shelljs , and allows us to
execute commands on the CLI through our scripts.

We require it at the top of our script, as well as the colors package, the native
path module and a pre-made config.json file containing URLs of the repositories
we would like to clone.

Please note: As mentioned earlier in the book, you will find a config.example.json .
This will need to be copied and created as config.json . The reasoning behind this
is that some examples will have sensitive data that you don’t want to commit and
push to a public repository. It is especially important when we deal with the
email and SMS examples later in this book.

In your own config.json file, you can add as many repositories as you’d like to the
array, and then run the script to see them be cloned.

examples/011/clone-repositories.js

require('colors');
const path = require('path');
const shell = require('shelljs');
const { repositories } = require('./config');
const repositoriesDirectory = path.join(__dirname, 'my-repositories');

Here is the output of the repositories directory path.


/Users/shaun/Workspace/nobot-examples/examples/011/my-repositories

Our config.json file looks like this:

examples/011/config.json

{
"repositories": [
"https://github.com/smks/nobot-repo-1",
"https://github.com/smks/nobot-repo-2"
]
}

A function can now be constructed to take a destination path and an array of


repositories, loop through each of the repositories, and use the shelljs module to
execute git clone on each of them. The repositories will be cloned into the ‘my-
repositories’ directory. We change into this directory initially.

examples/011/clone-repositories.js

function cloneRepositories(repositoryPath, repositoryList = []) {


const repositoryCount = repositoryList.length;

if (!repositoryPath || repositoryCount === 0) {


console.log('Invalid path or repository list');
return;
}

console.log(`Cloning repositories to: ${repositoriesDirectory}`.blue);

shell.cd(repositoryPath);

repositoryList.forEach((repositoryUrl, index) => {


console.log(`Cloning ${index + 1} of ${repositoryCount}`.cyan);
shell.exec(`git clone ${repositoryUrl} --progress -b master`);
});
console.log('Completed cloning of repositories'.green);
}

We can then call this function with the repositoriesDirectory we defined above and
the repositories array which we extracted from the config.json file.

cloneRepositories(repositoriesDirectory, repositories);

This script can be used to prepare your build tool for use and would run as a post
install script. So before you start releasing games, you want to ensure that you
have the templates of the games, and the website you want to deploy to readily
available.

$ node examples/011/clone-repositories.js
Cloning repositories to: /Users/shaun/Workspace/nobot-examples/examples/011/my-repositories
Cloning 1 of 2
Cloning into 'nobot-repo-1'...
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
Cloning 2 of 2
Cloning into 'nobot-repo-2'...
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
Completed cloning of repositories
Summary
When we move on to using the build tool, we will make use of external
repositories. One of our repositories will be a website, and another will be a
library of game templates we can build from. This process will help us clone
repositories that we depend on to deploy games. Once we’ve cloned them, we
can then look into doing a git pull on a repository whenever we want to make
use of it. By doing a git pull on the templates, we know we’ve got the latest
stable versions.
012 – Branching
If you are working on a repository for a project, and you need to implement a
new feature, the first thing you usually do is grab the latest changes from the
master or develop branch. Once all these changes have been pulled in, you then
create a feature branch. Likely your branch will have the same identifier as the
ticket you are working on. So I may have a ticket identified as MARKETING-248 and it
makes sense to have a branch to match. Project management tools – if setup
correctly – can integrate with git branches for better visibility such as commits
and merges.

We are going to clone the nobot-repo-1 repository from the previous chapter into
the ‘012’ directory, and create a feature branch from the base branch e.g. master .
The only question we will ask the user is, ‘What is the ticket ID?’.

Please note: In the config.json file you should provide your own forked version of
the repository so you have no problems with permissions.

Our config.json file looks like this:

examples/012/config.json

{
"repository": {
"delivery": "https://github.com/smks/nobot-repo-1",
"baseBranch": "master"
}
}

Start by running the setup script.


examples/012/setup.js

require('colors');
const shell = require('shelljs');
const { repository } = require('./config');

const { delivery } = repository;

console.log(`Cloning ${delivery}`.cyan);

shell.cd(__dirname);

shell.exec(`git clone ${delivery} --progress`);

This will clone the repository as seen in the source code above.

Please note: This needs to be run as a prerequisite before we can create a new
branch.

$ node examples/012/setup.js
Cloning https://github.com/smks/nobot-repo-1
Cloning into 'nobot-repo-1'...
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0

In this example we will be making use of a synchronous version of the native


readline module provided by Node. This module is installed via npm and is
identified as readline-sync . We want to use this module to stop execution until the
user has inputted the data we are expecting.
Synchronous and Asynchronous If something is synchronous, then the
code execution will block (or wait) for the execution to return before
continuing. In our case, when creating a directory, we will wait for the
directory to be created before moving onto the next operation.
Asynchronous in the context of Node will execute the code, but will not
block or wait for a result back. In the majority of cases you will get back the
result of what you have done as an argument to a callback.

Now the repository exists, we are going to create a script that will change into
the directory, run git pull to make sure it’s up to date, and then create a new
branch using the git executable. First we require the npm packages needed to
accomplish this functionality.
examples/012/new-branch.js

const shell = require('shelljs');


const readLineSync = require('readline-sync');
const path = require('path');
const { repository } = require('./config');

We’re going to need the repo name and the base branch. We can get these from
our config.json file.

const { delivery, baseBranch } = repository;


const repoName = delivery.substring(delivery.lastIndexOf('/'));

Using one of the shelljs methods cd we can change our current working
directory. Our new directory nobot-repo-1 now exists in our current directory, so
let’s get the path to nobot-repo-1 and then cd into it.

// Changing into the repo's directory


const repoPath = path.join(__dirname, repoName);
shell.cd(repoPath);

Now we are sitting in the directory of nobot-repo-1 . When we run git , it’s in the
scope of this repository. Before we create a branch, we want to make sure we are
on the right base branch, and that it’s the correct one to branch from. In this case
it’s master , but could be different for you. That’s why we included it in the
config.json file.

// Checkout to base branch


shell.exec(`git checkout ${baseBranch}`);

Let’s pull in all the changes from this base branch, just in case someone else has
pushed changes to the remote branch. This is good practice when creating a new
branch.

// Making sure we have the latest changes from the remote origin
shell.exec(`git pull origin ${baseBranch}`);

Perfect, now we want to create a branch based on the ticket we’re working on.
But first let’s prompt the user for it.

// prompt for the ticket ID


const ticketId = readLineSync.question('What is the ticket ID? ', {
limit: input => input.trim().length > 0,
limitMessage: 'Please enter a ticket ID (e.g. GOT-123)'
});

The limit property is a check to see if the user input meets the validation
requirements. This could be a regular expression or a function. In this case I used
a function which ensures that the user enters more than an empty string. The
limitMessage value will be displayed if the user failed the limit check.

We have what we need. Using the -b option we are telling Git we want to create
a new branch and check it out.

// Create a new branch


shell.exec(`git checkout -b ${ticketId}`);

Now let’s run the script.


$ node examples/012/new-branch.js
Already on 'master'
Your branch is up-to-date with 'origin/master'.
From https://github.com/smks/nobot-repo-1
* branch master -> FETCH_HEAD
Already up-to-date.
What is the ticket ID? GS-101
Switched to a new branch 'GS-101'
Summary
When working with source control we often need to create feature branches so
our work can be done in isolation. We’ve learned that this too can be automated
using Node.
013 – Retain State
When we are dealing with the process of a script, and it exits, then all data or
state is forgotten. So what should we do if we need to retain state? When you are
in a browser, you can make use of something like local storage or cookies, but
with a CLI application, we don’t have this. Instead what we can do, is create
hidden files (aka dot-files) to retain - or persist - our data. In Unix-like systems,
files prefixed with a dot are hidden by default, which is why they are called dot-
files. I know… what a revelation.

A great example to demonstrate this would be to store some reminders. We have


one script that adds a reminder, and one that lists the reminders. Ensure you have
.reminders.json present, if not, then run node setup.js in the root of the nobot-examples

repository. For convenience, I created a short list by default.

examples/013/.reminders.json

{
"reminders": [
"get banana from kitchen",
"put food in fridge",
"go to standup",
"commit work"
]
}

Yes, I do forget some of these things when I arrive to the office. Let’s begin with
the script that deals with outputting the stored list. It’s simply reading the
contents of the dot-file which stores our reminders.

The two scripts we will be writing soon will share the same constants, so we
create a new file called constants.js , which our scripts can import. This is very
good practice for many reasons: it avoids typos, it makes changing the values
much easier and less error-prone (as the change only needs to be done in one
place), and it makes our code self-documenting whenever we reference these
constants.

When writing to the JSON file, I am specifying the whitespace to be used using
a constant JSON_WHITESPACE . This will make the JSON file more human readable
when written back to the file system. With the constant NO_CHOICE_MADE , when
choosing what reminder to mark as done, we will get an index of -1 if the user
decided to cancel. These constants will be used soon in our scripts.

examples/013/constants.js

module.exports = {
JSON_WHITESPACE: 4,
NO_CHOICE_MADE: -1
};

Now let’s move on to our first script which lists the reminders we currently have
stored.
examples/013/list-reminders.js

require('colors');
const fs = require('fs');
const readLineSync = require('readline-sync');
const { JSON_WHITESPACE, NO_CHOICE_MADE } = require('./constants');

We want to read in the reminders stored in the JSON file. If none exist, there is
no need to show a list of reminders.

const { reminders } = require('./.reminders');

if (reminders.length === 0) {
console.log('No reminders!'.green);
process.exit(0);
}

Next thing to do is output the list of reminders which do exist and ask the user to
choose one to remove, i.e. mark as done.

const index = readLineSync.keyInSelect(reminders, 'What reminder have you dealt with? ');

if (index === NO_CHOICE_MADE) {


process.exit(0);
}

We use the readline-sync module here to let the user choose the reminder to
remove. We pass the reminders array to readLineSync.keyInSelect , which in turn
presents the reminders to the user as numbered options to choose from. The user
types in the desired option’s number - meaning they have dealt with that
reminder, or 0 to cancel the operation. When the user has chosen a reminder, it
gets removed from the array using splice , and the reminders file gets overridden
with the updated array.

console.log(`you removed '${reminders[index]}'`.red);

reminders.splice(index, 1);

fs.writeFileSync(`${__dirname}/.reminders.json`, JSON.stringify({ reminders }, null, JSON_WHITESPACE));

But what if we want to add new reminders? That’s what this next script is for.
We will show the output at the end of this chapter.

examples/013/add-reminder.js

const fs = require('fs');
const { JSON_WHITESPACE } = require('./constants');
const { reminders } = require('./.reminders');

const args = process.argv.slice(2);


let reminder = args[0];

if (reminder === undefined) {


console.log("Pass a reminder, e.g. 'pick up rabbit'");
process.exit(0);
}

reminder = reminder.trim();

const hasReminderAlready = reminders.indexOf(reminder) > -1;

if (hasReminderAlready) {
console.log(`Doh! Already have the reminder '${reminder}' set`);
process.exit(0);
}

reminders.push(reminder);
fs.writeFileSync(`${__dirname}/.reminders.json`, JSON.stringify({ reminders }, null, JSON_WHITESPACE));

console.log('Yes! Added reminder');

Now on to the demo. First we will add a reminder.

$ node examples/013/add-reminder.js 'jump around'


Yes! Added reminder

Okay I’ve just jumped around. Now I need to tick off that reminder.
$ node examples/013/list-reminders.js

[1] get banana from kitchen


[2] put food in fridge
[3] go to standup
[4] commit work
[5] jump around
[0] CANCEL

What reminder have you dealt with? [1...5 / 0]: 5


you removed 'jump around'
Summary
If you have a project you want to deploy, and do not want to re-enter data
associated with the deployment, you can retain the state in the root of a specific
project.
014 – Choose Template
Imagine this scenario. You work for a gaming company, let’s call it Games4U.
This company has a few game templates for simple games such as:
backgammon, chess, draughts and poker. Each game template acts as the
skeleton for the game, and can be used to create a custom game of that type for a
client.

Your manager has asked you to create a new poker game. You need to make a
copy of the poker template, and rename it to the project name. We will call it
poker-ten-stars.

Usually, you would navigate to the poker template’s directory, and copy it to
another location under a different name. But you find this tedious to do every
time, so you decide to write a script to automate it for you.

So first thing we do is create a new file called create-game-from-template.js . To read


input from the user we include the readline-sync module. We also include the
native path module to resolve paths on the file system, and the colors module to
change the colour of our text. Finally, we include a new library called fs-extra ,
which extends the native fs module and allows us to copy directories - a
capability which the original fs module doesn’t have.
examples/014/create-game-from-template.js

require('colors');
const readLineSync = require('readline-sync');
const path = require('path');
const fse = require('fs-extra');

If you look in the 014 example, you’ll find a a game-templates directory that
contains each of the templates available. They are simplified for demonstration.
└─game-templates
├─backgammon
├─chess
├─draughts
└─poker

We want to read this directory and return an array of the templates listed,
because one of these needs to be copied. First, we want to construct the path to
the game templates directory. This would be a string like so:

/Users/shaun/Workspace/nobot-examples/examples/014/game-templates

With the path to this directory, we can do a synchronous read on it to return the
subdirectories as an array.

const GAME_TEMPLATES = 'game-templates';


const NO_CHOICE_MADE = -1;

// 1. Use a game template already built


const templatesDir = path.join(__dirname, GAME_TEMPLATES);
const templates = fse.readdirSync(templatesDir);

Great, we have an array of strings with the name of each template directory. The
good thing about reading the directory is that whatever is added here will reflect
as a choice in the array. Using the keyInSelect method, we can spit this out to the
user so they can choose one. Here is an example.

$ node examples/014/create-game-from-template.js
[1] backgammon
[2] chess
[3] draughts
[4] poker
[0] CANCEL

Choose one from list [1...4 / 0]:

The index of the item chosen is returned as a constant. We exit if they didn’t
choose a template. Choosing 0 returns -1.

const index = readLineSync.keyInSelect(templates);

if (index === NO_CHOICE_MADE) {


process.exit(0);
}

By this point the user has chosen the template they want to copy from, but we
don’t know the name of the new project directory. Based on our requirements, it
needs to be called poker-ten-stars . Let’s prompt the user with a question, asking
them what they would like to call the project.

// 2. Create a new project reskin based on our template


const projectName = readLineSync.question('What is the name of your game? ', {
limit: input => input.trim().length > 0,
limitMessage: 'The project has to have a name, try again'
});

As a sanity check, we show the user the project name they have chosen, and ask
if they are ready to proceed with the copy.

const confirmCreateDirectory = readLineSync.keyInYN(`You entered '${projectName}', create directory with this name?
`);

If the user typed 'y' , it means we can proceed. First, we grab the chosen
template from the array of templates. We then construct the path to the template
by concatenating the path to the templates directory with the template name. We
do something similar with the destinations path by concatenating the path to the
current working directory with the chosen project name we inputted.

// 3. If happy to create, copy the template to the new location


if (confirmCreateDirectory) {
const template = templates[index];
const src = path.join(templatesDir, template);
const destination = path.join(__dirname, projectName);
fse.copy(src, destination)
.then(() => console.log(`Successfully created ${destination}`.green))
.catch(err => console.error(err));
} else {
console.log('Aborted creating a new game');
}

As you can see, we use the copy method and pass the source and destination as
arguments. The copy method returns a promise, so then gets called when the
copy was successful. If any problems occurred (such as permission errors), they
are printed to the console. Here is the entire output:

$ node examples/014/create-game-from-template.js

[1] backgammon
[2] chess
[3] draughts
[4] poker
[0] CANCEL

Choose one from list [1...4 / 0]: 4


What is the name of your game? poker-ten-stars
You entered 'poker-ten-stars', create directory with this name? [y/n]: y
Successfully created /Users/shaun/Workspace/nobot-examples/examples/014/poker-ten-stars
Summary
Here we are providing useful feedback to the user by showing them a list of
templates that are available. The user can make a more informed choice of what
template they would like to create.
015 - Email
Email is a great way to notify colleagues when something has been done. You
may have completed a task and you want to let your team know it’s been
processed successfully. Alternatively if something goes wrong, you can trigger
an email to provide visibility to the team.

If we start thinking about how this build tool will run, we can imagine a server
running locally or in the cloud where we can set up a Cron job that will call the
build tool at intervals - hourly, daily or weekly - so it can work without us
manually initiating it. When this process is running on its own though, we have
no idea what’s happening unless we have some sort of feedback.

Email is a good way to deal with this. At work, your company most likely has
some email system configured using a protocol such as SMTP (Simple Mail
Transfer Protocol). Your team can also be emailed at once with a group email
such as ‘game-studios@games4u.com’.

We can use this email group as the default email address for all error messages
encountered by the Cron job during the build. Here is an example of a
configuration for the SMTP Protocol.

examples/015/config.json
{
"FROM_EMAIL": "Nobot Test <sender@example.com>",
"TO_EMAIL": "Game Studios Team <gamestudios@games4u.com>",
"HOST": "mysmtp.domain.io",
"PORT": "2525",
"AUTH": {
"USERNAME": "ENTER_USERNAME",
"PASSWORD": "ENTER_PASSWORD"
}
}

Now we move on to the script that will send out emails. At the top we load in the
config.json file that holds the SMTP configuration. We are making use of an
external dependency called nodemailer .

examples/015/send-email.js

require('colors');
const config = require('./config');
const nodemailer = require('nodemailer');

Our script requires two arguments: the subject and body of the email. If these are
not passed, the script will terminate.

const args = process.argv.slice(2);


const REQUIRED_FIELDS_COUNT = 2;

if (args.length !== REQUIRED_FIELDS_COUNT) {


console.log(
'Two arguments required: subject and body.'.red,
'E.g. node send-email.js "Where\'s my tea?" "So yeah... where is it?"'.cyan
);
process.exit(0);
}
If it passes this conditional check, it means the arguments count is correct. Let’s
grab the two arguments and assign them. While we are at it, let’s extract the
configuration values to set up the SMTP transporter.

const [subject, body] = args;


const {
HOST, PORT, FROM_EMAIL, TO_EMAIL
} = config;
const { USERNAME, PASSWORD } = config.AUTH;

Now we pass in the configuration. For the demo we are setting secure to false,
but you would of course set it to true.

const transporter = nodemailer.createTransport({


host: HOST,
port: PORT,
secure: false,
auth: {
user: USERNAME,
pass: PASSWORD
}
});

Our transporter will have to send a message object. Here it is.

const message = {
from: FROM_EMAIL,
to: TO_EMAIL,
subject,
body,
html: `<p>${body}</p>`
};

Lastly, we use the sendMail method on the transporter, which takes a callback
function to call with error/info at the end of the operation.

transporter.sendMail(message, (err, info) => {


if (err) {
console.error(`Error occurred: ${err.message}`);
return process.exit(0);
}

return console.log('Message sent: %s', info.messageId);


});

I ran the script and it was sent successfully.


$ node examples/015/send-email.js "OHHHH NOOOO" "The game I was trying to build has missing information: Game name,
Primary Colour Theme"

Message sent: <bf6556eb-9732-717d-7f24-25e7e336e4cc@example.com>


Summary
Now we know how easy it is to send an email via SMTP by executing a Node
script. We pass two arguments and let the script deal with creating the transporter
via the nodemailer package and constructing the message object, and then sending
the email.

As a side note, if you want to send an email quickly and easily, create a script
that takes three simple arguments: recipient email address, subject and body. It’s
quicker than opening up a mail client and going through the steps of constructing
an email.
016 - SMS
So, we have done email, why don’t we expand our horizons by doing a similar
thing with SMS. Let’s create a quick script that will send an SMS to our spouse
informing him/her what the plan is tonight for food. I LOVE FOOD!

A great service known as Twilio allows you to buy a phone number and to
programmatically send messages - among other things. Lucky for you, they offer
a free trial account which allows you to get a free phone number to send
messages from. This applies at the time of writing of course. The catch is that
the trial account will only allow you to send messages to verified phone numbers
- meaning numbers you’ve added to your account and confirmed with the
verification code they send to that number.

So you’ve got two choices: use a free account to play around with this
functionality, or pay for a full account and not be limited.

To get started, go to http://www.twilio.com, and look for the sign up button.


Once signed up, click on ‘Get Started’, and then click on ‘Get a number’ in the
following page. Finally, from your project’s dashboard, click on ‘Show API
Credentials’ to see your ‘ACCOUNT SID’ and ‘AUTH TOKEN’. Now you’re
all set up and ready to code. Just remember that until you add verified numbers,
you will only be able to message yourself, as that is the only verified phone
number you have at the start.

If you have run the npm run init script initially, a config.json file has been pre-
created for you to hold the Twilio credentials and phone numbers. Replace the
’X’s with your own generated credentials, your new Twilio phone number, and
your spouse’s phone number (or the phone number you want to send messages
to).
examples/016/config.json

{
"TWILIO_SID": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"TWILIO_AUTH_TOKEN": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"TWILIO_PHONE_NUMBER": "+44XXXXXXXXXX",
"MY_SPOUSE_NUMBER": "+44XXXXXXXXXX"
}

examples/016/sms-spouse.js

First thing we want to get is the details from the config.json file. We will read
input using readline-sync and use the twilio library to send SMS messages.

const config = require('./config');


const readLineSync = require('readline-sync');
const Twilio = require('twilio');

Extract the values from the JSON file, then make an instance of Twilio. We
create a constant so that if the user doesn’t choose a food choice we can exit.
const NO_CHOICE_MADE = -1;

const {
TWILIO_SID,
TWILIO_AUTH_TOKEN,
TWILIO_PHONE_NUMBER,
MY_SPOUSE_NUMBER
} = config;

const client = new Twilio(TWILIO_SID, TWILIO_AUTH_TOKEN);

Mmmmm. We have some choices that are making me salivate already. Cooking
myself, hmm, maybe takeout tonight.

const foodChoices = [
'spag bowl 2nite',
'chinese takeaway 2nite',
'pie n mash 2nite',
'mushroom risotto',
'pizza and fries',
'2 recover from my lunch, no food plz!',
'2 cook 2nite'
];

Using keyInSelect we can allow the user to choose from the array. If they choose
0 we will abort sending a SMS message.

const index = readLineSync.keyInSelect(foodChoices, 'What would you like for dinner?');

if (index === NO_CHOICE_MADE) {


process.exit(0);
}

So to conform to Twilio’s expected SMS object, we create like so:

const smsMessage = {
body: `Hi Bub, I'd like ${foodChoices[index]}`,
from: TWILIO_PHONE_NUMBER,
to: MY_SPOUSE_NUMBER
};

console.log(`sending message: ${smsMessage.body}`);

And we send it using the create method:

// Send the text message.


client.messages.create(smsMessage)
.then(({ sid }) => {
console.log('SMS sent. Id:', sid);
})
.catch((error) => {
console.error('Error sending Twilio message', error);
});

Here is the script in action.


$ node examples/016/sms-spouse.js

[1] spag bowl 2nite


[2] chinese takeaway 2nite
[3] pie n mash 2nite
[4] mushroom risotto
[5] pizza and fries
[6] 2 recover from my lunch, no food plz!
[7] 2 cook 2nite
[0] CANCEL

What do you want for dinner? [1...7 / 0]: 2


SMS sent. Id: SM30b413a3ce42410e873c4c5875d1d3ca

Good it sent. I really hope the chips are crispy this time.
Summary
Yes, we have WhatsApp, but unfortunately they don’t have an API. For now we
will settle for second best. The benefit is that if your spouse doesn’t have an
Internet connection, there is more chance she will get your message. Yes, I have
experienced living in the middle of nowhere. Come on WhatsApp, you can do it.
017 - CSV
What if we wanted to batch-create ten games rather than creating them one-by-
one? There have been many times in my career where I have used CSV files for
the use of batch processing. I find a lot of spreadsheets used by other teams that
have valuable data in them that I can use to my advantage. Especially in my last
company, there was a huge schedule of promotions to go live with all the
information I needed to create them. I deployed all the promotions for a whole
month in one go. A stakeholder contacted me and asked if the promotion was
done. I said, “Yes mate, 22 days ago”.

Here I will demonstrate a simplified example of how to process records one-by-


one from a CSV file. We begin by ensuring that our CSV file is consistently
formatted and has two columns: the name of the game, and the game template
it’s based on.

Please note: If you have data stored in an application like Microsoft Excel or
Google Sheets, you can export that file to CSV directly - you don’t need to
recreate it manually.

examples/017/game-releases.csv

Check Mate Chess,chess,


Deluxe Backgammon,backgammon,
Chaps of Checkers,draughts,
Wild East Poker,poker,
Kent World Poker,poker,
Drake Draughts,draughts,
Golden Backgammon,backgammon,
BluffMe Poker,poker,
Challenge of Chess,chess,
SpinMe Slots,slots,

We want to write a script that reads the contents of this CSV file, parses it,
transforms it, and then pipes to standard output. But first we should explain the
concept of streams in Node.
Streams
As mentioned earlier in the book, streams are a pattern that makes a conversion
of huge operations and breaks it down into manageable chunks. Kids, don’t eat a
birthday cake whole, eat it in chunks.

It’s important to appreciate streams when dealing with large files. Using ten
records from a CSV like what will be used in our example is not much of an
issue, but if we were dealing with millions of records, this is where streams flex
their muscles.

If our CSV file did have millions of records, and we tried to load the entire
contents of it by using the fs.readFile function, then we would have to load all of
it into memory at once. That’s quite a load, and it would be a bad idea for many
reasons: bigger load on the server, bigger download for the user, longer loading
time, and in the context of cake, a crippling stomach ache.

There are many flavours of streams:

1. Readable streams - they act as the source.


2. Writable streams - they act as the destination.
3. Duplex - both readable and writable.
4. Transform - a special kind of Duplex stream. Input goes into the Transform
stream, and is returned modified or unchanged.

We will use a Transform stream when parsing our CSV file.

So, starting with our required modules. We have two new additions:

1. csv-parse is responsible for the parsing of a CSV file: it can convert CSV
into JSON at a rate of around 90,000 rows per second.
2. stream-transform is used to provide a callback for each row sequentially for
convenience.

examples/017/deploy-games.js

require('colors');
const path = require('path');
const { stdout } = require('process');
const parse = require('csv-parse');
const transform = require('stream-transform');
const fs = require('fs');

Let’s set up our parser, and inform it on how our data is separated. In our case,
it’s separated by commas. Now you’ll see instead of reading in the whole file
into memory with fs.readFile , we are setting up a read stream using the native fs

module, and declaring a simple iterator variable. The delay time is used to slow
down the process for demonstration purposes.

const DELAY_TIME = 500;


const CSV_FILE = 'game-releases.csv';
const parser = parse({ delimiter: ',' });
const gameReleasesPath = path.join(__dirname, CSV_FILE);
const input = fs.createReadStream(gameReleasesPath);
let iterator = 1;

For every record chunk we read, we want to do something. So we create a


callback function to run on each iteration.

const processRecord = (record, callback) => {


const [game, template] = record;
let message = `Deploying game ${iterator} '${game}' with template: '${template}'`;
message = (iterator % 2 === 0) ? message.bgGreen : message.bgBlue;
iterator += 1;
setTimeout(() => {
// build game here
callback(null, `${message}\n`);
}, DELAY_TIME);
};

We extract the game and template from the record array, then construct a
message to feed back to the standard output. We use colors to differentiate each
line from the next. This is where we would add the code to deal with actually
building a game, but we’ll leave that for a future lesson. As a side note, you
would remove the setTimeout from this script to speed things up of course.

const transformer = transform(processRecord);

The callback processRecord is passed to the transform function. Finally, below we


pipe the output of the parser to the transformer, which in turn passes its output to
standard output.

pipe The pipe function reads data from a readable stream as it becomes
available and writes it to a destination writing stream. We are doing three
pipes in our example below. First it reads in a line from the CSV file, sends
to the parser. The parser parses it and then passes it to the transformer, the
transformer forwards to standard output as we can see below when we run
the script.

input
.pipe(parser)
.pipe(transformer)
.pipe(stdout);

This is how the output looks. You can see it’s processing each line of the file:

$ node examples/017/deploy-games.js
Deploying game 1 'Check Mate Chess' with template: 'chess'
Deploying game 2 'Deluxe Backgammon' with template: 'backgammon'
Deploying game 3 'Chaps of Checkers' with template: 'draughts'
Deploying game 4 'Wild East Poker' with template: 'poker'
Deploying game 5 'Kent World Poker' with template: 'poker'
Deploying game 6 'Drake Draughts' with template: 'draughts'
Deploying game 7 'Golden Backgammon' with template: 'backgammon'
Deploying game 8 'BluffMe Poker' with template: 'poker'
Deploying game 9 'Challenge of Chess' with template: 'chess'
Deploying game 10 'SpinMe Slots' with template: 'slots'
Summary
CSV files are a powerful way to batch specific processes. There is always a
situation at work where they can be used to speed things up. Give it a go!
018 - Shorten URL
Social Media is paramount to the success of any company. As a games studio for
example, you would have to market your game and provide links. These links
can get quite long and take up space which is a problem when you have the
constraints of character limits.
Link Shorteners
There are services such as Bitly & TinyURL, which are referred to as ‘link
shorteners’. They take a long URL and shrink it into a smaller equivalent. Take
this URL from my blog as an example.

Long URL

http://smks.co.uk/travel/bali-2017/bali-2017-part-12/

After running it through a service such as Bitly.

Short URL

http://bit.ly/2jklAQb

The benefits are more than just fewer characters. If you sign up to one of the
services, you should be able to track when links have been shared and clicked
on. Let’s walk through an example of passing a URL as an argument and
receiving back the URL in the shortened form.

Please note: Before you proceed with this example ensure you have: created a
bit.ly account, created a generic access token, and copied this access token into
examples/018/config.json .
examples/018/config.json

{
"BITLY_TOKEN": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}

examples/018/shorten-url.js

As always, we require our dependencies at the top. We load in the bitly package
installed via npm, as well as the configuration that contains our access token.

const Bitly = require('bitly');


const { BITLY_TOKEN } = require('./config');

Now we have the token and dependency, we can instantiate Bitly and pass in the
token. When making a request to bit.ly, based on the documentation, we should
expect a HTTP status code response of 200 to confirm everything went okay and
that the URL has been shortened.

const STATUS_CODE_OK = 200;


const bitly = new Bitly(BITLY_TOKEN);

Because we want to pass a URL as an argument, as done before we grab the


URL string passed by the user.

const args = process.argv.slice(2);


const [urlToShorten] = args;

So urlToShorten will be something like ‘http://www.opencanvas.co.uk’. Just to


make sure it’s actually a URL before making a request to Bitly, we do some
validation to save the overheads of a HTTP Request if it’s invalid. To check it’s a
valid URL, we use a regular expression.

const expression = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/gi;


const regex = new RegExp(expression);

if (urlToShorten === undefined || urlToShorten.match(regex) === null) {


console.log("Please pass a string in URL form, e.g. 'http://www.opencanvas.co.uk'");
process.exit(0);
}

If the process is still running at this point, it means a valid URL was passed to
our script. Next, we call the shorten method, passing the URL as an argument.
This method returns a Promise, which once fulfilled - if the request was
successful - will contain our shortened URL. This new URL will be contained in
response.data.url .

bitly.shorten(urlToShorten)
.then((response) => {
const statusCode = response.status_code;
const statusText = response.status_txt;
if (statusCode !== STATUS_CODE_OK) {
console.error('Something went wrong:', statusText);
} else {
console.log(`Shortened URL is: ${response.data.url}`);
}
})
.catch(console.error);

And here is the output.


$ node examples/018/shorten-url.js "http://smks.co.uk/travel/bali-2017/bali-2017-part-12/"
Shortened URL is: http://bit.ly/2jklAQb
Summary
URL Shorteners are a great way to track clicks of links, and a quick way to
retrieve a shortened URL when you need to paste one on a social media
platform.
019 - Minimist
Up until now, we have been passing arguments to our scripts like so:

$ node my-script.js 'My Template' 'My Ticket' 'My Game'


// run script

But it isn’t very expressive. Like I may want to pass values without having to
concern myself with how they are ordered. I would want something like this.

$ node my-script.js --game='My Game' --ticket='My Ticket' --template='My Template'


// run script

This way, regardless of the order these values get passed, I know that the correct
value is being picked up. We can do this with a module called minimist . This
module will parse arguments from the process.argv array and transform it in to an
object. This allows you easy access to the arguments, as they will now be
available as key-value pairs inside the object.

We’ll stick with the convention above --option=value .

examples/019/minimist.js

We pass the argv array directly to minimist .

const argv = require('minimist')(process.argv.slice(2));


That’s it. We have the object we need. Passing the following arguments and
options, we can inspect the object of argv once it has run through minimist .
$ node examples/019/minimist.js apple bananas cherries --name='My Game' --ticket='My Ticket' --template='My
Template'

So what does our argv variable look like?

{ _: [ 'apple', 'bananas', 'cherries' ],


name: 'My Game',
ticket: 'My Ticket',
template: 'My Template'
}

As you can see it pushes any arguments into an ordered array under an
underscore key, but if you provide options, it will create key value pairs. Now on
with the rest of the script.

const readLineSync = require('readline-sync');

const NO_CHOICE_MADE = -1;

let { name, template, ticket } = argv;


const templates = ['pick-of-three', 'tic-tac-toe', 'spin-the-wheel'];

The good thing about this script is that it will have the possibility to pass the
values directly through instead of prompting us. But… if the values are not
found, only then will the script become interactive and prompt the user for the
value it needs.

You can see we can assign name, template and ticket right away, and below that
we have an array of supported templates. If the template chosen is not in this list
then we are going to have problems.

“We should not give up and we should not allow the problem to defeat us.”

– A. P. J. Abdul Kalam

We won’t let it happen, Abdul.


If the name wasn’t passed as an option, then we simply prompt the user to give it
to us. We’re not going to all this trouble for nothing, you know!

if (name === undefined) {


name = readLineSync.question('What is the name of the new game? ', {
limit: input => input.trim().length > 0,
limitMessage: 'The game must have a name'
});
}

Now we are going to check if the user even bothered to pass the template. But it
doesn’t stop there. The array of templates we declared earlier has to match our
entered choice too, otherwise we tell the user to make a choice from the ones
that do exist.

if (template === undefined || !templates.includes(template)) {


const templateIndex = readLineSync.keyInSelect(templates, 'Choose your template');
if (templateIndex === NO_CHOICE_MADE) {
console.log('No template chosen. Stopping execution.');
process.exit(0);
}
template = templates[templateIndex];
}

Now we are going to check if the ticket was passed, and ensure – due to business
rules – that the ticket begins with ‘GS-’. Once all 3 values have been acquired
and they satisfy our criteria, we can proceed to building the game.

if (ticket === undefined || ticket.indexOf('GS-')) {


ticket = `GS-${readLineSync.question('Enter ticket number: GS-', {
limit: input => input.trim().length > 0,
limitMessage: 'Cannot continue without a ticket number'
})}`;
}

console.log(`Creating game '${name}' with template '${template}' on branch '${ticket}'`);

Let’s see what the output is.


$ node examples/019/minimist.js --name="chess" --template="pick-of-three" --ticket="GS-100"
Creating game 'chess' with template 'pick-of-three' on branch 'GS-100'

Now let’s try it by not passing any options at all:


$ node examples/019/minimist.js
What is the name of the new game? choose-your-prize

[1] pick-of-three
[2] tic-tac-toe
[3] spin-the-wheel
[0] CANCEL

Choose your template [1, 2, 3, 0]: 1


Enter ticket number: GS-346
Creating game 'choose-your-prize' with template 'pick-of-three' on branch 'GS-346'
Summary
Prompting users is fine in some cases, but sometimes, like when you want to let
a Cron job handle the process, we need to pass arguments to the script so it can
be run automatically. Minimalist allows us to do that.
020 - Build
Now we are going to work on a slightly bigger example to demonstrate the build
of a game. This game would’ve been implemented already and released as a
specific version.

This example we will be using is a slightly tweaked version of the original


Firefox game that can be found here https://developer.mozilla.org/en-
US/docs/Games/Tutorials/2D_Breakout_game_pure_JavaScript.

The difference with this version is that you can create a configuration file that
can alter the visuals of the game. The values will be injected into the game. This
example will be based around two configurable values: a primary colour, and a
secondary colour. The game only has two colours, and they should compliment
each other.

examples/020/template/game.json

{
"primaryColor": "#fff",
"secondaryColor": "#000"
}

The primary colour will be used to determine the colour of the game objects, and
the secondary colour will paint the background. So we are empowering the
JSON file to configure the game’s ‘skin’.

└─020
├─core
| └─game-1.0.0.js
├─releases
├─template
└─new-reskin.js
The root of the 020 directory contains three directories and the script we will run
to build a new release. The first directory core will contain the bundled files of
the game using iterative versions. Using versioning will prevent backward
compatibility issues with existing game releases.

The releases directory will contain each of the reskins we make of the game
template. It’s as simple as cloning the template directory, and changing the name,
and updating the JSON file so it changes into its own unique version.

Please note: This example uses only two values, but later on we will use a more
advanced implementation that will contain images, fonts and custom styles.
There is nothing stopping you building upon this example.

Before we run the script, we want to run a simple server. To do this, you need to
run this in the root of nobot-examples repository in a separate CLI. Your public IP
address shown below will most likely be different.

$ npm run gameServe

> http-server examples/020

Starting up http-server, serving examples/020


Available on:
http://192.168.1.68:8080
http://127.0.0.1:8080

Great, we now have a server running and pointing to the root of the 020

directory. Now we can walk through the script.

At the top of the script we require the packages needed.

1. colors to be used to signify success or failure.


2. minimist from the last example to make it easier to pass arguments to our
script and to parse them optionally. Pass input without being prompted to
enter.
3. path to construct paths to the template and the destination of the new game.
4. readline -sync to prompt user for information if it’s missing.
5. fs-extra so we can copy and paste our game directories.
6. opn is a library that is cross platform and will open up our game in a
browser upon completion.

examples/020/new-reskin.js

require('colors');
const argv = require('minimist')(process.argv.slice(2));
const path = require('path');
const readLineSync = require('readline-sync');
const fse = require('fs-extra');
const open = require('opn');

As demonstrated in the previous example with the minimist library, we can pass
in the key value pair options directly to the script, therefore avoiding the need to
prompt the user for each required value. This is what we are obtaining here, and
our first check is to see if the game name has been passed in. If it hasn’t then we
prompt the user for it, applying some simple validation.

const GAME_JSON_FILENAME = 'game.json';


let { gameName, gamePrimaryColor, gameSecondaryColor } = argv;

if (gameName === undefined) {


gameName = readLineSync.question('What is the name of the new reskin? ', {
limit: input => input.trim().length > 0,
limitMessage: 'The project has to have a name, try again'
});
}
Because two of our values need to be Hex codes, we create a function that can
do the check for both colours: the primary and the secondary. If the colour
supplied by the user did not pass our validation, we prompt for the colour until it
does.

const confirmColorInput = (color, colorType = 'primary') => {


const hexColorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
if (hexColorRegex.test(color)) {
return color;
}
return readLineSync.question(`Enter a Hex Code for the game ${colorType} color `, {
limit: hexColorRegex,
limitMessage: 'Enter a valid hex code: #efefef'
});
};

We use the same function to obtain both the primary and secondary colours.

gamePrimaryColor = confirmColorInput(gamePrimaryColor, 'primary');


gameSecondaryColor = confirmColorInput(gameSecondaryColor, 'secondary');

In the next block of code we are printing to standard output to confirm the values
that will be used in the process of building the game. The statements that follow
are preparing the paths to the relevant files and directories. The src will point to
the template directory.

The destination will point to a new directory under releases. The configuration file
that will have its values updated will reside under this new game directory we
are creating. And finally, to preview our new game, we construct the URL using
the path to the local server we booted up earlier on.

console.log(`Creating a new reskin '${gameName}' with skin color: Primary: '${gamePrimaryColor}' Secondary:
'${gameSecondaryColor}'`);

const src = path.join(__dirname, 'template');


const destination = path.join(__dirname, 'releases', gameName);
const configurationFilePath = path.join(destination, GAME_JSON_FILENAME);
const projectToOpen = path.join('http://localhost:8080', 'releases', gameName, 'index.html');

In the following code:


1. We copy the template files to the releases directory.
2. After this is created, we read the JSON of the original template.
3. With the new configuration object, we override the existing primary and
secondary colours provided by the user.
4. We rewrite the JSON file so it has the new values.
5. When the JSON file has been updated, we ask the user if they would like to
open the new game in a browser.

Please note: We can only use the copy function below using the fs-extra

package. I have named it as fse to differentiate between this version and the
native fs package.

fse.copy(src, destination)
.then(() => {
console.log(`Successfully created ${destination}`.green);
return fse.readJson(configurationFilePath);
})
.then((config) => {
const newConfig = config;
newConfig.primaryColor = gamePrimaryColor;
newConfig.secondaryColor = gameSecondaryColor;
return fse.writeJson(configurationFilePath, newConfig);
})
.then(() => {
console.log(`Updated configuration file ${configurationFilePath}`.green);
openGameIfAgreed(projectToOpen);
})
.catch(console.error);

Below is the function that gets invoked when the copying has completed. It will
then prompt the user to see if they would like to open up the game in the
browser.

const openGameIfAgreed = (fileToOpen) => {


const isOpeningGame = readLineSync.keyInYN('Would you like to open the game? ');
if (isOpeningGame) {
open(fileToOpen);
}
};

Here is the entire script.


examples/020/new-reskin.js

require('colors');
const argv = require('minimist')(process.argv.slice(2));
const path = require('path');
const readLineSync = require('readline-sync');
const fse = require('fs-extra');
const open = require('opn');

const GAME_JSON_FILENAME = 'game.json';


let { gameName, gamePrimaryColor, gameSecondaryColor } = argv;

if (gameName === undefined) {


gameName = readLineSync.question('What is the name of the new reskin? ', {
limit: input => input.trim().length > 0,
limitMessage: 'The project has to have a name, try again'
});
}

const confirmColorInput = (color, colorType = 'primary') => {


const hexColorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
if (hexColorRegex.test(color)) {
return color;
}
return readLineSync.question(`Enter a Hex Code for the game ${colorType} color `, {
limit: hexColorRegex,
limitMessage: 'Enter a valid hex code: #efefef'
});
};

gamePrimaryColor = confirmColorInput(gamePrimaryColor, 'primary');


gameSecondaryColor = confirmColorInput(gameSecondaryColor, 'secondary');

console.log(`Creating a new reskin '${gameName}' with skin color: Primary: '${gamePrimaryColor}' Secondary:
'${gameSecondaryColor}'`);

const src = path.join(__dirname, 'template');


const destination = path.join(__dirname, 'releases', gameName);
const configurationFilePath = path.join(destination, GAME_JSON_FILENAME);
const projectToOpen = path.join('http://localhost:8080', 'releases', gameName, 'index.html');

fse.copy(src, destination)
.then(() => {
console.log(`Successfully created ${destination}`.green);
return fse.readJson(configurationFilePath);
})
.then((config) => {
const newConfig = config;
newConfig.primaryColor = gamePrimaryColor;
newConfig.secondaryColor = gameSecondaryColor;
return fse.writeJson(configurationFilePath, newConfig);
})
.then(() => {
console.log(`Updated configuration file ${configurationFilePath}`.green);
openGameIfAgreed(projectToOpen);
})
.catch(console.error);
const openGameIfAgreed = (fileToOpen) => {
const isOpeningGame = readLineSync.keyInYN('Would you like to open the game? ');
if (isOpeningGame) {
open(fileToOpen);
}
};

Let’s see it in action.


$ node examples/020/new-reskin.js
What is the name of the new reskin? shaun-the-sheep
Enter a Hex Code for the game primary color #fff
Enter a Hex Code for the game secondary color #000
Creating a new reskin 'shaun-the-sheep' with skin color: Primary: '#fff' Secondary: '#000'
Successfully created /Users/shaun/Workspace/nobot-examples/examples/020/releases/shaun-the-sheep
Updated configuration file /Users/shaun/Workspace/nobot-examples/examples/020/releases/shaun-the-sheep/game.json
Would you like to open the game? [y/n]: y
(opens game in browser)
Summary
Using a simple example of a small game, we have created a script that can build
a new game from a template, using custom colours provided by the user, we can
create many clones of the game and use different colours for each. A dumbed
down version of what is to come in part 2!
Part 2 - Build Tool
Excellent, you’ve now warmed up your fingers, but here is the real deal; part 2.
Before you proceed, carefully follow the instructions below so you’re ready to
rock.

Make sure you have a GitHub account if you want to write these scripts from
scratch. Alternatively you can browse the completed scripts sitting on the master

branch.

1. Fork the repository under your own name. This can be done by clicking the
Fork button on the GitHub page here https://github.com/smks/nobot. So
rather than the repository being under my own name ‘smks’ it will be under
yours instead.

2. Do another fork for the repository https://github.com/smks/nobot-website.


3. Do another fork for the repository https://github.com/smks/nobot-template-
rock-paper-scissors.
4. Clone your forked repository nobot to your own machine (No need to clone
other two forked repositories).
5. Switch to branch develop by doing git checkout develop .
6. Run npm install in the root of the repository.
7. Follow along with the nobot implementation.
8. Happy coding!
Scenario
You, a developer that works for Nobot Game Studios, make many new games on
a regular basis. The business has now decided that making multiple versions of
the same game is a more viable alternative. The idea is that the team builds a
game that can be configurable and reskinnable, but retains the same mechanics.

The first meeting that takes place involves the various teams involved:

1. Stakeholders and/or Product Owners - Responsible for the games and how
they are delivered.
2. Content Editors - Will provide the witty text content (Title, subtitles,
labels).
3. Designers - The creative geniuses who will design the assets for your game.
4. Developers - The poor souls who have to deal with all the functionality of
the game - and the many changes.
5. Quality Assurance - Will test and verify the initial game template and
reskins.

After many disagreements, back and forth suggestions and terrible jokes, you
agree on the following flow.

1. The product owner, after doing some heavily involved research, decides to
create an original game called ‘Rock Paper Scissors’. Five different
versions of this game will be created to show on the main games lobby. The
product owner communicates the idea to the other teams, and says that
he/she will create a ticket for each of the games that contains all of the
necessary content (obtained from the content editors and designers).
2. The game designers start off by creating a design template that will speed
up their real-world implementations. The game designers get to work on the
implementations for all five games, using the original template as the basis
for their designs. When they are finished, they upload their assets to a CDN
(Content Delivery Network) repository and provide the link to the assets for
the live game in our project management software. E.g.
http://cdn.opencanvas.co.uk/automatingwithnodejs/assets/rock-paper-
scissors/rock-paper-scissors-doodle
3. The developers get to work on the template implementation, using the
provided live links to the assets and making sure the template is
configurable to the point that it can satisfy the requirements of all five game
implementations. For this, it will require some team planning to ensure
things go smoothly.
4. The game gets released, and is tested firstly by QA and then anyone else
who wants to join in on the fun.
5. Rinse and repeat for new games.
Development Meeting
You HAVE to plan a meeting with your fellow team. Sit down in a meeting room
around a whiteboard, pick up a black or blue pen and each of you write what you
think happens from A to Z. We did this at my old company, and although there
was a lot of digression, we managed to come to an agreement after a few
months. Only joking, we never agree on anything. For this we did though.

Here is a mock example of something that would be conjured up in a meeting for


the new Rock Paper Scissors game.

Required Tech

1. Source Control - Git.


2. Vanilla JS - AKA - JavaScript (Don’t always need a framework, kids).
3. ESLint to ensure code quality and standards.
4. Babel so we can write for the future (ES6, 7 and beyond).
5. Agile Methodology to Software Development along with an issue
management tool (In house project management tool called Nira also has an
API).
6. Brunch - Used to watch, build, compile and concatenate game production
files (Like Webpack, Gulp). A lightweight simple implementation used for
this book.
7. Node.js to automate our delivery process.

Required Repositories

The team has agreed to create the repositories and give them the detailed
responsibilities below.
Nobot Content Delivery Network (CDN)

A place where designers upload assets for a game. These assets will be pulled
into the game. Rock Paper Scissors will have five different skins, each of which
having five collections of assets. The location of these assets is added to the Nira
ticket. This repository will be created and handed over to the designers to
manage and take ownership of. They will need to follow an approach that is
consistent. The repository below shows the five design implementations
exported from the designers.

GitHub Repository

https://github.com/smks/nobot-cdn

Production Link CDN

http://cdn.opencanvas.co.uk/automatingwithnodejs

Directory structure

└── assets
└── rock-paper-scissors
├── fire-water-earth-cute
│ ├── background.png
│ ├── banner.jpg
│ ├── paper.png
│ ├── rock.png
│ ├── scissors.png
│ └── theme.css
├── fire-water-earth-fantasy
│ ├── background.png
│ ├── banner.jpg
│ ├── paper.png
│ ├── rock.png
│ ├── scissors.png
│ └── theme.css
├── fire-water-earth-retro
│ ├── background.png
│ ├── banner.jpg
│ ├── paper.png
│ ├── rock.png
│ ├── scissors.png
│ └── theme.css
├── rock-paper-scissors-doodle
│ ├── background.png
│ ├── banner.jpg
│ ├── paper.png
│ ├── rock.png
│ ├── scissors.png
│ └── theme.css
└── rock-paper-scissors-modern
├── background.png
├── banner.jpg
├── paper.png
├── rock.png
├── scissors.png
└── theme.css

So each design implementation has the same asset names, and a theme.css script
which applies the styling to our game.

In our development implementation we would point to the correct directory to


load in the assets.

Nobot Website

This is the website where the players can play our games. Our games, once built,
will be deployed here.

GitHub Repository

https://github.com/smks/nobot-website
Production Link Website

http://ngs.opencanvas.co.uk

Directory structure

│ .gitignore
│ bulma.css
│ index.php
│ main.css
│ main.js
│ README.md
│ run.bat
│ run.sh
│ update-site.sh
├───core
│ .gitkeep
│ rock-paper-scissors.1.0.1.css
│ rock-paper-scissors.1.0.1.js
│ rock-paper-scissors.1.1.0.css
│ rock-paper-scissors.1.1.0.js
└───releases
│ .gitkeep
├───fire-water-earth-cute
│ game.json
│ index.html
├───fire-water-earth-fantasy
│ game.json
│ index.html
├───fire-water-earth-retro
│ game.json
│ index.html
├───rock-paper-scissors-doodle
│ game.json
│ index.html
└───rock-paper-scissors-modern
game.json
index.html

Under the core directory is where we will release our transpiled and minified
game bundled files. As mentioned previously, using versioning will allow us to
make future implementations and keep backwards compatibility to avoid
breakages of existing game bundles.

The releases directory will be where we deploy our various projects with their
unique identifiers. Each project release will contain an index.html file and a
configuration file game.json with all the values captured from the Nira ticket.

Nobot Template - Rock Paper Scissors


The source code for the game template ‘rock-paper-scissors’. This is where we
build our production code for our game. Remember you will be using your own
forked version.

GitHub Repository

https://github.com/smks/nobot-template-rock-paper-scissors

The index.html will load in the core CSS and JS bundle specified, and also provide
the HTML Markup that the JS bundle relies on to work correctly. Our games are
Document Object Model (DOM) driven.

app/assets/index.html

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Game</title>
<link rel="stylesheet" href="/core/rock-paper-scissors.1.1.0.css">
<script src="/core/rock-paper-scissors.1.1.0.js"></script>
<script>require('initialize');</script>
</head>
<body>
<div id="game">
<div id="loading-screen">
<h1 class="title">Loading...</h1>
</div>
<div id="choice-screen" class="is-hidden">
<h1 class="title">Rock Paper Scissors</h1>
<h2 class="subtitle">Make your choice</h2>
<div class="choices"></div>
</div>
<div id="result-screen" class="is-hidden">
<h2 class="subtitle"></h2>
<div class="choice-results"></div>
<div class="score"></div>
<div class="feedback"></div>
<div class="replay">
<button id="replay-button">Replay</button>
</div>
</div>
</div>
</body>

The good thing about this approach is that all versions will be pointing to the
same core bundle. So if a bug appears in one of them, you can deploy a fix for
the template and it will fix them all in one go, as opposed to rebuilding a bundle
for each game.

The JSON will differentiate our games and will be the single source of truth. The
team would come to an agreement on what needs to be dynamic. The final JSON
structure was agreed as follows (for any Rock Paper Scissors game).

app/assets/game.json

{
"id": 123,
"projectName": "rock-paper-scissors",
"theme": {
"fontFamily": "Cabin",
"path": "http://cdn.opencanvas.co.uk/automatingwithnodejs/assets/rock-paper-scissors/fire-water-earth-cute",
"customStyles": [
"https://fonts.googleapis.com/css?family=Cabin"
]
},
"images": {
"background": "background.png",
"rock": "rock.png",
"paper": "paper.png",
"scissors": "scissors.png"
},
"labels": {
"rock": "rock",
"paper": "paper",
"scissors": "scissors"
},
"screens": {
"choice": {
"title": "Rock Paper Scissors",
"subtitle": "Make your choice"
},
"result": {
"won": "you won!",
"lost": "you lost!",
"draw": "it's a draw",
"replay": "replay",
"feedback": {
"won": "{player} beats {cpu}",
"lost": "{cpu} beats {player}",
"draw": "Nobody won this time"
}
}
}
}
Each game we create will have a unique identifier id . This is needed so we can
store data on each game and evaluate which has more engagement with players.
Our config.json object also has a theme where we can pass the fonts we would like
to use in the game, the path to the game’s assets - such as the images (which are
retrieved from the CDN used by our designers), and any custom CSS files we’d
like to use. In our example, under customStyles , we are loading in a font we want to
render.

To avoid confusion, the images should remain consistent with their naming
convention. Labels and the content of the various screens are declared here too.
There are important advantages for this approach of using a JSON file to load
things like styles, content and game paths - rather than hard-coding them. Firstly,
it makes the game easier to configure. Secondly, it means you can allow your
non-technical colleagues to configure the game as well, whilst not worrying
about them breaking it, because they will not need to touch the source code.
Thirdly, the JSON configuration file acts as a contract detailing the parts of the
game which the business wants to be customisable.

Nobot - Build Tool

Finally we come on to the main tool we are building in the book that interacts
with a Project allocation tool API, pulls in values, builds the game, and then
deploys to the website’s releases directory. The explanation of how this is built
will follow this chapter.

GitHub Repository Build Tool

https://github.com/smks/nobot

High Level Overview

Check out this diagram which details how the repositories interact with one
another from a high level.

With an overview of every repository we need, and an understanding of the flow,


we can get to work on the planning of our build tool.
Build Tool Planning
Our team now wants to focus on the directory structure of our build tool, and try
to understand how it’s to be architected. As an overview, we would have
something along these lines drawn up on a whiteboard.

├─── repositories

│ └─── templates

└───src

├─── commands

├─── constants

├─── creators

│ └─── rock-paper-scissors

├─── helpers

└─── setup
repositories
Our first directory will be one identified as repositories . In here we will clone all
of the repositories we need, as mentioned in the previous chapter.

We want to clone all the templates under a subdirectory called templates . When
we release the template to the website, we clone the website repository too so we
can copy it there. So our directory structure as agreed with the team will be like
so:

├── templates
│ └── rock-paper-scissors
│ └── template-2
│ └── template-3
└── website

We shouldn’t clone all of these repositories manually. So at this point we agree


to create a command to setup the build tool. Our build tool is globally identified
as nobot , and will therefore have this command:

$ nobot setup
cloning repositories...
src
The src directory will contain all of our source code related to the build tool.
This is what we will have in this directory.

├── commands
├── constants
├── creators
├── helpers
├── nobot.js
└── setup

commands

In here will be a file for each command. What was initially agreed was to create
a command that builds the game and deploys it to the website, with another that
releases a new version of a template. So we would need three commands total.

Set up repositories:

nobot setup

Deploy a Game:

nobot game

Deploy a Template:

nobot template

One developer suggests that when creating a game, we should pass it the ticket
ID. The API would then retrieve all of the information needed, and feed it into
the game we are building. So we amend the command slightly.
nobot game <ticketId>

An issue raised about the template command comes up too. We should choose
the template we want to release. So we agree to provide an option to the
template.
nobot template --id="rock-paper-scissors"

If not provided as an option like above, or if the id does not match the available
templates, the script will prompt the user to choose one that is available for
release.

Please note: we could add more commands, but we are keeping to the basic idea
of the build tool and building a game.

constants

This directory will contain any constants that can be imported by other scripts
whenever they need them. For example, when creating a log script, we want
different levels of logging.

src/constants/log-level.js

const LOG_LEVELS = {
ERROR: 'error',
WARNING: 'warning',
INFO: 'info',
SUCCESS: 'success'
};

module.exports = LOG_LEVELS;

creators

The command game will delegate the creation of games to creators, rather than
containing all of the logic. This is because each template will have its own
process of creation. The command script will use a switch case to choose the
correct template, and then use a creator function, passing it the ID of the ticket
and the data from the API.

switch (template) {
case ROCK_PAPER_SCISSORS:
createRockPaperScissors(ticketId, data); // our creator script
break;

// ... other templates

default:
throw new Error(`Could not find template ${template}`);
}

helpers

Helpers are reusable functions that we can use anywhere. Here is a list of some
of the helpers we should build:

Create release build of a template.


Create a deployment branch.
Get the path of the repositories directory.
Updating a template.

These helper functions will be imported when needed, and will help us to avoid
code repetition.

setup

These scripts will deal with the process of cloning the repositories. If we needed
to do some more setting up for the build tool, we would add it in here.

nobot.js

This behaves as the umbrella/entry point into our CLI application. Take a
concierge as an example, retrieving input from the user and directing them to the
right door (our commands). This will be our focus in the next chapter, in which
we will use a library called commander .
Finally, we have to talk about the dummy API I set up for this book, called Nira,
which in your case would be something like Target Process or Jira. I thought it
would be wise to create my own dependency rather than relying on another API
that is constantly changing. I have used an endpoint contract similar to Jira’s.

http://opencanvas.co.uk/nira/rest/api/latest/ticket?authKey=NOBOT_123&ticketId=GS-100

The way this will work is that you make a simple GET request, and the API will
respond with a JSON object with all the data - fetched from the requested ticket -
about a specific game. It is operating like a REST API.

1. http://opencanvas.co.uk/nira/rest/api/latest/ticket is the API endpoint.


2. authKey is to grant the user access to the sensitive content. (Without this, it
returns a 404)
3. ticketId is the unique identifier of the game you need to create.

I have set up five API calls in the backend for this book listed below. Each API
call will return a JSON object with data associated with that game. Here is an
example response:

{
id: 36235,
template: "rock-paper-scissors",
projectName: "fire-water-earth-cute",
font: "Cabin",
fontUrl: "https://fonts.googleapis.com/css?family=Cabin",
assetsPath: "http://cdn.opencanvas.co.uk/automatingwithnodejs/assets/rock-paper-scissors/fire-water-earth-cute",
labelFirstOption: "fire",
labelSecondOption: "water",
labelThirdOption: "earth",
screenChoiceTitle: "Fire Water & Earth",
screenChoiceSubtitle: "Choose your element",
screenResultWon: "you won!",
screenResultLost: "you lost!",
screenResultDraw: "it's a draw!",
screenResultReplay: "replay",
screenResultFeedbackWon: "{player} beats {cpu}",
screenResultFeedbackLost: "{cpu} beats {player}",
screenResultFeedbackDraw: "Nobody won this time"
}
Games List

Fire Water Earth Cute

API call: http://opencanvas.co.uk/nira/rest/api/latest/ticket?


authKey=NOBOT_123&ticketId=GS-100

Fire Water Earth Fantasy

API call: http://opencanvas.co.uk/nira/rest/api/latest/ticket?


authKey=NOBOT_123&ticketId=GS-101

Fire Water Earth Retro

API call: http://opencanvas.co.uk/nira/rest/api/latest/ticket?


authKey=NOBOT_123&ticketId=GS-102
Rock Paper Scissors Doodle

API call: http://opencanvas.co.uk/nira/rest/api/latest/ticket?


authKey=NOBOT_123&ticketId=GS-103

Rock Paper Scissors Modern

API call: http://opencanvas.co.uk/nira/rest/api/latest/ticket?


authKey=NOBOT_123&ticketId=GS-104

This means that we will have to make a HTTP request in our build tool. This will
be done using the imported axios library.

Right, we have planned enough to know what we have to do. Let’s get to work!
Commander
If you haven’t heard of commander, you should. It’s a great way to bootstrap
your CLI application. I think it would be good to start with an overview of the
entire script, after which we will make it together, step by step.

src/nobot.js

const nobot = require('commander');


const { version } = require('../package');

// commands
const setup = require('./commands/setup');
const game = require('./commands/game');
const template = require('./commands/template');

nobot
.version(version);

nobot
.command('setup')
.description('clone repository dependencies')
.action(setup);

nobot
.command('game <ticketId>')
.description('create and deploy a new game reskin')
.action(game);

nobot
.command('template')
.description('release core files of template')
.option('-i, --id, [id]', 'what template to release')
.action(template);

nobot
.command('*')
.action(() => nobot.help());

nobot.parse(process.argv);

if (!process.argv.slice(2).length) {
nobot.help();
}
src/nobot.js

What we do first, is create a new program called nobot . This will be our instance
of commander. I extract the version key from package.json dynamically on the next
line.

const nobot = require('commander');


const { version } = require('../package');// e.g. 1.0.0

Next I require/import all of the commands which are found under the commands

directory. At present they would be empty JavaScript files.

// src/commands/setup.js
// src/commands/game.js
// src/commands/template.js

// commands
const setup = require('./commands/setup');
const game = require('./commands/game');
const template = require('./commands/template');

I pass the version number, e.g. 1.0.0 , to the version method on the commander
instance nobot . This will output the version in the CLI.

nobot
.version(version);

Commander allows each command to have a command identifier (e.g. setup ), a


description to explain what that command does, and a function to call as the
action to that command. Each of our commands are separate scripts that we
import and pass to the action function.

The first command we will declare is setup , as agreed in the meeting. This
command will clone the external repositories we depend on: The templates
repository, and the website repository.

nobot
.command('setup')
.description('clone repository dependencies')
.action(setup);

Our next command is game . This command will be used to create and deploy a
new game. In the example below, you can see that it expects an option to be
passed, enclosed in angled brackets <ticketId> . This value would be the ticket
number e.g. GS-101 , where all of the data related to the game will be fetched from
using the Nira API. Angle brackets signify that this is a mandatory value without
which the command will not be executed. Alternatively, you can wrap it in
square brackets, meaning it’s optional [ticketId] . When using square brackets, the
script would continue even if the optional value was not passed as an option.

nobot
.command('game <ticketId>')
.description('create and deploy a new game reskin')
.action(game);

Next up is the template command. Each template will use Semantic Versioning.
We want to create a command that will fetch the latest version of the template
and copy the bundled JavaScript and CSS to the core directory of the website.
So if I have version 1.0.0 as the current version, and 1.1.0 is the latest version,
the command template will build this and copy over the files rock-paper-scissors.1.0.1.js

and rock-paper-scissors.1.0.1.css to our nobot-website repository’s core directory.


Semantic Versioning A versioning system that is made up of 3 components,
X.Y.Z where: X is the major version, Y is the minor version, and Z is the
patch version. Semantic versioning is applied to projects. In our case, it
would be a game template. When releasing your application: if you are
fixing bugs without introducing breaking changes, you would increment the
patch version (Z); If you are adding a feature that doesn’t have breaking
changes, you would increment the minor version (Y); If you are making
breaking changes, you would increment the major version (X).

The ID of the template rock-paper-scissors can be passed as an option, and if it isn’t,


then we will prompt the user to choose from an array of supported templates.
This will be demonstrated later on.

Please note: The argument -i has been made optional for demonstration
purposes.

Another way to support options in your command is to use the option function.
This function takes two parameters: the option format and its description. The
option format accepts a string with comma-separated flags, e.g. ‘-i, —id, [id]’.
The description parameter accepts a string describing the option, e.g. ‘what
template to release’.

nobot
.command('template')
.description('release core files of template')
.option('-i, --id, [id]', 'what template to release')
.action(template);

Now if the user types a command in the CLI other than the three stated above,
then we want to capture that, and instead show the user what is actually
available. So to do this, we use an asterisk to catch everything other than the
three commands by doing this. It behaves like a regular expression.

nobot
.command('*')
.action(() => nobot.help());

nobot.parse(process.argv);

As a last catch, if the user types only nobot into the terminal and hits enter, then
we want to also output the help list so they can understand what else has to be
inputted.

if (!process.argv.slice(2).length) {
nobot.help();
}

So there we have it, the first script in our build tool. This will be the main entry
point into our build tool, and it will route us to the commands by typing them out
into the CLI. You can see commander provides a user friendly interface to try
and help the user understand the app’s capabilities. This is actioned by invoking
nobot.help .

When you run it you will see an output like this:


$ node src/nobot.js
Usage: nobot [options] [command]

Options:

-V, --version output the version number


-h, --help output usage information

Commands:

setup clone dependent repositories


game <ticketId> create and deploy a new game reskin
template [options] release core files of template
*

Now let’s make this script easier to use. At the moment, to use this script, we’d
need to run node [path to script] . We can do better. In your package.json file , there is an
object you can set called bin . Running npm link in the directory of the package.json

will make a global alias for each property set in bin . But you don’t have to
worry about doing that for this project, as it’s already taken care of by the init.js

script, which you can run by doing npm run init in the root of the nobot repository.

"bin": {
"nobot": "./src/nobot.js"
}

In here, I am declaring a global command called nobot, and pointing it to the file
src/nobot.js .

So now run npm run init . You will see something like this amongst the output of
this script.
/usr/local/bin/nobot -> /usr/local/lib/node_modules/nobot/src/nobot.js

Please note: This has been done on a Mac and will look different for a Windows
machine.

I am setting up an identifier called nobot and linking it to a JavaScript file. Pretty


neat. Let’s test it to see what happens.
$ nobot

Usage: nobot [options] [command]

Options:

-V, --version output the version number


-h, --help output usage information

Commands:

setup clone all the templates and deployment website


game <ticketId> creates and deploys a new game reskin
template [options] releases core of template
*

Splendid, we have an entry point into our application. Now let’s move on to API
configuration, template locations and the deployment process.
Configuration
Before we can build our setup command, we want to think about a configuration
file that can hold specific details about what templates we are using, what
repository are we deploying to, what base branch we branch off of: master ,
develop ? What is the API URL we are using to obtain the values we need to build
the game? All of this can be declared in a config.json file. This file is not included
in source control, because we would be committing sensitive data.

Please note: You might have noticed that you have a config.json file in your
project, alongside the config.example.json file. This was done by the init.js script
which you have run via npm run init in the last chapter.

If we look at the implementation used in nobot, we can see how it’s beneficial to
have dynamic configuration rather than hard coding it all into our scripts. You’ll
need to change “https://github.com/smks/nobot-website.git” and
“https://github.com/smks/nobot-template-rock-paper-scissors” to your forked
repositories’ URLs. The second is the api.authKey , which as shown below, needs to
be “NOBOT_123”. This key permits you to retrieve data from the API. Without
this key, the API will respond with a 404 page.

config.json
You will need to make two changes in this file. The first is the URLs of the
nobot repositories outlined in the initial part 2 chapter. The second is the authKey .
You are free to make API calls to Nira.

{
"api": {
"authKey": "NOBOT_123",
"endpoint": "http://opencanvas.co.uk/nira/rest/api/latest/ticket"
},
"deploy": {
"baseBranch": "master",
"name": "website",
"repo": "https://github.com/smks/nobot-website.git",
"coreDirectory": "core",
"releaseDirectory": "releases"
},
"templates": {
"rock-paper-scissors": {
"baseBranch": "master",
"repo": "https://github.com/smks/nobot-template-rock-paper-scissors"
}
}
}

So at the top we have an object that contains details about the API we are calling
to retrieve the data. The authKey , in the case of Jira (at the time of writing), would
be Base64 encoded Basic Auth. We have just set it as a GET parameter with the
value “NOBOT_123” for simplicity.

"api": {
"authKey": "NOBOT_123",
"endpoint": "http://opencanvas.co.uk/nira/rest/api/latest/ticket"
}

Next we want to contain details about the deployment process. This is under the
deploy object. We may choose to have our base branch as master or if we wanted
to trial the build tool first, set it to a separate branch such as develop . The name is
used to specify a different name to the actual repository for convenience. This is
the simple branching strategy we will be applying.
The repo is the repository we want to clone. When following along you would
have forked your own. This would be changed so that you have permissions to
do deployments. coreDirectory is when the command template is releasing a new
template version, it will copy it to the core directory of the website repository.
Similarly to the releaseDirectory , all games will be released to this directory when
running the game command.

"deploy": {
"baseBranch": "master",
"name": "website",
"repo": "https://github.com/smks/nobot-website.git",
"coreDirectory": "core",
"releaseDirectory": "releases"
}

Finally, we have a list of the templates that will be cloned. Only one template
exists for this book, but this would grow, as would your game template creations.

"templates": {
"rock-paper-scissors": {
"baseBranch": "master",
"repo": "https://github.com/smks/nobot-template-rock-paper-scissors"
}
}
Constants
A single place to declare constants is good. Plus, it helps avoid the mystifying
‘Magic Numbers’ problem.

Magic Numbers A magic number is a direct usage of a number in code.


Since it has the chances of changing at a later stage, it can be said that the
number is hard to update. It isn’t recommended and is considered to be a
breakage of one of the oldest rules of programming.

When a user chooses a template, they can optionally cancel. We are going to use
a constant rather than hard-coding -1 . We create a file that can contain many
common constants.
src/constants/common.js

const COMMON = {
JSON_WHITESPACE: 4,
GAME_JSON: 'game.json',
NO_CHOICE_MADE: -1
};

module.exports = COMMON;

Rather than passing the same strings, such as 'error' or 'info' , in many places,
we put them in constants so that if we change them, they get updated
everywhere. Although we only have two constants objects, this would
potentially grow as the features of the application increase.

src/constants/log-level.js

const LOG_LEVELS = {
ERROR: 'error',
WARNING: 'warning',
INFO: 'info',
SUCCESS: 'success'
};

module.exports = LOG_LEVELS;

When we want to use it in another script, we would import like so.

const { ERROR, INFO, SUCCESS, WARNING } = require('./../../constants/log-level');

These log level constants will be used for our log helper demonstrated in the
next chapter. For now we are getting it ready for use.

src/constants/templates.js

const TEMPLATES = {
ROCK_PAPER_SCISSORS: 'rock-paper-scissors'
};

module.exports = TEMPLATES;

Here is a place to declare all of our template constants. Simple and


straightforward, and is used for our switch case when the template has been
chosen. The template command will match this constant to a creator. Below is just
a example of what we will be creating later on.

const { ROCK_PAPER_SCISSORS } = require('./../constants/templates');

//...

switch (template) {

case ROCK_PAPER_SCISSORS:
// use creator
break;
// ...etc.

}
Helpers
This chapter will output each of the helpers and explain their purpose. You
should keep following along with the code examples, as we will be using these
helpers in our commands and creators.

src/helpers/build-template.js

Our templates should follow a consistent build process. When I refer to the build
process, I am talking about installing all of the node dependencies, and about the
npm task that transpiles, minifies, and does everything else necessary to make
the template ready for production. This helper will be needed for preparing the
game for release, and building the core functionality.

const { cd, exec } = require('shelljs');

const buildTemplate = (templatePath) => {


cd(templatePath);
exec('npm install');
exec('npm run build');
};

module.exports = buildTemplate;
src/helpers/create-deploy-branch.js

This helper is used to create a new branch for the website repository. It starts by
switching to the base branch (this could be master or develop) and pulling in all
of the latest commits. When these changes have been pulled through, it creates a
new branch - this would be prefixed with the ticket number and a short
description (e.g. the project name) so that it can be identified.

const { cd, exec } = require('shelljs');


const { deploy: { baseBranch } } = require('../../config');
const releasePath = require('./get-release-path');

const createDeployBranch = (branchName) => {


cd(releasePath);
exec(`git checkout ${baseBranch}`);
exec(`git pull origin ${baseBranch}`);
exec(`git checkout -b ${branchName}`);
};

module.exports = createDeployBranch;

src/helpers/deploy-game.js
This helper deals primarily with source control. Staging your project production
build, committing it with a message, switching to the base branch, pulling the
latest commits, and then merging your feature branch to the base branch. This
happens on the website repository.

const { cd, exec } = require('shelljs');


const { deploy: { baseBranch } } = require('../../config');
const releasePath = require('./get-release-path');
const log = require('./log');
const { INFO } = require('../constants/log-levels');

const deployGame = (branchName, projectName, ticketId) => {


log(`changing to path ${releasePath}`, INFO);
cd(releasePath);
exec(`git pull origin ${baseBranch}`);
log(`staging project ${projectName}`, INFO);
exec(`git add ${projectName}`);
exec(`git commit -m "${ticketId} - ${projectName} release"`);
log(`switching to base branch ${baseBranch}`, INFO);
exec(`git checkout ${baseBranch} && git pull origin ${baseBranch}`);
log(`merging ${branchName} into ${baseBranch}`, INFO);
exec(`git merge ${branchName}`);
exec(`git push origin ${baseBranch}`);
exec(`git branch -d ${branchName}`);
};

module.exports = deployGame;

Just to clean up, we delete the feature branch we created.

src/helpers/deploy-template.js

This helper is quite similar to the previous helper deploy-game.js , and although there
are not many differences, I would prefer that deployment for template and game
are not entwined just in case their process changes.
const { cd, exec } = require('shelljs');
const { deploy: { baseBranch } } = require('../../config');
const websitePath = require('./get-website-path');
const log = require('./log');
const { INFO } = require('../constants/log-levels');

const deployTemplate = (template, version) => {


const branchName = `${template}-${version}`;
log(`changing to path ${websitePath}`, INFO);
cd(websitePath);
exec(`git pull origin ${baseBranch}`);
log(`staging template ${branchName}`, INFO);
exec(`git checkout -b ${branchName}`);
exec('git add core/*');
exec(`git commit -m "${template}.${version}"`);
log(`switching to base branch ${baseBranch}`, INFO);
exec(`git checkout ${baseBranch} && git pull origin ${baseBranch}`);
log(`merging ${branchName} into ${baseBranch}`, INFO);
exec(`git merge ${branchName}`);
exec(`git push origin ${baseBranch}`);
exec(`git branch -d ${branchName}`);
};

module.exports = deployTemplate;

src/helpers/get-deploy-core-path.js

Our path to release the core bundle files is returned from this helper. It saves us
reconstructing the path in multiple places.

const { join } = require('path');


const repositoryPath = require('./get-repositories-path');
const { deploy: { name, coreDirectory } } = require('../../config');

module.exports = join(repositoryPath, name, coreDirectory);


src/helpers/get-release-path.js

Our path to release the project implementation is returned from this helper. It
saves us reconstructing the path in multiple places.

const { join } = require('path');


const repositoryPath = require('./get-repositories-path');
const { deploy: { name, releaseDirectory } } = require('../../config');

module.exports = join(repositoryPath, name, releaseDirectory);

src/helpers/get-repositories-path.js

The repositories path that contains all of our external repositories.

const { join } = require('path');

module.exports = join(__dirname, '..', '..', 'repositories');


src/helpers/get-templates-path.js

The path that has the list of templates we currently support.

const { join } = require('path');


const repositoryPath = require('./get-repositories-path');

module.exports = join(repositoryPath, 'templates');

src/helpers/get-ticket-data.js

This is the helper that will make a HTTP request to our API. For that we make
use of a library called axios , which deals with the underlying call. As you can
see, it’s importing data from our configuration to extract the authentication key
and endpoint. The axios library conveniently returns us a promise.

const axios = require('axios');


const { api: { authKey, endpoint } } = require('../../config');

const getTicketData = ticketId => axios({


url: endpoint,
params: {
authKey,
ticketId
}
});

module.exports = getTicketData;

src/helpers/get-website-path.js

This is the path to the repository where we deploy our projects.

const { join } = require('path');


const repositoryPath = require('./get-repositories-path');
const { deploy: { name } } = require('../../config');

module.exports = join(repositoryPath, name);

src/helpers/log.js

This was demonstrated in one of the examples, and is a personal preference of


mine for logging different levels with associated colours.
require('colors');
const {
ERROR, WARNING, INFO, SUCCESS
} = require('../constants/log-levels');

const log = (message, type) => {


let colorMessage;
switch (type) {
case ERROR:
colorMessage = `[${ERROR}] ${message}`.red;
break;
case WARNING:
colorMessage = `[${WARNING}] ${message}`.yellow;
break;
case INFO:
colorMessage = `[${INFO}] ${message}`.blue;
break;
case SUCCESS:
colorMessage = `[${SUCCESS}] ${message}`.green;
break;
default:
colorMessage = message;
}
console.log(colorMessage);
};

module.exports = log;

src/helpers/update-template.js

This helper is used to pull in any bug fixes or features from the latest version of
the template. We do this before running the build-template.js helper.

const { cd, exec } = require('shelljs');


const { templates } = require('../../config');

const updateTemplate = (template, templatePath) => {


cd(templatePath);
const { baseBranch } = templates[template];
exec(`git pull origin ${baseBranch}`);
};

module.exports = updateTemplate;
With our helpers, we can now proceed with other scripts.
Setup
The setup command exists so that we can initialise the build tool. Because we
need to retrieve templates and deploy projects to our games website, we need to
pull in these repositories, so the build tool can do what it was born to do.

First, we will clone the website under our repositories directory. The following
script will deal with that process.

src/setup/deployment.js

With the use of npm installed libraries, the native Node API, and some of our
helpers, we can achieve the task of cloning our deployment repository. In this
case, it’s the Nobot Game Studios website. This script’s goal is to check if the
repository exists, if it doesn’t, then we have to clone it.

const { cd, exec } = require('shelljs');


const { existsSync } = require('fs');
const { deploy: { name, repo } } = require('../../config');
const log = require('../helpers/log');
const repositoriesPath = require('../helpers/get-repositories-path');
const websitePath = require('../helpers/get-website-path');
const { INFO } = require('../constants/log-levels');

const setupDeployment = () => {


if (existsSync(websitePath)) {
return log(`Deployment Repository '${websitePath}' exists`, INFO);
}
cd(repositoriesPath);
return exec(`git clone ${repo} --progress ${name}`);
};
module.exports = setupDeployment;

Perfect! We now have a script that will clone our website, and now we want to
clone all of the production ready templates. The build tool will then be able to
pick up these templates for deployment when the script below is run.

src/setup/templates.js

A similar thing is done with the templates, but we are looping over an object’s
keys. For each, if they don’t exist already, we clone the repository. This makes it
work dynamically as new template repositories are introduced.

const { cd, exec } = require('shelljs');


const { existsSync } = require('fs');
const { join } = require('path');
const log = require('../helpers/log');
const templatesPath = require('../helpers/get-templates-path');
const { templates } = require('../../config');

const setupTemplates = () => {


cd(templatesPath);
Object.keys(templates).map((template) => {
const templatePath = join(templatesPath, template);
if (existsSync(templatePath)) {
return log(`Template ${template} exists`, 'info');
}
log(`Downloading ${template}`, 'info');
const { baseBranch, repo } = templates[template];
return exec(`git clone ${repo} --branch ${baseBranch} --progress ${template}`);
});
};

module.exports = setupTemplates;

These two scripts will be invoked when we run the setup command of nobot.
Below is a pseudo example of what will happen.
$ nobot setup
cloning website...
cloning templates...
Command - Setup
We include the setup scripts shown in the previous chapter, and invoke them.

src/commands/setup.js

const setupDeployment = require('../setup/deployment');


const setupTemplates = require('../setup/templates');

const setup = () => {


setupDeployment();
setupTemplates();
};

module.exports = setup;

So just to explain, when we run:

$ nobot setup
// running setup command

It will call both the deployment and template setup scripts, so that all of our
repositories are ready for the game release process.
Command - Template
This command is a bit more involved. Let’s step through each code block,
starting with the imported modules.

src/commands/template.js

const fse = require('fs-extra');


const { join } = require('path');
const templatesPath = require('../helpers/get-templates-path');
const deployCorePath = require('../helpers/get-deploy-core-path');
const buildTemplate = require('../helpers/build-template');
const updateTemplate = require('../helpers/update-template');
const log = require('../helpers/log');
const readlineSync = require('readline-sync');
const { SUCCESS, ERROR } = require('../constants/log-levels');
const { NO_CHOICE_MADE } = require('../constants/common');
const deployTemplate = require('../helpers/deploy-template');

fs-extra is being used to copy the template core files.


join is being used to construct the path to the chosen template.
templatesPath, deployCorePath are the helpers for getting the template and deploy
path.
buildTemplate is a helper to build the template.
updateTemplate is a helper to update the template before we build it to make
sure it’s the latest stable version.
log is our log helper to log anything that went right/wrong.
readlineSync is used to read input from the user.
SUCCESS, ERROR are log level constants. We only need these two in this case.
NO_CHOICE_MADE is a constant that signifies the user cancelled a choice of
template.
deployTemplate is used to deploy the template and merge to the base branch,
once we are ready to do so.

Now to the main function, assigned to template . The id parameter is optionally


passed as input from the user. This is captured in nobot.js . We scan the directory
of templates to return an array (making sure to filter out anything NOT template
related).

If the user does not pass the --id=rock-paper-scissors option when calling the
nobot template command or they did enter a template but it doesn’t exist, we prompt
the user with the readline-sync library to choose from templates that ‘do’ exist.

const template = ({ id }) => {


let choice = id;

const templates = fse.readdirSync(templatesPath).filter(t => t.match(/\./) === null);

if (choice === undefined || templates.includes(choice) === false) {


const index = readlineSync.keyInSelect(templates, 'choose template to release ');
if (index === NO_CHOICE_MADE) {
log('template release cancelled', ERROR);
process.exit(0);
}
choice = templates[index];
}

By this point, we would have the choice from the user. So we create a template
path, and then update the template using the helper we created earlier. Following
that, we build it.

// get template path


const templatePath = join(templatesPath, choice);

// update the template


updateTemplate(templatePath);

// build the core files


buildTemplate(templatePath);
Now that the template has built the core files, it’s just a case of copying them
into the core directory of our website repository.

const templateReleaseSource = join(templatePath, 'public', 'core');


const templateReleaseDestination = deployCorePath;
const templatePackageJson = join(templatePath, 'package.json');
const { version } = require(templatePackageJson);

fse.copy(templateReleaseSource, templateReleaseDestination)
.then(() => {
deployTemplate(choice, version);
log('released latest template version', SUCCESS);
})
.catch(e => log(e, ERROR));
};

module.exports = template;

The core directory would over time have something like this.

- core
-- template-1.0.0.css
-- template-1.0.0.js
-- template-1.0.1.css
-- template-1.0.1.js
-- template-1.0.2.css
-- template-1.0.2.js
-- template-2.0.0.css
-- template-2.0.0.js
Command - Game
As mentioned before, this command will be delegating each game creation to a
creator. This command’s responsibility is to pass the ticket information to the
creator and nothing more. So let’s take a look.

src/commands/game.js

We start by importing the templates, the error log level constant, the helper
created earlier to fetch the data from our API, our custom log function, and the
creator function.

require('colors');
const { ROCK_PAPER_SCISSORS } = require('../constants/templates');
const { ERROR } = require('../constants/log-levels');
const getTicketData = require('../helpers/get-ticket-data');
const log = require('../helpers/log');
// game creators
const createRockPaperScissors = require('../creators/rock-paper-scissors');

Our main function game receives the mandatory ticket ID parameter. The
getTicketData helper will use this ticket ID to fetch the associated data from Nira.
Because axios uses a promise implementation, we return the data part of the
response object. The ticket determines the template to be used (which should be
correctly decided by the product owner).

If the template matches one of the cases in the switch statement, it calls the
relevant creator. Otherwise, we log an error.

const game = (ticketId) => {


getTicketData(ticketId)
.then(({ data }) => {
const { template } = data;

switch (template) {
case ROCK_PAPER_SCISSORS:
createRockPaperScissors(ticketId, data);
break;

default:
throw new Error(`Could not find template ${template}`);
}
})
.catch(e => log(e, ERROR));
};

module.exports = game;

So this command simply fetches the data from Nira and passes it to the creator.
Creator - Rock Paper Scissors
I’ve created a transformer. Its sole purpose is to take the values from the API,
and transform them into our JSON configuration format. When I used Jira, there
were custom fields set that had no semantic meaning when returned in JSON. I
use the original configuration data from the template, so that any values that
don’t get overridden by our API data remain as default.

src/creators/rock-paper-scissors/transform.js

const fse = require('fs-extra');


const path = require('path');
const templatesPath = require('../../helpers/get-templates-path');
const { ROCK_PAPER_SCISSORS } = require('../../constants/templates');
const { GAME_JSON } = require('../../constants/common');

const transform = ({
id,
projectName,
font,
fontUrl,
assetsPath,
labelFirstOption,
labelSecondOption,
labelThirdOption,
screenChoiceTitle,
screenChoiceSubtitle,
screenResultWon,
screenResultLost,
screenResultDraw,
screenResultReplay,
screenResultFeedbackWon,
screenResultFeedbackLost,
screenResultFeedbackDraw
}) => new Promise((resolve, reject) => {
try {
const originalTemplateConfigPath = path.join(
templatesPath,
ROCK_PAPER_SCISSORS,
'public',
GAME_JSON
);
const originalTemplateConfig = fse.readJsonSync(originalTemplateConfigPath);
const newConfig = originalTemplateConfig;
newConfig.id = id;
newConfig.projectName = projectName;
newConfig.theme.fontFamily = font;
newConfig.customStyles = [
fontUrl
];
newConfig.theme.path = assetsPath;
newConfig.labels.rock = labelFirstOption;
newConfig.labels.paper = labelSecondOption;
newConfig.labels.scissors = labelThirdOption;
newConfig.screens.choice.title = screenChoiceTitle;
newConfig.screens.choice.subtitle = screenChoiceSubtitle;
newConfig.screens.result.won = screenResultWon;
newConfig.screens.result.lost = screenResultLost;
newConfig.screens.result.draw = screenResultDraw;
newConfig.screens.result.replay = screenResultReplay;
newConfig.screens.result.feedback.won = screenResultFeedbackWon;
newConfig.screens.result.feedback.lost = screenResultFeedbackLost;
newConfig.screens.result.feedback.draw = screenResultFeedbackDraw;
resolve(newConfig);
} catch (e) {
reject(e);
}
});

module.exports = transform;

The transform process acts as a bridge or translator between the API and the
build tool. Translating the data from one form to another form that the build tool
will understand. The function returns back the new configuration object.

src/creators/rock-paper-scissors/index.js
Now onto the actual creation of the game. As usual, we include all of our
necessary libraries and helpers.

const fse = require('fs-extra');


const { join } = require('path');
const templatesPath = require('../../helpers/get-templates-path');
const releasePath = require('../../helpers/get-release-path');
const buildTemplate = require('../../helpers/build-template');
const createDeployBranch = require('../../helpers/create-deploy-branch');
const deployGame = require('../../helpers/deploy-game');
const log = require('../../helpers/log');
const { ROCK_PAPER_SCISSORS } = require('../../constants/templates');
const { INFO, SUCCESS, ERROR } = require('../../constants/log-levels');
const { JSON_WHITESPACE, GAME_JSON } = require('../../constants/common');
const transform = require('./transform');

We want to use our create deploy branch helper, but first we construct a branch
name. This is composed of our ticket ID, followed by an underscore, and the
name of the project. This keeps our branch both unique so it doesn’t conflict
with other projects as well as being meaningful to anyone looking at it.

const create = (ticketId, ticketInformation) => {


const { projectName } = ticketInformation;

// 1. create a branch for deployment repository


const branchName = `${ticketId}_${projectName}`;
createDeployBranch(branchName);

Next we construct the path to our template we want to build. In this case it’s
‘Rock Paper Scissors’. This is passed to our buildTemplate helper.

// 2. run npm & build production version of template


const templatePath = join(templatesPath, ROCK_PAPER_SCISSORS);
buildTemplate(templatePath);

Now that the template is built for production, we can make a copy of our
template by grabbing the contents of index.html and game.json .

Please note: The JSON has not yet been updated.

The ignoreCoreFiles is a filter function for our copy function. This copy method is
only available with fs-extra and not the native fs module provided by Node.
// 3. create copy of template & update config values
const templateReleaseSource = join(templatePath, 'public');
const templateReleaseDestination = join(releasePath, projectName);

const ignoreCoreFiles = src => !src.match(/core/);

It’s now time to copy the files. As mentioned before, the good thing about the
fs-extra methods, is that they all use promises rather than callbacks, so we can
chain our calls like so.

fse.copy(templateReleaseSource, templateReleaseDestination, { filter: ignoreCoreFiles })


.then(() => transform(ticketInformation))
.then((newValues) => {
const configFile = join(templateReleaseDestination, GAME_JSON);
return fse.writeJsonSync(configFile, newValues, { spaces: JSON_WHITESPACE });
})
.then(() => {
log(`built ${templateReleaseDestination}`, SUCCESS);
log(`deploying ${branchName}`, INFO);
deployGame(branchName, projectName, ticketId);
})
.catch(e => log(e, ERROR));
};

module.exports = create;

1. We copy the index.html and game.json from the template repository. Passing
the filter function to ignore the subdirectory called core .
2. We pass the ticket information retrieved from the API to our transform
function shown earlier, which transforms the ticket information into our
game.json format.
3. The new transformed JSON then gets written synchronously to our project
in the releases directory of our website.
4. Finally, we have our modified changes in the website, all that we need to do
is stage, commit and merge the changes to our base branch.
5. We sigh with relief knowing it’s merged before the deadline.
End to end
And that’s the code side of it. Let’s see how it works end to end for each
command. We are going to start from cloning the nobot repository.

Please note: I clone https://github.com/smks/nobot.git , but this URL would be for your
own forked version.

$ git clone https://github.com/smks/nobot.git


Cloning into 'nobot'...
remote: Counting objects: 375, done.
remote: Compressing objects: 100% (65/65), done.
remote: Total 375 (delta 44), reused 69 (delta 30), pack-reused 276
Receiving objects: 100% (375/375), 388.19 KiB | 342.00 KiB/s, done.
Resolving deltas: 100% (158/158), done.

I change into the directory of the project and install all of my external node
modules.

$ cd nobot
nobot git:(master) npm install
added 256 packages in 3.026s

I run the command to create my new config.json and create a global alias named
nobot .

$ npm run init

> nobot@0.0.1 init /Users/shaun/Workspace/nobot


> node ./init.js

up to date in 0.779s
/usr/local/bin/nobot -> /usr/local/lib/node_modules/nobot/src/nobot.js
/usr/local/lib/node_modules/nobot -> /Users/shaun/Workspace/nobot
[success] created configuration file

Now in my config.json I update the authCode for our API so that we can receive
JSON from our endpoint (otherwise it would return a 404). If you haven’t done
this already, then now is the time to shine.

Please note: I have omitted commas for segments of JSON for readability. Your
actual config.json file has been structured as you would expect further down.

"authKey": "SECRET"

Changes to:

"authKey": "NOBOT_123"

My deployment repository will remain the same, but you should have forked it
and used your own. So your URL will be different. You can fork the game
template rock-paper-scissors as well if you want to add more features to the game.

config.json

{
"api": {
"authKey": "SECRET",
"endpoint": "http://opencanvas.co.uk/nira/rest/api/latest/ticket"
},
"deploy": {
"baseBranch": "master",
"name": "website",
"repo": "https://github.com/smks/nobot-website.git",
"coreDirectory": "core",
"releaseDirectory": "releases"
},
"templates": {
"rock-paper-scissors": {
"baseBranch": "master",
"repo": "https://github.com/smks/nobot-template-rock-paper-scissors"
}
}
}

I run the setup command to clone our repositories.


$ nobot setup
Cloning into 'website'...
remote: Counting objects: 122, done.
remote: Compressing objects: 100% (91/91), done.
remote: Total 122 (delta 57), reused 90 (delta 28), pack-reused 0
Receiving objects: 100% (122/122), 123.07 KiB | 275.00 KiB/s, done.
Resolving deltas: 100% (57/57), done.
[info] Downloading rock-paper-scissors
Cloning into 'rock-paper-scissors'...
remote: Counting objects: 100, done.
remote: Compressing objects: 100% (73/73), done.
remote: Total 100 (delta 37), reused 82 (delta 23), pack-reused 0
Receiving objects: 100% (100/100), 75.70 KiB | 332.00 KiB/s, done.
Resolving deltas: 100% (37/37), done.

These should now exist under your repositories directory.

Great, now one of my colleagues has applied a fix to the template. The problem
was that when saving the score to local storage, the result was being saved
across all games. We want it on a game by game basis. This means I need to use
the template command to release the latest version. Here it is in action.

$ nobot template

[1] rock-paper-scissors
[0] CANCEL

choose template to release [1/0]: 1


From https://github.com/smks/nobot-template-rock-paper-scissors
* branch master -> FETCH_HEAD
a84a476..0b0bb14 master -> origin/master
Updating a84a476..0b0bb14
Fast-forward
app/actions/save-score.js | 14 ++++++++++----
app/assets/index.html | 4 ++--
app/helpers/get-score.js | 8 +++++---
package.json | 2 +-
4 files changed, 18 insertions(+), 10 deletions(-)

> fsevents@1.1.3 install /Users/shaun/Workspace/nobot/repositories/templates/rock-paper-


scissors/node_modules/fsevents
> node install

[fsevents] Success: "/Users/shaun/Workspace/nobot/repositories/templates/rock-paper-


scissors/node_modules/fsevents/lib/binding/Release/node-v59-darwin-x64/fse.node" is installed via remote
added 969 packages in 9.13s

> rock-paper-scissors@1.1.0 build /Users/shaun/Workspace/nobot/repositories/templates/rock-paper-scissors


> brunch build --production

11:29:21 - info: compiled 24 files into 2 files, copied 2 in 3.7 sec


[info] changing to path /Users/shaun/Workspace/nobot/repositories/website
[info] staging template rock-paper-scissors-1.1.0
Switched to a new branch 'rock-paper-scissors-1.1.0'
[rock-paper-scissors-1.1.0 8706d73] rock-paper-scissors.1.1.0
2 files changed, 2 insertions(+)
create mode 100644 core/rock-paper-scissors.1.1.0.css
create mode 100644 core/rock-paper-scissors.1.1.0.js
[info] switching to base branch master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.
From https://github.com/smks/nobot-website
* branch master -> FETCH_HEAD
Already up-to-date.
[info] merging rock-paper-scissors-1.1.0 into master
Updating e9c394b..8706d73
Fast-forward
core/rock-paper-scissors.1.1.0.css | 1 +
core/rock-paper-scissors.1.1.0.js | 1 +
2 files changed, 2 insertions(+)
create mode 100644 core/rock-paper-scissors.1.1.0.css
create mode 100644 core/rock-paper-scissors.1.1.0.js
To https://github.com/smks/nobot-website.git
e9c394b..8706d73 master -> master
Deleted branch rock-paper-scissors-1.1.0 (was 8706d73).
[success] released latest template version

Brilliant! We have the latest version of our template. Now I can build five games
by running the command for each ticket. I will only show one example of this
being built and deployed, as it will spit out similar output.

$ nobot game GS-100


Already on 'master'
Your branch is up-to-date with 'origin/master'.
From https://github.com/smks/nobot-website
* branch master -> FETCH_HEAD
Already up-to-date.
Switched to a new branch 'GS-100_fire-water-earth-cute'
up to date in 2.899s

> rock-paper-scissors@1.1.0 build /Users/shaun/Workspace/nobot/repositories/templates/rock-paper-scissors


> brunch build --production

11:33:55 - info: compiled 24 files into 2 files, copied 2 in 2.3 sec


[success] built /Users/shaun/Workspace/nobot/repositories/website/releases/fire-water-earth-cute
[info] changing to path /Users/shaun/Workspace/nobot/repositories/website/releases
[info] staging project fire-water-earth-cute
[GS-100_fire-water-earth-cute d7a804d] GS-100 - fire-water-earth-cute release
2 files changed, 69 insertions(+)
create mode 100644 releases/fire-water-earth-cute/game.json
create mode 100644 releases/fire-water-earth-cute/index.html
[info] switching to base branch master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.
From https://github.com/smks/nobot-website
* branch master -> FETCH_HEAD
Already up-to-date.
[info] merging GS-100_fire-water-earth-cute into master
Updating 8706d73..d7a804d
Fast-forward
releases/fire-water-earth-cute/game.json | 39 +++++++++++++++++++++++++++++++
releases/fire-water-earth-cute/index.html | 30 ++++++++++++++++++++++++
2 files changed, 69 insertions(+)
create mode 100644 releases/fire-water-earth-cute/game.json
create mode 100644 releases/fire-water-earth-cute/index.html
To https://github.com/smks/nobot-website.git
8706d73..d7a804d master -> master
Deleted branch GS-100_fire-water-earth-cute (was d7a804d).

Our game has been built with the typing of the command and ticket ID, then…
the hit of an enter button. I have set up a Cron job on the website server-side to
pull in the latest changes every minute. Here is the live URL.
http://ngs.opencanvas.co.uk/

Please note: On the website, the index.php script scans the releases directory and
outputs tiles for each game that exists. So every time we deploy a new game, the
game tile will be added once the Cron job has pulled in the latest changes from
the repository.

I repeat running the build tool for the remaining four implementations.

nobot game GS-101


...
nobot game GS-102
...
nobot game GS-103
...
nobot game GS-104
...

Now we would have five games in the lobby on the website. They should pop up
as tiles on the main lobby page as demonstrated in the following screenshot.
When you click on one, it should open a modal containing the game in an
iframe. You should then be able to play the game we built with our tool.
Just to repeat, you can see the website here: http://ngs.opencanvas.co.uk/
Wrap up
Well… there you have it. An implementation that may prove to save you a lot of
time in the long run. I hope you find it useful! It doesn’t have to stop there
though. As you saw in some of the examples in part 1, you could add more
features such as email or SMS.

Here are a few that come to mind:

1. If a new template has been released, email your team with the update.
2. Set up a frontend UI that allows you to build a game and provide feedback.
Link it with the build tool.
3. Create your own templates with different functionality.
4. Set up a frontend UI that takes in CSV files, so you can batch create games.
5. Set up a hook on Jira (if you use it commercially) whenever a ticket is
created and allow the hook to call an endpoint on your server. That way it’s
fully automated, without any manual intervention.
6. Create a shortened link after creation and post a comment on the Jira ticket
with details on how to preview it.

It’s good to note that this is one approach to deployment, but there are perhaps
many better ways this can be done, such as continuous integration with Jenkins
or Travis. It’s something you can adopt in your workflow, but it’s outside the
scope of this book.

If you have any suggestions or improvements, feel free to let me know by


contacting me here: http://smks.co.uk/contact

Alternatively, you can follow me on the following social networks.


GitHub - https://github.com/smks
Twitter - https://twitter.com/shaunmstone
Facebook - https://www.facebook.com/automatingwithnodejs
YouTube - http://www.youtube.com/c/OpenCanvas

Or connect with me on LinkedIn for business-related requests.

LinkedIn - https://www.linkedin.com/in/shaunmstone

Thank you for reading. If you enjoyed it, please feel free to leave an online
review. I hope you can find a way to automate your workflow. Don’t do it too
much though… we all still need jobs!

Good luck!

S-ar putea să vă placă și