Build faster by generating code with Hygen.io
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 way to 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.