Skip to main content

Code generation

Code generation provides an easy mechanism for automating common development workflows and file structures. Whether it's scaffolding a new library or application, updating configuration, or standardizing patterns.

To accomplish this, we provide a generator, which is divided into two parts. The first being the templates and their files to be scaffolded. The second is our rendering engine that writes template files to a destination.

Creating a new template

To create a new template, run moon generate while passing the --template option. This will create a template directory and template.yml file in the 1st configured template location defined in generator.templates.

$ moon generate <name> --template

Configuring template.yml

Every template requires a template.yml file in the template's directory root. This file acts as a schema and declares metadata and variables required by the generator.

template.yml
title: 'npm package'
description: |
Scaffolds the initial structure for an npm package,
including source and test folders, a package.json, and more.
variables:
name:
type: 'string'
default: ''
required: true
prompt: 'Package name?'

Managing files

Feel free to add any files and folders to the template that you'd like to be generated by consumers! These files will then be scaffolded 1:1 in structure at the target destination.

An example of the templates folder structure may look something like the following:

templates/
├── npm-package/
│ ├── src/
│ ├── tests/
│ ├── package.json
│ └── template.yml
└── react-app/

Variables can be interpolated into file paths using the form [varName]. For example, if you had a template file src/[type].ts, and a variable type with a value of "bin", then the destination file path would be src/bin.ts.

File extensions

To enable syntax highlighting for template engine syntax, you may use the .tera (preferred) or .twig file extensions. These extensions are optional, but will be removed when the files are generated.

Depending on your preferred editor, these extensions may be supported through a plugin, or can be configured based on file type.

Partials

Partials are special template files that are used for composition and inheritance. Because of this, these files should not be generated into the target destination, and do not support frontmatter.

To ensure they are not generated, include the word "partial" anywhere in the file path. For example, partials/header.tpl or header.partial.tpl.

Frontmatter

Frontmatter is a well-known concept for "per file configuration", and is achieved by inserting YAML at the top of the file, delimited by wrapping ---. This is a very powerful feature that provides more control than the alternatives, and allows for some very cool integrations.

moon's frontmatter supports functionality like file skipping, force overwriting, and destination path rewriting. View the configuration docs for a full list of supported fields.

package.json
---
force: true
---

{
"name": "{{ name | kebab_case }}",
"version": "0.0.1"
}

Since frontmatter exists in the file itself, you can take advantage of the rendering engine to populate the field values dynamically. For example, if you're scaffolding a React component, you can convert the component name and file name to PascalCase.

{% set component_name = name | pascal_case %}

---
to: components/{{ component_name }}.tsx
---

export function {{ component_name }}() {
return <div />;
}

Template engine & syntax

Rendering templates is powered by Tera, a Rust based template engine with syntax similar to Twig, Liquid, Django, and more. We highly encourage everyone to read Tera's documentation for an in-depth understanding, but as a quick reference, Tera supports the following:

{{ varName }} -> foo
{{ varName | upper }} -> FOO
{% if price < 10 or always_show %}
Price is {{ price }}.
{% elif price > 1000 and not rich %}
That's expensive!
{% else %}
N/A
{% endif %}
{% for item in items %}
{{ loop.index }} - {{ item.name }}
{% endfor %}
  • And many more features, like auto-escaping, white space control, and math operators!

Filters

Filters are a mechanism for transforming values during interpolation and are written using pipes (|). Tera provides many built-in filters, but we also provide the following custom filters:

  • Strings - camel_case, pascal_case, snake_case, upper_snake_case, kebab_case, upper_kebab_case, lower_case, upper_case
{{ some_value | upper_case }}
  • Paths - path_join, path_relative
{{ some_path | path_join(part = "another/folder") }}
{{ some_path | path_relative(from = other_path) }}
{{ some_path | path_relative(to = other_path) }}

Variables

The following variables are always available within a template:

  • dest_dir - Absolute path to the destination folder.
  • dest_rel_dir - Relative path to the destination folder from the working directory.
  • working_dir - Current working directory.
  • workspace_root - The moon workspace root.

Generating code from a template

Once a template has been created and configured, you can generate files based on it using the moon generate command! This is also know as scaffolding or code generation.

This command requires the name of a template as the 1st argument. The template name is the folder name on the file system that houses all the template files.

$ moon generate npm-package

An optional destination path, relative from the current working directory, can be provided as the 2nd argument. If not provided, you'll be prompted during generation to provide one.

$ moon generate npm-package ./packages/example

This command is extremely interactive, as we'll prompt you for the destination path, variable values, whether to overwrite files, and more. If you'd prefer to avoid interactions, pass --defaults, or --force, or both.

Configuring template locations

Templates can be located anywhere, especially when being shared. Because of this, our generator will loop through all template paths configured in generator.templates, in order, until a match is found (template name matches the folder name).

.moon/workspace.yml
generator:
templates:
- './templates'
- './other/templates'

Declaring variables with CLI arguments

During generation, you'll be prompted in the terminal to provide a value for any configured variables. However, you can pre-fill these variable values by passing arbitrary command line arguments after -- to moon generate. Argument names must exactly match the variable names.

Using the package template example above, we could pre-fill the name variable like so:

$ moon generate npm-package ./packages/example -- --name '@company/example' --private

Boolean variables can be negated by prefixing the argument with --no-<arg>.

Sharing templates

Although moon is designed for a monorepo, you may be using multiple repositories and would like to use the same templates across all of them.

This is technically possible, but with 1 caveat, and that is that moon's generator requires templates to exist within the current repository, relative from the workspace root. So how can we share templates across repositories? Why not try...

  • git submodules
  • npm packages
  • another packaging system

Regardless of the choice, simply configure generator.templates to point to these locations:

.moon/workspace.yml
generator:
templates:
- './templates'
- './node_modules/@company/shared-templates'
- './path/to/submodules'