Skip to main content

Create a task

10 min

The primary focus of moon is a build system, and for it to operate in any capacity, it requires tasks to run. In moon, a task is an npm binary or system command that is ran as a child process within the context of a project (is the current working directory). Tasks are defined per project with moon.yml, or inherited by all projects with .moon/project.yml, but can also be inferred from package.json scripts (we'll talk about this later).

Configuring a task

Most -- if not all projects -- utilize the same core tasks: linting, testing, code formatting, and type-checking. Because these are so universal, let's implement the type-checking task in .moon/project.yml, which will be inherited by all projects.

This section assumes that typescript is installed as a dependency in your root, and that all tsconfig.json files have been setup correctly.

Begin by adding a typecheck field to the tasks setting. This task will use TypeScript and run tsc under the hood, since we defined the command setting.

.moon/project.yml
tasks:
typecheck:
command: 'tsc'

By itself, this isn't doing much. So let's add some arguments with the args setting. We encourage everyone to use TypeScript project references for project boundaries and strict encapsulation, and as such, we'll use it below.

.moon/project.yml
tasks:
typecheck:
command: 'tsc --build --verbose'

With this, the task can be ran from the command line with moon run <project>:typecheck! This is tasks in its most simplest form, but continue reading on how to take full advantage of our build system.

Inputs

Our task above works, but isn't very efficient as it always runs, regardless of what has changed since the last time it has ran. This becomes problematic in continuous integration environments, not just locally.

To mitigate this problem, moon provides a system known as inputs, which are file paths, globs, and environment variables that are used by the task when it's ran. moon will use and compare these inputs to calculate whether to run, or to return the previous run state from the cache.

If you're a bit confused, let's demonstrate this by expanding the task with the inputs setting. Since this is TypeScript, we expect a tsconfig.json to exist in the project, and probably in the workspace root too.

.moon/project.yml
tasks:
typecheck:
command: 'tsc --build --verbose'
inputs:
- 'src/**/*'
- 'tests/**/*'
- 'types/**/*'
- 'tsconfig.json'
- '/tsconfig.*.json'
- '/tsconfig.json'

This list of inputs may look complicated, but they are merely run checks. For example, when moon detects a change in...

  • Any files within the src, tests, and types folders, relative from the project's root.
  • A tsconfig.json in the project's root.
  • A tsconfig.json or any tsconfig.*.json in the workspace root (denoted by the leading /).

...the task will be ran! If the change occurs outside of the project or outside the list of inputs, the task will not be ran.

tip

Inputs are a powerful feature that can be fine-tuned to your project's need. Be as granular or open as you want, the choice is yours!

Outputs

Outputs are the opposite of inputs, as they are files and folders that are created as a result of running the task. With that being said, outputs are optional, as not all tasks require them, and the ones that do are typically build related.

Now why is declaring outputs important? For incremental builds and smart caching! When moon encounters a build that has already been built, it hydrates all necessary outputs from the cache, then immediately exits. No more waiting for long builds!

Continuing our example, since we're using TypeScript project references and it generates declaration files, we'll write them to a project local dts folder and mark it as an output. Let's expand our task with the outputs setting.

.moon/project.yml
tasks:
typecheck:
command: 'tsc --build --verbose'
inputs:
- 'src/**/*'
- 'tests/**/*'
- 'types/**/*'
- 'tsconfig.json'
- '/tsconfig.*.json'
- '/tsconfig.json'
outputs:
- 'dts'
tsconfig.json
{
// ...
"compilerOptions": {
// ...
"outDir": "dts"
}
}

Depending on other tasks

While not relating to our TypeScript example above, but is important to talk about, is the concept of task dependencies. For scenarios where you need run a task before another task, as you're expecting some repository state or artifact to exist, can be achieved with the deps setting, which requires a list of targets:

  • <project>:<task> - Full canonical target.
  • ~:<task> - A task within the current project.
  • ^:<task> - A task from all depended on projects.
dependsOn:
# ...

tasks:
build:
# ...
deps:
- '^:build'

Using file groups

Once you're familiar with configuring tasks, you may notice certain inputs being repeated constantly, like source files, test files, and configuration. To reduce the amount of boilerplate required, moon provides a feature known as file groups, which enables grouping of similar file types within a project using file glob patterns or literal file paths.

File groups are defined with the fileGroups setting, which maps a list of file paths/globs to a group, like so.

.moon/project.yml
fileGroups:
configs:
- '*.config.js'
- 'tsconfig.json'
- '/tsconfig.*.json'
- '/tsconfig.json'
sources:
- 'src/**/*'
- 'types/**/*'
tests:
- 'tests/**/*'

We can then replace the inputs in our task above with these new file groups using a syntax known as tokens, specifically the @globs and @files token functions. Tokens are an advanced feature, so please refer to their documentation for more information!

.moon/project.yml
tasks:
typecheck:
command: 'tsc --build --verbose'
inputs:
- '@globs(sources)'
- '@globs(tests)'
- '@files(configs)'
outputs:
- 'dts'

With file groups (and tokens), you're able to reduce the amount of configuration required and encourage certain file structures for consuming projects!

Next steps