Open Source

Code Generation with Mason

An overview of the open source tool Mason which automates code generation

Additional Contributors
No items found.

Mason is an open source tool that generates code from custom templates called bricks. It can be helpful for developers who find themselves writing the same code over and over again, or those looking for a way to customize reusable templates.

Mason can increase the efficiency of development teams by enabling greater day-to-day automation. For example, if there is a widget or method that your team will be using throughout an entire project, you can simply write that piece of code once as a brick template. You can then use mason at any time to generate that piece of code from the template with dynamic variables. Brick templates can be maintained locally as part of a specific project or remotely via a GitHub repository.

As an individual developer, mason can be extremely helpful for creating and generating your own templates to start new projects. In fact, that’s exactly what we did with Very Good CLI, which uses mason under the hood to generate Very Good Core, our VGV opinionated Flutter starter app template. Another example is groovin_cli, an app generator created by GroovinChip using mason and inspired by Very Good CLI.

Mason is written in Dart but can be used to generate code in any programming language. It can also generate code from templates of any size — ranging from a few lines to a complete application consisting of hundreds of files and directories.

How it Works

Mason uses the mustache templating syntax to enable developers to create and maintain complex templates called bricks without having to write any generator code. Internally, mason maintains references to all installed brick templates and surfaces them to the developer via the mason make command. Brick templates consist of a brick.yaml file and a __brick__ directory which contains the template code.

When you want to generate code from a brick, mason gathers the input variables needed for the brick, either via prompts or as command line arguments, and injects them into the brick before writing the newly generated code to disk. Since mason is a CLI tool, you can run it straight from your terminal.

Getting Started

Mason can be installed either via pub.dev or homebrew.

# Install from pub.dev
$ dart pub global activate mason

# Install from homebrew
$ brew tap felangel/mason
$ brew install mason

At this point, mason should be available as a command. You can verify by running mason in your terminal.

$ mason
⛏️  mason • lay the foundation!

Usage: mason  [arguments]

Global options:
-h, --help       Print this usage information.
    --version    Print the current version.

Available commands:
  bundle   Generates a bundle from a brick template
  cache    Interact with mason cache
  get      Gets all bricks.
  init     Initialize mason in the current directory.
  make     Generate code using an existing brick template.
  new      Creates a new brick template.

Run "mason help " for more information about a command.


Hello World Example

The first example we’ll take a look at is the hello world example which comes out of the box when you initialize mason.

Initializing Mason

Start by running mason init in your directory of choice. It should generate a mason.yaml along with a bricks directory.

$ mason init
✓ Initialized (0.1ms)
✓ Generated 3 file(s):
  mason.yaml (new)
  bricks/hello/brick.yaml (new)
  bricks/hello/__brick__/HELLO.md (new)

The mason.yaml is where bricks can be registered similar to the pubspec.yaml. Bricks can be referenced either via path or git url as seen below.

# Register bricks which can be consumed via the Mason CLI.
# https://pub.dev/packages/mason
bricks:
  # Sample Brick
  # Run `mason make hello` to try it out.
  hello:
    path: bricks/hello
  # Bricks can also be imported via git url.
  # Uncomment the following lines to import
  # a brick from a remote git url.
  # todos:
  #   git:
  #     url: git@github.com:felangel/mason.git
  #     path: bricks/todos

The bricks directory contains the brick templates. In this case, there is only a single brick called, hello. The brick.yaml contains important metadata about the template such as the name, description, and required variables.

name: hello
description: An example hello brick.
vars:
  - name

Each brick also has a __brick__ directory which contains the template itself. Any files or directories within __brick__ will be part of the generated code when the brick is run.

├── __brick__
│   └── HELLO.md
└── brick.yaml

If we take a quick look at the HELLO.md template file, we can see that we're able to access the name variable via {{name}}.

Hello {{name}}!

Now that we've taken a closer look at the parts of a brick template, let's install one.

Installing Brick Templates

We can install all bricks declared in the mason.yaml via the mason get command.

$ mason get
✓ getting bricks (0.0ms)

This will generate a .mason which contains relevant metadata needed to determine where the templates are installed. We recommend adding the .mason directory to your .gitignore.

# .gitignore
.mason/

Now we're ready to generate some code!

Generating Code

Once the bricks have been installed, we can run mason make hello to use the hello template.

$ mason make hello --name Felix
✓ Made brick hello (0.0ms)
✓ Generated 1 file(s):
  /HELLO.md (new)

HELLO.md will be generated in the current directory. The contents of the file should look something like:

Hello Felix!

Mason will prompt you for any variables that haven't been provided via command line arguments:

$ mason make hello
name: Dash
✓ Made brick hello (7.7ms)
✓ Generated 1 file(s):
  /HELLO.md (new)

HELLO.md should now have been overwritten to include the updated name: Dash.

Hello Dash!

Now let's take a look at how to create our own brick for a custom widget.

Custom Widget Example

We can use the mason new command to create a new brick.

mason new widget -d "A custom Flutter widget"
✓ Created new brick: widget (0.1ms)
✓ Generated 2 file(s):
  bricks/widget/brick.yaml (new)
  bricks/widget/__brick__/hello.md (new)

Note that mason automatically declared the new widget brick in the mason.yaml.

bricks:
  hello:
    path: bricks/hello
  widget:
    path: bricks/widget

Here is our new widget brick's brick.yaml file:

name: widget
description: A custom Flutter widget
vars:
  - name

Let's add a variable called routable which will determine whether or not the new widget should expose a route function.

name: widget
description: A custom Flutter widget
vars:
  - name
  - routable

Now the brick.yaml is complete. Let's start writing the template.

We can delete the default hello.md file within the __brick__ directory and create a new file called:

{{#snakeCase}}{{name}}{{/snakeCase}}.dart

The snakeCase lambda is provided by mason. Currently mason provides many lambdas out of the box such as:

  • camelCase
  • constantCase
  • dotCase
  • headerCase
  • lowerCase
  • pascalCase
  • paramCase
  • pathCase
  • sentenceCase
  • snakeCase
  • titleCase
  • upperCase

This will tell mason to create a file named, <name>.dart and ensure the file name is snake case. Next, let's modify the content of our widget template file to include the widget code.

import 'package:flutter/material.dart';

class {{#pascalCase}}{{name}}{{/pascalCase}} extends StatelessWidget {
  const {{#pascalCase}}{{name}}{{/pascalCase}}({Key? key}) : super(key: key);

  {{#routable}}
  static PageRoute route() {
    return MaterialPageRoute(builder: (context) => const {{#pascalCase}}{{name}}{{/pascalCase}}());
  }
  
  {{/routable}}
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return const SizedBox();
  }
}

Here we're defining a StatelessWidget called name and using the pascalCase lambda to ensure it is a valid class name. We're also using mustache sections to define a static route function only if the variable routable is true.

That's it, now anytime we want to generate a new widget, we can use mason make widget.

$ mason make widget
name: dash container
routable: true
✓ Made brick widget (12.5ms)
✓ Generated 1 file(s):
  dash_container.dart (new)

Then we can take a look at the generated code in dash_container.dart:

import 'package:flutter/material.dart';

class DashContainer extends StatelessWidget {
  const DashContainer({Key? key}) : super(key: key);

  static PageRoute route() {
    return MaterialPageRoute(builder: (context) => const DashContainer());
  }
  
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return const SizedBox();
  }
}

If we want a new widget that is not routable, we can set routable to false:

$ mason make widget
name: flutter box
routable: false
✓ Made brick widget (16.8ms)
✓ Generated 1 file(s):
  flutter_box.dart (new)

And if we take a look at the newly generated flutter_box.dart, we should see there is no route method defined:

import 'package:flutter/material.dart';

class FlutterBox extends StatelessWidget {
  const FlutterBox({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return const SizedBox();
  }
}


What's Next

We're actively working on several new features including support for global templates and template extensions. You can head over to the mason repo to check on the progress, provide feedback, and explore other examples.

Happy templating!

More Stories