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.
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 filesrc/[type].ts
, and a variabletype
with a value of "bin", then the destination file path would besrc/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.
- VS Code
- Atom
- Webstorm
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.
---
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:
- Variable interpolation (defined with the
variables
setting), with built-in filters.
{{ varName }} -> foo
{{ varName | upper }} -> FOO
- Conditional blocks and loops.
{% 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).
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:
generator:
templates:
- './templates'
- './node_modules/@company/shared-templates'
- './path/to/submodules'