Skip to main content

Templates

The files a module adds to an app are mason templates, bundled into Dart, and the pipeline renders them in memory with mustache. A template can use the variables below, and the pipeline checks the rules that follow. Most of these rules exist so that a module never breaks an app it cannot see.

Variables​

A template can read:

VariableValue
app_nameThe package name of the app, such as my_app. mason's case lambdas work on it: {{app_name.titleCase()}} is My App.
org_nameThe organization, such as com.example.
has_<role>true when the role is present. It is set for the roles the brick's owner provides, requires or uses, and for the app entry.
socket tagsThe rendered contributions of a socket, such as {{{smf_app_entry__bootstrap_late}}}.
variables of render hooksWhat the owner's render hooks return in RoleOutput.vars.
the brick's own variablesThe vars of its BrickContribution: plain data, or code that depends on the presence of a role.

Mustache renders a variable that none of these sets as nothing, so the pipeline reports it. The brick's own variables and the variables of render hooks cannot take the names that the pipeline or mason sets: app_name, org_name, smf_…, has_…, the case lambdas of mason, such as snakeCase, and its brace variables, __LEFT_CURLY_BRACKET__ and __RIGHT_CURLY_BRACKET__.

Mustache escapes a variable in two braces for HTML, so code and paths with a slash go in three: {{{name}}}.

Presence flags​

Code that refers to a role the module only uses goes inside the section of its presence flag. flutter_core switches the root widget on the router this way:

MaterialApp{{#has_router}}.router{{/has_router}}(
title: '{{app_name.titleCase()}}',
{{#has_router}}routerConfig: appRouter.config,{{/has_router}}{{^has_router}}home: const FallbackStartScreen(),{{/has_router}}

An import that only one branch needs goes into the same section:

{{#has_router}}import 'core/router/app_router.dart';{{/has_router}}{{^has_router}}import 'core/app/fallback_start_screen.dart';{{/has_router}}

A flag is set only for the roles the owner declares, and the pipeline reports a section over a flag it does not set.

Variables that depend on a role​

A template whose code differs with a role needs two branches under the presence flag of the role, around the code and again around its import. A variable of the brick with a RoleVar does the same with one line of the template. It has the code for an app with the role and the code for an app without it. The fixtures of smf_pipeline have a module that reads the time zones of a clock role this way:

BrickContribution(
fakeClockUserBundle,
vars: {
// One line of the template for the apps with the clock and for
// those without it; only the first get the import.
'clock_zones': RoleVar(
clockRole,
present: Fragment(
'createClock().zones',
imports: [ClockRole.createClock.importRef],
),
absent: 'const <String>[]',
),
},
),
List<String> clockUserZones() => {{{clock_zones}}};

In an app with the clock, the function returns createClock().zones and the file imports the file of createClock(). In an app without it, the function returns const <String>[]. The pipeline takes the value by the presence of the role, as it applies a contribution by its when.

  • The role is one that the contributor provides, requires or uses.
  • Each value is code: a Fragment with the imports that the code needs, or a string of code that needs none. A RoleVar holds no plain data for mustache, such as a list or a boolean. A template tests the presence of a role with its presence flag.
  • The variable is a fragment variable in every app, whichever value the app gets. A template reads it as it is, {{{clock_zones}}}, and not in a path. When either value has imports, only a Dart library may read it. The pipeline adds the imports of the value that the app gets to every Dart file of the brick that reads the variable.
  • A template reads it outside every mustache section, the section of the presence flag of another role included. Code that needs a second role goes into a brick of its own, contributed with when of that role.
  • A line that holds nothing but the variable goes away in an app whose value has no code.
  • Only the whole value of a variable of a brick is a RoleVar: it is no item of a list or a map. The variables of a render hook take none, since a hook asks for the presence of a role with input.has(role).

localizationRole.varsOf makes such variables for the texts of a module: context.l10n.<getter> in an app with the localization role, and the English text in an app without it. See Texts.

Socket tags​

A template marks a socket with its tag in three braces, {{{smf_<owner>__<name>}}}:

  • A tag stands outside mustache sections. The socket renders whatever the app has, and its contributions carry their own when.
  • A socket whose contributions carry imports, such as a code socket, has its tag in exactly one Dart file, which gets the imports. Only the tag of a socket for one value, such as the minimum iOS version, may appear several times.
  • Both tags of a wrapper, …_open and …_close, are in the same file, the opening one first.
  • The tag of a socket that renders whole lines, such as the native sockets and the README sections of the app entry, stands alone at the start of its line.
  • A tag must not come right after a {, which mustache would read as part of the tag. Put the parameters of a constructor on lines of their own, as the tags of parameter annotations need.
  • A line that holds nothing but a tag that gets nothing goes away with it, so an empty socket leaves no blank line.

The owner of a socket must hold its tag in its templates: the role's template or its provider for a socket of a role, and the module for a socket of its own. The tag of a member of a family, such as the annotations of a screen, goes into the file of the module whose data creates it.

Variables of render hooks​

A render hook returns variables for the bricks of its owner in RoleOutput.vars. They are plain data, such as strings, numbers, booleans and lists and maps of them, or a Fragment of code with imports:

  • A template reads a fragment variable as it is, {{{routes}}}, outside mustache sections. A path cannot read one.
  • A fragment variable with imports can be read only by a Dart library, not by a part file or a file that is not Dart. The pipeline adds the imports to every file of the owner that reads it.
  • A line that holds nothing but a fragment variable without code goes away.
  • A fragment variable that no template of its owner reads is an error, since its code would be lost.
  • A variable that two hooks of one owner, or a hook and a brick of the owner, both set is an error.

Files of render hooks​

A brick has the same files in every app. A file whose path depends on the data of a role or on its choice, such as one file for each item that the modules give the role, comes from a render hook instead, of the template of the role or of its provider. The hook returns it in RoleOutput.files, as the text of the file by its path from the root of the app, with forward slashes. gen_l10n generates the ARB file of each language of the app this way. In the fixtures of smf_pipeline, the provider of a clock role generates a Dart file for each time zone that the modules ask for, and gives its brick the imports of those files:

  
RoleOutput render(RoleHookInput<String> input) {
final zones = [
for (final (index, data) in input.data.indexed)
(number: index + 1, name: data.value),
];
return RoleOutput(
vars: {
'zone_constants': Fragment(
[for (final zone in zones) 'clockZone${zone.number}'].join(', '),
imports: [
for (final zone in zones)
ImportRef.app('core/clock/zones/zone_${zone.number}.dart'),
],
),
},
files: {
for (final zone in zones)
'lib/core/clock/zones/zone_${zone.number}.dart':
'/// A time zone that a module of the app asked for (fixture).\n'
'const clockZone${zone.number} = '
'${SmfNames.dartString(zone.name)};\n',
},
);
}

The pipeline takes such a file as it is, text and path. It renders no mustache in it, fills no tag of a socket and adds no import, and mason does not read it. Once the app is written, dart fix and dart format go over a Dart file among them as over the rest of the app.

Otherwise it is a file of the app like those of the bricks:

  • Its owner is the owner of the hook, the template of the role or the module of the provider, and the contract harness, with its checks of imports, sees it like any other file.
  • The path is inside the app, and is not of a file that belongs to one machine or one build.
  • The file of a provider is where the kind of its module may generate files.
  • Every file of the app is generated once. A path that a brick or another hook generates too is an error, also when the paths differ only in case.

The path comes from data, which the author of the hook does not see as the author of a brick sees its paths. So two more rules hold:

  • Every machine can write the file. The path has no control character or line break and none of < > : " | ? *, which Windows allows in no name of a file, and no segment of it ends with a dot or a space.
  • A path of the app is a file or a directory, not both. A file where another file of the app has a directory, or inside what is a file of the app, is an error, whatever the case of the paths.

What every app of the owner has stays in a brick.

Imports​

A fragment never imports anything itself. It lists the imports it needs, and the pipeline adds them to the file that holds the tag, merged with the imports of other fragments and sorted. ImportRef.app('core/router/app_router.dart') imports a file of the app by its path below lib/. After rendering, the pipeline runs dart fix for unused and duplicate imports, so a template may import a library that only some of its branches use.

Paths​

The path of a file in a brick is its path in the app. A path may use variables, such as the directory of the Kotlin package of the app, {{{android_package_path}}}, but no mustache sections or partials. Files that only some apps get go into a brick of their own, which the module contributes with when or in a variant. Files whose number or paths depend on the data of a role come from a render hook.

Every file of the app has exactly one owner, so two bricks, or a brick and a render hook, must not generate the same path. A brick must not contain files that belong to one machine or one build, such as .dart_tool/, build/, GeneratedPluginRegistrant files, .iml files or signing keys, and the pipeline rejects them.

What mason would change​

  • mason removes a backslash that comes right before a line break or a non-ASCII character, in templates and in everything it renders into them. The pipeline rejects such a backslash in fragments and variables.
  • The pipeline rejects a template that changes the mustache delimiters or includes a partial.
  • mason copies a text file without any tag as it is, and a binary file too.
  • Bricks have no hooks, and the pipeline rejects a bundle with mason hooks.