hygen, plop, sao, and yeoman-generator are tools designed to automate file creation and project scaffolding, helping teams enforce consistency and reduce boilerplate. hygen focuses on speed and simplicity using template files with embedded prompts, ideal for generating small, repetitive components. plop offers a JavaScript-centric configuration approach, allowing dynamic logic within the generation process itself. sao (Scaffolding Old) is a lightweight, modern alternative built on EJS templates, often used for initializing entire projects quickly. yeoman-generator is the engine behind the famous Yeoman ecosystem, providing a robust, class-based architecture for complex, multi-step scaffolding workflows that require deep integration with package managers and file systems.
When scaling a frontend application, consistency is key. Manually creating files leads to typos, inconsistent folder structures, and wasted time. This is where code generators come in. They automate the boring stuff so developers can focus on logic. We have four strong contenders: hygen, plop, sao, and yeoman-generator. While they all create files, they solve the problem in very different ways. Let's dig into how they work under the hood.
The biggest difference lies in where you define your logic. Do you write templates in separate files, or do you write JavaScript code to generate files?
hygen keeps everything in the file system. You create a template file with special markers for prompts. The tool reads these files and runs them. There is no separate "generator code" to maintain.
# hygen: Template file at _templates/component/new.ejs.t
---
to: src/components/<%= name %>/<%= name %>.js
---
import React from 'react';
export default function <%= name %>() {
return <div><%= name %> Component</div>;
}
plop moves the logic into a JavaScript file (plopfile.js). You define actions and prompts using code. This gives you more power to change behavior based on conditions.
// plop: plopfile.js
module.exports = function (plop) {
plop.setGenerator('component', {
prompts: [{ type: 'input', name: 'name' }],
actions: [{
type: 'add',
path: 'src/components/{{name}}/{{name}}.js',
templateFile: 'templates/component.hbs'
}]
});
};
sao uses a simple sao.js or index.js file but focuses on being extremely lightweight. It often relies on EJS templates similar to Hygen but wraps them in a minimal API designed for quick project initialization rather than daily component generation.
// sao: index.js (generator entry)
module.exports = {
prompts: [{
type: 'string',
name: 'name',
message: 'Project name'
}],
templateData() {
return { name: this.prompts.name };
}
};
yeoman-generator uses a class-based structure. You extend a base Generator class and define methods like writing() and installing(). This is heavy but allows for complex lifecycle management.
// yeoman-generator: generators/app/index.js
const Generator = require('yeoman-generator');
module.exports = class extends Generator {
writing() {
this.fs.copyTpl(
this.templatePath('component.js'),
this.destinationPath(`src/components/${this.options.name}.js`),
{ name: this.options.name }
);
}
};
How you write the actual content of the generated files matters for readability and power.
hygen uses EJS (Embedded JavaScript). Since it's just JavaScript inside your templates, you can use loops, conditionals, and helper functions directly in the template file.
<!-- hygen template -->
<% if (type === 'functional') { %>
const <%= name %> = () => <div>Hi</div>;
<% } else { %>
class <%= name %> extends React.Component { ... }
<% } %>
plop defaults to Handlebars ({{ }}). It is safer and less prone to accidental code execution, but sometimes feels limited if you need complex logic inside the template.
<!-- plop template (component.hbs) -->
{{#ifEquals type "functional"}}
const {{name}} = () => <div>Hi</div>;
{{else}}
class {{name}} extends React.Component { ... }
{{/ifEquals}}
sao also leans heavily on EJS, offering the same flexibility as Hygen for embedding logic directly into the output files.
<!-- sao template -->
<% features.forEach(feature => { %>
import { <%= feature %> } from 'lib';
<% }) %>
yeoman-generator supports both EJS and Handlebars via its internal file system engine (mem-fs-editor). It gives you the choice, but requires you to explicitly choose the rendering method in your code.
// yeoman-generator: Using EJS explicitly
this.fs.copyTpl(
this.templatePath('index.ejs'),
this.destinationPath('index.html'),
{ title: 'My App' } // Context passed to EJS
);
Not all generators are meant for the same job. Some are for setting up a project once; others are for use every day.
hygen is built for daily use. You run hygen component new dozens of times a week. It is fast, has no build step, and lives right next to your source code. It feels like part of your app, not a separate tool.
# Typical Hygen usage
$ hygen component new Button
β¨ Done in 0.5s
plop is also great for daily use but requires a Node environment to run the plopfile.js. It is slightly heavier to start but offers more interactive prompts out of the box.
# Typical Plop usage
$ npx plop component
? What is the component name? Button
β¨ File created
sao is typically used for "one-off" project scaffolding. You run it once to create a new repo, then you might never use it again for that specific project. It is optimized for initializing entire structures.
# Typical Sao usage
$ npx sao my-project-template new-project
β¨ Project initialized
yeoman-generator is the heavyweight champion for complex setups. It handles package installation (npm install), git initialization, and complex directory trees. It is overkill for a single component but perfect for generating a full microservice or library.
# Typical Yeoman usage
$ yo my-full-stack-app
? Choose database: PostgreSQL
π Installing dependencies...
β¨ App ready
What happens when you need to do more than just copy a file? Maybe you need to append to an existing file or run a shell command.
hygen allows you to inject code into existing files using specific templates. It is simple but effective for adding exports to an index.js.
# hygen: Injecting into index.js
$ hygen component inject Button
# Adds: export { default as Button } from './Button';
plop has a dedicated append action. You can precisely target a file and add text to the end or beginning, controlled entirely by your JS logic.
// plop: Append action
actions: [{
type: 'append',
path: 'src/components/index.js',
pattern: /(?=<\/exports>)/,
template: "export { default as {{name}} } from './{{name}}';"
}]
sao keeps it minimal. While you can run shell commands in the generator script, it doesn't have built-in "inject" helpers as rich as Plop or Hygen. You usually write the full file content.
// sao: Manual file writing
this.fs.write(
'src/index.js',
existingContent + '\nexport { default as Button } from "./Button";'
);
yeoman-generator shines here. It has a robust API for modifying JSON files, appending to files, and running shell commands asynchronously with proper error handling. It treats file manipulation as a transaction.
// yeoman-generator: Modifying package.json
this.packageJson.merge({
scripts: {
test: 'jest'
}
});
// Automatically handles reading/writing the JSON safely
Your team creates 5-10 new React components every day. You need speed and zero friction.
hygen# Developer experience
$ hygen component new Navbar
# Instantly creates files and injects exports
You need to generate a backend service where the file structure changes based on whether the user selects SQL or NoSQL.
plopif/else logic to decide which files to create dynamically.// plopfile.js logic
if (dbType === 'sql') {
actions.push({ type: 'add', path: 'db/schema.sql' });
} else {
actions.push({ type: 'add', path: 'db/models.js' });
}
You want to publish a template on GitHub that users can clone and initialize in one command.
saonpx, and perfect for one-time setup.# User runs this once
$ npx sao github:user/my-template my-app
You are building a tool that generates a full npm package, sets up CI/CD, configures TypeScript strictly, and publishes a initial commit.
yeoman-generatorinstall, end) to run npm install and git init reliably.// Yeoman lifecycle
install() {
this.npmInstall();
}
end() {
this.spawnCommand('git', ['init']);
}
| Feature | hygen | plop | sao | yeoman-generator |
|---|---|---|---|---|
| Config Style | File-based (EJS) | Code-based (JS) | Code-based (JS) | Class-based (JS) |
| Template Engine | EJS | Handlebars | EJS | EJS / Handlebars |
| Primary Use | Daily Components | Dynamic Generators | Project Starters | Complex Scaffolds |
| Speed | β‘ Very Fast | π Fast | π Fast | π’ Moderate |
| Learning Curve | Low | Medium | Low | High |
| Dependency Mgmt | Manual / Simple | Manual | Manual | Built-in (npmInstall) |
Think about frequency and complexity.
hygen. It stays out of your way.plop. The JS config handles the brain work.sao. It's light and easy to share.yeoman-generator. It handles the heavy lifting of project initialization.Choosing the right tool prevents your scaffolding from becoming a burden. Keep it simple when you can, and reach for power only when you need it.
Choose hygen if your team needs a fast, zero-config tool for generating components, hooks, or screens within an existing project. It excels when you want to keep templates as simple files in your repository without writing complex JavaScript logic for the generator itself. It is the best fit for teams that prioritize developer speed and want to avoid maintaining a separate build step for their scaffolding tool.
Choose plop if you prefer defining your generation logic entirely in JavaScript, giving you full programmatic control over prompts and file transformations. It is ideal for scenarios where the file structure depends on dynamic user input that requires conditional logic not easily expressed in simple template syntax. Use it when you need a highly customizable CLI experience that integrates tightly with your existing Node.js build tools.
hygen is the simple, fast, and scalable code generator that lives in your project.
New in hygen v4.0.0: a positional
NAMEparameter. Now you can use$ hygen component new MyComponentinstead of$ hygen component new --name MyComponent.
Β Β
Β Β
Β Β
Β Β Β
Β Β
Β Β Β
Β Β
Β
Β
Β Β Β
Β Β
Β Β
Β Β
Β Β Β
Β Β
Β Β
Hygen can be used to supercharge your workflow with Redux, React Native, Express and more, by allowing you avoid manual work and generate, add, inject and perform custom operations on your codebase.
If you're on macOS and have Homebrew:
$ brew tap jondot/tap
$ brew install hygen
If you have node.js installed, you can install globally with npm (or Yarn):
$ npm i -g hygen
If you like a no-strings-attached approach, you can use npx without installing globally:
$ npx hygen ...
For other platforms, see releases.
Initialize hygen in your project (do this once per project):
$ cd your-project
$ hygen init self
Build your first generator, called mygen:
$ hygen generator new mygen
Loaded templates: _templates
added: _templates/mygen/new/hello.ejs.t
Now you can use it:
$ hygen mygen new
Loaded templates: _templates
added: app/hello.js
You've generated content into the current working directory in app/. To see how the generator is built, look at _templates (which you should check-in to your project from now on, by the way).
You can build a generator that uses an interactive prompt to fill in a variable:
$ hygen generator with-prompt mygen
Loaded templates: _templates
added: _templates/mygen/with-prompt/hello.ejs.t
added: _templates/mygen/with-prompt/prompt.js
Done! Now let's use mygen:
$ hygen mygen with-prompt
? What's your message? hello
Loaded templates: _templates
added: app/hello.js
Want to start from a repo?
$ hygen init repo antfu/vitesse --to my-folder
Want to start from an existing repo on an existing project?
$ mkdir your-project && cd your-project
$ hygen init repo antfu/vitesse
Go to the documentation to get to know the rest of Hygen and generators.
If you're in a hurry:
hygen is a scalable generator. It has developer and team ergonomics as first priority.
It avoids "blessed" or dedicated projects that codifies code generation, which before you know it, become a product you build, needs testing, CI, separate pull request reviews, and ultimately sucks up your time and becomes this super hated thing no one likes to touch anymore.
Plus, since they are not the product you are building they become notoriously hard to reason about.
Because hygen templates live in your project, it cuts the time from having an itch for generating code (Redux, anyone?) in your current
project to code generated with it and others benefiting from it.
hygen picks up a local _templates folder
at any folder level of your project you're working from.
This is important because:
And yet, if you don't like project-local templates:
_templates folder (maybe a central git repo you maintain?) by populating an environment variable HYGEN_TMPLShygen at its core, and pack your own templates with it.The templates folder structure maps directly to the command structure:
$ hygen worker new jobrunner
For this command, hygen worker new maps to _templates/worker/new and all files within worker/new are rendered.
Template parameters are given with --flag VALUE, as many as you'd like. In this example we've set a parameter named name to the value jobrunner.
A subcommand is a file inside a your folder structure. So if the structure is this:
_templates/
worker/
new/
index.html.ejs
setup.html.ejs
And you only want setup, you can run:
$ hygen worker new:setup
You can also use the subcommand as a regular expression so, these will do the same:
$ hygen worker new:se
$ hygen worker new:se.*
Here's how a template looks like (in our example, index.ejs.t). Templates bodies are ejs:
---
to: app/workers/<%=name%>.js
---
class <%= h.capitalize(name) %> {
work(){
// your code here!
}
}
The first part of the template is a front matter, idea borrowed from Markdown, this is the metadata part of a hygen template and is part of the reason of why your templates will feel more lightweight and flexible.
All frontmatter metadata are also run through the template engine so feel free to use variables in the frontmatter as you wish.
There's one required metadata variable: to.
to points to where this file will be placed (folders are created as needed).
hygen provides ability to semantic case changing with change-case library, it's simple to use and very easy to understand.
There is a usecase for react based components generation:
---
to: components/<%= name %>/index.jsx
---
import React from 'react'
export const <%= name %> = ({ children }) => (
<div className="<%= h.changeCase.paramCase(name) %>">{children}</div>"
)
With name HelloWorld will be compiled to:
import React from 'react'
export const HelloWorld = ({ children }) => (
<div className="hello-world">{children}</div>"
)
You can see the full list here.
By default templates are 'added' to your project as a new target file, but you can also choose to inject a template into an existing target file.
For this to work, you need to use inject: true with the accompanied inject-specific props.
---
to: package.json
inject: true
after: dependencies
skip_if: react-native-fs
---
"react-native-fs":"*",
This template will add the react-native-fs dependency into a package.json file, but it will not add it twice (see skip_if).
Here are the available mutually-exclusive options for where to inject at:
before | after - a regular expression / text to locate. The inject line will appear before or after the located line.prepend | append - add a line to start or end of file respectively.line_at - add a line at this exact line number.You can guard against double injection:
skip_if - a regular expression / text. If exists injection is skipped.Also you can insert or remove empty line to injection body. That feature very useful if your editor or formatter automatically insert blank line at the end of file on save:
eof_last - if falsy - trim blank line from the end of injection body, if truthy - insert it.hygen is highly embeddable. You should be able to use it by simply listing it
as a dependency and having this kind of workflow in your binary.
const { runner } = require('hygen')
const Logger = require('hygen/dist/logger')
const path = require('path')
const defaultTemplates = path.join(__dirname, 'templates')
runner(process.argv.slice(2), {
templates: defaultTemplates,
cwd: process.cwd(),
logger: new Logger.default(console.log.bind(console)),
createPrompter: () => require('enquirer'),
exec: (action, body) => {
const opts = body && body.length > 0 ? { input: body } : {}
return require('execa').shell(action, opts)
},
debug: !!process.env.DEBUG
})
The Hygen codebase has a functional style where possible. This is because naturally, it feeds parameters and spits out files. Try to keep it that way.
Running hygen locally, rigged to your current codebase (note the additional -- to allow passing flags)
$ yarn hygen -- mailer new foobar
Running tests in watch mode:
$ yarn watch
The easiest way to make an impact is to use the built-in metaverse tests suite, and then add the tests here.
The idea here is to copy templates from any project that use hygen and to test that it works at all times. This keeps tabs on the hygen universe / ecosystem (nicknamed metaverse) and makes sure this test suite breaks before hygen clients break.
Many generators become painfully slow to use as the thing you want to generate grow (because, real life),
This is why hygen takes speed as a first class citizen, and sports a dedicated start-up timing suite:
$ yarn test:require
In addition, thought is given to which dependencies to take in, how their file structure fan out and what kind of disk access (due to require) would hygen ultimately have when we run it. This is recorded with every test run.
Bundling a single file was evaluated (and the infrastructure is still there, using webpack) and wasn't faster than what we have right now.
Fork, implement, add tests, pull request, get my everlasting thanks and a respectable place here :).
To all Contributors - you make this happen, thanks!
Copyright (c) 2018 Dotan Nahum @jondot. See LICENSE for further details.