Build faster by generating code with Hygen.io

Posted on 2018-01-24

The scalable code generator that lives in your project.

hygen is a code generator that lives in your project and scales with your team. It's fast, flexible, and perfectly fits modular code bases.

For the impatient, you check out the documentation and how to use it with Redux, React Native, and Express.js.

But if you’d like a story about software design as well, keep reading. The story is told by hijacking a couple of fundamental user experience design laws (well, they’re not really laws, but, yea).

Hick’s Law

Hick’s law states that:

The time it takes for a person to make a decision is a result of the possible choices he or she has: increasing the number of choices will increase the decision time.

Turning to technology, choosing a new tool is a massive uptake of new choices, and the process of learning that new tool amplifies it even more. Every step of the way, friction is the default.

As far as code generators go, hygen has a primary design goal that is to minimize this friction. The rest — is bonus.

Every code generator tool has a templating engine. But there’s also meta data; like where to place the new file at. To do metadata we placed a front-matter in the same template file, just like Jekyll does, and every Jekyll inspired blog generator that surfaced over the last few years (there’s plenty!).

If you have your own static blog generated with Jekyll, or any other tool that’s inspired by it, then this should be super familiar.

---
to: hygen-examples/mailers/<%= message || 'unnamed'%>/html.ejs
inject: true
before: "const modules = ["
skip_if: newModule
---

To render, we use ejs - a ubiquitous, non restricted (as opposed to logic-less) templating engine. We don’t deal with the logic-less flamewar. If you want logic in your templates, go ahead; we trust that you're responsible. There's plenty of ways to shoot your own foot but above all we want to be pragmatic.

From a syntax point of view a variant of ejs probably exists in what ever language you're coming from.

import View from './view'

storiesOf('<%= Name %>', module)
  .addDecorator(withKnobs)
  .addDecorator(withTests('<%= Name %>'))
  .add('default', () => <View />)

And we take a batteries included approach; a set of built-in commands to guide you to the next step.

$ hygen init self
Loaded templates: src/templates
       added: _templates/generator/with-prompt/hello.ejs.t
       added: _templates/generator/with-prompt/prompt.ejs.t
       added: _templates/generator/new/hello.ejs.t

$ hygen generator new  --name mygen
Loaded templates: _templates
       added: _templates/mygen/new/hello.ejs.t

There’s also intuitive argument parsing.

$ hygen module new --name auth --tokens bcrypt

And fantastic prompts, courtesy of inquirer.

$ hygen database setup --host localhost
? Select database (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◯ Postgres
 ◉ sqlite
 ◯ MySQL

hygen doesn't impose on you a new programming language, project setup, or workflow. Because it lives in your project, and in your existing workflow and setup, it just inherits those.

Parkinson’s Law

Let’s hijack Parkinson’s law for a moment. This is the problem with every generator framework I know.

Work expands so as to fill all of the time available for its completion.

When you have code generation set up as a separate project in or out of your repo (say, with cookiecutter or yeoman), it will become a thing. A shiny new toy.

At best, it becomes a product you look after that throws you out of context every time you want to tweak a template. At worst, it becomes stale and unmaintained — work invested, and value never extracted.### Rails Did It First

Wholesale project generators are still good for when you need an entire starter-project, although I doubt will survive given the surge of starter projects for which you can just git clone and move on.

They’re also less great if you want to embed a generator workflow into your existing project, like Rails did when it kickstarted the idea of having generators be a core part of developer productivity into mass adoption for the first time.

class CreateMigration < Thor::Actions::CreateFile
  def migration_dir
    File.dirname(@destination)
  end

  def migration_file_name
    @base.migration_file_name
  end

  def identical?
    exists? && File.binread(existing_migration) == render
  end
...

Rails and Thor, the generator framework it used, changed the way I thought about code generators. Up until then, when I needed to generate code (mostly for ORM entities, ah the times we had!) I was sucked up into .NET’s T4 Text Templates.

That was 7 years ago. I kept making generators with Thor and friends hoping for productivity spikes. But these became projects I maintained which sucked up valuable time, counter-intuitively — they were lowering the returns in general productivity.

The Shopping List

That only means one thing. A shopping list for a generator framework that stays out of your way.

  • Process ergonomics — I don’t want to compile my generators, or have them on a CI pipeline of their own.
  • Developer ergonomics — easily accessible and easily invoked.
  • Low friction — the pitfall of success. Each step I take should lead me to the next.
  • Technology agnostic — don’t want a new tech stack.
  • Contextual — if I’m on the data layer, I want data generators.
  • Scalable — should work for multiple teams iterating over a large and modular codebase.
  • Feature packed — simple design doesn’t mean a poor number of features.
  • Flexible — give me a way to shoot my foot if I want to.
  • Embeddable — can be composed into other projects.
  • Super customizable — defaults are OK, but give me escape hatches.
  • Clean, intentful — first be useful, only then be cool.

That’s what hygen became.

Hygen

You can use it right now, in what ever project you have open. Here’s how to make a generator that adds a markdown document to your docs/ folder.

$ npm i -g hygen
$ cd your-project
$ hygen init self
$ hygen generator new --name docs

Edit _templates/docs/new/hello.ejs.t:

---
to: docs/<%= name %>.md
---

Hi!
I'm a document about <%= name %>

And then rename. The name of the file doesn’t matter, it’s for you, for bookeeping purposes.

$ mv _templates/docs/new/{hello,new-doc}.ejs.t

That’s it! Let’s make a doc:

$ hygen docs new --name architecture

Loaded templates: _templates
       added: docs/architecture.md

And now let’s check our new generator in:

❯ gs
A  _templates/docs/new/new-doc.ejs.t
A  _templates/generator/new/hello.ejs.t
A  _templates/generator/with-prompt/hello.ejs.t
A  _templates/generator/with-prompt/prompt.ejs.t

_templates is a contextual folder. hygen looks for one, at the folder you’re running it from. This way you can have different flavors of the same generator happen from different parts of your project, or maybe in a mono-repo scenario just different teams having their own sets of generators based on where in the mono-repo a team works at.

Note that the hygen generator new command is also checked in (the generator/new and generator/with-prompt parts). This is part of the "flexibility" principle.

We acknowledge that it may be that you don't like the vanilla hygen generator for new generators. Go ahead and change it; then check it in and have your team review it. After this change all new generators you build will incorporate your new wayto make them.

Power to the Generator

hygen lets you build generators that add multiple files, make injections to existing files, have prompts for interactivity with the user, and more.

Here’s how to ease some Redux boilerplate fatigue.

These days I choose ducks, to remove a little bit of boilerplate and have modularity baked in any app I build.

My typical Redux architecture would look like this:

app/
  components/
    icon.js
    avatar.js
  modules/
    boot.js     <---- glues modules together, requires chat, app, and auth.
    chat/
      index.js  <---- the 'connect' bit for Redux.
      view.js   <---- the view, separated, for testing.
      state.js  <---- reducer, actions, types, selectors.
    app/
      index.js
      view.js
      state.js
    auth/
      index.js
      view.js
      state.js

Adding a Module

Adding a new module is very easy with hygen. Here's how your templates look like:

_templates/
  module/
    new/
      index.ejs.t
      view.ejs.t
      state.ejs.t
      inject_boot.ejs.t   <--- adds a 'require' clause to boot.js

Here’s how index looks like:

---
to: app/modules/<%= name %>/index.js
---
//
// requires, mappings, etc....
//
export default connect(...)(<%= Name %>)

A similar trick would do for view and state.

How would we add a require line given that boot.js looks like this?

// ... some bootstrapping code ...

const modules = [
    // <--- we want to inject a line here!
    require('chat').default,
    require('auth').default,
    require('app').default,
]

// ... rest of bootstrapping code ...

Let’s build inject_boot:

---
to: app/modules/boot.js
inject: true
skip_if: <%= name %>
after: "const modules = ["
---
require('./<%= name %>).default,

And we’re done! Generating a new module is saying this:

$ hygen module new --name settings

Use Hygen Today

hygen isn’t just for Redux or documentation, or the other use cases that we line out in the documentation. It’s for everything, because it imposes almost nothing.

There’s more to see on hygen.io where the documentation lives at. But if you want a more gentle intro there’s also the digest form in the README and hygen is built with zero configuration and no strings attached so no harm done trying it.

So if you like trying things first, just install it, make a few generators and see if it fits you.