Making Mantra-CLI

On a weekend in February, I built a command line interface for Mantra which is an application architecture for Meteor. After the release, it became the official CLI for Mantra. In this post, I am going to share my experience of shipping Mantra-CLI.

By the way, the source code for the CLI is available on this repo. You can install and use it directly from npm.

Motivation

Mantra is a community generated guideline for architecting a Meteor application. Following Mantra, application authors need to abide by the opinionated directory structures, naming conventions, and file formats.

Such practice sounds good because it establishes the best practice that gives developers a relief. But without an automation, generating a new file that conforms to the rules becomes very tedious and error-prone.

For instance, the below is how a ‘container’ looks like in Mantra. There are usually many containers in a single app.

import {useDeps, composeAll, composeWithTracker, compose} from 'mantra-core';
import MyComponent from '../components/my_component.jsx';

export const composer = ({context}, onData) => {
  onData(null, {});
};

export const depsMapper = (context, actions) => ({
  context: () => context
});

export default composeAll(
  composeWithTracker(composer),
  useDeps(depsMapper)
)(MyComponent);

When I needed to write a new container, I found myself copying and pasting from the old container that I wrote.

After that, I would have to delete all the code that belonged to the old container before I could even began implementing the new container. Such task is kind of annoying when all you want to do is write some code.

Some might argue that the problem of generating file skeleton can be easily resolved on the IDE level by having snippets.

But there are many IDEs out there and it is impossible to support them all. At best, the community would end up with a fragmented ecosystem of many mediocre IDE-based solutions.

Another annoying task is that when an author adds a file to the codebase, she often needs to modify another entry file to make sure the new file is loaded to the app.

It quickly becomes tedious to set up these files at the right places. Something screams automation.

Commands for Mantra

I wanted to automate the following tasks with a command line interface:

  • Creating a new Meteor app that conforms with the Mantra spec
  • Generating entities (action, collections, modules, …)
  • Removing entities

Create command

If people could simply create a Mantra application just by running, mantra create [appName], they won’t have to clone some ‘kickstarter’ projects from GitHub as a skeleton.

I have never been a fan of those kickstarter repos because they come with a lot of baggage. Before you know, you could be sitting on a heavily over-engineered application when you have not written a single line of code of your own.

People do not need prematurely optimized skeleton apps to start using Mantra. After all, Mantra is a simple application architecture and all you need to get started is the directory structure and some initial files.

Mantra-CLI allows you to run the following command to have your project ready to go:

mantra c appName

Generate/Destroy command

Having used Ruby on Rails before, I knew how convenient it was to run a command and have your file and test ready with a correct format at a designated directory.

For instance, you can run rails g controller posts to generate PostsController under /apps/controller/.

With Mantra-CLI, you can bring that convenience to Meteor apps. Run commands like the following to generate/destroy files without risking human error:

mantra generate action core:users
mantra g container posts:postList
mantra g collection comments
mantra destroy action core:users
mantra d container posts:postList

Challenges

It was kind of hard to programmatically insert and remove strings in a file. Mantra-CLI needs that functionality because when an entity is generated or removed, import statements and some code needs to be added or removed from index.js files.

Solving this problem with a simple regex matching would be too error prone and hard to maintain in the future. Also, an elegant solution would require so much code that it sounds like it should be a module in and of itself.

So I implemented a module called editer based on another module called locater that I published a while ago. I gave a talk and did a blog post on how I made editer. You can read it here.

With editer, you can programmatically insert or remove a string on a multi-line string with various conditions. Basically, you can do some very complicated stuff with a simple API call as shown below:

var target = "Whoa, whoa, whoa, whoa... stop right there.";
var options = {or: [
  {before: {regex: /unicorn/ig, last: true}},
  {after: {regex: /whoa,\s/ig, occurrence: 3}},
  {after: {regex: /stop/i}}
]};
var result = editer.insert("hey, ", target, options);
console.log(result);
// => "Whoa, whoa, whoa, hey, whoa... stop right there."

Using it, updating index.js files programmatically is as simple as calling an API. The below is how Mantra-CLI actually does it:

function editFile(type, pathToFile, string, options) {
  let fileContent = fs.readFileSync(pathToFile, {encoding: 'utf-8'});
  let updatedContent;

  if (type === 'insert') {
    updatedContent = editer.insert(string, fileContent, options);
  } else if (type === 'remove') {
    updatedContent = editer.remove(string, fileContent, options);
  }

  fs.writeFileSync(pathToFile, updatedContent);
}

I learned first-hand that small libraries that each do one thing very well can provide convenient solutions for many situations.

What is next?

Mantra-CLI still needs to generate test files. I have not been testing the apps I am writing with this CLI, so I have not felt urgency to implement it yet (I am dog-fooding heavily on Mantra-CLI).

I think generating test files is the reasonable next step for the near future.

After becoming the official command line interface for Mantra, Mantra-CLI has been garnering a lot of community attention and contributions.

I am very proud of the community-based effort and pretty sure that whatever is next for the project, we will make it happen.