Arguably few jobs require more repetitive tasks than programming. Undoubtedly, few people hate redundant tasks more than programmers. A programmer typically would be willing to spend as much or even potentially more time automating a redundant task than actually doing it. Thankfully, this can lead to a number of useful tools for automation. Popular ones in the HTML and JavaScript community recently include Yeoman and Grunt.
A lesser known, but equally useful tool, is Automaton. Heavily inspired by Grunt, it allows you to create a number of tasks using built-in file system and scaffolding tools. It isn’t just inspired by Grunt, but it will also integrate with Grunt tasks. While you can use Automaton via code in Node.js, in this article I will focus on how to use it as a command-line interface (CLI) tool. We’ll examine how to create some common tasks and finally how to put it all together into a task that accomplishes a number of repetitive tasks in a single, short command line statement.
Installing Automaton
Provided you already have Node.js installed, installing Automaton is very easy.
npm install -g automaton
Example Automaton task
Over the course of this article we will look at all the pieces required to create a theoretical Automaton task that represents a number of repetitive items a typical developer might want to automate. In this case, our task will be one you might use when starting on a new site or application. It will take some default template files and copy them to a new site folder, modify some text in the templates based upon the command line options supplied, load the necessary NPM dependencies and finally start a Grunt watch task that will begin auto-compiling LESS files as they are modified.
To start, let’s create an autofile.js in our root web sites or projects directory:
var myTask = {
tasks: [
]
};
module.exports = myTask;
Creating common file system tasks
Automaton supports a number of common file system tasks that you might normally enter manually via the command line.
- chmod: used to change the permissions of files
- cp: used to copy files and/or directories
- mv: moves files and directories
- mkdir: create directories recursively
- rm: used to remove files and/or directories
- symlink: creates a symbolic link to another file or directory
Creating directories
The first task we need creates a folder where we will place our default site files. Of course, before we can do that, we need the user to supply the name of the directory to be created. Automaton allows you to create options which will then be supplied to each task. Below, we create an option called “dir” that, as no default value is supplied, will be required. This code should be added prior to the tasks array within the base autofile created above.
options: {
dir: {
description : 'The name of the project folder'
}
}
In order to specify this option via the command line, we would enter something like follows, where “my-project” is the name of the folder being created:
automaton --dir my-project
Now let’s write the task to create the directory. Place this code inside the tasks array in our autofile.js.
{
task: 'mkdir',
description: 'Creating the project root folder',
options: {
dirs: ['{{dir}}']
}
}
In the above task, the value “{{dir}}” will be automatically replaced with the option value supplied by the user, “my-project” in our example.
Copying files or directories
The next step in our example task will be to copy our default site/application files. In my example, I assume there is a template folder holding my default files in my sites root directory, however in reality these could be anywhere on your machine as long as you provide a proper path. In this case, I am using a template created with initializr (into which I have thrown a few sample LESS files simply for the purposes of our example task). Add this task after the mkdir task above (being sure to add a comma after the end of that task).
{
task: 'cp',
description: 'Copying files',
options: {
files: {
'initializr/**': '{{dir}}',
'_package.json': '{{dir}}/package.json',
'_Gruntfile.js': '{{dir}}/Gruntfile.js'
}
}
}
As you can see, we specified to copy everything (both directories and files) under the initializr directory. We’ve also listed to template files I created for a package.json and Gruntfile.js. As we’ll see in detail in the next step, the package.json includes the NPM dependencies our project will need while the Gruntfile specifies some default Grunt tasks. Both of these files are being renamed when they are moved to remove the leading underscore.
Using Scaffolding
The great news is that Automaton includes a number of very useful scaffolding tasks. The bad news is that it is not at all documented how to use them on the Automaton page. You’ll also be hard pressed to find out much by searching the main project code. However, I was able to locate sub-projects containing the task details at https://github.com/IU-Automaton. Each of these task repositories includes tests that are very revealing of the usage.
In my example task, the package.json file I copied in the above task is a template that contains two field that will be replaced with some options supplied by the user for the project name and version. First, we need to update our options from above to add these values:
options: {
dir: {
description : 'The name of the project folder'
},
p: {
description : 'The name of the project for the package.json'
},
v: {
description : 'The version number for the package.json',
'default': '0.0.1'
}
}
In the above code, I’ve added options for p, representing my project name, and v, representing my version. I’ve made these names short to simplify the command line statement required. You may notice the v value has a default, which means it is not required to be supplied by the user. We can now modify our command line call to run the task as follows:
automaton --dir my-project --p sampleapp
In the above statement, the value “sampleapp” will be used for the project name. Now let’s see our sample package.json template to see how to specify values to be replaced.
{
"name": "{{project_name}}",
"version": "{{version}}",
"devDependencies": {
"grunt": "~0.4.1",
"grunt-contrib-less": "~0.5.0",
"grunt-contrib-watch": "~0.3.1"
}
}
You may notice the items surrounded my two curly braces (or mustaches). These are the values that will be replaced. You may also notice that they are not identical to the value names from our options as I’ve made them more descriptive. As you’ll see in the scaffolding-replace task below, we can map options to values in the template.
{
task: 'scaffolding-replace',
description: 'Updating package.json',
options: {
files: ['{{dir}}/package.json'],
data: {
project_name: '{{p}}',
version: '{{v}}'
}
}
}
In the options within our task, I specify an array of files that I would like to have any values replaced (in this case, I am only specifying the one file). Next, my data object maps the name of the value in the template (on the left) with the value from our options (p and v respectively on the right).
Running the task as specified above will result in the following package.json file:
{
"name": "sampleapp",
"version": "0.0.1",
"devDependencies": {
"grunt": "~0.4.1",
"grunt-contrib-less": "~0.5.0",
"grunt-contrib-watch": "~0.3.1"
}
}
If you are familiar with package.json files, you may notice I have supplied a number of default dependencies. The first is Grunt itself, the second the LESS plugin and the last is the watch plugin. In the next step, we’ll look at how my task will utilize those.
Running arbitrary command line inputs
At this point, I have a complete set of sample project files in my project directory. The next step would be to run NPM and install any dependencies. Obviously, it would be better to simply automate this simple task. Thankfully, Automaton allows you to create tasks that run arbitrary command line statements, so let’s set one up to run the NPM install command in my project directory.
{
task: 'run',
options: {
cmd: 'npm install',
cwd: '{{dir}}'
}
}
As you can see, this will execute the command (cmd) “npm install” and, importantly, I can specify the directory in which this command should be executed, using the “cwd” option for the working directory.
When this command is executed, you see a lot of output indicating all the dependencies being downloaded for Grunt, the Grunt LESS plugin and the Grunt watch plugin, as specified in the package.json listed previously.
Let’s take a look at the default Gruntfile we’ve included in the default project files. It simply defines two simple tasks, one for compiling LESS files and one for watching any .less files for changes, and running the less task when one is.
/*global module:false*/
module.exports = function(grunt) {
// Project configuration.
grunt.initConfig({
watch: {
scripts: {
files: ['**/*.less'],
tasks: ['less'],
options: {
nospawn: true
}
}
},
less: {
development: {
options: {
paths: ["less"]
},
files: {
"less/sample.css": "less/*.less"
}
}
}
});
grunt.loadNpmTasks('grunt-contrib-less');
grunt.loadNpmTasks('grunt-contrib-watch');
// Default task.
grunt.registerTask('default', ['less']);
};
I’d like Automaton to go ahead and start the watch task so that I can begin working in my .less files and have them start auto-compiling. While Automaton includes the ability to integrate with Grunt files, I can’t seem to use that as the file needs to exist prior to starting the task (in this case, it doesn’t until we’ve copied our default files into the new directory). Instead, we’ll again use Automaton’s ability to run an arbitrary command.
{
task: 'run',
options: {
cmd: 'grunt watch',
cwd: '{{dir}}'
}
}
Putting it all together
Here’s the code for the entire complete task that we’ve built.
var myTask = {
options: {
dir: {
description : 'The name of the project folder'
},
p: {
description : 'The name of the project for the package.json'
},
v: {
description : 'The version number for the package.json',
'default': '0.0.1'
}
},
tasks: [
{
task: 'mkdir',
description: 'Creating the project root folder',
options: {
dirs: ['{{dir}}']
}
},
{
task: 'cp',
description: 'Copying files',
options: {
files: {
'initializr/**': '{{dir}}',
'_package.json': '{{dir}}/package.json',
'_Gruntfile.js': '{{dir}}/Gruntfile.js'
}
}
},
{
task: 'scaffolding-replace',
description: 'Updating package.json',
options: {
files: ['{{dir}}/package.json'],
data: {
project_name: '{{p}}',
version: '{{v}}'
}
}
},
{
task: 'run',
options: {
cmd: 'npm install',
cwd: '{{dir}}'
}
},
{
task: 'run',
options: {
cmd: 'grunt watch',
cwd: '{{dir}}'
}
}
]
};
module.exports = myTask;
Running the Task
As mentioned earlier, in order to run the task I created, I simply need to type the following statement in the command line (of course, in the same folder as the task):
automaton --dir my-project --p sampleapp
Let’s see what happens when I run this task. I’ve recorded a screencast to demonstrate. This will hopefully offer some clarity as to the full extent of what our pretty easy-to-create task does.
Where to go from here
Obviously, you could expand or modify this task however it suits you. For example, you might want to fire up a local web server in the project’s working directory. Or you might expand upon the templating to do more customizations. These are just a couple of ideas.
The thing I love most about tools like Automaton is that they can be extremely helpful and improve your workflow but are low commitment. You don’t need to adopt a new framework or modify your application code or do anything different other than hopefully eliminate some boring, redundant tasks. As we’ve seen, building tasks is easy. There’s quite a bit more you can do, so I encourage you to explore the documentation on GitHub. Hopefully, Indigo United, the creator of the project, will continue to improve and expand it (and document some of the undocumented portions get better documentation).
Quite inspiring. I am gonna try this.
I’d be interested in the motivation for creating something new: how is Automaton different from (better than?) Grunt?
@Axel – it has a different focus than Grunt and as I mention even integrates with Grunt if you want it to. It doesn’t go nearly a far as Grunt in terms of integrating with plugins but it is very easy and focused on basic command line file and directory based tasks.
I loved finding this tutorial after watching the video of your talk at google. This type of automation can save more time then most understand. Thank you so much for taking the time to explain this for others to use.
One thing though: It might be nice to add in using the “fatal:false” command to avoid any errors. This was particularly useful for the cp task as it would throw a “Enoent, no such file or directory ‘_package.json'” even though everything was correct. After adding the fatal:false command to the root of the cp task everything works perfectly! A git repo with branches that parallel the steps of the tutorial might be helpful to some, but I doubt you have the time to spend doing that. Regardless, you made my workflow so much faster already! Thank you again