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.
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.
- 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
.
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.
---
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:
- 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) }}
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.
generator:
templates:
- './templates'
- './other/templates'
Git repositoriesv1.23.0
Templates locations can also reference templates in an external Git repository using the git://
locator protocol. This locator requires the Git host, repository path, and revision (branch, tag,
commit, etc).
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
protocol. This locator requires a package name and published version.
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://
protocol) - Node.js modules
- npm packages (using
npm://
protocol) - Another packaging system
Regardless of the choice, simply configure generator.templates
to point to these
locations:
generator:
templates:
- './templates'
- 'file://./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 intemplate.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.