felangel / felangel/mason

[proposal] feat: support brick template inheritance

Open
#215 9 comments 36 reactions 1 assignee Claimed by @felangel View on GitHub
customer:πŸ¦„ enhancement candidate feedback wanted
Dominant language
Dart
Stars
1.1k
Forks
113
PR merge metrics
No merged PRs in 30d

Description

# Proposal: Brick Template Inheritance

## Description

As a developer, I want to be able to compose bricks from other bricks so that I can simplify, reuse, and reduce the maintenance cost of bricks.

For example, suppose we are maintaining a Flutter app brick called `my_flutter_app` which builds on the standard `flutter create` template. Currently, we must duplicate and keep up-to-date the entire application (including `iOS`, `android`, `web`, etc.) which are not specific to our brick. It would be a lot simpler and easier to maintain if it were possible to create a `flutter_core` brick and specify that `my_flutter_app` extends `flutter_core`.

## Proposal

I propose adding the ability to have a brick `extend` another `brick`.

```yaml
name: my_flutter_app
description: My opinioned started Flutter application
extends: flutter_core@v1.0.0
```

The above `brick.yaml` specifies that `my_flutter_app` builds on top of the `v1.0.0` of the `flutter_core` brick.

As a result, when generating `my_flutter_app`, `mason` will:
1. Install `v1.0.0` of `flutter_core`
2. Generate the `flutter_core` brick code
3. Generate the `my_flutter_app` brick on top of `flutter_core`
a. Any conflicting files will be overwritten

### brick.yaml extends keyword

The `extends` value must be of the format:

- `@v` when referencing a brick hosted in a registry (coming soon).

`version` can be one of the following:

- `v` - signifying the latest minor version (e.g. `v1` ~> `>= 1.0.0 <2.0.0`>)
- `v.` - signifying the latest patch version (e.g. `v1.1` ~> `>= 1.1.0 < 1.2.0`>)
- `v..` - the exact version (e.g. `v1.2.3` ~> `1.2.3`)

- `:@` when referencing a brick hosted in a git repository.

`ref` can be one of the following:

- A branch name (e.g. `main`)
- A commit hash (e.g. `d45751b`)
- A tag name (e.g. `v1`)

#### Example Uses

```yaml
# extends hosted brick `flutter_core` version: `1.0.0`
name: my_flutter_app
description: My opinioned started Flutter application
extends: flutter_core@v1.0.0

# --------------------------------------------------- #

# extends hosted brick `flutter_core` version: `>=1.0.0 <2.0.0`
name: my_flutter_app
description: My opinioned started Flutter application
extends: flutter_core@v1

# --------------------------------------------------- #

# extends git brick `flutter_core` default branch
name: my_flutter_app
description: My opinioned started Flutter application
extends: https://github.com/felangel/flutter_core

# --------------------------------------------------- #

# extends git brick `flutter_core` release/v1 branch
name: my_flutter_app
description: My opinioned started Flutter application
extends: https://github.com/felangel/flutter_core@release/v1

# --------------------------------------------------- #

# extends git brick `flutter_core` at path `/templates/core` commit d45751b
name: my_flutter_app
description: My opinioned started Flutter application
extends: https://github.com/felangel/flutter_core:templates/core@d45751b
```

### Brick Inheritance Conflict Resolution

Suppose we have:

```yaml
name: sub_brick
extends: super_brick@v1
```

If both `sub_brick` and `super_brick` generate a file with path `P` and contents `C` and `C’` respectively, the outcome of generating `sub_brick` will be a file with path `P` and contents: `C'`.

For example, if `super_brick` contains:

```
β”œβ”€β”€ LICENSE
β”œβ”€β”€ README.md
β”œβ”€β”€ android
β”‚   β”œβ”€β”€ app
β”‚   β”‚   β”œβ”€β”€ build.gradle
β”‚   β”‚   └── src
β”‚   β”œβ”€β”€ app_android.iml
β”‚   β”œβ”€β”€ build.gradle
β”‚   β”œβ”€β”€ gradle
β”‚   β”‚   └── wrapper
β”‚   β”œβ”€β”€ gradle.properties
β”‚   β”œβ”€β”€ gradlew
β”‚   β”œβ”€β”€ gradlew.bat
β”‚   β”œβ”€β”€ local.properties
β”‚   └── settings.gradle
β”œβ”€β”€ ios
β”‚   β”œβ”€β”€ Flutter
β”‚   β”‚   β”œβ”€β”€ AppFrameworkInfo.plist
β”‚   β”‚   β”œβ”€β”€ Debug.xcconfig
β”‚   β”‚   β”œβ”€β”€ Generated.xcconfig
β”‚   β”‚   β”œβ”€β”€ Release.xcconfig
β”‚   β”‚   └── flutter_export_environment.sh
β”‚   β”œβ”€β”€ Runner
β”‚   β”‚   β”œβ”€β”€ AppDelegate.swift
β”‚   β”‚   β”œβ”€β”€ Assets.xcassets
β”‚   β”‚   β”œβ”€β”€ Base.lproj
β”‚   β”‚   β”œβ”€β”€ GeneratedPluginRegistrant.h
β”‚   β”‚   β”œβ”€β”€ GeneratedPluginRegistrant.m
β”‚   β”‚   β”œβ”€β”€ Info.plist
β”‚   β”‚   └── Runner-Bridging-Header.h
β”‚   β”œβ”€β”€ Runner.xcodeproj
β”‚   β”‚   β”œβ”€β”€ project.pbxproj
β”‚   β”‚   β”œβ”€β”€ project.xcworkspace
β”‚   β”‚   └── xcshareddata
β”‚   └── Runner.xcworkspace
β”‚   β”œβ”€β”€ contents.xcworkspacedata
β”‚   β”œβ”€β”€ xcshareddata
β”‚   └── xcuserdata
β”œβ”€β”€ l10n.yaml
β”œβ”€β”€ lib
β”‚   └── main.dart
β”œβ”€β”€ pubspec.yaml
└── web
β”œβ”€β”€ favicon.png
β”œβ”€β”€ icons
β”‚   β”œβ”€β”€ Icon-192.png
β”‚   β”œβ”€β”€ Icon-512.png
β”‚   └── favicon.png
β”œβ”€β”€ index.html
└── manifest.json
```

And if `sub_brick` contains:

```
β”œβ”€β”€ LICENSE
β”œβ”€β”€ README.md
β”œβ”€β”€ lib
β”‚   β”œβ”€β”€ counter
β”‚   β”‚   β”œβ”€β”€ counter.dart
β”‚   β”‚   β”œβ”€β”€ cubit
β”‚   β”‚   └── view
β”‚   └── main.dart
└── pubspec.yaml
```

The result of generating `sub_brick` would be:

```
β”œβ”€β”€ LICENSE **
β”œβ”€β”€ README.md **
β”œβ”€β”€ android
β”‚   β”œβ”€β”€ app
β”‚   β”‚   β”œβ”€β”€ build.gradle
β”‚   β”‚   └── src
β”‚   β”œβ”€β”€ app_android.iml
β”‚   β”œβ”€β”€ build.gradle
β”‚   β”œβ”€β”€ gradle
β”‚   β”‚   └── wrapper
β”‚   β”œβ”€β”€ gradle.properties
β”‚   β”œβ”€β”€ gradlew
β”‚   β”œβ”€β”€ gradlew.bat
β”‚   β”œβ”€β”€ local.properties
β”‚   └── settings.gradle
β”œβ”€β”€ ios
β”‚   β”œβ”€β”€ Flutter
β”‚   β”‚   β”œβ”€β”€ AppFrameworkInfo.plist
β”‚   β”‚   β”œβ”€β”€ Debug.xcconfig
β”‚   β”‚   β”œβ”€β”€ Generated.xcconfig
β”‚   β”‚   β”œβ”€β”€ Release.xcconfig
β”‚   β”‚   └── flutter_export_environment.sh
β”‚   β”œβ”€β”€ Runner
β”‚   β”‚   β”œβ”€β”€ AppDelegate.swift
β”‚   β”‚   β”œβ”€β”€ Assets.xcassets
β”‚   β”‚   β”œβ”€β”€ Base.lproj
β”‚   β”‚   β”œβ”€β”€ GeneratedPluginRegistrant.h
β”‚   β”‚   β”œβ”€β”€ GeneratedPluginRegistrant.m
β”‚   β”‚   β”œβ”€β”€ Info.plist
β”‚   β”‚   └── Runner-Bridging-Header.h
β”‚   β”œβ”€β”€ Runner.xcodeproj
β”‚   β”‚   β”œβ”€β”€ project.pbxproj
β”‚   β”‚   β”œβ”€β”€ project.xcworkspace
β”‚   β”‚   └── xcshareddata
β”‚   └── Runner.xcworkspace
β”‚   β”œβ”€β”€ contents.xcworkspacedata
β”‚   β”œβ”€β”€ xcshareddata
β”‚   └── xcuserdata
β”œβ”€β”€ l10n.yaml
β”œβ”€β”€ lib
β”‚   β”œβ”€β”€ counter **
β”‚   β”‚   β”œβ”€β”€ counter.dart **
β”‚   β”‚   β”œβ”€β”€ cubit **
β”‚   β”‚   └── view **
β”‚   └── main.dart **
β”œβ”€β”€ pubspec.yaml **
└── web
β”œβ”€β”€ favicon.png
β”œβ”€β”€ icons
β”‚   β”œβ”€β”€ Icon-192.png
β”‚   β”œβ”€β”€ Icon-512.png
β”‚   └── favicon.png
β”œβ”€β”€ index.html
└── manifest.json
```

Where files marked with `**` came from `sub_brick`.

### Replaceable Blocks

A file in `sub_brick` can reference the contents of the conflicting file in the `super_brick` via `{{ return 'bar';
```

The resulting `main.dart` would look like:

```dart
void main() {
print('Hello Dash!');
print('goodbye');
}

void foo() => return 'bar';
```

### Variable Resolution

When a brick `B` which requires variables {x, y}, extends brick `A` which requires variables {x, z}, variables {x, y, z} are required when generating brick `B`. The variables can still either be passed via args: `mason make B --x --y --z `, via a configuration file, via commandline prompt.

### Hook Execution

When a brick `B` which has a `pre_gen` and `post_gen` hook extends brick `A` which also has a `pre_gen` and `post_gen` hook, the result of generating brick `B` is:

1. run `pre_gen` A
2. generate brick A
3. run `post_gen` A
4. run `pre_gen` B
5. generate brick B
6. run `post_gen` B

### References / Inspiration

- https://docs.docker.com/engine/reference/builder/#from
- https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsuses
- https://groups.google.com/g/mustachejava/c/2z_73dJIAO4
- https://github.com/mustache/spec/issues/38
- https://gist.github.com/spullara/1854699

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.