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 file-based 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/

Interpolation

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.

This syntax also supports filters, such as [varName | camel_case]. However, spaces may cause issues with file path encoding, so this functionality is primarily recommended for the destination setting.

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.

Rawsv1.11.0

Raw template files are another special type of file that bypass all Tera rendering, and are used as-is instead. This is useful for files that contain syntax that conflicts with Tera.

To mark a file as raw, add a .raw extension, for example: file.raw.js or file.js.raw. When the file is generated, the .raw extension will be removed.

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 />;
}

Assets

Assets are binary files that are copied as-is to the destination, without any rendering, and no support for frontmatter. This applies to all non-text based files, like images, audio, video, etc.

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) }}

Functions

The following functions are available within a template:

  • variables() - Returns an object containing all variables within the current template. v1.23.0

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, or the id setting configured in template.yml.

$ moon generate npm-package

An optional destination path, relative from the current working directory, can be provided as the 2nd argument. If not provided, the destination setting configured in template.yml will be used, or 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.

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

Git repositoriesv1.23.0

Templates locations can also reference templates in an external Git repository using the git: locator prefix. This locator requires the Git host, repository path, and revision (branch, tag, commit, etc).

.moon/workspace.yml
generator:
templates:
- 'git:github.com/moonrepo/templates#master'
- 'git:gitlab.com/org/repo#v1.2.3'

Git repositories will be cloned to ~/.moon/templates using an HTTPS URL (not a Git URL), and will be cached for future use.

npm packagesv1.23.0

Additionally, template locations can also reference npm packages using the npm: locator prefix. This locator requires a package name and published version.

.moon/workspace.yml
generator:
templates:
- 'npm:@moonrepo/templates#1.2.3'
- 'npm:other-templates#4.5.6'

npm packages will be downloaded and unpacked to ~/.moon/templates and cached for future use.

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. So how can we share templates across repositories? Why not try...

  • Git submodules
  • Git repositories (using git: locator)
  • Node.js modules
  • npm packages (using npm: locator)
  • Another packaging system

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

.moon/workspace.yml
generator:
templates:
- './templates'
# Git
- './path/to/submodule'
- 'git:github.com/org/repo#branch'
# npm
- './node_modules/@company/shared-templates'
- 'npm:@company/shared-templates#1.2.3'

Git and npm layout structure

If you plan to share templates using Git repositories (git:) or npm packages (npm:), then the layout of those projects must follow these guidelines:

  • A project must support multiple templates
  • A template is denoted by a folder in the root of the project
  • Each template must have a template.yml file
  • Template names are derived from the folder name, or the id field in template.yml

An example of this layout structure may look something like the following:

<root>
├── template-one/
│ └── template.yml
├── template-two/
│ └── template.yml
├── template-three/
│ └── template.yml
└── package.json, etc

These templates can then be referenced by name, such as []moon generate template-one]command.